mirror of
https://github.com/venera-app/venera.git
synced 2025-09-27 07:47:24 +00:00
history page, comic menu
This commit is contained in:
@@ -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<MenuEntry>? 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<Comic> comics;
|
||||
|
||||
final void Function()? onLastItemBuild;
|
||||
|
||||
final String? Function(Comic)? badgeBuilder;
|
||||
|
||||
final List<MenuEntry> Function(Comic)? menuBuilder;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return StateBuilder<SliverGridComicsController>(
|
||||
@@ -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<Comic> comics;
|
||||
|
||||
final void Function()? onLastItemBuild;
|
||||
|
||||
final String? Function(Comic)? badgeBuilder;
|
||||
|
||||
final List<MenuEntry> 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,
|
||||
),
|
||||
|
@@ -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,
|
||||
|
@@ -172,11 +172,12 @@ class FlyoutContent extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IntrinsicWidth(
|
||||
child: BlurEffect(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Material(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
type: MaterialType.card,
|
||||
elevation: 1,
|
||||
surfaceTintColor: Theme.of(context).colorScheme.surfaceTint,
|
||||
color: context.colorScheme.surface.withOpacity(0.82),
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: minFlyoutWidth,
|
||||
@@ -190,10 +191,7 @@ class FlyoutContent extends StatelessWidget {
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold, fontSize: 16)),
|
||||
if (content != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: content!,
|
||||
),
|
||||
content!,
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
@@ -206,6 +204,7 @@ class FlyoutContent extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
).paddingAll(4),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -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,
|
||||
|
@@ -1,16 +1,15 @@
|
||||
part of "components.dart";
|
||||
|
||||
void showDesktopMenu(
|
||||
BuildContext context, Offset location, List<DesktopMenuEntry> entries) {
|
||||
Navigator.of(context).push(DesktopMenuRoute(entries, location));
|
||||
void showMenuX(BuildContext context, Offset location, List<MenuEntry> entries) {
|
||||
Navigator.of(context).push(_MenuRoute(entries, location));
|
||||
}
|
||||
|
||||
class DesktopMenuRoute<T> extends PopupRoute<T> {
|
||||
final List<DesktopMenuEntry> entries;
|
||||
class _MenuRoute<T> extends PopupRoute<T> {
|
||||
final List<MenuEntry> 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<T> extends PopupRoute<T> {
|
||||
@override
|
||||
Widget buildPage(BuildContext context, Animation<double> animation,
|
||||
Animation<double> 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<T> extends PopupRoute<T> {
|
||||
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: Colors.black.withOpacity(0.2),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
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),
|
||||
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(),
|
||||
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(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
onTap: () {
|
||||
@@ -73,24 +83,22 @@ class DesktopMenuRoute<T> extends PopupRoute<T> {
|
||||
entry.onClick();
|
||||
},
|
||||
child: SizedBox(
|
||||
height: 32,
|
||||
height: App.isMobile ? 42 : 36,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
const SizedBox(
|
||||
width: 4,
|
||||
),
|
||||
if (entry.icon != null)
|
||||
Icon(
|
||||
entry.icon,
|
||||
size: 18,
|
||||
),
|
||||
const SizedBox(
|
||||
width: 4,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(entry.text),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -108,10 +116,10 @@ class DesktopMenuRoute<T> extends PopupRoute<T> {
|
||||
}
|
||||
}
|
||||
|
||||
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});
|
||||
}
|
||||
|
@@ -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 {
|
||||
|
133
lib/pages/history_page.dart
Normal file
133
lib/pages/history_page.dart
Normal 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;
|
||||
}
|
||||
}
|
@@ -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: [
|
||||
|
@@ -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();
|
||||
|
@@ -102,6 +102,7 @@ class _ReaderState extends State<Reader> with _ReaderLocation, _ReaderWindow {
|
||||
chapter = widget.initialChapter ?? 1;
|
||||
mode = ReaderMode.fromKey(appdata.settings['readerMode']);
|
||||
history = widget.history;
|
||||
updateHistory();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user