mirror of
https://github.com/venera-app/venera.git
synced 2025-09-27 15:57:25 +00:00
favorite
This commit is contained in:
@@ -195,6 +195,9 @@ class _ButtonState extends State<Button> {
|
|||||||
child: AnimatedContainer(
|
child: AnimatedContainer(
|
||||||
duration: const Duration(milliseconds: 160),
|
duration: const Duration(milliseconds: 160),
|
||||||
padding: padding,
|
padding: padding,
|
||||||
|
constraints: const BoxConstraints(
|
||||||
|
minWidth: 76,
|
||||||
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: buttonColor,
|
color: buttonColor,
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
@@ -220,11 +223,14 @@ class _ButtonState extends State<Button> {
|
|||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: width,
|
width: width,
|
||||||
height: height,
|
height: height,
|
||||||
|
child: Center(
|
||||||
|
widthFactor: 1,
|
||||||
child: child,
|
child: child,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -7,6 +7,7 @@ import 'dart:math' as math;
|
|||||||
|
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:venera/foundation/app.dart';
|
import 'package:venera/foundation/app.dart';
|
||||||
|
import 'package:venera/foundation/comic_type.dart';
|
||||||
import 'package:venera/foundation/history.dart';
|
import 'package:venera/foundation/history.dart';
|
||||||
import 'package:venera/foundation/res.dart';
|
import 'package:venera/foundation/res.dart';
|
||||||
import 'package:venera/utils/ext.dart';
|
import 'package:venera/utils/ext.dart';
|
||||||
@@ -514,6 +515,8 @@ class ComicDetails with HistoryMixin {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get id => comicId;
|
String get id => comicId;
|
||||||
|
|
||||||
|
ComicType get comicType => ComicType(sourceKey.hashCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
typedef CategoryComicsLoader = Future<Res<List<Comic>>> Function(
|
typedef CategoryComicsLoader = Future<Res<List<Comic>>> Function(
|
||||||
|
@@ -96,7 +96,7 @@ class LocalFavoritesManager {
|
|||||||
""");
|
""");
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<String>> find(String id, ComicType type) async {
|
List<String> find(String id, ComicType type) {
|
||||||
var res = <String>[];
|
var res = <String>[];
|
||||||
for (var folder in folderNames) {
|
for (var folder in folderNames) {
|
||||||
var rows = _db.select("""
|
var rows = _db.select("""
|
||||||
|
@@ -8,6 +8,7 @@ import 'package:venera/foundation/favorites.dart';
|
|||||||
import 'package:venera/foundation/history.dart';
|
import 'package:venera/foundation/history.dart';
|
||||||
import 'package:venera/foundation/image_provider/cached_image.dart';
|
import 'package:venera/foundation/image_provider/cached_image.dart';
|
||||||
import 'package:venera/foundation/res.dart';
|
import 'package:venera/foundation/res.dart';
|
||||||
|
import 'package:venera/pages/favorites/favorite_actions.dart';
|
||||||
import 'package:venera/utils/translations.dart';
|
import 'package:venera/utils/translations.dart';
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
@@ -178,8 +179,7 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
icon: const Icon(Icons.favorite_border),
|
icon: const Icon(Icons.favorite_border),
|
||||||
activeIcon: const Icon(Icons.favorite),
|
activeIcon: const Icon(Icons.favorite),
|
||||||
isActive: isLiked,
|
isActive: isLiked,
|
||||||
text: (data!.likesCount ??
|
text: (data!.likesCount ?? (isLiked ? 'Liked'.tl : 'Like'.tl))
|
||||||
(isLiked ? 'Liked'.tl : 'Like'.tl))
|
|
||||||
.toString(),
|
.toString(),
|
||||||
isLoading: isLiking,
|
isLoading: isLiking,
|
||||||
onPressed: likeOrUnlike,
|
onPressed: likeOrUnlike,
|
||||||
@@ -407,7 +407,7 @@ abstract mixin class _ComicPageActions {
|
|||||||
if (isLiking) return;
|
if (isLiking) return;
|
||||||
isLiking = true;
|
isLiking = true;
|
||||||
update();
|
update();
|
||||||
var res = await comicSource.likeOrUnlikeComic!(comic.id, isLiked ?? false);
|
var res = await comicSource.likeOrUnlikeComic!(comic.id, isLiked);
|
||||||
if (res.error) {
|
if (res.error) {
|
||||||
App.rootContext.showMessage(message: res.errorMessage!);
|
App.rootContext.showMessage(message: res.errorMessage!);
|
||||||
} else {
|
} else {
|
||||||
@@ -421,7 +421,33 @@ abstract mixin class _ComicPageActions {
|
|||||||
|
|
||||||
bool isFavorite = false;
|
bool isFavorite = false;
|
||||||
|
|
||||||
void openFavPanel() {}
|
void openFavPanel() {
|
||||||
|
var tags = <String>[];
|
||||||
|
for (var e in comic.tags.entries) {
|
||||||
|
tags.addAll(e.value.map((tag) => '${e.key}:$tag'));
|
||||||
|
}
|
||||||
|
|
||||||
|
showSideBar(
|
||||||
|
App.rootContext,
|
||||||
|
_FavoritePanel(
|
||||||
|
cid: comic.id,
|
||||||
|
type: comic.comicType,
|
||||||
|
isFavorite: isFavorite,
|
||||||
|
onFavorite: (b) {
|
||||||
|
isFavorite = b;
|
||||||
|
update();
|
||||||
|
},
|
||||||
|
favoriteItem: FavoriteItem(
|
||||||
|
id: comic.id,
|
||||||
|
name: comic.title,
|
||||||
|
coverPath: comic.cover,
|
||||||
|
author: comic.subTitle ?? comic.uploader ?? '',
|
||||||
|
type: comic.comicType,
|
||||||
|
tags: tags,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void share() {}
|
void share() {}
|
||||||
|
|
||||||
@@ -751,3 +777,333 @@ class _ComicThumbnailsState extends State<_ComicThumbnails> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _FavoritePanel extends StatefulWidget {
|
||||||
|
const _FavoritePanel({
|
||||||
|
required this.cid,
|
||||||
|
required this.type,
|
||||||
|
required this.isFavorite,
|
||||||
|
required this.onFavorite,
|
||||||
|
required this.favoriteItem,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String cid;
|
||||||
|
|
||||||
|
final ComicType type;
|
||||||
|
|
||||||
|
/// whether the comic is in the network favorite list
|
||||||
|
///
|
||||||
|
/// if null, the comic source does not support favorite or support multiple favorite lists
|
||||||
|
final bool? isFavorite;
|
||||||
|
|
||||||
|
final void Function(bool) onFavorite;
|
||||||
|
|
||||||
|
final FavoriteItem favoriteItem;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_FavoritePanel> createState() => _FavoritePanelState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FavoritePanelState extends State<_FavoritePanel> {
|
||||||
|
late ComicSource comicSource;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
comicSource = widget.type.comicSource!;
|
||||||
|
localFolders = LocalFavoritesManager().folderNames;
|
||||||
|
added = LocalFavoritesManager().find(widget.cid, widget.type);
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var hasNetwork = comicSource.favoriteData != null && comicSource.isLogged;
|
||||||
|
return Scaffold(
|
||||||
|
appBar: Appbar(
|
||||||
|
title: Text("Favorite".tl),
|
||||||
|
),
|
||||||
|
body: DefaultTabController(
|
||||||
|
length: comicSource.favoriteData == null ? 1 : 2,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
TabBar(tabs: [
|
||||||
|
Tab(text: "Local".tl),
|
||||||
|
if (hasNetwork) Tab(text: "Network".tl),
|
||||||
|
]),
|
||||||
|
Expanded(
|
||||||
|
child: TabBarView(
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (var folder in selectedLocalFolders) {
|
||||||
|
LocalFavoritesManager().addComic(folder, widget.favoriteItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
context.pop();
|
||||||
|
},
|
||||||
|
child: isRemove ? Text("Remove".tl) : Text("Add".tl),
|
||||||
|
).paddingVertical(8),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildNetwork() {
|
||||||
|
return _NetworkFavorites(
|
||||||
|
cid: widget.cid,
|
||||||
|
comicSource: comicSource,
|
||||||
|
isFavorite: widget.isFavorite,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _NetworkFavorites extends StatefulWidget {
|
||||||
|
const _NetworkFavorites(
|
||||||
|
{required this.cid, required this.comicSource, required this.isFavorite});
|
||||||
|
|
||||||
|
final String cid;
|
||||||
|
|
||||||
|
final ComicSource comicSource;
|
||||||
|
|
||||||
|
final bool? isFavorite;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_NetworkFavorites> createState() => _NetworkFavoritesState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _NetworkFavoritesState extends State<_NetworkFavorites> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
bool isMultiFolder = widget.comicSource.favoriteData!.loadFolders != null;
|
||||||
|
|
||||||
|
return isMultiFolder ? buildMultiFolder() : buildSingleFolder();
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
if (res.success) {
|
||||||
|
context.pop();
|
||||||
|
} else {
|
||||||
|
setState(() {
|
||||||
|
isLoading = false;
|
||||||
|
});
|
||||||
|
context.showMessage(message: res.errorMessage!);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: isFavorite ? Text("Remove".tl) : Text("Add".tl),
|
||||||
|
).paddingVertical(8),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, String>? folders;
|
||||||
|
|
||||||
|
var addedFolders = <String>{};
|
||||||
|
|
||||||
|
var isLoadingFolders = true;
|
||||||
|
|
||||||
|
// for network favorites, only one selection is allowed
|
||||||
|
String? selected;
|
||||||
|
|
||||||
|
void loadFolders() async {
|
||||||
|
var res = await widget.comicSource.favoriteData!.loadFolders!(widget.cid);
|
||||||
|
if (res.error) {
|
||||||
|
context.showMessage(message: res.errorMessage!);
|
||||||
|
} else {
|
||||||
|
folders = res.data;
|
||||||
|
if (res.subData is List) {
|
||||||
|
addedFolders = List<String>.from(res.subData).toSet();
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
isLoadingFolders = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildMultiFolder() {
|
||||||
|
if (isLoadingFolders) {
|
||||||
|
loadFolders();
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
} else {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: ListView.builder(
|
||||||
|
itemCount: folders!.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
var name = folders!.values.elementAt(index);
|
||||||
|
var id = folders!.keys.elementAt(index);
|
||||||
|
return CheckboxListTile(
|
||||||
|
title: Row(
|
||||||
|
children: [
|
||||||
|
Text(name),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
if (addedFolders.contains(id))
|
||||||
|
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: selected == id,
|
||||||
|
onChanged: (v) {
|
||||||
|
setState(() {
|
||||||
|
selected = id;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Center(
|
||||||
|
child: FilledButton(
|
||||||
|
onPressed: () {
|
||||||
|
if (selected == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
widget.comicSource.favoriteData!.addOrDelFavorite!(
|
||||||
|
widget.cid, selected!, !addedFolders.contains(selected!));
|
||||||
|
context.pop();
|
||||||
|
},
|
||||||
|
child: addedFolders.contains(selected!)
|
||||||
|
? Text("Remove".tl)
|
||||||
|
: Text("Add".tl),
|
||||||
|
).paddingVertical(8),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
61
lib/pages/favorites/favorite_actions.dart
Normal file
61
lib/pages/favorites/favorite_actions.dart
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:venera/components/components.dart';
|
||||||
|
import 'package:venera/foundation/app.dart';
|
||||||
|
import 'package:venera/foundation/favorites.dart';
|
||||||
|
import 'package:venera/utils/translations.dart';
|
||||||
|
|
||||||
|
/// Open a dialog to create a new favorite folder.
|
||||||
|
Future<void> newFolder() async {
|
||||||
|
return showDialog(context: App.rootContext, builder: (context) {
|
||||||
|
var controller = TextEditingController();
|
||||||
|
var folders = LocalFavoritesManager().folderNames;
|
||||||
|
String? error;
|
||||||
|
|
||||||
|
return StatefulBuilder(builder: (context, setState) {
|
||||||
|
return ContentDialog(
|
||||||
|
title: "New Folder".tl,
|
||||||
|
content: Column(
|
||||||
|
children: [
|
||||||
|
TextField(
|
||||||
|
controller: controller,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: "Folder Name".tl,
|
||||||
|
errorText: error,
|
||||||
|
),
|
||||||
|
onChanged: (s) {
|
||||||
|
if(error != null) {
|
||||||
|
setState(() {
|
||||||
|
error = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
).paddingHorizontal(16),
|
||||||
|
actions: [
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () {
|
||||||
|
if(controller.text.isEmpty) {
|
||||||
|
setState(() {
|
||||||
|
error = "Folder name cannot be empty".tl;
|
||||||
|
});
|
||||||
|
} else if(controller.text.length > 50) {
|
||||||
|
setState(() {
|
||||||
|
error = "Folder name is too long".tl;
|
||||||
|
});
|
||||||
|
} else if(folders.contains(controller.text)) {
|
||||||
|
setState(() {
|
||||||
|
error = "Folder already exists".tl;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
LocalFavoritesManager().createFolder(controller.text);
|
||||||
|
context.pop();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Text("Create".tl),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
Reference in New Issue
Block a user