diff --git a/lib/components/grid.dart b/lib/components/grid.dart index 071fad5..e2dc809 100644 --- a/lib/components/grid.dart +++ b/lib/components/grid.dart @@ -25,6 +25,45 @@ class SliverGridViewWithFixedItemHeight extends StatelessWidget { ))); } + double calcChildAspectRatio(double width) { + var crossItems = width ~/ maxCrossAxisExtent; + if (width % maxCrossAxisExtent != 0) { + crossItems += 1; + } + final itemWidth = width / crossItems; + return itemWidth / itemHeight; + } +} + +class GridViewWithFixedItemHeight extends StatelessWidget { + const GridViewWithFixedItemHeight( + { required this.builder, + required this.itemCount, + required this.maxCrossAxisExtent, + required this.itemHeight, + super.key}); + + final Widget Function(BuildContext, int) builder; + + final int itemCount; + + final double maxCrossAxisExtent; + + final double itemHeight; + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: ((context, constraints) => GridView.builder( + gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: maxCrossAxisExtent, + childAspectRatio: + calcChildAspectRatio(constraints.maxWidth)), + itemBuilder: builder, + itemCount: itemCount, + ))); + } + double calcChildAspectRatio(double width) { var crossItems = width ~/ maxCrossAxisExtent; if (width % maxCrossAxisExtent != 0) { diff --git a/lib/network/download.dart b/lib/network/download.dart index e9fe25c..a3da621 100644 --- a/lib/network/download.dart +++ b/lib/network/download.dart @@ -266,4 +266,30 @@ class DownloadManager { } }); } + + void delete(DownloadedIllust illust) { + _db.execute(''' + delete from download + where illust_id = ?; + ''', [illust.illustId]); + var images = _db.select(''' + select * from images + where illust_id = ?; + ''', [illust.illustId]); + for(var image in images) { + File(image["path"] as String).deleteIfExists(); + } + _db.execute(''' + delete from images + where illust_id = ?; + ''', [illust.illustId]); + } + + List getImagePaths(int illustId) { + var res = _db.select(''' + select * from images + where illust_id = ?; + ''', [illustId]); + return res.map((e) => e["path"] as String).toList(); + } } \ No newline at end of file diff --git a/lib/pages/downloaded_page.dart b/lib/pages/downloaded_page.dart index 9b2a734..deba521 100644 --- a/lib/pages/downloaded_page.dart +++ b/lib/pages/downloaded_page.dart @@ -1,4 +1,18 @@ +import 'dart:io'; + import 'package:fluent_ui/fluent_ui.dart'; +import 'package:flutter/gestures.dart'; +import 'package:photo_view/photo_view_gallery.dart'; +import 'package:pixes/components/grid.dart'; +import 'package:pixes/components/md.dart'; +import 'package:pixes/components/page_route.dart'; +import 'package:pixes/components/title_bar.dart'; +import 'package:pixes/foundation/app.dart'; +import 'package:pixes/network/download.dart'; +import 'package:pixes/utils/translation.dart'; +import 'package:window_manager/window_manager.dart'; + +import 'main_page.dart'; class DownloadedPage extends StatefulWidget { const DownloadedPage({super.key}); @@ -8,8 +22,288 @@ class DownloadedPage extends StatefulWidget { } class _DownloadedPageState extends State { + var illusts = []; + var flyoutControllers = []; + + void loadData() { + illusts = DownloadManager().listAll(); + flyoutControllers = List.generate(illusts.length, (index) => FlyoutController()); + } + + @override + void initState() { + loadData(); + super.initState(); + } + @override Widget build(BuildContext context) { - return const Placeholder(); + return Column( + children: [ + TitleBar(title: "Downloaded".tl), + Expanded( + child: buildBody(), + ), + ], + ); + } + + Widget buildBody() { + return GridViewWithFixedItemHeight( + itemCount: illusts.length, + itemHeight: 152, + maxCrossAxisExtent: 560, + builder: (context, index) { + return Card( + margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), + child: Row( + children: [ + Container( + width: 96, + height: double.infinity, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: ColorScheme.of(context).secondaryContainer + ), + clipBehavior: Clip.antiAlias, + child: Image( + image: FileImage(DownloadManager().getImage( + illusts[index].illustId, 0)!), + fit: BoxFit.cover, + filterQuality: FilterQuality.medium, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + illusts[index].title, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + Text( + illusts[index].author, + style: const TextStyle( + fontSize: 12, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + Text( + "${illusts[index].imageCount}P", + style: const TextStyle( + fontSize: 12, + ), + ), + const Spacer(), + Row( + children: [ + const Spacer(), + Button( + child: Text("View".tl).fixWidth(42), + onPressed: () { + App.rootNavigatorKey.currentState?.push( + AppPageRoute(builder: (context) { + return _DownloadedIllustViewPage( + DownloadManager().getImagePaths( + illusts[index].illustId)); + })); + }, + ), + const SizedBox(width: 8), + FlyoutTarget( + controller: flyoutControllers[index], + child: Button( + child: Text("Delete".tl).fixWidth(42), + onPressed: () { + flyoutControllers[index].showFlyout( + navigatorKey: App.rootNavigatorKey.currentState, + builder: (context) { + return FlyoutContent( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Are you sure you want to delete?'.tl, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 12.0), + Button( + onPressed: () { + Flyout.of(context).close(); + DownloadManager().delete(illusts[index]); + setState(() { + illusts.removeAt(index); + flyoutControllers.removeAt(index); + }); + }, + child: Text('Yes'.tl), + ), + ], + ), + ); + }); + }, + ), + ), + ], + ), + ], + ), + ), + ], + ), + ); + } + ).paddingHorizontal(8); } } + +class _DownloadedIllustViewPage extends StatefulWidget { + const _DownloadedIllustViewPage(this.imagePaths); + + final List imagePaths; + + @override + State<_DownloadedIllustViewPage> createState() => _DownloadedIllustViewPageState(); +} + +class _DownloadedIllustViewPageState extends State<_DownloadedIllustViewPage> with WindowListener{ + int windowButtonKey = 0; + + @override + void initState() { + windowManager.addListener(this); + super.initState(); + } + + @override + void dispose() { + windowManager.removeListener(this); + super.dispose(); + } + + @override + void onWindowMaximize() { + setState(() { + windowButtonKey++; + }); + } + + @override + void onWindowUnmaximize() { + setState(() { + windowButtonKey++; + }); + } + + var controller = PageController(); + + @override + Widget build(BuildContext context) { + return ScaffoldPage( + padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top), + content: Listener( + onPointerSignal: (event) { + if(event is PointerScrollEvent) { + if(event.scrollDelta.dy > 0 + && controller.page!.toInt() < widget.imagePaths.length - 1) { + controller.jumpToPage(controller.page!.toInt() + 1); + } else if(event.scrollDelta.dy < 0 && controller.page!.toInt() > 0){ + controller.jumpToPage(controller.page!.toInt() - 1); + } + } + }, + child: LayoutBuilder( + builder: (context, constrains) { + var height = constrains.maxHeight; + return Stack( + children: [ + Positioned.fill(child: PhotoViewGallery.builder( + pageController: controller, + backgroundDecoration: BoxDecoration( + color: FluentTheme.of(context).micaBackgroundColor + ), + itemCount: widget.imagePaths.length, + builder: (context, index) { + return PhotoViewGalleryPageOptions( + imageProvider: FileImage(File(widget.imagePaths[index])), + ); + }, + onPageChanged: (index) { + setState(() {}); + }, + )), + Positioned( + top: 0, + left: 0, + right: 0, + child: SizedBox( + height: 36, + child: Row( + children: [ + const SizedBox(width: 6,), + IconButton( + icon: const Icon(FluentIcons.back).paddingAll(2), + onPressed: () => context.pop() + ), + const Expanded( + child: DragToMoveArea(child: SizedBox.expand(),), + ), + if(App.isDesktop) + WindowButtons(key: ValueKey(windowButtonKey),), + ], + ), + ), + ), + Positioned( + left: 0, + top: height / 2 - 9, + child: IconButton( + icon: const Icon(FluentIcons.chevron_left, size: 18,), + onPressed: () { + controller.previousPage( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + }, + ).paddingAll(8), + ), + Positioned( + right: 0, + top: height / 2 - 9, + child: IconButton( + icon: const Icon(FluentIcons.chevron_right, size: 18), + onPressed: () { + controller.nextPage( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + }, + ).paddingAll(8), + ), + Positioned( + left: 12, + bottom: 8, + child: Text( + "${controller.page!.toInt() + 1}/${widget.imagePaths.length}", + ), + ) + ], + ); + }, + ), + ), + ); + } +} +