mirror of
https://github.com/venera-app/venera.git
synced 2025-09-27 07:47:24 +00:00
favorites page
This commit is contained in:
@@ -342,4 +342,34 @@ 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);
|
App.rootContext.showMessage(message: 'Title copied'.tl);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
MenuEntry(
|
||||||
|
icon: Icons.stars_outlined,
|
||||||
|
text: 'Add to favorites'.tl,
|
||||||
|
onClick: () {
|
||||||
|
addFavorite(comic);
|
||||||
|
},
|
||||||
|
),
|
||||||
...?menuOptions,
|
...?menuOptions,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
@@ -23,6 +23,7 @@ import 'package:venera/foundation/local.dart';
|
|||||||
import 'package:venera/foundation/res.dart';
|
import 'package:venera/foundation/res.dart';
|
||||||
import 'package:venera/foundation/state_controller.dart';
|
import 'package:venera/foundation/state_controller.dart';
|
||||||
import 'package:venera/pages/comic_page.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/ext.dart';
|
||||||
import 'package:venera/utils/translations.dart';
|
import 'package:venera/utils/translations.dart';
|
||||||
|
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
part of "components.dart";
|
part of "components.dart";
|
||||||
|
|
||||||
void showMenuX(BuildContext context, Offset location, List<MenuEntry> entries) {
|
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> {
|
class _MenuRoute<T> extends PopupRoute<T> {
|
||||||
|
@@ -1,12 +1,17 @@
|
|||||||
part of "components.dart";
|
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(
|
var newEntry = OverlayEntry(
|
||||||
builder: (context) => _ToastOverlay(
|
builder: (context) => _ToastOverlay(
|
||||||
message: message,
|
message: message,
|
||||||
icon: icon,
|
icon: icon,
|
||||||
trailing: trailing,
|
trailing: trailing,
|
||||||
));
|
));
|
||||||
|
|
||||||
var state = context.findAncestorStateOfType<OverlayWidgetState>();
|
var state = context.findAncestorStateOfType<OverlayWidgetState>();
|
||||||
|
|
||||||
@@ -36,9 +41,11 @@ class _ToastOverlay extends StatelessWidget {
|
|||||||
color: Theme.of(context).colorScheme.inverseSurface,
|
color: Theme.of(context).colorScheme.inverseSurface,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
elevation: 2,
|
elevation: 2,
|
||||||
textStyle: ts.withColor(Theme.of(context).colorScheme.onInverseSurface),
|
textStyle:
|
||||||
|
ts.withColor(Theme.of(context).colorScheme.onInverseSurface),
|
||||||
child: IconTheme(
|
child: IconTheme(
|
||||||
data: IconThemeData(color: Theme.of(context).colorScheme.onInverseSurface),
|
data: IconThemeData(
|
||||||
|
color: Theme.of(context).colorScheme.onInverseSurface),
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 16),
|
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 16),
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -121,23 +128,28 @@ void showDialogMessage(BuildContext context, String title, String message) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void showConfirmDialog(BuildContext context, String title, String content,
|
void showConfirmDialog({
|
||||||
void Function() onConfirm) {
|
required BuildContext context,
|
||||||
|
required String title,
|
||||||
|
required String content,
|
||||||
|
required void Function() onConfirm,
|
||||||
|
}) {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => ContentDialog(
|
||||||
title: Text(title),
|
title: title,
|
||||||
content: Text(content),
|
content: Text(content).paddingHorizontal(16).paddingVertical(8),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(onPressed: context.pop, child: Text("Cancel".tl)),
|
FilledButton(
|
||||||
TextButton(
|
onPressed: () {
|
||||||
onPressed: () {
|
context.pop();
|
||||||
context.pop();
|
onConfirm();
|
||||||
onConfirm();
|
},
|
||||||
},
|
child: Text("Confirm".tl),
|
||||||
child: Text("Confirm".tl)),
|
),
|
||||||
],
|
],
|
||||||
));
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class LoadingDialogController {
|
class LoadingDialogController {
|
||||||
@@ -234,6 +246,7 @@ class ContentDialog extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var content = Column(
|
var content = Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Appbar(
|
Appbar(
|
||||||
leading: IconButton(
|
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;
|
controller.value = target;
|
||||||
} else {
|
} else {
|
||||||
StateController.findOrNull<NaviPaddingWidgetController>()
|
StateController.findOrNull<NaviPaddingWidgetController>()
|
||||||
?.setWithPadding(true, false, false);
|
?.setWithPadding(false, false, false);
|
||||||
controller.animateTo(target);
|
controller.animateTo(target);
|
||||||
}
|
}
|
||||||
animationTarget = target;
|
animationTarget = target;
|
||||||
@@ -555,7 +555,15 @@ class _SingleBottomNaviWidgetState extends State<_SingleBottomNaviWidget>
|
|||||||
class NaviObserver extends NavigatorObserver implements Listenable {
|
class NaviObserver extends NavigatorObserver implements Listenable {
|
||||||
var routes = Queue<Route>();
|
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
|
@override
|
||||||
void didPop(Route route, Route? previousRoute) {
|
void didPop(Route route, Route? previousRoute) {
|
||||||
@@ -693,7 +701,12 @@ class NaviPaddingWidget extends StatelessWidget {
|
|||||||
: 0),
|
: 0),
|
||||||
)
|
)
|
||||||
: EdgeInsets.zero,
|
: EdgeInsets.zero,
|
||||||
child: child,
|
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.current,
|
||||||
required this.values,
|
required this.values,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
|
this.minWidth,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String current;
|
final String? current;
|
||||||
|
|
||||||
final List<String> values;
|
final List<String> values;
|
||||||
|
|
||||||
final void Function(int index)? onTap;
|
final void Function(int index)? onTap;
|
||||||
|
|
||||||
|
final double? minWidth;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
return Container(
|
||||||
@@ -58,7 +61,12 @@ class Select extends StatelessWidget {
|
|||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
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),
|
const SizedBox(width: 8),
|
||||||
Icon(Icons.arrow_drop_down, color: context.colorScheme.primary),
|
Icon(Icons.arrow_drop_down, color: context.colorScheme.primary),
|
||||||
],
|
],
|
||||||
|
@@ -47,8 +47,9 @@ class _Appdata {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> init() async {
|
Future<void> init() async {
|
||||||
|
var dataPath = (await getApplicationSupportDirectory()).path;
|
||||||
var file = File(FilePath.join(
|
var file = File(FilePath.join(
|
||||||
(await getApplicationSupportDirectory()).path,
|
dataPath,
|
||||||
'appdata.json',
|
'appdata.json',
|
||||||
));
|
));
|
||||||
if (!await file.exists()) {
|
if (!await file.exists()) {
|
||||||
@@ -61,6 +62,10 @@ class _Appdata {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
searchHistory = List.from(json['searchHistory']);
|
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() {
|
Map<String, dynamic> toJson() {
|
||||||
@@ -69,6 +74,13 @@ class _Appdata {
|
|||||||
'searchHistory': searchHistory,
|
'searchHistory': searchHistory,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var implicitData = <String, dynamic>{};
|
||||||
|
|
||||||
|
void writeImplicitData() {
|
||||||
|
var file = File(FilePath.join(App.dataPath, 'implicitData.json'));
|
||||||
|
file.writeAsString(jsonEncode(implicitData));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final appdata = _Appdata();
|
final appdata = _Appdata();
|
||||||
|
@@ -319,9 +319,6 @@ class LocalFavoritesManager {
|
|||||||
/// delete a folder
|
/// delete a folder
|
||||||
void deleteFolder(String name) {
|
void deleteFolder(String name) {
|
||||||
_modifiedAfterLastCache = true;
|
_modifiedAfterLastCache = true;
|
||||||
_db.execute("""
|
|
||||||
delete from folder_sync where folder_name == ?;
|
|
||||||
""", [name]);
|
|
||||||
_db.execute("""
|
_db.execute("""
|
||||||
drop table "$name";
|
drop table "$name";
|
||||||
""");
|
""");
|
||||||
|
@@ -47,7 +47,7 @@ class CategoriesPage extends StatelessWidget {
|
|||||||
key: Key(e),
|
key: Key(e),
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
),
|
).paddingTop(context.padding.top),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TabBarView(
|
child: TabBarView(
|
||||||
children:
|
children:
|
||||||
|
@@ -8,7 +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/pages/favorites/favorites_page.dart';
|
||||||
import 'package:venera/pages/reader/reader.dart';
|
import 'package:venera/pages/reader/reader.dart';
|
||||||
import 'package:venera/utils/translations.dart';
|
import 'package:venera/utils/translations.dart';
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
@@ -247,7 +247,7 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
child: SelectableText(comic.description!),
|
child: SelectableText(comic.description!).fixWidth(double.infinity),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
|
@@ -49,12 +49,17 @@ class ComicSourcePage extends StatefulWidget {
|
|||||||
msg += "${ComicSource.find(key)?.name}: v${versions[key]}\n";
|
msg += "${ComicSource.find(key)?.name}: v${versions[key]}\n";
|
||||||
}
|
}
|
||||||
msg = msg.trim();
|
msg = msg.trim();
|
||||||
showConfirmDialog(App.rootContext, "Updates Available".tl, msg, () {
|
showConfirmDialog(
|
||||||
for (var key in shouldUpdate) {
|
context: App.rootContext,
|
||||||
var source = ComicSource.find(key);
|
title: "Updates Available".tl,
|
||||||
_BodyState.update(source!);
|
content: msg,
|
||||||
}
|
onConfirm: () {
|
||||||
});
|
for (var key in shouldUpdate) {
|
||||||
|
var source = ComicSource.find(key);
|
||||||
|
_BodyState.update(source!);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -149,10 +154,10 @@ class _BodyState extends State<_Body> {
|
|||||||
|
|
||||||
void delete(ComicSource source) {
|
void delete(ComicSource source) {
|
||||||
showConfirmDialog(
|
showConfirmDialog(
|
||||||
App.rootContext,
|
context: App.rootContext,
|
||||||
"Delete".tl,
|
title: "Delete".tl,
|
||||||
"Are you sure you want to delete it?".tl,
|
content: "Are you sure you want to delete it?".tl,
|
||||||
() {
|
onConfirm: () {
|
||||||
var file = File(source.filePath);
|
var file = File(source.filePath);
|
||||||
file.delete();
|
file.delete();
|
||||||
ComicSource.remove(source.key);
|
ComicSource.remove(source.key);
|
||||||
@@ -243,7 +248,8 @@ class _BodyState extends State<_Body> {
|
|||||||
.paddingBottom(32),
|
.paddingBottom(32),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
TextButton(onPressed: selectFile, child: Text("Select file".tl))
|
TextButton(
|
||||||
|
onPressed: selectFile, child: Text("Select file".tl))
|
||||||
.paddingLeft(8),
|
.paddingLeft(8),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
TextButton(
|
TextButton(
|
||||||
|
@@ -64,7 +64,7 @@ class _ExplorePageState extends State<ExplorePage>
|
|||||||
tabs: pages.map((e) => buildTab(e)).toList(),
|
tabs: pages.map((e) => buildTab(e)).toList(),
|
||||||
controller: controller,
|
controller: controller,
|
||||||
),
|
),
|
||||||
);
|
).paddingTop(context.padding.top);
|
||||||
|
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
|
@@ -1,61 +1,115 @@
|
|||||||
import 'package:flutter/material.dart';
|
part of 'favorites_page.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.
|
/// Open a dialog to create a new favorite folder.
|
||||||
Future<void> newFolder() async {
|
Future<void> newFolder() async {
|
||||||
return showDialog(context: App.rootContext, builder: (context) {
|
return showDialog(
|
||||||
var controller = TextEditingController();
|
context: App.rootContext,
|
||||||
var folders = LocalFavoritesManager().folderNames;
|
builder: (context) {
|
||||||
String? error;
|
var controller = TextEditingController();
|
||||||
|
var folders = LocalFavoritesManager().folderNames;
|
||||||
|
String? error;
|
||||||
|
|
||||||
return StatefulBuilder(builder: (context, setState) {
|
return StatefulBuilder(builder: (context, setState) {
|
||||||
return ContentDialog(
|
return ContentDialog(
|
||||||
title: "New Folder".tl,
|
title: "New Folder".tl,
|
||||||
content: Column(
|
content: Column(
|
||||||
children: [
|
children: [
|
||||||
TextField(
|
TextField(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: "Folder Name".tl,
|
hintText: "Folder Name".tl,
|
||||||
errorText: error,
|
errorText: error,
|
||||||
|
),
|
||||||
|
onChanged: (s) {
|
||||||
|
if (error != null) {
|
||||||
|
setState(() {
|
||||||
|
error = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
).paddingHorizontal(16),
|
||||||
|
actions: [
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () {
|
||||||
|
var e = validateFolderName(controller.text);
|
||||||
|
if (e != null) {
|
||||||
|
setState(() {
|
||||||
|
error = e;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
LocalFavoritesManager().createFolder(controller.text);
|
||||||
|
context.pop();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Text("Create".tl),
|
||||||
),
|
),
|
||||||
onChanged: (s) {
|
],
|
||||||
if(error != null) {
|
);
|
||||||
setState(() {
|
});
|
||||||
error = null;
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
).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),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var widget = const SmoothCustomScrollView(
|
var widget = SmoothCustomScrollView(
|
||||||
slivers: [
|
slivers: [
|
||||||
_SearchBar(),
|
SliverPadding(padding: EdgeInsets.only(top: context.padding.top)),
|
||||||
_History(),
|
const _SearchBar(),
|
||||||
_Local(),
|
const _History(),
|
||||||
_ComicSourceWidget(),
|
const _Local(),
|
||||||
_AccountsWidget(),
|
const _ComicSourceWidget(),
|
||||||
|
const _AccountsWidget(),
|
||||||
|
SliverPadding(padding: EdgeInsets.only(top: context.padding.bottom)),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
return context.width > changePoint ? widget.paddingHorizontal(8) : widget;
|
return context.width > changePoint ? widget.paddingHorizontal(8) : widget;
|
||||||
|
@@ -8,7 +8,7 @@ import '../components/components.dart';
|
|||||||
import '../foundation/app.dart';
|
import '../foundation/app.dart';
|
||||||
import '../foundation/app_page_route.dart';
|
import '../foundation/app_page_route.dart';
|
||||||
import 'explore_page.dart';
|
import 'explore_page.dart';
|
||||||
import 'favorites_page.dart';
|
import 'favorites/favorites_page.dart';
|
||||||
import 'home_page.dart';
|
import 'home_page.dart';
|
||||||
|
|
||||||
class MainPage extends StatefulWidget {
|
class MainPage extends StatefulWidget {
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
Reference in New Issue
Block a user