mirror of
https://github.com/venera-app/venera.git
synced 2025-09-27 15:57:25 +00:00
favorites page
This commit is contained in:
@@ -343,3 +343,33 @@ class _IconButtonState extends State<_IconButton> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MenuButton extends StatefulWidget {
|
||||
const MenuButton({super.key, required this.entries});
|
||||
|
||||
final List<MenuEntry> entries;
|
||||
|
||||
@override
|
||||
State<MenuButton> createState() => _MenuButtonState();
|
||||
}
|
||||
|
||||
class _MenuButtonState extends State<MenuButton> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Tooltip(
|
||||
message: 'more'.tl,
|
||||
child: Button.icon(
|
||||
icon: const Icon(Icons.more_horiz),
|
||||
onPressed: () {
|
||||
var renderBox = context.findRenderObject() as RenderBox;
|
||||
var offset = renderBox.localToGlobal(Offset.zero);
|
||||
showMenuX(
|
||||
context,
|
||||
offset,
|
||||
widget.entries,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -53,6 +53,13 @@ class ComicTile extends StatelessWidget {
|
||||
App.rootContext.showMessage(message: 'Title copied'.tl);
|
||||
},
|
||||
),
|
||||
MenuEntry(
|
||||
icon: Icons.stars_outlined,
|
||||
text: 'Add to favorites'.tl,
|
||||
onClick: () {
|
||||
addFavorite(comic);
|
||||
},
|
||||
),
|
||||
...?menuOptions,
|
||||
],
|
||||
);
|
||||
|
@@ -23,6 +23,7 @@ import 'package:venera/foundation/local.dart';
|
||||
import 'package:venera/foundation/res.dart';
|
||||
import 'package:venera/foundation/state_controller.dart';
|
||||
import 'package:venera/pages/comic_page.dart';
|
||||
import 'package:venera/pages/favorites/favorites_page.dart';
|
||||
import 'package:venera/utils/ext.dart';
|
||||
import 'package:venera/utils/translations.dart';
|
||||
|
||||
|
@@ -1,7 +1,7 @@
|
||||
part of "components.dart";
|
||||
|
||||
void showMenuX(BuildContext context, Offset location, List<MenuEntry> entries) {
|
||||
Navigator.of(context).push(_MenuRoute(entries, location));
|
||||
Navigator.of(context, rootNavigator: true).push(_MenuRoute(entries, location));
|
||||
}
|
||||
|
||||
class _MenuRoute<T> extends PopupRoute<T> {
|
||||
|
@@ -1,6 +1,11 @@
|
||||
part of "components.dart";
|
||||
|
||||
void showToast({required String message, required BuildContext context, Widget? icon, Widget? trailing,}) {
|
||||
void showToast({
|
||||
required String message,
|
||||
required BuildContext context,
|
||||
Widget? icon,
|
||||
Widget? trailing,
|
||||
}) {
|
||||
var newEntry = OverlayEntry(
|
||||
builder: (context) => _ToastOverlay(
|
||||
message: message,
|
||||
@@ -36,9 +41,11 @@ class _ToastOverlay extends StatelessWidget {
|
||||
color: Theme.of(context).colorScheme.inverseSurface,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
elevation: 2,
|
||||
textStyle: ts.withColor(Theme.of(context).colorScheme.onInverseSurface),
|
||||
textStyle:
|
||||
ts.withColor(Theme.of(context).colorScheme.onInverseSurface),
|
||||
child: IconTheme(
|
||||
data: IconThemeData(color: Theme.of(context).colorScheme.onInverseSurface),
|
||||
data: IconThemeData(
|
||||
color: Theme.of(context).colorScheme.onInverseSurface),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 16),
|
||||
child: Row(
|
||||
@@ -121,23 +128,28 @@ void showDialogMessage(BuildContext context, String title, String message) {
|
||||
);
|
||||
}
|
||||
|
||||
void showConfirmDialog(BuildContext context, String title, String content,
|
||||
void Function() onConfirm) {
|
||||
void showConfirmDialog({
|
||||
required BuildContext context,
|
||||
required String title,
|
||||
required String content,
|
||||
required void Function() onConfirm,
|
||||
}) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(title),
|
||||
content: Text(content),
|
||||
builder: (context) => ContentDialog(
|
||||
title: title,
|
||||
content: Text(content).paddingHorizontal(16).paddingVertical(8),
|
||||
actions: [
|
||||
TextButton(onPressed: context.pop, child: Text("Cancel".tl)),
|
||||
TextButton(
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
context.pop();
|
||||
onConfirm();
|
||||
},
|
||||
child: Text("Confirm".tl)),
|
||||
child: Text("Confirm".tl),
|
||||
),
|
||||
],
|
||||
));
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class LoadingDialogController {
|
||||
@@ -234,6 +246,7 @@ class ContentDialog extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
var content = Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Appbar(
|
||||
leading: IconButton(
|
||||
@@ -281,3 +294,83 @@ class ContentDialog extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void showInputDialog({
|
||||
required BuildContext context,
|
||||
required String title,
|
||||
required String hintText,
|
||||
required FutureOr<Object?> Function(String) onConfirm,
|
||||
String? initialValue,
|
||||
String confirmText = "Confirm",
|
||||
String cancelText = "Cancel",
|
||||
}) {
|
||||
var controller = TextEditingController(text: initialValue);
|
||||
bool isLoading = false;
|
||||
String? error;
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return StatefulBuilder(
|
||||
builder: (context, setState) {
|
||||
return ContentDialog(
|
||||
title: title,
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
decoration: InputDecoration(
|
||||
hintText: hintText,
|
||||
border: const OutlineInputBorder(),
|
||||
errorText: error,
|
||||
),
|
||||
).paddingHorizontal(12),
|
||||
actions: [
|
||||
Button.filled(
|
||||
isLoading: isLoading,
|
||||
onPressed: () async {
|
||||
var futureOr = onConfirm(controller.text);
|
||||
Object? result;
|
||||
if (futureOr is Future) {
|
||||
setState(() => isLoading = true);
|
||||
result = await futureOr;
|
||||
setState(() => isLoading = false);
|
||||
} else {
|
||||
result = futureOr;
|
||||
}
|
||||
if(result == null) {
|
||||
context.pop();
|
||||
} else {
|
||||
setState(() => error = result.toString());
|
||||
}
|
||||
},
|
||||
child: Text(confirmText.tl),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void showInfoDialog({
|
||||
required BuildContext context,
|
||||
required String title,
|
||||
required String content,
|
||||
String confirmText = "OK",
|
||||
}) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return ContentDialog(
|
||||
title: title,
|
||||
content: Text(content).paddingHorizontal(16).paddingVertical(8),
|
||||
actions: [
|
||||
Button.filled(
|
||||
onPressed: context.pop,
|
||||
child: Text(confirmText.tl),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@@ -135,7 +135,7 @@ class _NaviPaneState extends State<NaviPane>
|
||||
controller.value = target;
|
||||
} else {
|
||||
StateController.findOrNull<NaviPaddingWidgetController>()
|
||||
?.setWithPadding(true, false, false);
|
||||
?.setWithPadding(false, false, false);
|
||||
controller.animateTo(target);
|
||||
}
|
||||
animationTarget = target;
|
||||
@@ -555,7 +555,15 @@ class _SingleBottomNaviWidgetState extends State<_SingleBottomNaviWidget>
|
||||
class NaviObserver extends NavigatorObserver implements Listenable {
|
||||
var routes = Queue<Route>();
|
||||
|
||||
int get pageCount => routes.length;
|
||||
int get pageCount {
|
||||
int count = 0;
|
||||
for (var route in routes) {
|
||||
if (route is AppPageRoute) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
@override
|
||||
void didPop(Route route, Route? previousRoute) {
|
||||
@@ -693,7 +701,12 @@ class NaviPaddingWidget extends StatelessWidget {
|
||||
: 0),
|
||||
)
|
||||
: EdgeInsets.zero,
|
||||
child: MediaQuery.removePadding(
|
||||
removeTop: controller._withPadding,
|
||||
removeBottom: controller._withPadding,
|
||||
context: context,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@@ -6,14 +6,17 @@ class Select extends StatelessWidget {
|
||||
required this.current,
|
||||
required this.values,
|
||||
this.onTap,
|
||||
this.minWidth,
|
||||
});
|
||||
|
||||
final String current;
|
||||
final String? current;
|
||||
|
||||
final List<String> values;
|
||||
|
||||
final void Function(int index)? onTap;
|
||||
|
||||
final double? minWidth;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
@@ -58,7 +61,12 @@ class Select extends StatelessWidget {
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(current, style: ts.s14),
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minWidth: minWidth != null ? (minWidth! - 32) : 0,
|
||||
),
|
||||
child: Text(current ?? ' ', style: ts.s14),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Icon(Icons.arrow_drop_down, color: context.colorScheme.primary),
|
||||
],
|
||||
|
@@ -47,8 +47,9 @@ class _Appdata {
|
||||
}
|
||||
|
||||
Future<void> init() async {
|
||||
var dataPath = (await getApplicationSupportDirectory()).path;
|
||||
var file = File(FilePath.join(
|
||||
(await getApplicationSupportDirectory()).path,
|
||||
dataPath,
|
||||
'appdata.json',
|
||||
));
|
||||
if (!await file.exists()) {
|
||||
@@ -61,6 +62,10 @@ class _Appdata {
|
||||
}
|
||||
}
|
||||
searchHistory = List.from(json['searchHistory']);
|
||||
var implicitDataFile = File(FilePath.join(dataPath, 'implicitData.json'));
|
||||
if (await implicitDataFile.exists()) {
|
||||
implicitData = jsonDecode(await implicitDataFile.readAsString());
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
@@ -69,6 +74,13 @@ class _Appdata {
|
||||
'searchHistory': searchHistory,
|
||||
};
|
||||
}
|
||||
|
||||
var implicitData = <String, dynamic>{};
|
||||
|
||||
void writeImplicitData() {
|
||||
var file = File(FilePath.join(App.dataPath, 'implicitData.json'));
|
||||
file.writeAsString(jsonEncode(implicitData));
|
||||
}
|
||||
}
|
||||
|
||||
final appdata = _Appdata();
|
||||
|
@@ -319,9 +319,6 @@ class LocalFavoritesManager {
|
||||
/// delete a folder
|
||||
void deleteFolder(String name) {
|
||||
_modifiedAfterLastCache = true;
|
||||
_db.execute("""
|
||||
delete from folder_sync where folder_name == ?;
|
||||
""", [name]);
|
||||
_db.execute("""
|
||||
drop table "$name";
|
||||
""");
|
||||
|
@@ -47,7 +47,7 @@ class CategoriesPage extends StatelessWidget {
|
||||
key: Key(e),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
).paddingTop(context.padding.top),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
children:
|
||||
|
@@ -8,7 +8,7 @@ import 'package:venera/foundation/favorites.dart';
|
||||
import 'package:venera/foundation/history.dart';
|
||||
import 'package:venera/foundation/image_provider/cached_image.dart';
|
||||
import 'package:venera/foundation/res.dart';
|
||||
import 'package:venera/pages/favorites/favorite_actions.dart';
|
||||
import 'package:venera/pages/favorites/favorites_page.dart';
|
||||
import 'package:venera/pages/reader/reader.dart';
|
||||
import 'package:venera/utils/translations.dart';
|
||||
import 'dart:math' as math;
|
||||
@@ -247,7 +247,7 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: SelectableText(comic.description!),
|
||||
child: SelectableText(comic.description!).fixWidth(double.infinity),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Divider(),
|
||||
|
@@ -49,12 +49,17 @@ class ComicSourcePage extends StatefulWidget {
|
||||
msg += "${ComicSource.find(key)?.name}: v${versions[key]}\n";
|
||||
}
|
||||
msg = msg.trim();
|
||||
showConfirmDialog(App.rootContext, "Updates Available".tl, msg, () {
|
||||
showConfirmDialog(
|
||||
context: App.rootContext,
|
||||
title: "Updates Available".tl,
|
||||
content: msg,
|
||||
onConfirm: () {
|
||||
for (var key in shouldUpdate) {
|
||||
var source = ComicSource.find(key);
|
||||
_BodyState.update(source!);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -149,10 +154,10 @@ class _BodyState extends State<_Body> {
|
||||
|
||||
void delete(ComicSource source) {
|
||||
showConfirmDialog(
|
||||
App.rootContext,
|
||||
"Delete".tl,
|
||||
"Are you sure you want to delete it?".tl,
|
||||
() {
|
||||
context: App.rootContext,
|
||||
title: "Delete".tl,
|
||||
content: "Are you sure you want to delete it?".tl,
|
||||
onConfirm: () {
|
||||
var file = File(source.filePath);
|
||||
file.delete();
|
||||
ComicSource.remove(source.key);
|
||||
@@ -243,7 +248,8 @@ class _BodyState extends State<_Body> {
|
||||
.paddingBottom(32),
|
||||
Row(
|
||||
children: [
|
||||
TextButton(onPressed: selectFile, child: Text("Select file".tl))
|
||||
TextButton(
|
||||
onPressed: selectFile, child: Text("Select file".tl))
|
||||
.paddingLeft(8),
|
||||
const Spacer(),
|
||||
TextButton(
|
||||
|
@@ -64,7 +64,7 @@ class _ExplorePageState extends State<ExplorePage>
|
||||
tabs: pages.map((e) => buildTab(e)).toList(),
|
||||
controller: controller,
|
||||
),
|
||||
);
|
||||
).paddingTop(context.padding.top);
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
|
@@ -1,12 +1,10 @@
|
||||
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';
|
||||
part of 'favorites_page.dart';
|
||||
|
||||
/// Open a dialog to create a new favorite folder.
|
||||
Future<void> newFolder() async {
|
||||
return showDialog(context: App.rootContext, builder: (context) {
|
||||
return showDialog(
|
||||
context: App.rootContext,
|
||||
builder: (context) {
|
||||
var controller = TextEditingController();
|
||||
var folders = LocalFavoritesManager().folderNames;
|
||||
String? error;
|
||||
@@ -23,7 +21,7 @@ Future<void> newFolder() async {
|
||||
errorText: error,
|
||||
),
|
||||
onChanged: (s) {
|
||||
if(error != null) {
|
||||
if (error != null) {
|
||||
setState(() {
|
||||
error = null;
|
||||
});
|
||||
@@ -35,17 +33,10 @@ Future<void> newFolder() async {
|
||||
actions: [
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
if(controller.text.isEmpty) {
|
||||
var e = validateFolderName(controller.text);
|
||||
if (e != null) {
|
||||
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;
|
||||
error = e;
|
||||
});
|
||||
} else {
|
||||
LocalFavoritesManager().createFolder(controller.text);
|
||||
@@ -59,3 +50,66 @@ Future<void> newFolder() async {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
String? validateFolderName(String newFolderName) {
|
||||
var folders = LocalFavoritesManager().folderNames;
|
||||
if (newFolderName.isEmpty) {
|
||||
return "Folder name cannot be empty".tl;
|
||||
} else if (newFolderName.length > 50) {
|
||||
return "Folder name is too long".tl;
|
||||
} else if (folders.contains(newFolderName)) {
|
||||
return "Folder already exists".tl;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
void addFavorite(Comic comic) {
|
||||
var folders = LocalFavoritesManager().folderNames;
|
||||
|
||||
showDialog(
|
||||
context: App.rootContext,
|
||||
builder: (context) {
|
||||
String? selectedFolder;
|
||||
|
||||
return StatefulBuilder(builder: (context, setState) {
|
||||
return ContentDialog(
|
||||
title: "Select a folder".tl,
|
||||
content: ListTile(
|
||||
title: Text("Folder".tl),
|
||||
trailing: Select(
|
||||
current: selectedFolder,
|
||||
values: folders,
|
||||
minWidth: 112,
|
||||
onTap: (v) {
|
||||
setState(() {
|
||||
selectedFolder = folders[v];
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
if (selectedFolder != null) {
|
||||
LocalFavoritesManager().addComic(
|
||||
selectedFolder!,
|
||||
FavoriteItem(
|
||||
id: comic.id,
|
||||
name: comic.title,
|
||||
coverPath: comic.cover,
|
||||
author: comic.subtitle ?? '',
|
||||
type: ComicType(comic.sourceKey.hashCode),
|
||||
tags: comic.tags ?? [],
|
||||
),
|
||||
);
|
||||
context.pop();
|
||||
}
|
||||
},
|
||||
child: Text("Confirm".tl),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
158
lib/pages/favorites/favorites_page.dart
Normal file
158
lib/pages/favorites/favorites_page.dart
Normal file
@@ -0,0 +1,158 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_reorderable_grid_view/widgets/reorderable_builder.dart';
|
||||
import 'package:venera/components/components.dart';
|
||||
import 'package:venera/foundation/app.dart';
|
||||
import 'package:venera/foundation/appdata.dart';
|
||||
import 'package:venera/foundation/comic_source/comic_source.dart';
|
||||
import 'package:venera/foundation/comic_type.dart';
|
||||
import 'package:venera/foundation/favorites.dart';
|
||||
import 'package:venera/foundation/res.dart';
|
||||
import 'package:venera/utils/translations.dart';
|
||||
|
||||
part 'favorite_actions.dart';
|
||||
part 'side_bar.dart';
|
||||
part 'local_favorites_page.dart';
|
||||
part 'network_favorites_page.dart';
|
||||
|
||||
const _kLeftBarWidth = 256.0;
|
||||
|
||||
const _kTwoPanelChangeWidth = 720.0;
|
||||
|
||||
class FavoritesPage extends StatefulWidget {
|
||||
const FavoritesPage({super.key});
|
||||
|
||||
@override
|
||||
State<FavoritesPage> createState() => _FavoritesPageState();
|
||||
}
|
||||
|
||||
class _FavoritesPageState extends State<FavoritesPage> {
|
||||
String? folder;
|
||||
|
||||
bool isNetwork = false;
|
||||
|
||||
FolderList? folderList;
|
||||
|
||||
void setFolder(bool isNetwork, String? folder) {
|
||||
setState(() {
|
||||
this.isNetwork = isNetwork;
|
||||
this.folder = folder;
|
||||
});
|
||||
folderList?.update();
|
||||
appdata.implicitData['favoriteFolder'] = {
|
||||
'name': folder,
|
||||
'isNetwork': isNetwork,
|
||||
};
|
||||
appdata.writeImplicitData();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
var data = appdata.implicitData['favoriteFolder'];
|
||||
if(data != null){
|
||||
folder = data['name'];
|
||||
isNetwork = data['isNetwork'] ?? false;
|
||||
}
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IconTheme(
|
||||
data: IconThemeData(color: Theme.of(context).colorScheme.secondary),
|
||||
child: Stack(
|
||||
children: [
|
||||
AnimatedPositioned(
|
||||
left: context.width <= _kTwoPanelChangeWidth ? -_kLeftBarWidth : 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: (const _LeftBar()).fixWidth(_kLeftBarWidth),
|
||||
),
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: context.width <= _kTwoPanelChangeWidth ? 0 : _kLeftBarWidth,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: buildBody(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void showFolderSelector() {
|
||||
Navigator.of(App.rootContext).push(PageRouteBuilder(
|
||||
barrierDismissible: true,
|
||||
fullscreenDialog: true,
|
||||
opaque: false,
|
||||
barrierColor: Colors.black.withOpacity(0.36),
|
||||
pageBuilder: (context, animation, secondary) {
|
||||
return Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Material(
|
||||
color: context.colorScheme.surfaceContainerLow,
|
||||
child: SizedBox(
|
||||
width: 256,
|
||||
child: _LeftBar(
|
||||
withAppbar: true,
|
||||
favPage: this,
|
||||
onSelected: () {
|
||||
context.pop();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
transitionsBuilder: (context, animation, secondary, child) {
|
||||
var offset =
|
||||
Tween<Offset>(begin: const Offset(-1, 0), end: const Offset(0, 0));
|
||||
return SlideTransition(
|
||||
position: offset.animate(CurvedAnimation(
|
||||
parent: animation,
|
||||
curve: Curves.fastOutSlowIn,
|
||||
)),
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
Widget buildBody() {
|
||||
if (folder == null) {
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
SliverAppbar(
|
||||
leading: Tooltip(
|
||||
message: "Folders".tl,
|
||||
child: context.width <= _kTwoPanelChangeWidth
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.menu),
|
||||
color: context.colorScheme.primary,
|
||||
onPressed: showFolderSelector,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
title: Text("Unselected".tl),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
if (!isNetwork) {
|
||||
return _LocalFavoritesPage(folder: folder!, key: Key(folder!));
|
||||
} else {
|
||||
var favoriteData = getFavoriteDataOrNull(folder!);
|
||||
if (favoriteData == null) {
|
||||
return const Center(child: Text("Unknown source"));
|
||||
} else {
|
||||
return NetworkFavoritePage(favoriteData, key: Key(folder!));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract interface class FolderList {
|
||||
void update();
|
||||
|
||||
void updateFolders();
|
||||
}
|
257
lib/pages/favorites/local_favorites_page.dart
Normal file
257
lib/pages/favorites/local_favorites_page.dart
Normal file
@@ -0,0 +1,257 @@
|
||||
part of 'favorites_page.dart';
|
||||
|
||||
class _LocalFavoritesPage extends StatefulWidget {
|
||||
const _LocalFavoritesPage({required this.folder, super.key});
|
||||
|
||||
final String folder;
|
||||
|
||||
@override
|
||||
State<_LocalFavoritesPage> createState() => _LocalFavoritesPageState();
|
||||
}
|
||||
|
||||
class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
|
||||
late _FavoritesPageState favPage;
|
||||
|
||||
late List<FavoriteItem> comics;
|
||||
|
||||
void updateComics() {
|
||||
setState(() {
|
||||
comics = LocalFavoritesManager().getAllComics(widget.folder);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
favPage = context.findAncestorStateOfType<_FavoritesPageState>()!;
|
||||
comics = LocalFavoritesManager().getAllComics(widget.folder);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SmoothCustomScrollView(
|
||||
slivers: [
|
||||
SliverAppbar(
|
||||
leading: Tooltip(
|
||||
message: "Folders".tl,
|
||||
child: context.width <= _kTwoPanelChangeWidth
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.menu),
|
||||
color: context.colorScheme.primary,
|
||||
onPressed: favPage.showFolderSelector,
|
||||
)
|
||||
: const SizedBox(),
|
||||
),
|
||||
title: Text(favPage.folder ?? "Unselected".tl),
|
||||
actions: [
|
||||
MenuButton(
|
||||
entries: [
|
||||
MenuEntry(
|
||||
icon: Icons.delete_outline,
|
||||
text: "Delete Folder".tl,
|
||||
onClick: () {
|
||||
showConfirmDialog(
|
||||
context: App.rootContext,
|
||||
title: "Delete".tl,
|
||||
content:
|
||||
"Are you sure you want to delete this folder?".tl,
|
||||
onConfirm: () {
|
||||
favPage.setFolder(false, null);
|
||||
LocalFavoritesManager().deleteFolder(widget.folder);
|
||||
favPage.folderList?.updateFolders();
|
||||
context.pop();
|
||||
},
|
||||
);
|
||||
}),
|
||||
MenuEntry(
|
||||
icon: Icons.edit_outlined,
|
||||
text: "Rename".tl,
|
||||
onClick: () {
|
||||
showInputDialog(
|
||||
context: App.rootContext,
|
||||
title: "Rename".tl,
|
||||
hintText: "New Name".tl,
|
||||
onConfirm: (value) {
|
||||
var err = validateFolderName(value.toString());
|
||||
if (err != null) {
|
||||
return err;
|
||||
}
|
||||
LocalFavoritesManager().rename(
|
||||
widget.folder,
|
||||
value.toString(),
|
||||
);
|
||||
favPage.folderList?.updateFolders();
|
||||
favPage.setFolder(false, value.toString());
|
||||
return null;
|
||||
},
|
||||
);
|
||||
}),
|
||||
MenuEntry(
|
||||
icon: Icons.reorder,
|
||||
text: "Reorder".tl,
|
||||
onClick: () {
|
||||
context.to(
|
||||
() {
|
||||
return _ReorderComicsPage(
|
||||
widget.folder,
|
||||
(comics) {
|
||||
this.comics = comics;
|
||||
},
|
||||
);
|
||||
},
|
||||
).then(
|
||||
(value) {
|
||||
setState(() {});
|
||||
},
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
SliverGridComics(
|
||||
comics: comics.map((e) {
|
||||
var comicSource = e.type.comicSource;
|
||||
return Comic(
|
||||
e.name,
|
||||
e.coverPath,
|
||||
e.id,
|
||||
e.author,
|
||||
e.tags,
|
||||
"${e.time} | ${comicSource?.name ?? "Unknown"}",
|
||||
comicSource?.key ?? "Unknown",
|
||||
null,
|
||||
);
|
||||
}).toList(),
|
||||
menuBuilder: (c) {
|
||||
return [
|
||||
MenuEntry(
|
||||
icon: Icons.delete_outline,
|
||||
text: "Delete".tl,
|
||||
onClick: () {
|
||||
showConfirmDialog(
|
||||
context: context,
|
||||
title: "Delete".tl,
|
||||
content: "Are you sure you want to delete this comic?".tl,
|
||||
onConfirm: () {
|
||||
LocalFavoritesManager().deleteComicWithId(
|
||||
widget.folder,
|
||||
c.id,
|
||||
ComicType(c.sourceKey.hashCode),
|
||||
);
|
||||
updateComics();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
];
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ReorderComicsPage extends StatefulWidget {
|
||||
const _ReorderComicsPage(this.name, this.onReorder);
|
||||
|
||||
final String name;
|
||||
|
||||
final void Function(List<FavoriteItem>) onReorder;
|
||||
|
||||
@override
|
||||
State<_ReorderComicsPage> createState() => _ReorderComicsPageState();
|
||||
}
|
||||
|
||||
class _ReorderComicsPageState extends State<_ReorderComicsPage> {
|
||||
final _key = GlobalKey();
|
||||
var reorderWidgetKey = UniqueKey();
|
||||
final _scrollController = ScrollController();
|
||||
late var comics = LocalFavoritesManager().getAllComics(widget.name);
|
||||
bool changed = false;
|
||||
|
||||
Color lightenColor(Color color, double lightenValue) {
|
||||
int red = (color.red + ((255 - color.red) * lightenValue)).round();
|
||||
int green = (color.green + ((255 - color.green) * lightenValue)).round();
|
||||
int blue = (color.blue + ((255 - color.blue) * lightenValue)).round();
|
||||
|
||||
return Color.fromARGB(color.alpha, red, green, blue);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (changed) {
|
||||
LocalFavoritesManager().reorder(comics, widget.name);
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var tiles = comics.map(
|
||||
(e) {
|
||||
var comicSource = e.type.comicSource;
|
||||
return ComicTile(
|
||||
key: Key(e.hashCode.toString()),
|
||||
comic: Comic(
|
||||
e.name,
|
||||
e.coverPath,
|
||||
e.id,
|
||||
e.author,
|
||||
e.tags,
|
||||
"${e.time} | ${comicSource?.name ?? "Unknown"}",
|
||||
comicSource?.key ?? "Unknown",
|
||||
null,
|
||||
),
|
||||
);
|
||||
},
|
||||
).toList();
|
||||
return Scaffold(
|
||||
appBar: Appbar(
|
||||
title: Text("Reorder".tl),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.info_outline),
|
||||
onPressed: () {
|
||||
showInfoDialog(
|
||||
context: context,
|
||||
title: "Reorder".tl,
|
||||
content: "Long press and drag to reorder.".tl,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: ReorderableBuilder(
|
||||
key: reorderWidgetKey,
|
||||
scrollController: _scrollController,
|
||||
longPressDelay: App.isDesktop
|
||||
? const Duration(milliseconds: 100)
|
||||
: const Duration(milliseconds: 500),
|
||||
onReorder: (reorderFunc) {
|
||||
changed = true;
|
||||
setState(() {
|
||||
comics = reorderFunc(comics) as List<FavoriteItem>;
|
||||
});
|
||||
widget.onReorder(comics);
|
||||
},
|
||||
dragChildBoxDecoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
color: lightenColor(
|
||||
Theme.of(context).splashColor.withOpacity(1),
|
||||
0.2,
|
||||
),
|
||||
),
|
||||
builder: (children) {
|
||||
return GridView(
|
||||
key: _key,
|
||||
controller: _scrollController,
|
||||
gridDelegate: SliverGridDelegateWithComics(),
|
||||
children: children,
|
||||
);
|
||||
},
|
||||
children: tiles,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
433
lib/pages/favorites/network_favorites_page.dart
Normal file
433
lib/pages/favorites/network_favorites_page.dart
Normal file
@@ -0,0 +1,433 @@
|
||||
part of 'favorites_page.dart';
|
||||
|
||||
class NetworkFavoritePage extends StatelessWidget {
|
||||
const NetworkFavoritePage(this.data, {super.key});
|
||||
|
||||
final FavoriteData data;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return data.multiFolder
|
||||
? _MultiFolderFavoritesPage(data)
|
||||
: _NormalFavoritePage(data);
|
||||
}
|
||||
}
|
||||
|
||||
class _NormalFavoritePage extends StatelessWidget {
|
||||
const _NormalFavoritePage(this.data);
|
||||
|
||||
final FavoriteData data;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ComicList(
|
||||
leadingSliver: SliverAppbar(
|
||||
leading: Tooltip(
|
||||
message: "Folders".tl,
|
||||
child: context.width <= _kTwoPanelChangeWidth
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.menu),
|
||||
color: context.colorScheme.primary,
|
||||
onPressed: context
|
||||
.findAncestorStateOfType<_FavoritesPageState>()!
|
||||
.showFolderSelector,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
title: Text(data.title),
|
||||
),
|
||||
errorLeading: Appbar(
|
||||
leading: Tooltip(
|
||||
message: "Folders".tl,
|
||||
child: context.width <= _kTwoPanelChangeWidth
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.menu),
|
||||
color: context.colorScheme.primary,
|
||||
onPressed: context
|
||||
.findAncestorStateOfType<_FavoritesPageState>()!
|
||||
.showFolderSelector,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
title: Text(data.title),
|
||||
),
|
||||
loadPage: (i) => data.loadComic(i),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MultiFolderFavoritesPage extends StatefulWidget {
|
||||
const _MultiFolderFavoritesPage(this.data);
|
||||
|
||||
final FavoriteData data;
|
||||
|
||||
@override
|
||||
State<_MultiFolderFavoritesPage> createState() =>
|
||||
_MultiFolderFavoritesPageState();
|
||||
}
|
||||
|
||||
class _MultiFolderFavoritesPageState extends State<_MultiFolderFavoritesPage> {
|
||||
bool _loading = true;
|
||||
|
||||
String? _errorMessage;
|
||||
|
||||
Map<String, String>? folders;
|
||||
|
||||
void loadPage() async {
|
||||
var res = await widget.data.loadFolders!();
|
||||
_loading = false;
|
||||
if (res.error) {
|
||||
setState(() {
|
||||
_errorMessage = res.errorMessage;
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
folders = res.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void openFolder(String key, String title) {
|
||||
context.to(() => _FavoriteFolder(widget.data, key, title));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var sliverAppBar = SliverAppbar(
|
||||
leading: Tooltip(
|
||||
message: "Folders".tl,
|
||||
child: context.width <= _kTwoPanelChangeWidth
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.menu),
|
||||
color: context.colorScheme.primary,
|
||||
onPressed: context
|
||||
.findAncestorStateOfType<_FavoritesPageState>()!
|
||||
.showFolderSelector,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
title: Text(widget.data.title),
|
||||
);
|
||||
|
||||
var appBar = Appbar(
|
||||
leading: Tooltip(
|
||||
message: "Folders".tl,
|
||||
child: context.width <= _kTwoPanelChangeWidth
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.menu),
|
||||
color: context.colorScheme.primary,
|
||||
onPressed: context
|
||||
.findAncestorStateOfType<_FavoritesPageState>()!
|
||||
.showFolderSelector,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
title: Text(widget.data.title),
|
||||
);
|
||||
|
||||
if (_loading) {
|
||||
loadPage();
|
||||
return Column(
|
||||
children: [
|
||||
appBar,
|
||||
const Expanded(
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
} else if (_errorMessage != null) {
|
||||
return Column(
|
||||
children: [
|
||||
appBar,
|
||||
Expanded(
|
||||
child: NetworkError(
|
||||
message: _errorMessage!,
|
||||
withAppbar: false,
|
||||
retry: () {
|
||||
setState(() {
|
||||
_loading = true;
|
||||
_errorMessage = null;
|
||||
});
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
} else {
|
||||
var length = folders!.length;
|
||||
if (widget.data.allFavoritesId != null) length++;
|
||||
final keys = folders!.keys.toList();
|
||||
|
||||
return SmoothCustomScrollView(
|
||||
slivers: [
|
||||
sliverAppBar,
|
||||
SliverGridViewWithFixedItemHeight(
|
||||
delegate:
|
||||
SliverChildBuilderDelegate(childCount: length, (context, i) {
|
||||
if (widget.data.allFavoritesId != null) {
|
||||
if (i == 0) {
|
||||
return _FolderTile(
|
||||
name: "All".tl,
|
||||
onTap: () =>
|
||||
openFolder(widget.data.allFavoritesId!, "All".tl));
|
||||
} else {
|
||||
i--;
|
||||
return _FolderTile(
|
||||
name: folders![keys[i]]!,
|
||||
onTap: () => openFolder(keys[i], folders![keys[i]]!),
|
||||
deleteFolder: widget.data.deleteFolder == null
|
||||
? null
|
||||
: () => widget.data.deleteFolder!(keys[i]),
|
||||
updateState: () => setState(() {
|
||||
_loading = true;
|
||||
}),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return _FolderTile(
|
||||
name: folders![keys[i]]!,
|
||||
onTap: () => openFolder(keys[i], folders![keys[i]]!),
|
||||
deleteFolder: widget.data.deleteFolder == null
|
||||
? null
|
||||
: () => widget.data.deleteFolder!(keys[i]),
|
||||
updateState: () => setState(() {
|
||||
_loading = true;
|
||||
}),
|
||||
);
|
||||
}
|
||||
}),
|
||||
maxCrossAxisExtent: 450,
|
||||
itemHeight: 64,
|
||||
),
|
||||
if (widget.data.addFolder != null)
|
||||
SliverToBoxAdapter(
|
||||
child: SizedBox(
|
||||
height: 60,
|
||||
width: double.infinity,
|
||||
child: Center(
|
||||
child: TextButton(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text("Create a folder".tl),
|
||||
const Icon(
|
||||
Icons.add,
|
||||
size: 18,
|
||||
),
|
||||
],
|
||||
),
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return _CreateFolderDialog(
|
||||
widget.data,
|
||||
() => setState(() {
|
||||
_loading = true;
|
||||
}));
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _FolderTile extends StatelessWidget {
|
||||
const _FolderTile(
|
||||
{required this.name,
|
||||
required this.onTap,
|
||||
this.deleteFolder,
|
||||
this.updateState});
|
||||
|
||||
final String name;
|
||||
|
||||
final Future<Res<bool>> Function()? deleteFolder;
|
||||
|
||||
final void Function()? updateState;
|
||||
|
||||
final void Function() onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(8, 8, 16, 8),
|
||||
child: Row(
|
||||
children: [
|
||||
const SizedBox(
|
||||
width: 16,
|
||||
),
|
||||
Icon(
|
||||
Icons.folder,
|
||||
size: 35,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
const SizedBox(
|
||||
width: 16,
|
||||
),
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
name,
|
||||
style: const TextStyle(
|
||||
fontSize: 16, fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (deleteFolder != null)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete_forever_outlined),
|
||||
onPressed: () => onDeleteFolder(context),
|
||||
)
|
||||
else
|
||||
const Icon(Icons.arrow_right),
|
||||
if (deleteFolder == null)
|
||||
const SizedBox(
|
||||
width: 8,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void onDeleteFolder(BuildContext context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
bool loading = false;
|
||||
return StatefulBuilder(builder: (context, setState) {
|
||||
return ContentDialog(
|
||||
title: "Delete".tl,
|
||||
content: Text("Are you sure you want to delete this folder?".tl),
|
||||
actions: [
|
||||
Button.filled(
|
||||
isLoading: loading,
|
||||
color: context.colorScheme.error,
|
||||
onPressed: () async {
|
||||
setState(() {
|
||||
loading = true;
|
||||
});
|
||||
var res = await deleteFolder!();
|
||||
if (res.success) {
|
||||
context.showMessage(message: "Deleted".tl);
|
||||
context.pop();
|
||||
updateState?.call();
|
||||
} else {
|
||||
setState(() {
|
||||
loading = false;
|
||||
});
|
||||
context.showMessage(message: res.errorMessage!);
|
||||
}
|
||||
},
|
||||
child: Text("Confirm".tl),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CreateFolderDialog extends StatefulWidget {
|
||||
const _CreateFolderDialog(this.data, this.updateState);
|
||||
|
||||
final FavoriteData data;
|
||||
|
||||
final void Function() updateState;
|
||||
|
||||
@override
|
||||
State<_CreateFolderDialog> createState() => _CreateFolderDialogState();
|
||||
}
|
||||
|
||||
class _CreateFolderDialogState extends State<_CreateFolderDialog> {
|
||||
var controller = TextEditingController();
|
||||
bool loading = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SimpleDialog(
|
||||
title: Text("Create a folder".tl),
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 0, 20, 0),
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: "name".tl,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 200,
|
||||
height: 10,
|
||||
),
|
||||
if (loading)
|
||||
const SizedBox(
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
)
|
||||
else
|
||||
SizedBox(
|
||||
height: 35,
|
||||
child: Center(
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
loading = true;
|
||||
});
|
||||
widget.data.addFolder!(controller.text).then((b) {
|
||||
if (b.error) {
|
||||
context.showMessage(message: b.errorMessage!);
|
||||
setState(() {
|
||||
loading = false;
|
||||
});
|
||||
} else {
|
||||
context.pop();
|
||||
context.showMessage(
|
||||
message: "Created successfully".tl);
|
||||
widget.updateState();
|
||||
}
|
||||
});
|
||||
},
|
||||
child: Text("Submit".tl)),
|
||||
))
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FavoriteFolder extends StatelessWidget {
|
||||
const _FavoriteFolder(this.data, this.folderID, this.title);
|
||||
|
||||
final FavoriteData data;
|
||||
|
||||
final String folderID;
|
||||
|
||||
final String title;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ComicList(
|
||||
leadingSliver: SliverAppbar(
|
||||
title: Text(title),
|
||||
),
|
||||
loadPage: (i) => data.loadComic(i, folderID),
|
||||
);
|
||||
}
|
||||
}
|
208
lib/pages/favorites/side_bar.dart
Normal file
208
lib/pages/favorites/side_bar.dart
Normal file
@@ -0,0 +1,208 @@
|
||||
part of 'favorites_page.dart';
|
||||
|
||||
class _LeftBar extends StatefulWidget {
|
||||
const _LeftBar({this.favPage, this.onSelected, this.withAppbar = false});
|
||||
|
||||
final _FavoritesPageState? favPage;
|
||||
|
||||
final VoidCallback? onSelected;
|
||||
|
||||
final bool withAppbar;
|
||||
|
||||
@override
|
||||
State<_LeftBar> createState() => _LeftBarState();
|
||||
}
|
||||
|
||||
class _LeftBarState extends State<_LeftBar> implements FolderList {
|
||||
late _FavoritesPageState favPage;
|
||||
|
||||
var folders = <String>[];
|
||||
|
||||
var networkFolders = <String>[];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
favPage = widget.favPage ??
|
||||
context.findAncestorStateOfType<_FavoritesPageState>()!;
|
||||
favPage.folderList = this;
|
||||
folders = LocalFavoritesManager().folderNames;
|
||||
networkFolders = ComicSource.all()
|
||||
.where((e) => e.favoriteData != null)
|
||||
.map((e) => e.favoriteData!.key)
|
||||
.toList();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
right: BorderSide(
|
||||
color: context.colorScheme.outlineVariant,
|
||||
width: 0.6,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
if (widget.withAppbar)
|
||||
SizedBox(
|
||||
height: 56,
|
||||
child: Row(
|
||||
children: [
|
||||
const SizedBox(width: 8),
|
||||
const CloseButton(),
|
||||
const SizedBox(width: 8),
|
||||
Text("Folders".tl, style: ts.s18,),
|
||||
],
|
||||
),
|
||||
).paddingTop(context.padding.top),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
padding: widget.withAppbar ? EdgeInsets.zero : EdgeInsets.only(top: context.padding.top),
|
||||
itemCount: folders.length + networkFolders.length + 2,
|
||||
itemBuilder: (context, index) {
|
||||
if (index == 0) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
const SizedBox(width: 16),
|
||||
const Icon(Icons.local_activity),
|
||||
const SizedBox(width: 8),
|
||||
Text("Local".tl),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add),
|
||||
color: context.colorScheme.primary,
|
||||
onPressed: () {
|
||||
newFolder().then((value) {
|
||||
setState(() {
|
||||
folders = LocalFavoritesManager().folderNames;
|
||||
});
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
index--;
|
||||
if (index < folders.length) {
|
||||
return buildLocalFolder(folders[index]);
|
||||
}
|
||||
index -= folders.length;
|
||||
if (index == 0) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
const SizedBox(width: 16),
|
||||
const Icon(Icons.cloud),
|
||||
const SizedBox(width: 8),
|
||||
Text("Network".tl),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
index--;
|
||||
return buildNetworkFolder(networkFolders[index]);
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildLocalFolder(String name) {
|
||||
bool isSelected = name == favPage.folder && !favPage.isNetwork;
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
if (isSelected) {
|
||||
return;
|
||||
}
|
||||
favPage.setFolder(false, name);
|
||||
widget.onSelected?.call();
|
||||
},
|
||||
child: Container(
|
||||
height: 42,
|
||||
alignment: Alignment.centerLeft,
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? context.colorScheme.primaryContainer.withOpacity(0.36)
|
||||
: null,
|
||||
border: Border(
|
||||
left: BorderSide(
|
||||
color:
|
||||
isSelected ? context.colorScheme.primary : Colors.transparent,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.only(left: 16),
|
||||
child: Text(name),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildNetworkFolder(String key) {
|
||||
var data = getFavoriteDataOrNull(key);
|
||||
if (data == null) {
|
||||
return const SizedBox();
|
||||
}
|
||||
bool isSelected = key == favPage.folder && favPage.isNetwork;
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
if (isSelected) {
|
||||
return;
|
||||
}
|
||||
favPage.setFolder(true, key);
|
||||
widget.onSelected?.call();
|
||||
},
|
||||
child: Container(
|
||||
height: 42,
|
||||
alignment: Alignment.centerLeft,
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? context.colorScheme.primaryContainer.withOpacity(0.36)
|
||||
: null,
|
||||
border: Border(
|
||||
left: BorderSide(
|
||||
color:
|
||||
isSelected ? context.colorScheme.primary : Colors.transparent,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.only(left: 16),
|
||||
child: Text(data.title),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void update() {
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
void updateFolders() {
|
||||
setState(() {
|
||||
folders = LocalFavoritesManager().folderNames;
|
||||
networkFolders = ComicSource.all()
|
||||
.where((e) => e.favoriteData != null)
|
||||
.map((e) => e.favoriteData!.key)
|
||||
.toList();
|
||||
});
|
||||
}
|
||||
}
|
@@ -1,10 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class FavoritesPage extends StatelessWidget {
|
||||
const FavoritesPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Placeholder();
|
||||
}
|
||||
}
|
@@ -22,13 +22,15 @@ class HomePage extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var widget = const SmoothCustomScrollView(
|
||||
var widget = SmoothCustomScrollView(
|
||||
slivers: [
|
||||
_SearchBar(),
|
||||
_History(),
|
||||
_Local(),
|
||||
_ComicSourceWidget(),
|
||||
_AccountsWidget(),
|
||||
SliverPadding(padding: EdgeInsets.only(top: context.padding.top)),
|
||||
const _SearchBar(),
|
||||
const _History(),
|
||||
const _Local(),
|
||||
const _ComicSourceWidget(),
|
||||
const _AccountsWidget(),
|
||||
SliverPadding(padding: EdgeInsets.only(top: context.padding.bottom)),
|
||||
],
|
||||
);
|
||||
return context.width > changePoint ? widget.paddingHorizontal(8) : widget;
|
||||
|
@@ -8,7 +8,7 @@ import '../components/components.dart';
|
||||
import '../foundation/app.dart';
|
||||
import '../foundation/app_page_route.dart';
|
||||
import 'explore_page.dart';
|
||||
import 'favorites_page.dart';
|
||||
import 'favorites/favorites_page.dart';
|
||||
import 'home_page.dart';
|
||||
|
||||
class MainPage extends StatefulWidget {
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
Reference in New Issue
Block a user