From b68d52dfd710197305af7f6c2a445b8f368e1de1 Mon Sep 17 00:00:00 2001 From: nyne Date: Wed, 9 Oct 2024 17:09:28 +0800 Subject: [PATCH] history page, comic menu --- lib/components/comic.dart | 70 ++++++++++++++++-- lib/components/effects.dart | 6 +- lib/components/flyout.dart | 61 ++++++++-------- lib/components/layout.dart | 10 +-- lib/components/menu.dart | 86 ++++++++++++---------- lib/foundation/history.dart | 9 +++ lib/pages/history_page.dart | 133 ++++++++++++++++++++++++++++++++++ lib/pages/home_page.dart | 5 +- lib/pages/reader/gesture.dart | 10 +-- lib/pages/reader/reader.dart | 1 + 10 files changed, 300 insertions(+), 91 deletions(-) create mode 100644 lib/pages/history_page.dart diff --git a/lib/components/comic.dart b/lib/components/comic.dart index 6104a2f..f00e08f 100644 --- a/lib/components/comic.dart +++ b/lib/components/comic.dart @@ -6,6 +6,7 @@ class ComicTile extends StatelessWidget { required this.comic, this.enableLongPressed = true, this.badge, + this.menuOptions, }); final Comic comic; @@ -14,14 +15,48 @@ class ComicTile extends StatelessWidget { final String? badge; + final List? menuOptions; + void onTap() { App.mainNavigatorKey?.currentContext ?.to(() => ComicPage(id: comic.id, sourceKey: comic.sourceKey)); } - void onLongPress() {} + void onLongPress(BuildContext context) { + var renderBox = context.findRenderObject() as RenderBox; + var size = renderBox.size; + var location = renderBox.localToGlobal( + Offset(size.width / 2, size.height / 2), + ); + showMenu(location); + } - void onSecondaryTap(TapDownDetails details) {} + void onSecondaryTap(TapDownDetails details) { + showMenu(details.globalPosition); + } + + void showMenu(Offset location) { + showMenuX( + App.rootContext, + location, + [ + MenuEntry( + icon: Icons.chrome_reader_mode_outlined, + text: 'Details'.tl, + onClick: onTap, + ), + MenuEntry( + icon: Icons.copy, + text: 'Copy Title'.tl, + onClick: () { + Clipboard.setData(ClipboardData(text: comic.title)); + App.rootContext.showMessage(message: 'Title copied'.tl); + }, + ), + ...?menuOptions, + ], + ); + } @override Widget build(BuildContext context) { @@ -114,7 +149,7 @@ class ComicTile extends StatelessWidget { return InkWell( borderRadius: BorderRadius.circular(12), onTap: onTap, - onLongPress: enableLongPressed ? onLongPress : null, + onLongPress: enableLongPressed ? () => onLongPress(context) : null, onSecondaryTapDown: onSecondaryTap, child: Padding( padding: const EdgeInsets.fromLTRB(16, 8, 24, 8), @@ -209,7 +244,8 @@ class ComicTile extends StatelessWidget { color: Colors.transparent, child: InkWell( onTap: onTap, - onLongPress: enableLongPressed ? onLongPress : null, + onLongPress: + enableLongPressed ? () => onLongPress(context) : null, onSecondaryTapDown: onSecondaryTap, borderRadius: BorderRadius.circular(8), child: const SizedBox.expand(), @@ -300,9 +336,8 @@ class _ComicDescription extends StatelessWidget { ), ), ), - const SizedBox( - height: 2, - ), + const SizedBox(height: 2), + const Spacer(), Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ @@ -420,12 +455,18 @@ class SliverGridComics extends StatelessWidget { super.key, required this.comics, this.onLastItemBuild, + this.badgeBuilder, + this.menuBuilder, }); final List comics; final void Function()? onLastItemBuild; + final String? Function(Comic)? badgeBuilder; + + final List Function(Comic)? menuBuilder; + @override Widget build(BuildContext context) { return StateBuilder( @@ -440,6 +481,8 @@ class SliverGridComics extends StatelessWidget { return _SliverGridComics( comics: comics, onLastItemBuild: onLastItemBuild, + badgeBuilder: badgeBuilder, + menuBuilder: menuBuilder, ); }, ); @@ -450,12 +493,18 @@ class _SliverGridComics extends StatelessWidget { const _SliverGridComics({ required this.comics, this.onLastItemBuild, + this.badgeBuilder, + this.menuBuilder, }); final List comics; final void Function()? onLastItemBuild; + final String? Function(Comic)? badgeBuilder; + + final List Function(Comic)? menuBuilder; + @override Widget build(BuildContext context) { return SliverGrid( @@ -464,7 +513,12 @@ class _SliverGridComics extends StatelessWidget { if (index == comics.length - 1) { onLastItemBuild?.call(); } - return ComicTile(comic: comics[index]); + var badge = badgeBuilder?.call(comics[index]); + return ComicTile( + comic: comics[index], + badge: badge, + menuOptions: menuBuilder?.call(comics[index]), + ); }, childCount: comics.length, ), diff --git a/lib/components/effects.dart b/lib/components/effects.dart index 7bc4b81..01ae233 100644 --- a/lib/components/effects.dart +++ b/lib/components/effects.dart @@ -5,15 +5,19 @@ class BlurEffect extends StatelessWidget { final double blur; + final BorderRadius? borderRadius; + const BlurEffect({ required this.child, + this.borderRadius, this.blur = 15, super.key, }); @override Widget build(BuildContext context) { - return ClipRect( + return ClipRRect( + borderRadius: borderRadius ?? BorderRadius.zero, child: BackdropFilter( filter: ImageFilter.blur( sigmaX: blur, diff --git a/lib/components/flyout.dart b/lib/components/flyout.dart index 1d1cd4c..b915697 100644 --- a/lib/components/flyout.dart +++ b/lib/components/flyout.dart @@ -172,40 +172,39 @@ class FlyoutContent extends StatelessWidget { @override Widget build(BuildContext context) { return IntrinsicWidth( - child: Material( + child: BlurEffect( borderRadius: BorderRadius.circular(16), - type: MaterialType.card, - elevation: 1, - surfaceTintColor: Theme.of(context).colorScheme.surfaceTint, - child: Container( - constraints: const BoxConstraints( - minWidth: minFlyoutWidth, - ), - padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(title, - style: const TextStyle( - fontWeight: FontWeight.bold, fontSize: 16)), - if (content != null) - Padding( - padding: const EdgeInsets.all(8), - child: content!, + child: Material( + borderRadius: BorderRadius.circular(16), + type: MaterialType.card, + color: context.colorScheme.surface.withOpacity(0.82), + child: Container( + constraints: const BoxConstraints( + minWidth: minFlyoutWidth, + ), + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, + style: const TextStyle( + fontWeight: FontWeight.bold, fontSize: 16)), + if (content != null) + content!, + const SizedBox( + height: 12, ), - const SizedBox( - height: 12, - ), - Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.end, - children: [const Spacer(), ...actions], - ), - ], + Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, + children: [const Spacer(), ...actions], + ), + ], + ), ), - ), - ).paddingAll(4), + ).paddingAll(4), + ), ); } } diff --git a/lib/components/layout.dart b/lib/components/layout.dart index 2a84429..6cab85a 100644 --- a/lib/components/layout.dart +++ b/lib/components/layout.dart @@ -91,13 +91,11 @@ class SliverGridDelegateWithComics extends SliverGridDelegate{ } SliverGridLayout getDetailedModeLayout(SliverConstraints constraints, double scale){ - const maxCrossAxisExtent = 650; - final itemHeight = 164 * scale; + const minCrossAxisExtent = 360; + final itemHeight = 152 * scale; final width = constraints.crossAxisExtent; - var crossItems = width ~/ maxCrossAxisExtent; - if (width % maxCrossAxisExtent != 0) { - crossItems += 1; - } + var crossItems = width ~/ minCrossAxisExtent; + crossItems = math.max(1, crossItems); return SliverGridRegularTileLayout( crossAxisCount: crossItems, mainAxisStride: itemHeight, diff --git a/lib/components/menu.dart b/lib/components/menu.dart index 70704c0..ed46738 100644 --- a/lib/components/menu.dart +++ b/lib/components/menu.dart @@ -1,16 +1,15 @@ part of "components.dart"; -void showDesktopMenu( - BuildContext context, Offset location, List entries) { - Navigator.of(context).push(DesktopMenuRoute(entries, location)); +void showMenuX(BuildContext context, Offset location, List entries) { + Navigator.of(context).push(_MenuRoute(entries, location)); } -class DesktopMenuRoute extends PopupRoute { - final List entries; +class _MenuRoute extends PopupRoute { + final List entries; final Offset location; - DesktopMenuRoute(this.entries, this.location); + _MenuRoute(this.entries, this.location); @override Color? get barrierColor => Colors.transparent; @@ -24,7 +23,7 @@ class DesktopMenuRoute extends PopupRoute { @override Widget buildPage(BuildContext context, Animation animation, Animation secondaryAnimation) { - const width = 196.0; + var width = entries.first.icon == null ? 216.0 : 242.0; final size = MediaQuery.of(context).size; var left = location.dx; if (left + width > size.width - 10) { @@ -41,22 +40,33 @@ class DesktopMenuRoute extends PopupRoute { left: left, top: top, child: Container( - width: width, - padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 6), decoration: BoxDecoration( - color: context.colorScheme.surface, + borderRadius: BorderRadius.circular(4), + boxShadow: [ + BoxShadow( + color: context.colorScheme.shadow.withOpacity(0.2), + blurRadius: 8, + blurStyle: BlurStyle.outer, + ), + ], + ), + child: BlurEffect( + borderRadius: BorderRadius.circular(4), + child: Material( + color: context.brightness == Brightness.light + ? const Color(0xFFFAFAFA).withOpacity(0.72) + : const Color(0xFF090909).withOpacity(0.72), borderRadius: BorderRadius.circular(4), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.2), - blurRadius: 4, - offset: const Offset(0, 2), + child: Container( + width: width, + padding: + const EdgeInsets.symmetric(vertical: 12, horizontal: 6), + child: Column( + mainAxisSize: MainAxisSize.min, + children: + entries.map((e) => buildEntry(e, context)).toList(), ), - ]), - child: Material( - child: Column( - mainAxisSize: MainAxisSize.min, - children: entries.map((e) => buildEntry(e, context)).toList(), + ), ), ), ), @@ -65,7 +75,7 @@ class DesktopMenuRoute extends PopupRoute { ); } - Widget buildEntry(DesktopMenuEntry entry, BuildContext context) { + Widget buildEntry(MenuEntry entry, BuildContext context) { return InkWell( borderRadius: BorderRadius.circular(4), onTap: () { @@ -73,22 +83,20 @@ class DesktopMenuRoute extends PopupRoute { entry.onClick(); }, child: SizedBox( - height: 32, - child: Row( - children: [ - const SizedBox( - width: 4, - ), - if (entry.icon != null) - Icon( - entry.icon, - size: 18, - ), - const SizedBox( - width: 4, - ), - Text(entry.text), - ], + height: App.isMobile ? 42 : 36, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Row( + children: [ + if (entry.icon != null) + Icon( + entry.icon, + size: 18, + ), + const SizedBox(width: 12), + Text(entry.text), + ], + ), ), ), ); @@ -108,10 +116,10 @@ class DesktopMenuRoute extends PopupRoute { } } -class DesktopMenuEntry { +class MenuEntry { final String text; final IconData? icon; final void Function() onClick; - DesktopMenuEntry({required this.text, this.icon, required this.onClick}); + MenuEntry({required this.text, this.icon, required this.onClick}); } diff --git a/lib/foundation/history.dart b/lib/foundation/history.dart index 9912d3c..47ee14a 100644 --- a/lib/foundation/history.dart +++ b/lib/foundation/history.dart @@ -129,6 +129,14 @@ class History { HistoryManager().addHistory(history); return history; } + + @override + bool operator ==(Object other) { + return other is History && type == other.type && id == other.id; + } + + @override + int get hashCode => Object.hash(id, type); } class HistoryManager with ChangeNotifier { @@ -190,6 +198,7 @@ class HistoryManager with ChangeNotifier { void clearHistory() { _db.execute("delete from history;"); updateCache(); + notifyListeners(); } void remove(String id, ComicType type) async { diff --git a/lib/pages/history_page.dart b/lib/pages/history_page.dart new file mode 100644 index 0000000..fde879f --- /dev/null +++ b/lib/pages/history_page.dart @@ -0,0 +1,133 @@ +import 'package:flutter/material.dart'; +import 'package:venera/components/components.dart'; +import 'package:venera/foundation/app.dart'; +import 'package:venera/foundation/comic_source/comic_source.dart'; +import 'package:venera/foundation/comic_type.dart'; +import 'package:venera/foundation/history.dart'; +import 'package:venera/utils/translations.dart'; + +class HistoryPage extends StatefulWidget { + const HistoryPage({super.key}); + + @override + State createState() => _HistoryPageState(); +} + +class _HistoryPageState extends State { + @override + void initState() { + HistoryManager().addListener(onUpdate); + super.initState(); + } + + @override + void dispose() { + HistoryManager().removeListener(onUpdate); + super.dispose(); + } + + void onUpdate() { + setState(() { + comics = HistoryManager().getAll(); + }); + } + + var comics = HistoryManager().getAll(); + + var controller = FlyoutController(); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SmoothCustomScrollView( + slivers: [ + SliverAppbar( + title: Text('History'.tl), + actions: [ + Tooltip( + message: 'Clear History'.tl, + child: Flyout( + controller: controller, + flyoutBuilder: (context) { + return FlyoutContent( + title: 'Clear History'.tl, + content: Text( + 'Are you sure you want to clear your history?'.tl), + actions: [ + Button.filled( + color: context.colorScheme.error, + onPressed: () { + HistoryManager().clearHistory(); + context.pop(); + }, + child: Text('Clear'.tl), + ), + ], + ); + }, + child: IconButton( + icon: const Icon(Icons.clear_all), + onPressed: () { + controller.show(); + }, + ), + ), + ) + ], + ), + SliverGridComics( + comics: comics.map( + (e) { + return Comic( + e.title, + e.cover, + e.id, + e.subtitle, + null, + getDescription(e), + e.type.comicSource?.key ?? "Invalid", + null, + ); + }, + ).toList(), + badgeBuilder: (c) { + return ComicSource.find(c.sourceKey)?.name; + }, + menuBuilder: (c) { + return [ + MenuEntry( + icon: Icons.remove, + text: 'Remove'.tl, + onClick: () { + HistoryManager().remove( + c.id, + ComicType(c.sourceKey.hashCode), + ); + }, + ), + ]; + }, + ), + ], + ), + ); + } + + String getDescription(History h) { + var res = ""; + if (h.ep >= 1) { + res += "Chapter @ep".tlParams({ + "ep": h.ep, + }); + } + if (h.page >= 1) { + if (h.ep >= 1) { + res += " - "; + } + res += "Page @page".tlParams({ + "page": h.page, + }); + } + return res; + } +} diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index e2b4f03..03d21ca 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -11,6 +11,7 @@ import 'package:venera/foundation/local.dart'; import 'package:venera/foundation/log.dart'; import 'package:venera/pages/comic_page.dart'; import 'package:venera/pages/comic_source_page.dart'; +import 'package:venera/pages/history_page.dart'; import 'package:venera/pages/search_page.dart'; import 'package:venera/utils/io.dart'; import 'package:venera/utils/translations.dart'; @@ -113,7 +114,9 @@ class _HistoryState extends State<_History> { ), child: InkWell( borderRadius: BorderRadius.circular(8), - onTap: () {}, + onTap: () { + context.to(() => const HistoryPage()); + }, child: Column( mainAxisSize: MainAxisSize.min, children: [ diff --git a/lib/pages/reader/gesture.dart b/lib/pages/reader/gesture.dart index 7bb6acf..8d6a0ad 100644 --- a/lib/pages/reader/gesture.dart +++ b/lib/pages/reader/gesture.dart @@ -178,26 +178,26 @@ class _ReaderGestureDetectorState extends State<_ReaderGestureDetector> { } void onSecondaryTapUp(Offset location) { - showDesktopMenu( + showMenuX( context, location, [ - DesktopMenuEntry( + MenuEntry( text: "Settings".tl, onClick: () { context.readerScaffold.openSetting(); }), - DesktopMenuEntry( + MenuEntry( text: "Chapters".tl, onClick: () { context.readerScaffold.openChapterDrawer(); }), - DesktopMenuEntry( + MenuEntry( text: "Fullscreen".tl, onClick: () { context.reader.fullscreen(); }), - DesktopMenuEntry( + MenuEntry( text: "Exit".tl, onClick: () { context.pop(); diff --git a/lib/pages/reader/reader.dart b/lib/pages/reader/reader.dart index 9526b93..55bf4d0 100644 --- a/lib/pages/reader/reader.dart +++ b/lib/pages/reader/reader.dart @@ -102,6 +102,7 @@ class _ReaderState extends State with _ReaderLocation, _ReaderWindow { chapter = widget.initialChapter ?? 1; mode = ReaderMode.fromKey(appdata.settings['readerMode']); history = widget.history; + updateHistory(); super.initState(); }