From d247455c19600f79773e0168c8e36495d086d463 Mon Sep 17 00:00:00 2001 From: wgh19 Date: Wed, 12 Jun 2024 15:43:06 +0800 Subject: [PATCH] local history --- assets/tr.json | 6 +- lib/components/illust_widget.dart | 160 ++++++++++++++++++++++++++++++ lib/foundation/history.dart | 95 ++++++++++++++++++ lib/main.dart | 2 + lib/network/network.dart | 10 ++ lib/pages/history.dart | 121 +++++++++++++++++----- lib/pages/illust_page.dart | 17 ++++ 7 files changed, 383 insertions(+), 28 deletions(-) create mode 100644 lib/foundation/history.dart diff --git a/assets/tr.json b/assets/tr.json index 340f5c7..09de87a 100644 --- a/assets/tr.json +++ b/assets/tr.json @@ -165,7 +165,8 @@ "Read": "阅读", "Error": "错误", "Failed to register URL scheme.": "注册URL协议失败", - "Retry": "重试" + "Retry": "重试", + "Network": "网络" }, "zh_TW": { "Search": "搜索", @@ -333,6 +334,7 @@ "Read": "閱讀", "Error": "錯誤", "Failed to register URL scheme.": "註冊URL協議失敗", - "Retry": "重試" + "Retry": "重試", + "Network": "網絡" } } \ No newline at end of file diff --git a/lib/components/illust_widget.dart b/lib/components/illust_widget.dart index 56495a7..f7df746 100644 --- a/lib/components/illust_widget.dart +++ b/lib/components/illust_widget.dart @@ -1,6 +1,7 @@ import 'package:fluent_ui/fluent_ui.dart'; import 'package:pixes/components/animated_image.dart'; import 'package:pixes/foundation/app.dart'; +import 'package:pixes/foundation/history.dart'; import 'package:pixes/foundation/image_provider.dart'; import 'package:pixes/network/download.dart'; import 'package:pixes/utils/translation.dart'; @@ -307,3 +308,162 @@ class _IllustWidgetState extends State { ); } } + +class IllustHistoryWidget extends StatelessWidget { + const IllustHistoryWidget(this.illust, {super.key}); + + final IllustHistory illust; + + @override + Widget build(BuildContext context) { + return LayoutBuilder(builder: (context, constrains) { + final width = constrains.maxWidth; + final height = illust.height * width / illust.width; + return SizedBox( + width: width, + height: height, + child: Stack( + children: [ + Positioned.fill( + child: Container( + width: width, + height: height, + padding: + const EdgeInsets.symmetric(horizontal: 8.0, vertical: 8.0), + child: Card( + padding: EdgeInsets.zero, + margin: EdgeInsets.zero, + child: GestureDetector( + onTap: () { + context.to(() => IllustPageWithId(illust.id.toString())); + }, + child: ClipRRect( + borderRadius: BorderRadius.circular(4.0), + child: AnimatedImage( + image: CachedImageProvider( + illust.imgPath), + fit: BoxFit.cover, + width: width - 16.0, + height: height - 16.0, + ), + ), + ), + ), + )), + if (illust.imageCount > 1) + Positioned( + top: 12, + left: 12, + child: Container( + width: 28, + height: 20, + decoration: BoxDecoration( + color: FluentTheme.of(context) + .micaBackgroundColor + .withOpacity(0.72), + borderRadius: BorderRadius.circular(4), + border: Border.all( + color: ColorScheme.of(context).outlineVariant, + width: 0.6), + ), + child: Center( + child: Text( + "${illust.imageCount}P", + style: const TextStyle(fontSize: 12), + ), + )), + ), + if (illust.isAi) + Positioned( + bottom: 12, + left: 12, + child: Container( + width: 28, + height: 20, + decoration: BoxDecoration( + color: ColorScheme.of(context) + .errorContainer + .withOpacity(0.8), + borderRadius: BorderRadius.circular(4), + border: Border.all( + color: ColorScheme.of(context).outlineVariant, + width: 0.6), + ), + child: const Center( + child: Text( + "AI", + style: TextStyle(fontSize: 12), + ), + )), + ), + if (illust.isGif) + Positioned( + bottom: 12, + left: 12, + child: Container( + width: 28, + height: 20, + decoration: BoxDecoration( + color: ColorScheme.of(context) + .primaryContainer + .withOpacity(0.8), + borderRadius: BorderRadius.circular(4), + border: Border.all( + color: ColorScheme.of(context).outlineVariant, + width: 0.6), + ), + child: const Center( + child: Text( + "GIF", + style: TextStyle(fontSize: 12), + ), + )), + ), + if (illust.isR18) + Positioned( + bottom: 12, + right: 12, + child: Container( + width: 28, + height: 20, + decoration: BoxDecoration( + color: ColorScheme.of(context).errorContainer, + borderRadius: BorderRadius.circular(4), + border: Border.all( + color: ColorScheme.of(context).outlineVariant, + width: 0.6), + ), + child: const Center( + child: Text( + "R18", + style: TextStyle(fontSize: 12), + ), + )), + ), + if (illust.isR18G) + Positioned( + bottom: 12, + right: 12, + child: Container( + width: 28, + height: 20, + decoration: BoxDecoration( + color: ColorScheme.of(context).errorContainer, + borderRadius: BorderRadius.circular(4), + border: Border.all( + color: ColorScheme.of(context).outlineVariant, + width: 0.6), + ), + child: const Center( + child: Text( + "R18G", + style: TextStyle(fontSize: 12), + ), + )), + ), + ], + ), + ); + }); + } +} \ No newline at end of file diff --git a/lib/foundation/history.dart b/lib/foundation/history.dart new file mode 100644 index 0000000..117a778 --- /dev/null +++ b/lib/foundation/history.dart @@ -0,0 +1,95 @@ +import 'package:pixes/foundation/app.dart'; +import 'package:sqlite3/sqlite3.dart'; +import 'package:pixes/network/models.dart'; + +class IllustHistory { + final int id; + final String imgPath; + final DateTime time; + final int imageCount; + final bool isR18; + final bool isR18G; + final bool isAi; + final bool isGif; + final int width; + final int height; + + IllustHistory(this.id, this.imgPath, this.time, this.imageCount, this.isR18, + this.isR18G, this.isAi, this.isGif, this.width, this.height); +} + +class HistoryManager { + static HistoryManager? instance; + + factory HistoryManager() => instance ??= HistoryManager._create(); + + HistoryManager._create(); + + late Database _db; + + init() { + _db = sqlite3.open("${App.dataPath}/history.db"); + _db.execute(''' + create table if not exists history ( + id integer primary key not null, + imgPath text not null, + time integer not null, + imageCount integer not null, + isR18 integer not null, + isR18g integer not null, + isAi integer not null, + isGif integer not null, + width integer not null, + height integer not null + ) + '''); + } + + void addHistory(Illust illust) { + var time = DateTime.now(); + _db.execute(''' + insert or replace into history (id, imgPath, time, imageCount, isR18, isR18g, isAi, isGif, width, height) + values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ''', [ + illust.id, + illust.images.first.medium, + time.millisecondsSinceEpoch, + illust.pageCount, + illust.isR18 ? 1 : 0, + illust.isR18G ? 1 : 0, + illust.isAi ? 1 : 0, + illust.isUgoira ? 1 : 0, + illust.width, + illust.height + ]); + } + + List getHistories(int page) { + var rows = _db.select(''' + select * from history + limit 20 offset ? + ''', [(page - 1) * 20]); + List res = []; + for (var row in rows) { + res.add(IllustHistory( + row['id'], + row['imgPath'], + DateTime.fromMillisecondsSinceEpoch(row['time']), + row['imageCount'], + row['isR18'] == 1, + row['isR18g'] == 1, + row['isAi'] == 1, + row['isGif'] == 1, + row['width'], + row['height'])); + } + return res; + } + + int get length { + var rows = _db.select(''' + select count(*) from history + '''); + return rows.first.values.first! as int; + } +} diff --git a/lib/main.dart b/lib/main.dart index 7f6f13b..76f1d89 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -11,6 +11,7 @@ import "package:pixes/components/keyboard.dart"; import "package:pixes/components/md.dart"; import "package:pixes/components/message.dart"; import "package:pixes/foundation/app.dart"; +import "package:pixes/foundation/history.dart"; import "package:pixes/foundation/log.dart"; import "package:pixes/network/app_dio.dart"; import "package:pixes/pages/main_page.dart"; @@ -29,6 +30,7 @@ void main() async { await App.init(); await appdata.readData(); await Translation.init(); + HistoryManager().init(); handleLinks(); if (App.isDesktop) { await flutter_acrylic.Window.initialize(); diff --git a/lib/network/network.dart b/lib/network/network.dart index 2293edf..82b4d23 100644 --- a/lib/network/network.dart +++ b/lib/network/network.dart @@ -573,4 +573,14 @@ class Network { return Res.error(res.errorMessage); } } + + Future> sendHistory(List ids) async{ + var res = await apiPost("/v2/user/browsing-history/illust/add", + data: {"illust_ids": ids}); + if (res.success) { + return const Res(true); + } else { + return Res.fromErrorRes(res); + } + } } diff --git a/lib/pages/history.dart b/lib/pages/history.dart index 31bf647..aaffbe7 100644 --- a/lib/pages/history.dart +++ b/lib/pages/history.dart @@ -2,8 +2,10 @@ import 'package:fluent_ui/fluent_ui.dart'; import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; import 'package:pixes/appdata.dart'; import 'package:pixes/components/loading.dart'; +import 'package:pixes/components/segmented_button.dart'; import 'package:pixes/components/title_bar.dart'; import 'package:pixes/foundation/app.dart'; +import 'package:pixes/foundation/history.dart'; import 'package:pixes/network/network.dart'; import 'package:pixes/utils/translation.dart'; @@ -17,42 +19,109 @@ class HistoryPage extends StatefulWidget { State createState() => _HistoryPageState(); } -class _HistoryPageState extends MultiPageLoadingState { +class _HistoryPageState extends State { + int page = 0; + @override - Widget buildContent(BuildContext context, final List data) { + Widget build(BuildContext context) { return Column( children: [ - TitleBar(title: "History".tl), + TitleBar( + title: "History".tl, + action: SegmentedButton( + options: [ + SegmentedButtonOption(0, "Local".tl,), + SegmentedButtonOption(1, "Network".tl,), + ], + value: page, + onPressed: (key) { + setState(() { + page = key; + }); + }, + ), + ), Expanded( - child: LayoutBuilder(builder: (context, constrains){ - return MasonryGridView.builder( - padding: const EdgeInsets.symmetric(horizontal: 8) - + EdgeInsets.only(bottom: context.padding.bottom), - gridDelegate: const SliverSimpleGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 240, - ), - itemCount: data.length, - itemBuilder: (context, index) { - if(index == data.length - 1){ - nextPage(); - } - return IllustWidget(data[index], onTap: () { - context.to(() => IllustGalleryPage( - illusts: data, - initialPage: index, - )); - }); - }, - ); - }), - ) + child: page == 0 + ? const LocalHistoryPage() + : const NetworkHistoryPage(), + ), ], ); } +} + +class LocalHistoryPage extends StatefulWidget { + const LocalHistoryPage({super.key}); + + @override + State createState() => _LocalHistoryPageState(); +} + +class _LocalHistoryPageState extends State { + int page = 1; + + var data = []; + + @override + Widget build(BuildContext context) { + return LayoutBuilder(builder: (context, constrains) { + return MasonryGridView.builder( + padding: const EdgeInsets.symmetric(horizontal: 8) + + EdgeInsets.only(bottom: context.padding.bottom), + gridDelegate: const SliverSimpleGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 240, + ), + itemCount: HistoryManager().length, + itemBuilder: (context, index) { + if (index == data.length) { + data.addAll(HistoryManager().getHistories(page)); + page++; + } + return IllustHistoryWidget(data[index]); + }, + ); + }); + } +} + +class NetworkHistoryPage extends StatefulWidget { + const NetworkHistoryPage({super.key}); + + @override + State createState() => _NetworkHistoryPageState(); +} + +class _NetworkHistoryPageState + extends MultiPageLoadingState { + @override + Widget buildContent(BuildContext context, final List data) { + return LayoutBuilder(builder: (context, constrains) { + return MasonryGridView.builder( + padding: const EdgeInsets.symmetric(horizontal: 8) + + EdgeInsets.only(bottom: context.padding.bottom), + gridDelegate: const SliverSimpleGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 240, + ), + itemCount: data.length, + itemBuilder: (context, index) { + if (index == data.length - 1) { + nextPage(); + } + return IllustWidget(data[index], onTap: () { + context.to(() => IllustGalleryPage( + illusts: data, + initialPage: index, + )); + }); + }, + ); + }); + } @override Future>> loadData(page) { - if(appdata.account?.user.isPremium != true) { + if (appdata.account?.user.isPremium != true) { return Future.value(Res.error("Premium Required".tl)); } return Network().getHistory(page); diff --git a/lib/pages/illust_page.dart b/lib/pages/illust_page.dart index 1c8b927..a7c10dc 100644 --- a/lib/pages/illust_page.dart +++ b/lib/pages/illust_page.dart @@ -14,6 +14,7 @@ import 'package:pixes/components/page_route.dart'; import 'package:pixes/components/title_bar.dart'; import 'package:pixes/components/user_preview.dart'; import 'package:pixes/foundation/app.dart'; +import 'package:pixes/foundation/history.dart'; import 'package:pixes/foundation/image_provider.dart'; import 'package:pixes/network/download.dart'; import 'package:pixes/network/network.dart'; @@ -44,6 +45,8 @@ class IllustGalleryPage extends StatefulWidget { final String? nextUrl; + static var cachedHistoryIds = {}; + @override State createState() => _IllustGalleryPageState(); } @@ -68,6 +71,16 @@ class _IllustGalleryPageState extends State { super.initState(); } + @override + void dispose() { + if(IllustGalleryPage.cachedHistoryIds.length > 5) { + Network().sendHistory( + IllustGalleryPage.cachedHistoryIds.toList().reversed.toList()); + IllustGalleryPage.cachedHistoryIds.clear(); + } + super.dispose(); + } + void nextPage() { var length = illusts.length; if (controller.page == length - 1) return; @@ -169,6 +182,10 @@ class _IllustPageState extends State { widget.illust.author.isFollowed = v; }); }; + HistoryManager().addHistory(widget.illust); + if(appdata.account!.user.isPremium) { + IllustGalleryPage.cachedHistoryIds.add(widget.illust.id); + } super.initState(); }