mirror of
https://github.com/venera-app/venera.git
synced 2025-12-16 07:01:16 +00:00
Merge pull request #534 from lings03/v1.5.1-dev
favorite page && cover page
This commit is contained in:
@@ -83,7 +83,10 @@
|
|||||||
"New Folder": "新建文件夹",
|
"New Folder": "新建文件夹",
|
||||||
"Reading": "阅读中",
|
"Reading": "阅读中",
|
||||||
"Appearance": "外观",
|
"Appearance": "外观",
|
||||||
|
"Network Favorites": "网络收藏",
|
||||||
"Local Favorites": "本地收藏",
|
"Local Favorites": "本地收藏",
|
||||||
|
"Show local favorites before network favorites": "在网络收藏之前显示本地收藏",
|
||||||
|
"Auto close favorite panel after operation": "自动关闭收藏面板",
|
||||||
"APP": "应用",
|
"APP": "应用",
|
||||||
"About": "关于",
|
"About": "关于",
|
||||||
"Display mode of comic tile": "漫画缩略图的显示模式",
|
"Display mode of comic tile": "漫画缩略图的显示模式",
|
||||||
@@ -497,7 +500,10 @@
|
|||||||
"New Folder": "建立資料夾",
|
"New Folder": "建立資料夾",
|
||||||
"Reading": "閱讀中",
|
"Reading": "閱讀中",
|
||||||
"Appearance": "外觀",
|
"Appearance": "外觀",
|
||||||
|
"Network Favorites": "網路收藏",
|
||||||
"Local Favorites": "本機收藏",
|
"Local Favorites": "本機收藏",
|
||||||
|
"Show local favorites before network favorites": "在網路收藏之前顯示本機收藏",
|
||||||
|
"Auto close favorite panel after operation": "自動關閉收藏面板",
|
||||||
"APP": "應用",
|
"APP": "應用",
|
||||||
"About": "關於",
|
"About": "關於",
|
||||||
"Display mode of comic tile": "漫畫縮圖的顯示模式",
|
"Display mode of comic tile": "漫畫縮圖的顯示模式",
|
||||||
|
|||||||
@@ -192,6 +192,8 @@ class Settings with ChangeNotifier {
|
|||||||
'comicSpecificSettings': <String, Map<String, dynamic>>{},
|
'comicSpecificSettings': <String, Map<String, dynamic>>{},
|
||||||
'ignoreBadCertificate': false,
|
'ignoreBadCertificate': false,
|
||||||
'readerScrollSpeed': 1.0, // 0.5 - 3.0
|
'readerScrollSpeed': 1.0, // 0.5 - 3.0
|
||||||
|
'localFavoritesFirst': true,
|
||||||
|
'autoCloseFavoritePanel': false,
|
||||||
};
|
};
|
||||||
|
|
||||||
operator [](String key) {
|
operator [](String key) {
|
||||||
|
|||||||
@@ -56,8 +56,12 @@ abstract mixin class _ComicPageActions {
|
|||||||
type: comic.comicType,
|
type: comic.comicType,
|
||||||
isFavorite: isFavorite,
|
isFavorite: isFavorite,
|
||||||
onFavorite: (local, network) {
|
onFavorite: (local, network) {
|
||||||
isFavorite = network ?? isFavorite;
|
if (network != null) {
|
||||||
isAddToLocalFav = local ?? isAddToLocalFav;
|
isFavorite = network;
|
||||||
|
}
|
||||||
|
if (local != null) {
|
||||||
|
isAddToLocalFav = local;
|
||||||
|
}
|
||||||
update();
|
update();
|
||||||
},
|
},
|
||||||
favoriteItem: _toFavoriteItem(),
|
favoriteItem: _toFavoriteItem(),
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'dart:collection';
|
import 'dart:collection';
|
||||||
|
import 'dart:ui';
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:photo_view/photo_view.dart';
|
||||||
import 'package:shimmer_animation/shimmer_animation.dart';
|
import 'package:shimmer_animation/shimmer_animation.dart';
|
||||||
import 'package:sliver_tools/sliver_tools.dart';
|
import 'package:sliver_tools/sliver_tools.dart';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
@@ -17,10 +20,12 @@ import 'package:venera/foundation/image_provider/cached_image.dart';
|
|||||||
import 'package:venera/foundation/local.dart';
|
import 'package:venera/foundation/local.dart';
|
||||||
import 'package:venera/foundation/res.dart';
|
import 'package:venera/foundation/res.dart';
|
||||||
import 'package:venera/network/download.dart';
|
import 'package:venera/network/download.dart';
|
||||||
|
import 'package:venera/network/cache.dart';
|
||||||
import 'package:venera/pages/favorites/favorites_page.dart';
|
import 'package:venera/pages/favorites/favorites_page.dart';
|
||||||
import 'package:venera/pages/reader/reader.dart';
|
import 'package:venera/pages/reader/reader.dart';
|
||||||
import 'package:venera/utils/app_links.dart';
|
import 'package:venera/utils/app_links.dart';
|
||||||
import 'package:venera/utils/ext.dart';
|
import 'package:venera/utils/ext.dart';
|
||||||
|
import 'package:venera/utils/file_type.dart';
|
||||||
import 'package:venera/utils/io.dart';
|
import 'package:venera/utils/io.dart';
|
||||||
import 'package:venera/utils/tags_translation.dart';
|
import 'package:venera/utils/tags_translation.dart';
|
||||||
import 'package:venera/utils/translations.dart';
|
import 'package:venera/utils/translations.dart';
|
||||||
@@ -38,6 +43,8 @@ part 'comments_preview.dart';
|
|||||||
|
|
||||||
part 'actions.dart';
|
part 'actions.dart';
|
||||||
|
|
||||||
|
part 'cover_viewer.dart';
|
||||||
|
|
||||||
class ComicPage extends StatefulWidget {
|
class ComicPage extends StatefulWidget {
|
||||||
const ComicPage({
|
const ComicPage({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -256,6 +263,18 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
Future<void> onDataLoaded() async {
|
Future<void> onDataLoaded() async {
|
||||||
isLiked = comic.isLiked ?? false;
|
isLiked = comic.isLiked ?? false;
|
||||||
isFavorite = comic.isFavorite ?? false;
|
isFavorite = comic.isFavorite ?? false;
|
||||||
|
// For sources with multi-folder favorites, prefer querying folders to get accurate favorite status
|
||||||
|
// Some sources may not set isFavorite reliably when multi-folder is enabled
|
||||||
|
if (comicSource.favoriteData?.loadFolders != null && comicSource.isLogged) {
|
||||||
|
var res = await comicSource.favoriteData!.loadFolders!(comic.id);
|
||||||
|
if (!res.error) {
|
||||||
|
if (res.subData is List) {
|
||||||
|
var list = List<String>.from(res.subData);
|
||||||
|
isFavorite = list.isNotEmpty;
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if (comic.chapters == null) {
|
if (comic.chapters == null) {
|
||||||
isDownloaded = LocalManager().isDownloaded(comic.id, comic.comicType, 0);
|
isDownloaded = LocalManager().isDownloaded(comic.id, comic.comicType, 0);
|
||||||
}
|
}
|
||||||
@@ -283,7 +302,10 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
Hero(
|
GestureDetector(
|
||||||
|
onTap: () => _viewCover(context),
|
||||||
|
onLongPress: () => _saveCover(context),
|
||||||
|
child: Hero(
|
||||||
tag: "cover${widget.heroID}",
|
tag: "cover${widget.heroID}",
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -311,6 +333,7 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -710,6 +733,54 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
}
|
}
|
||||||
return _CommentsPart(comments: comic.comments!, showMore: showComments);
|
return _CommentsPart(comments: comic.comments!, showMore: showComments);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _viewCover(BuildContext context) {
|
||||||
|
final imageProvider = CachedImageProvider(
|
||||||
|
widget.cover ?? comic.cover,
|
||||||
|
sourceKey: comic.sourceKey,
|
||||||
|
cid: comic.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
context.to(
|
||||||
|
() => _CoverViewer(
|
||||||
|
imageProvider: imageProvider,
|
||||||
|
title: comic.title,
|
||||||
|
heroTag: "cover${widget.heroID}",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _saveCover(BuildContext context) async {
|
||||||
|
try {
|
||||||
|
final imageProvider = CachedImageProvider(
|
||||||
|
widget.cover ?? comic.cover,
|
||||||
|
sourceKey: comic.sourceKey,
|
||||||
|
cid: comic.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
final imageStream = imageProvider.resolve(const ImageConfiguration());
|
||||||
|
final completer = Completer<Uint8List>();
|
||||||
|
|
||||||
|
imageStream.addListener(
|
||||||
|
ImageStreamListener((ImageInfo info, bool _) async {
|
||||||
|
final byteData = await info.image.toByteData(
|
||||||
|
format: ImageByteFormat.png,
|
||||||
|
);
|
||||||
|
if (byteData != null) {
|
||||||
|
completer.complete(byteData.buffer.asUint8List());
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
final data = await completer.future;
|
||||||
|
final fileType = detectFileType(data);
|
||||||
|
await saveFile(filename: "cover${fileType.ext}", data: data);
|
||||||
|
} catch (e) {
|
||||||
|
if (context.mounted) {
|
||||||
|
context.showMessage(message: "Error".tl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ActionButton extends StatelessWidget {
|
class _ActionButton extends StatelessWidget {
|
||||||
|
|||||||
140
lib/pages/comic_details_page/cover_viewer.dart
Normal file
140
lib/pages/comic_details_page/cover_viewer.dart
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
part of 'comic_page.dart';
|
||||||
|
|
||||||
|
class _CoverViewer extends StatefulWidget {
|
||||||
|
const _CoverViewer({
|
||||||
|
required this.imageProvider,
|
||||||
|
required this.title,
|
||||||
|
required this.heroTag,
|
||||||
|
});
|
||||||
|
|
||||||
|
final ImageProvider imageProvider;
|
||||||
|
final String title;
|
||||||
|
final String heroTag;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_CoverViewer> createState() => _CoverViewerState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CoverViewerState extends State<_CoverViewer> {
|
||||||
|
bool isAppBarShow = true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return PopScope(
|
||||||
|
canPop: true,
|
||||||
|
child: Scaffold(
|
||||||
|
backgroundColor: context.colorScheme.surface,
|
||||||
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
Positioned.fill(
|
||||||
|
child: PhotoView(
|
||||||
|
imageProvider: widget.imageProvider,
|
||||||
|
minScale: PhotoViewComputedScale.contained * 1.0,
|
||||||
|
maxScale: PhotoViewComputedScale.covered * 3.0,
|
||||||
|
backgroundDecoration: BoxDecoration(
|
||||||
|
color: context.colorScheme.surface,
|
||||||
|
),
|
||||||
|
loadingBuilder: (context, event) => Center(
|
||||||
|
child: SizedBox(
|
||||||
|
width: 24.0,
|
||||||
|
height: 24.0,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
value: event == null || event.expectedTotalBytes == null
|
||||||
|
? null
|
||||||
|
: event.cumulativeBytesLoaded /
|
||||||
|
event.expectedTotalBytes!,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTapUp: (context, details, controllerValue) {
|
||||||
|
setState(() {
|
||||||
|
isAppBarShow = !isAppBarShow;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
heroAttributes: PhotoViewHeroAttributes(tag: widget.heroTag),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
AnimatedPositioned(
|
||||||
|
top: isAppBarShow ? 0 : -(context.padding.top + 52),
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
duration: const Duration(milliseconds: 180),
|
||||||
|
child: _buildAppBar(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildAppBar() {
|
||||||
|
return Material(
|
||||||
|
color: context.colorScheme.surface.toOpacity(0.72),
|
||||||
|
child: BlurEffect(
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border(
|
||||||
|
bottom: BorderSide(
|
||||||
|
color: context.colorScheme.outlineVariant,
|
||||||
|
width: 0.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
height: 52,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.close),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
widget.title,
|
||||||
|
style: const TextStyle(fontSize: 18),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.save_alt),
|
||||||
|
onPressed: _saveCover,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
).paddingTop(context.padding.top),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _saveCover() async {
|
||||||
|
try {
|
||||||
|
final imageStream = widget.imageProvider.resolve(
|
||||||
|
const ImageConfiguration(),
|
||||||
|
);
|
||||||
|
final completer = Completer<Uint8List>();
|
||||||
|
|
||||||
|
imageStream.addListener(
|
||||||
|
ImageStreamListener((ImageInfo info, bool _) async {
|
||||||
|
final byteData = await info.image.toByteData(
|
||||||
|
format: ImageByteFormat.png,
|
||||||
|
);
|
||||||
|
if (byteData != null) {
|
||||||
|
completer.complete(byteData.buffer.asUint8List());
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
final data = await completer.future;
|
||||||
|
final fileType = detectFileType(data);
|
||||||
|
await saveFile(filename: "cover_${widget.title}${fileType.ext}", data: data);
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
context.showMessage(message: "Error".tl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,198 +33,122 @@ class _FavoritePanelState extends State<_FavoritePanel>
|
|||||||
with SingleTickerProviderStateMixin {
|
with SingleTickerProviderStateMixin {
|
||||||
late ComicSource comicSource;
|
late ComicSource comicSource;
|
||||||
|
|
||||||
late TabController tabController;
|
|
||||||
|
|
||||||
late bool hasNetwork;
|
late bool hasNetwork;
|
||||||
|
|
||||||
|
late List<String> localFolders;
|
||||||
|
|
||||||
|
late List<String> added;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
comicSource = widget.type.comicSource!;
|
comicSource = widget.type.comicSource!;
|
||||||
localFolders = LocalFavoritesManager().folderNames;
|
localFolders = LocalFavoritesManager().folderNames;
|
||||||
added = LocalFavoritesManager().find(widget.cid, widget.type);
|
added = LocalFavoritesManager().find(widget.cid, widget.type);
|
||||||
hasNetwork = comicSource.favoriteData != null && comicSource.isLogged;
|
hasNetwork = comicSource.favoriteData != null && comicSource.isLogged;
|
||||||
var initIndex = 0;
|
|
||||||
if (appdata.implicitData['favoritePanelIndex'] is int) {
|
|
||||||
initIndex = appdata.implicitData['favoritePanelIndex'];
|
|
||||||
}
|
|
||||||
initIndex = initIndex.clamp(0, hasNetwork ? 1 : 0);
|
|
||||||
tabController = TabController(
|
|
||||||
initialIndex: initIndex,
|
|
||||||
length: hasNetwork ? 2 : 1,
|
|
||||||
vsync: this,
|
|
||||||
);
|
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
var currentIndex = tabController.index;
|
|
||||||
appdata.implicitData['favoritePanelIndex'] = currentIndex;
|
|
||||||
appdata.writeImplicitData();
|
|
||||||
tabController.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: Appbar(
|
appBar: Appbar(title: Text("Favorite".tl)),
|
||||||
title: Text("Favorite".tl),
|
body: _FavoriteList(
|
||||||
),
|
|
||||||
body: Column(
|
|
||||||
children: [
|
|
||||||
TabBar(
|
|
||||||
controller: tabController,
|
|
||||||
tabs: [
|
|
||||||
Tab(text: "Local".tl),
|
|
||||||
if (hasNetwork) Tab(text: "Network".tl),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: TabBarView(
|
|
||||||
controller: tabController,
|
|
||||||
children: [
|
|
||||||
buildLocal(),
|
|
||||||
if (hasNetwork) buildNetwork(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
late List<String> localFolders;
|
|
||||||
|
|
||||||
late List<String> added;
|
|
||||||
|
|
||||||
var selectedLocalFolders = <String>{};
|
|
||||||
|
|
||||||
Widget buildLocal() {
|
|
||||||
var isRemove = selectedLocalFolders.isNotEmpty &&
|
|
||||||
added.contains(selectedLocalFolders.first);
|
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: ListView.builder(
|
|
||||||
itemCount: localFolders.length + 1,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
if (index == localFolders.length) {
|
|
||||||
return SizedBox(
|
|
||||||
height: 36,
|
|
||||||
child: Center(
|
|
||||||
child: TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
newFolder().then((v) {
|
|
||||||
setState(() {
|
|
||||||
localFolders = LocalFavoritesManager().folderNames;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
const Icon(Icons.add, size: 20),
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
Text("New Folder".tl)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
var folder = localFolders[index];
|
|
||||||
var disabled = false;
|
|
||||||
if (selectedLocalFolders.isNotEmpty) {
|
|
||||||
if (added.contains(folder) &&
|
|
||||||
!added.contains(selectedLocalFolders.first)) {
|
|
||||||
disabled = true;
|
|
||||||
} else if (!added.contains(folder) &&
|
|
||||||
added.contains(selectedLocalFolders.first)) {
|
|
||||||
disabled = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return CheckboxListTile(
|
|
||||||
title: Row(
|
|
||||||
children: [
|
|
||||||
Text(folder),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
if (added.contains(folder))
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 8,
|
|
||||||
vertical: 4,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: context.colorScheme.primaryContainer,
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: Text("Added".tl, style: ts.s12),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
value: selectedLocalFolders.contains(folder),
|
|
||||||
onChanged: disabled
|
|
||||||
? null
|
|
||||||
: (v) {
|
|
||||||
setState(() {
|
|
||||||
if (v!) {
|
|
||||||
selectedLocalFolders.add(folder);
|
|
||||||
} else {
|
|
||||||
selectedLocalFolders.remove(folder);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Center(
|
|
||||||
child: FilledButton(
|
|
||||||
onPressed: () {
|
|
||||||
if (selectedLocalFolders.isEmpty) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (isRemove) {
|
|
||||||
for (var folder in selectedLocalFolders) {
|
|
||||||
LocalFavoritesManager()
|
|
||||||
.deleteComicWithId(folder, widget.cid, widget.type);
|
|
||||||
}
|
|
||||||
widget.onFavorite(false, null);
|
|
||||||
} else {
|
|
||||||
for (var folder in selectedLocalFolders) {
|
|
||||||
LocalFavoritesManager().addComic(
|
|
||||||
folder,
|
|
||||||
widget.favoriteItem,
|
|
||||||
null,
|
|
||||||
widget.updateTime,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
widget.onFavorite(true, null);
|
|
||||||
}
|
|
||||||
context.pop();
|
|
||||||
},
|
|
||||||
child: isRemove ? Text("Remove".tl) : Text("Add".tl),
|
|
||||||
).paddingVertical(8),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget buildNetwork() {
|
|
||||||
return _NetworkFavorites(
|
|
||||||
cid: widget.cid,
|
cid: widget.cid,
|
||||||
|
type: widget.type,
|
||||||
|
isFavorite: widget.isFavorite,
|
||||||
|
onFavorite: widget.onFavorite,
|
||||||
|
favoriteItem: widget.favoriteItem,
|
||||||
|
updateTime: widget.updateTime,
|
||||||
comicSource: comicSource,
|
comicSource: comicSource,
|
||||||
|
hasNetwork: hasNetwork,
|
||||||
|
localFolders: localFolders,
|
||||||
|
added: added,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FavoriteList extends StatefulWidget {
|
||||||
|
const _FavoriteList({
|
||||||
|
required this.cid,
|
||||||
|
required this.type,
|
||||||
|
required this.isFavorite,
|
||||||
|
required this.onFavorite,
|
||||||
|
required this.favoriteItem,
|
||||||
|
this.updateTime,
|
||||||
|
required this.comicSource,
|
||||||
|
required this.hasNetwork,
|
||||||
|
required this.localFolders,
|
||||||
|
required this.added,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String cid;
|
||||||
|
final ComicType type;
|
||||||
|
final bool? isFavorite;
|
||||||
|
final void Function(bool?, bool?) onFavorite;
|
||||||
|
final FavoriteItem favoriteItem;
|
||||||
|
final String? updateTime;
|
||||||
|
final ComicSource comicSource;
|
||||||
|
final bool hasNetwork;
|
||||||
|
final List<String> localFolders;
|
||||||
|
final List<String> added;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_FavoriteList> createState() => _FavoriteListState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FavoriteListState extends State<_FavoriteList> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final localFavoritesFirst = appdata.settings['localFavoritesFirst'] ?? true;
|
||||||
|
|
||||||
|
final localSection = _LocalSection(
|
||||||
|
cid: widget.cid,
|
||||||
|
type: widget.type,
|
||||||
|
favoriteItem: widget.favoriteItem,
|
||||||
|
updateTime: widget.updateTime,
|
||||||
|
localFolders: widget.localFolders,
|
||||||
|
added: widget.added,
|
||||||
|
onFavorite: (local) {
|
||||||
|
widget.onFavorite(local, null);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
final networkSection = widget.hasNetwork
|
||||||
|
? _NetworkSection(
|
||||||
|
cid: widget.cid,
|
||||||
|
comicSource: widget.comicSource,
|
||||||
isFavorite: widget.isFavorite,
|
isFavorite: widget.isFavorite,
|
||||||
onFavorite: (network) {
|
onFavorite: (network) {
|
||||||
widget.onFavorite(null, network);
|
widget.onFavorite(null, network);
|
||||||
},
|
},
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
final divider = widget.hasNetwork
|
||||||
|
? Container(
|
||||||
|
height: 1,
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
color: context.colorScheme.outlineVariant.withValues(alpha: 0.3),
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return ListView(
|
||||||
|
children: [
|
||||||
|
if (localFavoritesFirst) ...[
|
||||||
|
localSection,
|
||||||
|
if (widget.hasNetwork) ...[divider!, networkSection!],
|
||||||
|
] else ...[
|
||||||
|
if (widget.hasNetwork) ...[networkSection!, divider!],
|
||||||
|
localSection,
|
||||||
|
],
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _NetworkFavorites extends StatefulWidget {
|
class _NetworkSection extends StatefulWidget {
|
||||||
const _NetworkFavorites({
|
const _NetworkSection({
|
||||||
required this.cid,
|
required this.cid,
|
||||||
required this.comicSource,
|
required this.comicSource,
|
||||||
required this.isFavorite,
|
required this.isFavorite,
|
||||||
@@ -232,82 +156,55 @@ class _NetworkFavorites extends StatefulWidget {
|
|||||||
});
|
});
|
||||||
|
|
||||||
final String cid;
|
final String cid;
|
||||||
|
|
||||||
final ComicSource comicSource;
|
final ComicSource comicSource;
|
||||||
|
|
||||||
final bool? isFavorite;
|
final bool? isFavorite;
|
||||||
|
|
||||||
final void Function(bool) onFavorite;
|
final void Function(bool) onFavorite;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_NetworkFavorites> createState() => _NetworkFavoritesState();
|
State<_NetworkSection> createState() => _NetworkSectionState();
|
||||||
}
|
|
||||||
|
|
||||||
class _NetworkFavoritesState extends State<_NetworkFavorites> {
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
bool isMultiFolder = widget.comicSource.favoriteData!.loadFolders != null;
|
|
||||||
|
|
||||||
return isMultiFolder ? buildMultiFolder() : buildSingleFolder();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _NetworkSectionState extends State<_NetworkSection> {
|
||||||
bool isLoading = false;
|
bool isLoading = false;
|
||||||
|
|
||||||
Widget buildSingleFolder() {
|
|
||||||
var isFavorite = widget.isFavorite ?? false;
|
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Center(
|
|
||||||
child: Text(isFavorite ? "Added to favorites".tl : "Not added".tl),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Center(
|
|
||||||
child: Button.filled(
|
|
||||||
isLoading: isLoading,
|
|
||||||
onPressed: () async {
|
|
||||||
setState(() {
|
|
||||||
isLoading = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
var res = await widget.comicSource.favoriteData!
|
|
||||||
.addOrDelFavorite!(widget.cid, '', !isFavorite, null);
|
|
||||||
if (res.success) {
|
|
||||||
widget.onFavorite(!isFavorite);
|
|
||||||
context.pop();
|
|
||||||
App.rootContext.showMessage(
|
|
||||||
message: isFavorite ? "Removed".tl : "Added".tl);
|
|
||||||
} else {
|
|
||||||
setState(() {
|
|
||||||
isLoading = false;
|
|
||||||
});
|
|
||||||
context.showMessage(message: res.errorMessage!);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: isFavorite ? Text("Remove".tl) : Text("Add".tl),
|
|
||||||
).paddingVertical(8),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, String>? folders;
|
Map<String, String>? folders;
|
||||||
|
|
||||||
var addedFolders = <String>{};
|
var addedFolders = <String>{};
|
||||||
|
|
||||||
var isLoadingFolders = true;
|
var isLoadingFolders = true;
|
||||||
|
bool? localIsFavorite;
|
||||||
|
final Map<String, bool> _itemLoading = {};
|
||||||
|
late List<double> _skeletonWidths;
|
||||||
|
|
||||||
// for network favorites, only one selection is allowed
|
@override
|
||||||
String? selected;
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
localIsFavorite = widget.isFavorite;
|
||||||
|
_skeletonWidths = List.generate(3, (_) => 0.3 + math.Random().nextDouble() * 0.5);
|
||||||
|
if (widget.comicSource.favoriteData!.loadFolders != null) {
|
||||||
|
loadFolders();
|
||||||
|
} else {
|
||||||
|
isLoadingFolders = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void loadFolders() async {
|
void loadFolders() async {
|
||||||
var res = await widget.comicSource.favoriteData!.loadFolders!(widget.cid);
|
var res = await widget.comicSource.favoriteData!.loadFolders!(widget.cid);
|
||||||
if (res.error) {
|
if (res.error) {
|
||||||
context.showMessage(message: res.errorMessage!);
|
context.showMessage(message: res.errorMessage!);
|
||||||
|
setState(() {
|
||||||
|
isLoadingFolders = false;
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
folders = res.data;
|
folders = res.data;
|
||||||
if (res.subData is List) {
|
if (res.subData is List) {
|
||||||
addedFolders = List<String>.from(res.subData).toSet();
|
final list = List<String>.from(res.subData);
|
||||||
|
if (list.isNotEmpty) {
|
||||||
|
addedFolders = {list.first};
|
||||||
|
} else {
|
||||||
|
addedFolders.clear();
|
||||||
|
}
|
||||||
|
localIsFavorite = addedFolders.isNotEmpty;
|
||||||
|
} else {
|
||||||
|
addedFolders.clear();
|
||||||
|
localIsFavorite = false;
|
||||||
}
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
isLoadingFolders = false;
|
isLoadingFolders = false;
|
||||||
@@ -315,61 +212,91 @@ class _NetworkFavoritesState extends State<_NetworkFavorites> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildMultiFolder() {
|
Widget _buildLoadingSkeleton() {
|
||||||
if (widget.isFavorite == true &&
|
|
||||||
widget.comicSource.favoriteData!.singleFolderForSingleComic) {
|
|
||||||
return Column(
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Padding(
|
||||||
child: Center(
|
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
|
||||||
child: Text("Added to favorites".tl),
|
child: Text(
|
||||||
|
"Network Favorites".tl,
|
||||||
|
style: ts.s14.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: context.colorScheme.primary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Center(
|
),
|
||||||
child: Button.filled(
|
Shimmer(
|
||||||
isLoading: isLoading,
|
child: Column(
|
||||||
onPressed: () async {
|
children: List.generate(3, (index) {
|
||||||
setState(() {
|
return ListTile(
|
||||||
isLoading = true;
|
title: Container(
|
||||||
});
|
height: 20,
|
||||||
|
width: double.infinity,
|
||||||
var res = await widget.comicSource.favoriteData!
|
margin: const EdgeInsets.only(right: 16),
|
||||||
.addOrDelFavorite!(widget.cid, '', false, null);
|
child: FractionallySizedBox(
|
||||||
if (res.success) {
|
widthFactor: _skeletonWidths[index],
|
||||||
widget.onFavorite(false);
|
alignment: Alignment.centerLeft,
|
||||||
context.pop();
|
child: Container(
|
||||||
App.rootContext.showMessage(message: "Removed".tl);
|
decoration: BoxDecoration(
|
||||||
} else {
|
color: context.colorScheme.surfaceContainerLow,
|
||||||
setState(() {
|
borderRadius: BorderRadius.circular(4),
|
||||||
isLoading = false;
|
),
|
||||||
});
|
),
|
||||||
context.showMessage(message: res.errorMessage!);
|
),
|
||||||
}
|
),
|
||||||
},
|
trailing: Container(
|
||||||
child: Text("Remove".tl),
|
height: 28,
|
||||||
).paddingVertical(8),
|
width: 60 + (index * 2),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: context.colorScheme.surfaceContainerLow,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
if (isLoadingFolders) {
|
if (isLoadingFolders) {
|
||||||
loadFolders();
|
return _buildLoadingSkeleton();
|
||||||
return const Center(child: CircularProgressIndicator());
|
}
|
||||||
|
|
||||||
|
bool isMultiFolder = widget.comicSource.favoriteData!.loadFolders != null;
|
||||||
|
|
||||||
|
if (isMultiFolder) {
|
||||||
|
return _buildMultiFolder();
|
||||||
} else {
|
} else {
|
||||||
|
return _buildSingleFolder();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSingleFolder() {
|
||||||
|
var isFavorite = localIsFavorite ?? false;
|
||||||
return Column(
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Padding(
|
||||||
child: ListView.builder(
|
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
|
||||||
itemCount: folders!.length,
|
child: Text(
|
||||||
itemBuilder: (context, index) {
|
"Network Favorites".tl,
|
||||||
var name = folders!.values.elementAt(index);
|
style: ts.s14.copyWith(
|
||||||
var id = folders!.keys.elementAt(index);
|
fontWeight: FontWeight.w600,
|
||||||
return CheckboxListTile(
|
color: context.colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
title: Row(
|
title: Row(
|
||||||
children: [
|
children: [
|
||||||
Text(name),
|
Text("Network Favorites".tl),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
if (addedFolders.contains(id))
|
if (isFavorite)
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 8,
|
horizontal: 8,
|
||||||
@@ -383,50 +310,372 @@ class _NetworkFavoritesState extends State<_NetworkFavorites> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
value: selected == id,
|
trailing: isLoading
|
||||||
onChanged: (v) {
|
? const SizedBox(
|
||||||
setState(() {
|
width: 20,
|
||||||
selected = id;
|
height: 20,
|
||||||
});
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
},
|
)
|
||||||
);
|
: _HoverButton(
|
||||||
},
|
isFavorite: isFavorite,
|
||||||
),
|
onTap: () async {
|
||||||
),
|
|
||||||
Center(
|
|
||||||
child: Button.filled(
|
|
||||||
isLoading: isLoading,
|
|
||||||
onPressed: () async {
|
|
||||||
if (selected == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setState(() {
|
setState(() {
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
});
|
});
|
||||||
var res =
|
|
||||||
await widget.comicSource.favoriteData!.addOrDelFavorite!(
|
var res = await widget
|
||||||
widget.cid,
|
.comicSource
|
||||||
selected!,
|
.favoriteData!
|
||||||
!addedFolders.contains(selected!),
|
.addOrDelFavorite!(widget.cid, '', !isFavorite, null);
|
||||||
null,
|
|
||||||
);
|
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
context.showMessage(message: "Success".tl);
|
setState(() {
|
||||||
|
localIsFavorite = !isFavorite;
|
||||||
|
});
|
||||||
|
widget.onFavorite(!isFavorite);
|
||||||
|
App.rootContext.showMessage(
|
||||||
|
message: isFavorite ? "Removed".tl : "Added".tl,
|
||||||
|
);
|
||||||
|
if (appdata.settings['autoCloseFavoritePanel'] ?? false) {
|
||||||
context.pop();
|
context.pop();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
context.showMessage(message: res.errorMessage!);
|
context.showMessage(message: res.errorMessage!);
|
||||||
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildMultiFolder() {
|
||||||
|
if (localIsFavorite == true &&
|
||||||
|
widget.comicSource.favoriteData!.singleFolderForSingleComic) {
|
||||||
|
return ListTile(
|
||||||
|
title: Row(
|
||||||
|
children: [
|
||||||
|
Text("Network Favorites".tl),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: context.colorScheme.primaryContainer,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Text("Added".tl, style: ts.s12),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
trailing: isLoading
|
||||||
|
? const SizedBox(
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
)
|
||||||
|
: _HoverButton(
|
||||||
|
isFavorite: true,
|
||||||
|
onTap: () async {
|
||||||
|
setState(() {
|
||||||
|
isLoading = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
var res = await widget
|
||||||
|
.comicSource
|
||||||
|
.favoriteData!
|
||||||
|
.addOrDelFavorite!(widget.cid, '', false, null);
|
||||||
|
if (res.success) {
|
||||||
|
// Invalidate network cache so subsequent loads see latest
|
||||||
|
NetworkCacheManager().clear();
|
||||||
|
setState(() {
|
||||||
|
localIsFavorite = false;
|
||||||
|
});
|
||||||
|
widget.onFavorite(false);
|
||||||
|
App.rootContext.showMessage(message: "Removed".tl);
|
||||||
|
if (appdata.settings['autoCloseFavoritePanel'] ?? false) {
|
||||||
|
context.pop();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
context.showMessage(message: res.errorMessage!);
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
isLoading = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
|
||||||
|
child: Text(
|
||||||
|
"Network Favorites".tl,
|
||||||
|
style: ts.s14.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: context.colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
...folders!.entries.map((entry) {
|
||||||
|
var name = entry.value;
|
||||||
|
var id = entry.key;
|
||||||
|
var isAdded = addedFolders.contains(id);
|
||||||
|
var hasSelection = addedFolders.isNotEmpty;
|
||||||
|
var enabled = !hasSelection || isAdded;
|
||||||
|
|
||||||
|
return ListTile(
|
||||||
|
title: Row(
|
||||||
|
children: [
|
||||||
|
Text(name),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
if (isAdded)
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 8,
|
||||||
|
vertical: 4,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: context.colorScheme.primaryContainer,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Text("Added".tl, style: ts.s12),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
trailing: (_itemLoading[id] ?? false)
|
||||||
|
? const SizedBox(
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
)
|
||||||
|
: _HoverButton(
|
||||||
|
isFavorite: isAdded,
|
||||||
|
enabled: enabled,
|
||||||
|
onTap: () async {
|
||||||
|
setState(() {
|
||||||
|
_itemLoading[id] = true;
|
||||||
|
});
|
||||||
|
var res = await widget
|
||||||
|
.comicSource
|
||||||
|
.favoriteData!
|
||||||
|
.addOrDelFavorite!(widget.cid, id, !isAdded, null);
|
||||||
|
if (res.success) {
|
||||||
|
// Invalidate network cache so folders/pages reload with fresh data
|
||||||
|
NetworkCacheManager().clear();
|
||||||
|
setState(() {
|
||||||
|
if (isAdded) {
|
||||||
|
addedFolders.clear();
|
||||||
|
} else {
|
||||||
|
addedFolders
|
||||||
|
..clear()
|
||||||
|
..add(id);
|
||||||
|
}
|
||||||
|
// sync local flag for single-folder-per-comic logic and parent
|
||||||
|
localIsFavorite = addedFolders.isNotEmpty;
|
||||||
|
});
|
||||||
|
// notify parent so page state updates when closing and reopening panel
|
||||||
|
widget.onFavorite(addedFolders.isNotEmpty);
|
||||||
|
context.showMessage(message: "Success".tl);
|
||||||
|
if (appdata.settings['autoCloseFavoritePanel'] ?? false) {
|
||||||
|
context.pop();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
context.showMessage(message: res.errorMessage!);
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_itemLoading[id] = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LocalSection extends StatefulWidget {
|
||||||
|
const _LocalSection({
|
||||||
|
required this.cid,
|
||||||
|
required this.type,
|
||||||
|
required this.favoriteItem,
|
||||||
|
this.updateTime,
|
||||||
|
required this.localFolders,
|
||||||
|
required this.added,
|
||||||
|
required this.onFavorite,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String cid;
|
||||||
|
final ComicType type;
|
||||||
|
final FavoriteItem favoriteItem;
|
||||||
|
final String? updateTime;
|
||||||
|
final List<String> localFolders;
|
||||||
|
final List<String> added;
|
||||||
|
final void Function(bool) onFavorite;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_LocalSection> createState() => _LocalSectionState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LocalSectionState extends State<_LocalSection> {
|
||||||
|
late List<String> localFolders;
|
||||||
|
late Set<String> localAdded;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
localFolders = widget.localFolders;
|
||||||
|
localAdded = widget.added.toSet();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
|
||||||
|
child: Text(
|
||||||
|
"Local Favorites".tl,
|
||||||
|
style: ts.s14.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: context.colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
...localFolders.map((folder) {
|
||||||
|
var isAdded = localAdded.contains(folder);
|
||||||
|
|
||||||
|
return ListTile(
|
||||||
|
title: Row(
|
||||||
|
children: [
|
||||||
|
Text(folder),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
if (isAdded)
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 8,
|
||||||
|
vertical: 4,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: context.colorScheme.primaryContainer,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Text("Added".tl, style: ts.s12),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
trailing: _HoverButton(
|
||||||
|
isFavorite: isAdded,
|
||||||
|
onTap: () {
|
||||||
|
if (isAdded) {
|
||||||
|
LocalFavoritesManager().deleteComicWithId(
|
||||||
|
folder,
|
||||||
|
widget.cid,
|
||||||
|
widget.type,
|
||||||
|
);
|
||||||
|
setState(() {
|
||||||
|
localAdded.remove(folder);
|
||||||
|
});
|
||||||
|
widget.onFavorite(false);
|
||||||
|
} else {
|
||||||
|
LocalFavoritesManager().addComic(
|
||||||
|
folder,
|
||||||
|
widget.favoriteItem,
|
||||||
|
null,
|
||||||
|
widget.updateTime,
|
||||||
|
);
|
||||||
|
setState(() {
|
||||||
|
localAdded.add(folder);
|
||||||
|
});
|
||||||
|
widget.onFavorite(true);
|
||||||
|
}
|
||||||
|
if (appdata.settings['autoCloseFavoritePanel'] ?? false) {
|
||||||
|
context.pop();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: selected != null && addedFolders.contains(selected!)
|
),
|
||||||
? Text("Remove".tl)
|
);
|
||||||
: Text("Add".tl),
|
}),
|
||||||
).paddingVertical(8),
|
// New folder button
|
||||||
|
ListTile(
|
||||||
|
title: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.add, size: 20),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text("New Folder".tl),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
newFolder().then((v) {
|
||||||
|
setState(() {
|
||||||
|
localFolders = LocalFavoritesManager().folderNames;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _HoverButton extends StatefulWidget {
|
||||||
|
const _HoverButton({
|
||||||
|
required this.isFavorite,
|
||||||
|
required this.onTap,
|
||||||
|
this.enabled = true,
|
||||||
|
});
|
||||||
|
|
||||||
|
final bool isFavorite;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
final bool enabled;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_HoverButton> createState() => _HoverButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HoverButtonState extends State<_HoverButton> {
|
||||||
|
bool isHovered = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final removeColor = context.colorScheme.error;
|
||||||
|
final removeHoverColor = Color.lerp(removeColor, Colors.black, 0.2)!;
|
||||||
|
final addColor = context.colorScheme.primary;
|
||||||
|
final addHoverColor = Color.lerp(addColor, Colors.black, 0.2)!;
|
||||||
|
|
||||||
|
return MouseRegion(
|
||||||
|
onEnter: widget.enabled ? (_) => setState(() => isHovered = true) : null,
|
||||||
|
onExit: widget.enabled ? (_) => setState(() => isHovered = false) : null,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: widget.enabled ? widget.onTap : null,
|
||||||
|
child: AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: widget.enabled
|
||||||
|
? (widget.isFavorite
|
||||||
|
? (isHovered ? removeHoverColor : removeColor)
|
||||||
|
: (isHovered ? addHoverColor : addColor))
|
||||||
|
: context.colorScheme.surfaceContainerLow,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
widget.isFavorite ? "Remove".tl : "Add".tl,
|
||||||
|
style: ts.s12.copyWith(
|
||||||
|
color: widget.enabled
|
||||||
|
? context.colorScheme.onPrimary
|
||||||
|
: context.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import 'package:venera/foundation/local.dart';
|
|||||||
import 'package:venera/foundation/log.dart';
|
import 'package:venera/foundation/log.dart';
|
||||||
import 'package:venera/foundation/res.dart';
|
import 'package:venera/foundation/res.dart';
|
||||||
import 'package:venera/network/download.dart';
|
import 'package:venera/network/download.dart';
|
||||||
|
import 'package:venera/network/cache.dart';
|
||||||
import 'package:venera/pages/comic_details_page/comic_page.dart';
|
import 'package:venera/pages/comic_details_page/comic_page.dart';
|
||||||
import 'package:venera/pages/reader/reader.dart';
|
import 'package:venera/pages/reader/reader.dart';
|
||||||
import 'package:venera/pages/settings/settings_page.dart';
|
import 'package:venera/pages/settings/settings_page.dart';
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ Future<bool> _deleteComic(
|
|||||||
favId,
|
favId,
|
||||||
);
|
);
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
|
// Invalidate network cache so next loads fetch fresh data
|
||||||
|
NetworkCacheManager().clear();
|
||||||
context.showMessage(message: "Deleted".tl);
|
context.showMessage(message: "Deleted".tl);
|
||||||
result = true;
|
result = true;
|
||||||
context.pop();
|
context.pop();
|
||||||
@@ -115,6 +117,8 @@ class _NormalFavoritePageState extends State<_NormalFavoritePage> {
|
|||||||
child: IconButton(
|
child: IconButton(
|
||||||
icon: const Icon(Icons.refresh),
|
icon: const Icon(Icons.refresh),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
// Force refresh bypassing cache
|
||||||
|
NetworkCacheManager().clear();
|
||||||
comicListKey.currentState!.refresh();
|
comicListKey.currentState!.refresh();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -13,6 +13,14 @@ class _LocalFavoritesSettingsState extends State<LocalFavoritesSettings> {
|
|||||||
return SmoothCustomScrollView(
|
return SmoothCustomScrollView(
|
||||||
slivers: [
|
slivers: [
|
||||||
SliverAppbar(title: Text("Local Favorites".tl)),
|
SliverAppbar(title: Text("Local Favorites".tl)),
|
||||||
|
_SwitchSetting(
|
||||||
|
title: "Show local favorites before network favorites".tl,
|
||||||
|
settingKey: "localFavoritesFirst",
|
||||||
|
).toSliver(),
|
||||||
|
_SwitchSetting(
|
||||||
|
title: "Auto close favorite panel after operation".tl,
|
||||||
|
settingKey: "autoCloseFavoritePanel",
|
||||||
|
).toSliver(),
|
||||||
SelectSetting(
|
SelectSetting(
|
||||||
title: "Add new favorite to".tl,
|
title: "Add new favorite to".tl,
|
||||||
settingKey: "newFavoriteAddTo",
|
settingKey: "newFavoriteAddTo",
|
||||||
|
|||||||
Reference in New Issue
Block a user