improve ui

This commit is contained in:
2024-11-13 12:21:57 +08:00
parent 8e99e94620
commit 9bdcba1270
11 changed files with 199 additions and 212 deletions

View File

@@ -156,7 +156,7 @@ class _ButtonState extends State<Button> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var padding = widget.padding ?? var padding = widget.padding ??
const EdgeInsets.symmetric(horizontal: 16, vertical: 4); const EdgeInsets.symmetric(horizontal: 16);
var width = widget.width; var width = widget.width;
if (width != null) { if (width != null) {
width = width - padding.horizontal; width = width - padding.horizontal;
@@ -206,6 +206,7 @@ class _ButtonState extends State<Button> {
padding: padding, padding: padding,
constraints: const BoxConstraints( constraints: const BoxConstraints(
minWidth: 76, minWidth: 76,
minHeight: 32,
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
color: buttonColor, color: buttonColor,

View File

@@ -158,12 +158,16 @@ class ComicTile extends StatelessWidget {
image = FileImage(File(comic.cover.substring(7))); image = FileImage(File(comic.cover.substring(7)));
} else if (comic.sourceKey == 'local') { } else if (comic.sourceKey == 'local') {
var localComic = LocalManager().find(comic.id, ComicType.local); var localComic = LocalManager().find(comic.id, ComicType.local);
if(localComic == null) { if (localComic == null) {
return const SizedBox(); return const SizedBox();
} }
image = FileImage(localComic.coverFile); image = FileImage(localComic.coverFile);
} else { } else {
image = CachedImageProvider(comic.cover, sourceKey: comic.sourceKey); image = CachedImageProvider(
comic.cover,
sourceKey: comic.sourceKey,
cid: comic.id,
);
} }
return AnimatedImage( return AnimatedImage(
image: image, image: image,
@@ -485,12 +489,11 @@ class _ComicDescription extends StatelessWidget {
borderRadius: const BorderRadius.all(Radius.circular(8)), borderRadius: const BorderRadius.all(Radius.circular(8)),
), ),
child: Center( child: Center(
child:Text( child: Text(
"${badge![0].toUpperCase()}${badge!.substring(1).toLowerCase()}", "${badge![0].toUpperCase()}${badge!.substring(1).toLowerCase()}",
style: const TextStyle(fontSize: 12), style: const TextStyle(fontSize: 12),
), ),
) )),
),
], ],
) )
], ],
@@ -574,15 +577,14 @@ class _ReadingHistoryPainter extends CustomPainter {
} }
class SliverGridComics extends StatefulWidget { class SliverGridComics extends StatefulWidget {
const SliverGridComics({ const SliverGridComics(
super.key, {super.key,
required this.comics, required this.comics,
this.onLastItemBuild, this.onLastItemBuild,
this.badgeBuilder, this.badgeBuilder,
this.menuBuilder, this.menuBuilder,
this.onTap, this.onTap,
this.selections this.selections});
});
final List<Comic> comics; final List<Comic> comics;
@@ -681,37 +683,23 @@ class _SliverGridComics extends StatelessWidget {
onLastItemBuild?.call(); onLastItemBuild?.call();
} }
var badge = badgeBuilder?.call(comics[index]); var badge = badgeBuilder?.call(comics[index]);
return Stack( var isSelected =
children: [ selection == null ? false : selection![comics[index]] ?? false;
ComicTile( var comic = ComicTile(
comic: comics[index], comic: comics[index],
badge: badge, badge: badge,
menuOptions: menuBuilder?.call(comics[index]), menuOptions: menuBuilder?.call(comics[index]),
onTap: onTap != null ? () => onTap!(comics[index]) : null, onTap: onTap != null ? () => onTap!(comics[index]) : null,
);
return Container(
decoration: BoxDecoration(
color: isSelected
? Theme.of(context).colorScheme.surfaceContainer
: null,
borderRadius: BorderRadius.circular(12),
), ),
Positioned( margin: const EdgeInsets.all(4),
bottom: 10, child: comic,
right: 8,
child: Visibility(
visible: selection == null ? false : selection![comics[index]] ?? false,
child: Stack(
children: [
Transform.scale(
scale: 0.9,
child: const Icon(
Icons.circle_rounded,
color: Colors.white,
)
),
const Icon(
Icons.check_circle_rounded,
color: Colors.green,
)
],
)
)
)
],
); );
}, },
childCount: comics.length, childCount: comics.length,
@@ -910,7 +898,7 @@ class ComicListState extends State<ComicList> {
try { try {
if (widget.loadPage != null) { if (widget.loadPage != null) {
var res = await widget.loadPage!(page); var res = await widget.loadPage!(page);
if(!mounted) return; if (!mounted) return;
if (res.success) { if (res.success) {
if (res.data.isEmpty) { if (res.data.isEmpty) {
_data[page] = const []; _data[page] = const [];

View File

@@ -8,7 +8,7 @@ import 'cached_image.dart' as image_provider;
class CachedImageProvider class CachedImageProvider
extends BaseImageProvider<image_provider.CachedImageProvider> { extends BaseImageProvider<image_provider.CachedImageProvider> {
/// Image provider for normal image. /// Image provider for normal image.
const CachedImageProvider(this.url, {this.headers, this.sourceKey}); const CachedImageProvider(this.url, {this.headers, this.sourceKey, this.cid});
final String url; final String url;
@@ -16,9 +16,11 @@ class CachedImageProvider
final String? sourceKey; final String? sourceKey;
final String? cid;
@override @override
Future<Uint8List> load(StreamController<ImageChunkEvent> chunkEvents) async { Future<Uint8List> load(StreamController<ImageChunkEvent> chunkEvents) async {
await for (var progress in ImageDownloader.loadThumbnail(url, sourceKey)) { await for (var progress in ImageDownloader.loadThumbnail(url, sourceKey, cid)) {
chunkEvents.add(ImageChunkEvent( chunkEvents.add(ImageChunkEvent(
cumulativeBytesLoaded: progress.currentBytes, cumulativeBytesLoaded: progress.currentBytes,
expectedTotalBytes: progress.totalBytes, expectedTotalBytes: progress.totalBytes,
@@ -36,5 +38,5 @@ class CachedImageProvider
} }
@override @override
String get key => url; String get key => url + (sourceKey ?? "") + (cid ?? "");
} }

View File

@@ -357,6 +357,7 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
} }
LocalManager().completeTask(this); LocalManager().completeTask(this);
stopRecorder();
} }
@override @override
@@ -534,6 +535,9 @@ class _ImageDownloadWrapper {
} }
} }
} catch (e, s) { } catch (e, s) {
if (isCancelled) {
return;
}
Log.error("Download", e.toString(), s); Log.error("Download", e.toString(), s);
retry--; retry--;
if (retry > 0) { if (retry > 0) {

View File

@@ -10,8 +10,9 @@ import 'app_dio.dart';
class ImageDownloader { class ImageDownloader {
static Stream<ImageDownloadProgress> loadThumbnail( static Stream<ImageDownloadProgress> loadThumbnail(
String url, String? sourceKey) async* { String url, String? sourceKey,
final cacheKey = "$url@$sourceKey"; [String? cid]) async* {
final cacheKey = "$url@$sourceKey${cid != null ? '@$cid' : ''}";
final cache = await CacheManager().findCache(cacheKey); final cache = await CacheManager().findCache(cacheKey);
if (cache != null) { if (cache != null) {
@@ -34,6 +35,16 @@ class ImageDownloader {
configs['headers']['user-agent'] = webUA; configs['headers']['user-agent'] = webUA;
} }
if (((configs['url'] as String?) ?? url).startsWith('cover.') &&
sourceKey != null) {
var comicSource = ComicSource.find(sourceKey);
if(comicSource != null) {
var comicInfo = await comicSource.loadComicInfo!(cid!);
yield* loadThumbnail(comicInfo.data.cover, sourceKey);
return;
}
}
var dio = AppDio(BaseOptions( var dio = AppDio(BaseOptions(
headers: Map<String, dynamic>.from(configs['headers']), headers: Map<String, dynamic>.from(configs['headers']),
method: configs['method'] ?? 'GET', method: configs['method'] ?? 'GET',
@@ -171,9 +182,8 @@ class ImageDownloader {
} }
configs = newConfig; configs = newConfig;
retryLimit--; retryLimit--;
} } finally {
finally { if (onLoadFailed != null) {
if(onLoadFailed != null) {
(configs['onLoadFailed'] as JSInvokable).free(); (configs['onLoadFailed'] as JSInvokable).free();
} }
} }

View File

@@ -27,8 +27,10 @@ class _DownloadingPageState extends State<DownloadingPage> {
} }
void update() { void update() {
if(mounted) {
setState(() {}); setState(() {});
} }
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@@ -258,6 +258,7 @@ class _HistoryState extends State<_History> {
ImageProvider imageProvider = CachedImageProvider( ImageProvider imageProvider = CachedImageProvider(
cover, cover,
sourceKey: history[index].type.comicSource?.key, sourceKey: history[index].type.comicSource?.key,
cid: history[index].id,
); );
if (!cover.isURL) { if (!cover.isURL) {
var localComic = LocalManager().find( var localComic = LocalManager().find(
@@ -567,7 +568,7 @@ class _ImportComicsWidgetState extends State<_ImportComicsWidget> {
onPressed: () { onPressed: () {
showDialog( showDialog(
context: context, context: context,
barrierColor: Colors.transparent, barrierColor: Colors.black.withOpacity(0.2),
builder: (context) { builder: (context) {
var help = ''; var help = '';
help += help +=

View File

@@ -100,7 +100,7 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
actions: [ actions: [
FilledButton( FilledButton(
onPressed: () { onPressed: () {
appdata.implicitData["local_sort"] =sortType.value; appdata.implicitData["local_sort"] = sortType.value;
appdata.writeImplicitData(); appdata.writeImplicitData();
Navigator.pop(context); Navigator.pop(context);
update(); update();
@@ -116,19 +116,18 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final double screenWidth = MediaQuery.of(context).size.width; void selectAll() {
final bool isScreenSmall = screenWidth < 500.0;
void selectAll(){
setState(() { setState(() {
selectedComics = comics.asMap().map((k, v) => MapEntry(v, true)); selectedComics = comics.asMap().map((k, v) => MapEntry(v, true));
}); });
} }
void deSelect() { void deSelect() {
setState(() { setState(() {
selectedComics.clear(); selectedComics.clear();
}); });
} }
void invertSelection() { void invertSelection() {
setState(() { setState(() {
comics.asMap().forEach((k, v) { comics.asMap().forEach((k, v) {
@@ -137,86 +136,46 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
selectedComics.removeWhere((k, v) => !v); selectedComics.removeWhere((k, v) => !v);
}); });
} }
void selectRange() { void selectRange() {
setState(() { setState(() {
List<int> l = []; List<int> l = [];
selectedComics.forEach((k, v) { selectedComics.forEach((k, v) {
l.add(comics.indexOf(k as LocalComic)); l.add(comics.indexOf(k as LocalComic));
}); });
if(l.isEmpty) { if (l.isEmpty) {
return; return;
} }
l.sort(); l.sort();
int start = l.first; int start = l.first;
int end = l.last; int end = l.last;
selectedComics.clear(); selectedComics.clear();
selectedComics.addEntries( selectedComics.addEntries(List.generate(end - start + 1, (i) {
List.generate(end - start + 1, (i) {
return MapEntry(comics[start + i], true); return MapEntry(comics[start + i], true);
}) }));
);
}); });
} }
List<Widget> selectActions = []; List<Widget> selectActions = [
if(isScreenSmall) {
selectActions.add(
IconButton( IconButton(
onPressed: () { icon: const Icon(Icons.select_all),
showMenu(
context: context,
position: RelativeRect.fromLTRB(screenWidth, App.isMobile ? 64 : 96, 0, 0),
items: <PopupMenuEntry>[
PopupMenuItem(
onTap: selectAll,
child: Text("Select All".tl),
),
PopupMenuItem(
onTap: deSelect,
child: Text("Deselect".tl),
),
PopupMenuItem(
onTap: invertSelection,
child: Text("Invert Selection".tl),
),
PopupMenuItem(
onTap: selectRange,
child: Text("Select in range".tl),
)
]
);
},
icon: const Icon(
Icons.list
))
);
}else {
selectActions = [
IconButton(
icon: const Icon(Icons.check_box_rounded),
tooltip: "Select All".tl, tooltip: "Select All".tl,
onPressed: selectAll onPressed: selectAll),
),
IconButton( IconButton(
icon: const Icon(Icons.check_box_outline_blank_outlined), icon: const Icon(Icons.deselect),
tooltip: "Deselect".tl, tooltip: "Deselect".tl,
onPressed: deSelect onPressed: deSelect),
),
IconButton( IconButton(
icon: const Icon(Icons.check_box_outlined), icon: const Icon(Icons.flip),
tooltip: "Invert Selection".tl, tooltip: "Invert Selection".tl,
onPressed: invertSelection onPressed: invertSelection),
),
IconButton( IconButton(
icon: const Icon(Icons.indeterminate_check_box_rounded), icon: const Icon(Icons.border_horizontal_outlined),
tooltip: "Select in range".tl, tooltip: "Select in range".tl,
onPressed: selectRange onPressed: selectRange),
),
]; ];
}
return Scaffold( var body = Scaffold(
body: SmoothCustomScrollView( body: SmoothCustomScrollView(
slivers: [ slivers: [
if (!searchMode && !multiSelectMode) if (!searchMode && !multiSelectMode)
@@ -267,12 +226,10 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
) )
else if (multiSelectMode) else if (multiSelectMode)
SliverAppbar( SliverAppbar(
title: Text("Selected @c comics".tlParams({"c": selectedComics.length})), leading: Tooltip(
actions: [ message: "Cancel".tl,
...selectActions, child: IconButton(
IconButton(
icon: const Icon(Icons.close), icon: const Icon(Icons.close),
tooltip: "Exit Multi-Select".tl,
onPressed: () { onPressed: () {
setState(() { setState(() {
multiSelectMode = false; multiSelectMode = false;
@@ -280,11 +237,26 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
}); });
}, },
), ),
),
], title: Text(
"Selected @c comics".tlParams({"c": selectedComics.length})),
actions: selectActions,
) )
else if (searchMode) else if (searchMode)
SliverAppbar( SliverAppbar(
leading: Tooltip(
message: "Cancel".tl,
child: IconButton(
icon: const Icon(Icons.close),
onPressed: () {
setState(() {
searchMode = false;
keyword = "";
update();
});
},
),
),
title: TextField( title: TextField(
autofocus: true, autofocus: true,
decoration: InputDecoration( decoration: InputDecoration(
@@ -296,18 +268,6 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
update(); update();
}, },
), ),
actions: [
IconButton(
icon: const Icon(Icons.close),
onPressed: () {
setState(() {
searchMode = false;
keyword = "";
update();
});
},
),
],
), ),
SliverGridComics( SliverGridComics(
comics: comics, comics: comics,
@@ -335,50 +295,51 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
context: context, context: context,
builder: (context) { builder: (context) {
bool removeComicFile = true; bool removeComicFile = true;
return StatefulBuilder( return StatefulBuilder(builder: (context, state) {
builder: (context, state) {
return ContentDialog( return ContentDialog(
title: "Delete".tl, title: "Delete".tl,
content: Column( content: Column(
children: [ children: [
Text("Delete selected comics?".tl).paddingVertical(8), Text("Delete selected comics?".tl)
.paddingVertical(8),
Transform.scale( Transform.scale(
scale: 0.9, scale: 0.9,
child: CheckboxListTile( child: CheckboxListTile(
title: Text("Also remove files on disk".tl), title: Text(
"Also remove files on disk".tl),
value: removeComicFile, value: removeComicFile,
onChanged: (v) { onChanged: (v) {
state(() { state(() {
removeComicFile = !removeComicFile; removeComicFile =
!removeComicFile;
}); });
} })),
)
),
], ],
).paddingHorizontal(16).paddingVertical(8), ).paddingHorizontal(16).paddingVertical(8),
actions: [ actions: [
FilledButton( FilledButton(
onPressed: () { onPressed: () {
context.pop(); context.pop();
if(multiSelectMode) { if (multiSelectMode) {
for (var comic in selectedComics.keys) { for (var comic in selectedComics.keys) {
LocalManager().deleteComic(comic as LocalComic, removeComicFile); LocalManager().deleteComic(
comic as LocalComic,
removeComicFile);
} }
setState(() { setState(() {
selectedComics.clear(); selectedComics.clear();
}); });
} else { } else {
LocalManager().deleteComic(c as LocalComic, removeComicFile); LocalManager().deleteComic(
c as LocalComic, removeComicFile);
} }
}, },
child: Text("Confirm".tl), child: Text("Confirm".tl),
), ),
], ],
); );
} });
); });
}
);
}), }),
MenuEntry( MenuEntry(
icon: Icons.outbox_outlined, icon: Icons.outbox_outlined,
@@ -414,5 +375,24 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
], ],
), ),
); );
return PopScope(
canPop: !multiSelectMode && !searchMode,
onPopInvokedWithResult: (didPop, result) {
if(multiSelectMode) {
setState(() {
multiSelectMode = false;
selectedComics.clear();
});
} else if(searchMode) {
setState(() {
searchMode = false;
keyword = "";
update();
});
}
},
child: body,
);
} }
} }

View File

@@ -16,12 +16,12 @@ class _AboutSettingsState extends State<AboutSettings> {
slivers: [ slivers: [
SliverAppbar(title: Text("About".tl)), SliverAppbar(title: Text("About".tl)),
SizedBox( SizedBox(
height: 136, height: 112,
width: double.infinity, width: double.infinity,
child: Center( child: Center(
child: Container( child: Container(
width: 136, width: 112,
height: 136, height: 112,
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(136), borderRadius: BorderRadius.circular(136),
), ),

View File

@@ -10,7 +10,6 @@ import 'package:venera/foundation/app.dart';
import 'package:venera/foundation/appdata.dart'; import 'package:venera/foundation/appdata.dart';
import 'package:venera/foundation/cache_manager.dart'; import 'package:venera/foundation/cache_manager.dart';
import 'package:venera/foundation/comic_source/comic_source.dart'; import 'package:venera/foundation/comic_source/comic_source.dart';
import 'package:venera/foundation/consts.dart';
import 'package:venera/foundation/favorites.dart'; import 'package:venera/foundation/favorites.dart';
import 'package:venera/foundation/local.dart'; import 'package:venera/foundation/local.dart';
import 'package:venera/foundation/log.dart'; import 'package:venera/foundation/log.dart';
@@ -44,7 +43,7 @@ class _SettingsPageState extends State<SettingsPage> implements PopEntry {
ColorScheme get colors => Theme.of(context).colorScheme; ColorScheme get colors => Theme.of(context).colorScheme;
bool get enableTwoViews => context.width > changePoint; bool get enableTwoViews => context.width > 720;
final categories = <String>[ final categories = <String>[
"Explore", "Explore",

View File

@@ -18,11 +18,11 @@ export 'package:flutter_inappwebview/flutter_inappwebview.dart'
extension WebviewExtension on InAppWebViewController { extension WebviewExtension on InAppWebViewController {
Future<List<io.Cookie>?> getCookies(String url) async { Future<List<io.Cookie>?> getCookies(String url) async {
if(url.contains("https://")){ if (url.contains("https://")) {
url.replaceAll("https://", ""); url.replaceAll("https://", "");
} }
if(url[url.length-1] == '/'){ if (url[url.length - 1] == '/') {
url = url.substring(0, url.length-1); url = url.substring(0, url.length - 1);
} }
CookieManager cookieManager = CookieManager.instance(); CookieManager cookieManager = CookieManager.instance();
final cookies = await cookieManager.getCookies(url: WebUri(url)); final cookies = await cookieManager.getCookies(url: WebUri(url));
@@ -89,29 +89,29 @@ class _AppWebviewState extends State<AppWebview> {
child: IconButton( child: IconButton(
icon: const Icon(Icons.more_horiz), icon: const Icon(Icons.more_horiz),
onPressed: () { onPressed: () {
showMenu( showMenuX(
context: context, context,
position: RelativeRect.fromLTRB( Offset(context.width, context.padding.top),
MediaQuery.of(context).size.width, [
0, MenuEntry(
MediaQuery.of(context).size.width, icon: Icons.open_in_browser,
0), text: "Open in browser".tl,
items: [ onClick: () async =>
PopupMenuItem(
child: Text("Open in browser".tl),
onTap: () async =>
launchUrlString((await controller?.getUrl())!.toString()), launchUrlString((await controller?.getUrl())!.toString()),
), ),
PopupMenuItem( MenuEntry(
child: Text("Copy link".tl), icon: Icons.copy,
onTap: () async => Clipboard.setData(ClipboardData( text: "Copy link".tl,
onClick: () async => Clipboard.setData(ClipboardData(
text: (await controller?.getUrl())!.toString())), text: (await controller?.getUrl())!.toString())),
), ),
PopupMenuItem( MenuEntry(
child: Text("Reload".tl), icon: Icons.refresh,
onTap: () => controller?.reload(), text: "Reload".tl,
onClick: () => controller?.reload(),
), ),
]); ],
);
}, },
), ),
) )