history page, comic menu

This commit is contained in:
nyne
2024-10-09 17:09:28 +08:00
parent 90b9265ca0
commit b68d52dfd7
10 changed files with 300 additions and 91 deletions

View File

@@ -6,6 +6,7 @@ class ComicTile extends StatelessWidget {
required this.comic, required this.comic,
this.enableLongPressed = true, this.enableLongPressed = true,
this.badge, this.badge,
this.menuOptions,
}); });
final Comic comic; final Comic comic;
@@ -14,14 +15,48 @@ class ComicTile extends StatelessWidget {
final String? badge; final String? badge;
final List<MenuEntry>? menuOptions;
void onTap() { void onTap() {
App.mainNavigatorKey?.currentContext App.mainNavigatorKey?.currentContext
?.to(() => ComicPage(id: comic.id, sourceKey: comic.sourceKey)); ?.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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -114,7 +149,7 @@ class ComicTile extends StatelessWidget {
return InkWell( return InkWell(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
onTap: onTap, onTap: onTap,
onLongPress: enableLongPressed ? onLongPress : null, onLongPress: enableLongPressed ? () => onLongPress(context) : null,
onSecondaryTapDown: onSecondaryTap, onSecondaryTapDown: onSecondaryTap,
child: Padding( child: Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 24, 8), padding: const EdgeInsets.fromLTRB(16, 8, 24, 8),
@@ -209,7 +244,8 @@ class ComicTile extends StatelessWidget {
color: Colors.transparent, color: Colors.transparent,
child: InkWell( child: InkWell(
onTap: onTap, onTap: onTap,
onLongPress: enableLongPressed ? onLongPress : null, onLongPress:
enableLongPressed ? () => onLongPress(context) : null,
onSecondaryTapDown: onSecondaryTap, onSecondaryTapDown: onSecondaryTap,
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
child: const SizedBox.expand(), child: const SizedBox.expand(),
@@ -300,9 +336,8 @@ class _ComicDescription extends StatelessWidget {
), ),
), ),
), ),
const SizedBox( const SizedBox(height: 2),
height: 2, const Spacer(),
),
Row( Row(
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
@@ -420,12 +455,18 @@ class SliverGridComics extends StatelessWidget {
super.key, super.key,
required this.comics, required this.comics,
this.onLastItemBuild, this.onLastItemBuild,
this.badgeBuilder,
this.menuBuilder,
}); });
final List<Comic> comics; final List<Comic> comics;
final void Function()? onLastItemBuild; final void Function()? onLastItemBuild;
final String? Function(Comic)? badgeBuilder;
final List<MenuEntry> Function(Comic)? menuBuilder;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return StateBuilder<SliverGridComicsController>( return StateBuilder<SliverGridComicsController>(
@@ -440,6 +481,8 @@ class SliverGridComics extends StatelessWidget {
return _SliverGridComics( return _SliverGridComics(
comics: comics, comics: comics,
onLastItemBuild: onLastItemBuild, onLastItemBuild: onLastItemBuild,
badgeBuilder: badgeBuilder,
menuBuilder: menuBuilder,
); );
}, },
); );
@@ -450,12 +493,18 @@ class _SliverGridComics extends StatelessWidget {
const _SliverGridComics({ const _SliverGridComics({
required this.comics, required this.comics,
this.onLastItemBuild, this.onLastItemBuild,
this.badgeBuilder,
this.menuBuilder,
}); });
final List<Comic> comics; final List<Comic> comics;
final void Function()? onLastItemBuild; final void Function()? onLastItemBuild;
final String? Function(Comic)? badgeBuilder;
final List<MenuEntry> Function(Comic)? menuBuilder;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SliverGrid( return SliverGrid(
@@ -464,7 +513,12 @@ class _SliverGridComics extends StatelessWidget {
if (index == comics.length - 1) { if (index == comics.length - 1) {
onLastItemBuild?.call(); 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, childCount: comics.length,
), ),

View File

@@ -5,15 +5,19 @@ class BlurEffect extends StatelessWidget {
final double blur; final double blur;
final BorderRadius? borderRadius;
const BlurEffect({ const BlurEffect({
required this.child, required this.child,
this.borderRadius,
this.blur = 15, this.blur = 15,
super.key, super.key,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ClipRect( return ClipRRect(
borderRadius: borderRadius ?? BorderRadius.zero,
child: BackdropFilter( child: BackdropFilter(
filter: ImageFilter.blur( filter: ImageFilter.blur(
sigmaX: blur, sigmaX: blur,

View File

@@ -172,11 +172,12 @@ class FlyoutContent extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return IntrinsicWidth( return IntrinsicWidth(
child: BlurEffect(
borderRadius: BorderRadius.circular(16),
child: Material( child: Material(
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
type: MaterialType.card, type: MaterialType.card,
elevation: 1, color: context.colorScheme.surface.withOpacity(0.82),
surfaceTintColor: Theme.of(context).colorScheme.surfaceTint,
child: Container( child: Container(
constraints: const BoxConstraints( constraints: const BoxConstraints(
minWidth: minFlyoutWidth, minWidth: minFlyoutWidth,
@@ -190,10 +191,7 @@ class FlyoutContent extends StatelessWidget {
style: const TextStyle( style: const TextStyle(
fontWeight: FontWeight.bold, fontSize: 16)), fontWeight: FontWeight.bold, fontSize: 16)),
if (content != null) if (content != null)
Padding( content!,
padding: const EdgeInsets.all(8),
child: content!,
),
const SizedBox( const SizedBox(
height: 12, height: 12,
), ),
@@ -206,6 +204,7 @@ class FlyoutContent extends StatelessWidget {
), ),
), ),
).paddingAll(4), ).paddingAll(4),
),
); );
} }
} }

View File

@@ -91,13 +91,11 @@ class SliverGridDelegateWithComics extends SliverGridDelegate{
} }
SliverGridLayout getDetailedModeLayout(SliverConstraints constraints, double scale){ SliverGridLayout getDetailedModeLayout(SliverConstraints constraints, double scale){
const maxCrossAxisExtent = 650; const minCrossAxisExtent = 360;
final itemHeight = 164 * scale; final itemHeight = 152 * scale;
final width = constraints.crossAxisExtent; final width = constraints.crossAxisExtent;
var crossItems = width ~/ maxCrossAxisExtent; var crossItems = width ~/ minCrossAxisExtent;
if (width % maxCrossAxisExtent != 0) { crossItems = math.max(1, crossItems);
crossItems += 1;
}
return SliverGridRegularTileLayout( return SliverGridRegularTileLayout(
crossAxisCount: crossItems, crossAxisCount: crossItems,
mainAxisStride: itemHeight, mainAxisStride: itemHeight,

View File

@@ -1,16 +1,15 @@
part of "components.dart"; part of "components.dart";
void showDesktopMenu( void showMenuX(BuildContext context, Offset location, List<MenuEntry> entries) {
BuildContext context, Offset location, List<DesktopMenuEntry> entries) { Navigator.of(context).push(_MenuRoute(entries, location));
Navigator.of(context).push(DesktopMenuRoute(entries, location));
} }
class DesktopMenuRoute<T> extends PopupRoute<T> { class _MenuRoute<T> extends PopupRoute<T> {
final List<DesktopMenuEntry> entries; final List<MenuEntry> entries;
final Offset location; final Offset location;
DesktopMenuRoute(this.entries, this.location); _MenuRoute(this.entries, this.location);
@override @override
Color? get barrierColor => Colors.transparent; Color? get barrierColor => Colors.transparent;
@@ -24,7 +23,7 @@ class DesktopMenuRoute<T> extends PopupRoute<T> {
@override @override
Widget buildPage(BuildContext context, Animation<double> animation, Widget buildPage(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation) { Animation<double> secondaryAnimation) {
const width = 196.0; var width = entries.first.icon == null ? 216.0 : 242.0;
final size = MediaQuery.of(context).size; final size = MediaQuery.of(context).size;
var left = location.dx; var left = location.dx;
if (left + width > size.width - 10) { if (left + width > size.width - 10) {
@@ -41,22 +40,33 @@ class DesktopMenuRoute<T> extends PopupRoute<T> {
left: left, left: left,
top: top, top: top,
child: Container( child: Container(
width: width,
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 6),
decoration: BoxDecoration( decoration: BoxDecoration(
color: context.colorScheme.surface,
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.black.withOpacity(0.2), color: context.colorScheme.shadow.withOpacity(0.2),
blurRadius: 4, blurRadius: 8,
offset: const Offset(0, 2), blurStyle: BlurStyle.outer,
), ),
]), ],
),
child: BlurEffect(
borderRadius: BorderRadius.circular(4),
child: Material( child: Material(
color: context.brightness == Brightness.light
? const Color(0xFFFAFAFA).withOpacity(0.72)
: const Color(0xFF090909).withOpacity(0.72),
borderRadius: BorderRadius.circular(4),
child: Container(
width: width,
padding:
const EdgeInsets.symmetric(vertical: 12, horizontal: 6),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: entries.map((e) => buildEntry(e, context)).toList(), children:
entries.map((e) => buildEntry(e, context)).toList(),
),
),
), ),
), ),
), ),
@@ -65,7 +75,7 @@ class DesktopMenuRoute<T> extends PopupRoute<T> {
); );
} }
Widget buildEntry(DesktopMenuEntry entry, BuildContext context) { Widget buildEntry(MenuEntry entry, BuildContext context) {
return InkWell( return InkWell(
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
onTap: () { onTap: () {
@@ -73,24 +83,22 @@ class DesktopMenuRoute<T> extends PopupRoute<T> {
entry.onClick(); entry.onClick();
}, },
child: SizedBox( child: SizedBox(
height: 32, height: App.isMobile ? 42 : 36,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Row( child: Row(
children: [ children: [
const SizedBox(
width: 4,
),
if (entry.icon != null) if (entry.icon != null)
Icon( Icon(
entry.icon, entry.icon,
size: 18, size: 18,
), ),
const SizedBox( const SizedBox(width: 12),
width: 4,
),
Text(entry.text), Text(entry.text),
], ],
), ),
), ),
),
); );
} }
@@ -108,10 +116,10 @@ class DesktopMenuRoute<T> extends PopupRoute<T> {
} }
} }
class DesktopMenuEntry { class MenuEntry {
final String text; final String text;
final IconData? icon; final IconData? icon;
final void Function() onClick; final void Function() onClick;
DesktopMenuEntry({required this.text, this.icon, required this.onClick}); MenuEntry({required this.text, this.icon, required this.onClick});
} }

View File

@@ -129,6 +129,14 @@ class History {
HistoryManager().addHistory(history); HistoryManager().addHistory(history);
return 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 { class HistoryManager with ChangeNotifier {
@@ -190,6 +198,7 @@ class HistoryManager with ChangeNotifier {
void clearHistory() { void clearHistory() {
_db.execute("delete from history;"); _db.execute("delete from history;");
updateCache(); updateCache();
notifyListeners();
} }
void remove(String id, ComicType type) async { void remove(String id, ComicType type) async {

133
lib/pages/history_page.dart Normal file
View File

@@ -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<HistoryPage> createState() => _HistoryPageState();
}
class _HistoryPageState extends State<HistoryPage> {
@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;
}
}

View File

@@ -11,6 +11,7 @@ import 'package:venera/foundation/local.dart';
import 'package:venera/foundation/log.dart'; import 'package:venera/foundation/log.dart';
import 'package:venera/pages/comic_page.dart'; import 'package:venera/pages/comic_page.dart';
import 'package:venera/pages/comic_source_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/pages/search_page.dart';
import 'package:venera/utils/io.dart'; import 'package:venera/utils/io.dart';
import 'package:venera/utils/translations.dart'; import 'package:venera/utils/translations.dart';
@@ -113,7 +114,9 @@ class _HistoryState extends State<_History> {
), ),
child: InkWell( child: InkWell(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
onTap: () {}, onTap: () {
context.to(() => const HistoryPage());
},
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [

View File

@@ -178,26 +178,26 @@ class _ReaderGestureDetectorState extends State<_ReaderGestureDetector> {
} }
void onSecondaryTapUp(Offset location) { void onSecondaryTapUp(Offset location) {
showDesktopMenu( showMenuX(
context, context,
location, location,
[ [
DesktopMenuEntry( MenuEntry(
text: "Settings".tl, text: "Settings".tl,
onClick: () { onClick: () {
context.readerScaffold.openSetting(); context.readerScaffold.openSetting();
}), }),
DesktopMenuEntry( MenuEntry(
text: "Chapters".tl, text: "Chapters".tl,
onClick: () { onClick: () {
context.readerScaffold.openChapterDrawer(); context.readerScaffold.openChapterDrawer();
}), }),
DesktopMenuEntry( MenuEntry(
text: "Fullscreen".tl, text: "Fullscreen".tl,
onClick: () { onClick: () {
context.reader.fullscreen(); context.reader.fullscreen();
}), }),
DesktopMenuEntry( MenuEntry(
text: "Exit".tl, text: "Exit".tl,
onClick: () { onClick: () {
context.pop(); context.pop();

View File

@@ -102,6 +102,7 @@ class _ReaderState extends State<Reader> with _ReaderLocation, _ReaderWindow {
chapter = widget.initialChapter ?? 1; chapter = widget.initialChapter ?? 1;
mode = ReaderMode.fromKey(appdata.settings['readerMode']); mode = ReaderMode.fromKey(appdata.settings['readerMode']);
history = widget.history; history = widget.history;
updateHistory();
super.initState(); super.initState();
} }