mirror of
https://github.com/venera-app/venera.git
synced 2025-09-27 07:47:24 +00:00
settings page
This commit is contained in:
BIN
assets/app_icon.png
Normal file
BIN
assets/app_icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 62 KiB |
@@ -83,7 +83,7 @@ class _AppbarState extends State<Appbar> {
|
|||||||
message: "Back".tl,
|
message: "Back".tl,
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
icon: const Icon(Icons.arrow_back),
|
icon: const Icon(Icons.arrow_back),
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.maybePop(context),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
@@ -187,7 +187,7 @@ class _MySliverAppBarDelegate extends SliverPersistentHeaderDelegate {
|
|||||||
message: "Back".tl,
|
message: "Back".tl,
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
icon: const Icon(Icons.arrow_back),
|
icon: const Icon(Icons.arrow_back),
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.maybePop(context),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: const SizedBox()),
|
: const SizedBox()),
|
||||||
@@ -322,7 +322,9 @@ class _FilledTabBarState extends State<FilledTabBar> {
|
|||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
controller: controller,
|
controller: controller,
|
||||||
physics: physics,
|
physics: physics is BouncingScrollPhysics
|
||||||
|
? const ClampingScrollPhysics()
|
||||||
|
: physics,
|
||||||
child: CustomPaint(
|
child: CustomPaint(
|
||||||
painter: painter,
|
painter: painter,
|
||||||
child: _TabRow(
|
child: _TabRow(
|
||||||
|
@@ -60,7 +60,7 @@ class Select extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
Text(current, style: ts.s14),
|
Text(current, style: ts.s14),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
const Icon(Icons.arrow_drop_down),
|
Icon(Icons.arrow_drop_down, color: context.colorScheme.primary),
|
||||||
],
|
],
|
||||||
).padding(const EdgeInsets.symmetric(horizontal: 12, vertical: 4)),
|
).padding(const EdgeInsets.symmetric(horizontal: 12, vertical: 4)),
|
||||||
),
|
),
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:venera/foundation/app.dart';
|
import 'package:venera/foundation/app.dart';
|
||||||
import 'package:venera/utils/io.dart';
|
import 'package:venera/utils/io.dart';
|
||||||
|
|
||||||
@@ -11,7 +12,7 @@ class _Appdata {
|
|||||||
bool _isSavingData = false;
|
bool _isSavingData = false;
|
||||||
|
|
||||||
Future<void> saveData() async {
|
Future<void> saveData() async {
|
||||||
if(_isSavingData) {
|
if (_isSavingData) {
|
||||||
await Future.doWhile(() async {
|
await Future.doWhile(() async {
|
||||||
await Future.delayed(const Duration(milliseconds: 20));
|
await Future.delayed(const Duration(milliseconds: 20));
|
||||||
return _isSavingData;
|
return _isSavingData;
|
||||||
@@ -25,11 +26,11 @@ class _Appdata {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void addSearchHistory(String keyword) {
|
void addSearchHistory(String keyword) {
|
||||||
if(searchHistory.contains(keyword)) {
|
if (searchHistory.contains(keyword)) {
|
||||||
searchHistory.remove(keyword);
|
searchHistory.remove(keyword);
|
||||||
}
|
}
|
||||||
searchHistory.insert(0, keyword);
|
searchHistory.insert(0, keyword);
|
||||||
if(searchHistory.length > 50) {
|
if (searchHistory.length > 50) {
|
||||||
searchHistory.removeLast();
|
searchHistory.removeLast();
|
||||||
}
|
}
|
||||||
saveData();
|
saveData();
|
||||||
@@ -46,13 +47,16 @@ class _Appdata {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> init() async {
|
Future<void> init() async {
|
||||||
var file = File(FilePath.join(App.dataPath, 'appdata.json'));
|
var file = File(FilePath.join(
|
||||||
if(!await file.exists()) {
|
(await getApplicationSupportDirectory()).path,
|
||||||
|
'appdata.json',
|
||||||
|
));
|
||||||
|
if (!await file.exists()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var json = jsonDecode(await file.readAsString());
|
var json = jsonDecode(await file.readAsString());
|
||||||
for(var key in (json['settings'] as Map<String, dynamic>).keys) {
|
for (var key in (json['settings'] as Map<String, dynamic>).keys) {
|
||||||
if(json['settings'][key] != null) {
|
if (json['settings'][key] != null) {
|
||||||
settings[key] = json['settings'][key];
|
settings[key] = json['settings'][key];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -74,7 +78,7 @@ class _Settings {
|
|||||||
|
|
||||||
final _data = <String, dynamic>{
|
final _data = <String, dynamic>{
|
||||||
'comicDisplayMode': 'detailed', // detailed, brief
|
'comicDisplayMode': 'detailed', // detailed, brief
|
||||||
'comicTileScale': 1.0, // 0.8-1.2
|
'comicTileScale': 1.00, // 0.75-1.25
|
||||||
'color': 'blue', // red, pink, purple, green, orange, blue
|
'color': 'blue', // red, pink, purple, green, orange, blue
|
||||||
'theme_mode': 'system', // light, dark, system
|
'theme_mode': 'system', // light, dark, system
|
||||||
'newFavoriteAddTo': 'end', // start, end
|
'newFavoriteAddTo': 'end', // start, end
|
||||||
@@ -91,13 +95,14 @@ class _Settings {
|
|||||||
'readerMode': 'galleryLeftToRight', // values of [ReaderMode]
|
'readerMode': 'galleryLeftToRight', // values of [ReaderMode]
|
||||||
'enableTapToTurnPages': true,
|
'enableTapToTurnPages': true,
|
||||||
'enablePageAnimation': true,
|
'enablePageAnimation': true,
|
||||||
|
'language': 'system', // system, zh-CN, zh-TW, en-US
|
||||||
};
|
};
|
||||||
|
|
||||||
operator[](String key) {
|
operator [](String key) {
|
||||||
return _data[key];
|
return _data[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
operator[]=(String key, dynamic value) {
|
operator []=(String key, dynamic value) {
|
||||||
_data[key] = value;
|
_data[key] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -100,6 +100,29 @@ class LocalManager with ChangeNotifier {
|
|||||||
|
|
||||||
late String path;
|
late String path;
|
||||||
|
|
||||||
|
// return error message if failed
|
||||||
|
Future<String?> setNewPath(String newPath) async {
|
||||||
|
var newDir = Directory(newPath);
|
||||||
|
if(!await newDir.exists()) {
|
||||||
|
return "Directory does not exist";
|
||||||
|
}
|
||||||
|
if(!await newDir.list().isEmpty) {
|
||||||
|
return "Directory is not empty";
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await copyDirectory(
|
||||||
|
Directory(path),
|
||||||
|
newDir,
|
||||||
|
);
|
||||||
|
await File(FilePath.join(App.dataPath, 'local_path')).writeAsString(path);
|
||||||
|
} catch (e) {
|
||||||
|
return e.toString();
|
||||||
|
}
|
||||||
|
await Directory(path).deleteIgnoreError();
|
||||||
|
path = newPath;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> init() async {
|
Future<void> init() async {
|
||||||
_db = sqlite3.open(
|
_db = sqlite3.open(
|
||||||
'${App.dataPath}/local.db',
|
'${App.dataPath}/local.db',
|
||||||
@@ -118,18 +141,18 @@ class LocalManager with ChangeNotifier {
|
|||||||
PRIMARY KEY (id, comic_type)
|
PRIMARY KEY (id, comic_type)
|
||||||
);
|
);
|
||||||
''');
|
''');
|
||||||
if (File('${App.dataPath}/local_path').existsSync()) {
|
if (File(FilePath.join(App.dataPath, 'local_path')).existsSync()) {
|
||||||
path = File('${App.dataPath}/local_path').readAsStringSync();
|
path = File(FilePath.join(App.dataPath, 'local_path')).readAsStringSync();
|
||||||
} else {
|
} else {
|
||||||
if (App.isAndroid) {
|
if (App.isAndroid) {
|
||||||
var external = await getExternalStorageDirectories();
|
var external = await getExternalStorageDirectories();
|
||||||
if (external != null && external.isNotEmpty) {
|
if (external != null && external.isNotEmpty) {
|
||||||
path = '${external.first.path}/local';
|
path = FilePath.join(external.first.path, 'local_path');
|
||||||
} else {
|
} else {
|
||||||
path = '${App.dataPath}/local';
|
path = FilePath.join(App.dataPath, 'local_path');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
path = '${App.dataPath}/local';
|
path = FilePath.join(App.dataPath, 'local_path');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!Directory(path).existsSync()) {
|
if (!Directory(path).existsSync()) {
|
||||||
|
@@ -12,8 +12,8 @@ import 'foundation/appdata.dart';
|
|||||||
|
|
||||||
Future<void> init() async {
|
Future<void> init() async {
|
||||||
await AppTranslation.init();
|
await AppTranslation.init();
|
||||||
await App.init();
|
|
||||||
await appdata.init();
|
await appdata.init();
|
||||||
|
await App.init();
|
||||||
await HistoryManager().init();
|
await HistoryManager().init();
|
||||||
await LocalManager().init();
|
await LocalManager().init();
|
||||||
await LocalFavoritesManager().init();
|
await LocalFavoritesManager().init();
|
||||||
|
@@ -104,6 +104,18 @@ class _MyAppState extends State<MyApp> {
|
|||||||
GlobalWidgetsLocalizations.delegate,
|
GlobalWidgetsLocalizations.delegate,
|
||||||
GlobalCupertinoLocalizations.delegate,
|
GlobalCupertinoLocalizations.delegate,
|
||||||
],
|
],
|
||||||
|
locale: () {
|
||||||
|
var lang = appdata.settings['language'];
|
||||||
|
if(lang == 'system') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return switch(lang) {
|
||||||
|
'zh-CN' => const Locale('zh', 'CN'),
|
||||||
|
'zh-TW' => const Locale('zh', 'TW'),
|
||||||
|
'en-US' => const Locale('en'),
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
}(),
|
||||||
supportedLocales: const [
|
supportedLocales: const [
|
||||||
Locale('en'),
|
Locale('en'),
|
||||||
Locale('zh', 'CN'),
|
Locale('zh', 'CN'),
|
||||||
|
@@ -112,7 +112,7 @@ class AppDio with DioMixin {
|
|||||||
static HttpClient createHttpClient() {
|
static HttpClient createHttpClient() {
|
||||||
final client = HttpClient();
|
final client = HttpClient();
|
||||||
client.connectionTimeout = const Duration(seconds: 5);
|
client.connectionTimeout = const Duration(seconds: 5);
|
||||||
client.findProxy = (uri) => proxy ?? "DIRECT";
|
client.findProxy = (uri) => proxy == null ? "DIRECT" : "PROXY $proxy";
|
||||||
client.idleTimeout = const Duration(seconds: 100);
|
client.idleTimeout = const Duration(seconds: 100);
|
||||||
client.badCertificateCallback = (X509Certificate cert, String host, int port) {
|
client.badCertificateCallback = (X509Certificate cert, String host, int port) {
|
||||||
if (host.contains("cdn")) return true;
|
if (host.contains("cdn")) return true;
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:venera/pages/categories_page.dart';
|
import 'package:venera/pages/categories_page.dart';
|
||||||
import 'package:venera/pages/search_page.dart';
|
import 'package:venera/pages/search_page.dart';
|
||||||
|
import 'package:venera/pages/settings/settings_page.dart';
|
||||||
import 'package:venera/utils/translations.dart';
|
import 'package:venera/utils/translations.dart';
|
||||||
|
|
||||||
import '../components/components.dart';
|
import '../components/components.dart';
|
||||||
@@ -89,7 +90,9 @@ class _MainPageState extends State<MainPage> {
|
|||||||
PaneActionEntry(
|
PaneActionEntry(
|
||||||
icon: Icons.settings,
|
icon: Icons.settings,
|
||||||
label: "Settings".tl,
|
label: "Settings".tl,
|
||||||
onTap: () {},
|
onTap: () {
|
||||||
|
to(() => const SettingsPage());
|
||||||
|
},
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
pageBuilder: (index) {
|
pageBuilder: (index) {
|
||||||
|
121
lib/pages/settings/about.dart
Normal file
121
lib/pages/settings/about.dart
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
part of 'settings_page.dart';
|
||||||
|
|
||||||
|
class AboutSettings extends StatefulWidget {
|
||||||
|
const AboutSettings({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AboutSettings> createState() => _AboutSettingsState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AboutSettingsState extends State<AboutSettings> {
|
||||||
|
bool isCheckingUpdate = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SmoothCustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
SliverAppbar(title: Text("About".tl)),
|
||||||
|
SizedBox(
|
||||||
|
height: 136,
|
||||||
|
width: double.infinity,
|
||||||
|
child: Center(
|
||||||
|
child: Container(
|
||||||
|
width: 136,
|
||||||
|
height: 136,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(136),
|
||||||
|
),
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: const Image(
|
||||||
|
image: AssetImage("assets/app_icon.png"),
|
||||||
|
filterQuality: FilterQuality.medium,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).paddingTop(16).toSliver(),
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
"V${App.version}",
|
||||||
|
style: const TextStyle(fontSize: 16),
|
||||||
|
),
|
||||||
|
Text("Venera is a free and open-source app for comic reading.".tl),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
],
|
||||||
|
).toSliver(),
|
||||||
|
ListTile(
|
||||||
|
title: Text("Check for updates".tl),
|
||||||
|
trailing: Button.filled(
|
||||||
|
isLoading: isCheckingUpdate,
|
||||||
|
child: Text("Check".tl),
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
isCheckingUpdate = true;
|
||||||
|
});
|
||||||
|
checkUpdate().then((value) {
|
||||||
|
if (value) {
|
||||||
|
showDialog(
|
||||||
|
context: App.rootContext,
|
||||||
|
builder: (context) {
|
||||||
|
return ContentDialog(
|
||||||
|
title: "New version available".tl,
|
||||||
|
content: Text(
|
||||||
|
"A new version is available. Do you want to update now?"
|
||||||
|
.tl),
|
||||||
|
actions: [
|
||||||
|
Button.text(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
launchUrlString(
|
||||||
|
"https://github.com/venera-app/venera/releases");
|
||||||
|
},
|
||||||
|
child: Text("Update".tl),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
context.showMessage(message: "No new version available".tl);
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
isCheckingUpdate = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
).fixHeight(32),
|
||||||
|
).toSliver(),
|
||||||
|
ListTile(
|
||||||
|
title: const Text("Github"),
|
||||||
|
trailing: const Icon(Icons.open_in_new),
|
||||||
|
onTap: () {
|
||||||
|
launchUrlString("https://github.com/venera-app/venera");
|
||||||
|
},
|
||||||
|
).toSliver(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> checkUpdate() async {
|
||||||
|
var res = await AppDio().get(
|
||||||
|
"https://raw.githubusercontent.com/venera-app/venera/refs/heads/master/pubspec.yaml");
|
||||||
|
if (res.statusCode == 200) {
|
||||||
|
var data = loadYaml(res.data);
|
||||||
|
if (data["version"] != null) {
|
||||||
|
return _compareVersion(data["version"].split("+")[0], App.version);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// return true if version1 > version2
|
||||||
|
bool _compareVersion(String version1, String version2) {
|
||||||
|
var v1 = version1.split(".");
|
||||||
|
var v2 = version2.split(".");
|
||||||
|
for (var i = 0; i < v1.length; i++) {
|
||||||
|
if (int.parse(v1[i]) > int.parse(v2[i])) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
219
lib/pages/settings/app.dart
Normal file
219
lib/pages/settings/app.dart
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
part of 'settings_page.dart';
|
||||||
|
|
||||||
|
class AppSettings extends StatefulWidget {
|
||||||
|
const AppSettings({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AppSettings> createState() => _AppSettingsState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AppSettingsState extends State<AppSettings> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SmoothCustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
SliverAppbar(title: Text("App".tl)),
|
||||||
|
_SettingPartTitle(
|
||||||
|
title: "Data".tl,
|
||||||
|
icon: Icons.storage,
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
title: Text("Storage Path for local comics".tl),
|
||||||
|
subtitle: Text(LocalManager().path, softWrap: false),
|
||||||
|
).toSliver(),
|
||||||
|
_CallbackSetting(
|
||||||
|
title: "Set New Storage Path".tl,
|
||||||
|
actionTitle: "Set".tl,
|
||||||
|
callback: () async {
|
||||||
|
var picker = FilePicker.platform;
|
||||||
|
var result = await picker.getDirectoryPath();
|
||||||
|
if (result == null) return;
|
||||||
|
var loadingDialog = showLoadingDialog(
|
||||||
|
App.rootContext,
|
||||||
|
barrierDismissible: false,
|
||||||
|
allowCancel: false,
|
||||||
|
);
|
||||||
|
var res = await LocalManager().setNewPath(result);
|
||||||
|
loadingDialog.close();
|
||||||
|
if (res != null) {
|
||||||
|
context.showMessage(message: res);
|
||||||
|
} else {
|
||||||
|
context.showMessage(message: "Path set successfully".tl);
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
).toSliver(),
|
||||||
|
ListTile(
|
||||||
|
title: Text("Cache Size".tl),
|
||||||
|
subtitle: Text(bytesToReadableString(CacheManager().currentSize)),
|
||||||
|
).toSliver(),
|
||||||
|
_CallbackSetting(
|
||||||
|
title: "Clear Cache".tl,
|
||||||
|
actionTitle: "Clear".tl,
|
||||||
|
callback: () async {
|
||||||
|
var loadingDialog = showLoadingDialog(
|
||||||
|
App.rootContext,
|
||||||
|
barrierDismissible: false,
|
||||||
|
allowCancel: false,
|
||||||
|
);
|
||||||
|
await CacheManager().clear();
|
||||||
|
loadingDialog.close();
|
||||||
|
context.showMessage(message: "Cache cleared".tl);
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
).toSliver(),
|
||||||
|
_SettingPartTitle(
|
||||||
|
title: "Log".tl,
|
||||||
|
icon: Icons.error_outline,
|
||||||
|
),
|
||||||
|
_CallbackSetting(
|
||||||
|
title: "Open Log".tl,
|
||||||
|
callback: () {
|
||||||
|
context.to(() => const LogsPage());
|
||||||
|
},
|
||||||
|
actionTitle: 'Open'.tl,
|
||||||
|
).toSliver(),
|
||||||
|
_SettingPartTitle(
|
||||||
|
title: "User".tl,
|
||||||
|
icon: Icons.person_outline,
|
||||||
|
),
|
||||||
|
SelectSetting(
|
||||||
|
title: "Language".tl,
|
||||||
|
settingKey: "language",
|
||||||
|
optionTranslation: const {
|
||||||
|
"system": "System",
|
||||||
|
"zh-CN": "简体中文",
|
||||||
|
"zh-TW": "繁體中文",
|
||||||
|
"en-US": "English",
|
||||||
|
},
|
||||||
|
onChanged: () {
|
||||||
|
App.forceRebuild();
|
||||||
|
},
|
||||||
|
).toSliver(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LogsPage extends StatefulWidget {
|
||||||
|
const LogsPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<LogsPage> createState() => _LogsPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LogsPageState extends State<LogsPage> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: Appbar(
|
||||||
|
title: const Text("Logs"),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: () => setState(() {
|
||||||
|
final RelativeRect position = RelativeRect.fromLTRB(
|
||||||
|
MediaQuery.of(context).size.width,
|
||||||
|
MediaQuery.of(context).padding.top + kToolbarHeight,
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
);
|
||||||
|
showMenu(context: context, position: position, items: [
|
||||||
|
PopupMenuItem(
|
||||||
|
child: Text("Clear".tl),
|
||||||
|
onTap: () => setState(() => Log.clear()),
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
child: Text("Disable Length Limitation".tl),
|
||||||
|
onTap: () {
|
||||||
|
Log.ignoreLimitation = true;
|
||||||
|
context.showMessage(
|
||||||
|
message: "Only valid for this run");
|
||||||
|
},
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
child: Text("Export".tl),
|
||||||
|
onTap: () => saveLog(Log().toString()),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}),
|
||||||
|
icon: const Icon(Icons.more_horiz))
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: ListView.builder(
|
||||||
|
reverse: true,
|
||||||
|
controller: ScrollController(),
|
||||||
|
itemCount: Log.logs.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
index = Log.logs.length - index - 1;
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
|
||||||
|
child: SelectionArea(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.surfaceContainerHighest,
|
||||||
|
borderRadius:
|
||||||
|
const BorderRadius.all(Radius.circular(16)),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(5, 0, 5, 1),
|
||||||
|
child: Text(Log.logs[index].title),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: 3,
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: [
|
||||||
|
Theme.of(context).colorScheme.error,
|
||||||
|
Theme.of(context).colorScheme.errorContainer,
|
||||||
|
Theme.of(context).colorScheme.primaryContainer
|
||||||
|
][Log.logs[index].level.index],
|
||||||
|
borderRadius:
|
||||||
|
const BorderRadius.all(Radius.circular(16)),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(5, 0, 5, 1),
|
||||||
|
child: Text(
|
||||||
|
Log.logs[index].level.name,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Log.logs[index].level.index == 0
|
||||||
|
? Colors.white
|
||||||
|
: Colors.black),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Text(Log.logs[index].content),
|
||||||
|
Text(Log.logs[index].time
|
||||||
|
.toString()
|
||||||
|
.replaceAll(RegExp(r"\.\w+"), "")),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Clipboard.setData(
|
||||||
|
ClipboardData(text: Log.logs[index].content));
|
||||||
|
},
|
||||||
|
child: Text("Copy".tl),
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void saveLog(String log) async {
|
||||||
|
saveFile(data: utf8.encode(log), filename: 'log.txt');
|
||||||
|
}
|
||||||
|
}
|
44
lib/pages/settings/appearance.dart
Normal file
44
lib/pages/settings/appearance.dart
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
part of 'settings_page.dart';
|
||||||
|
|
||||||
|
class AppearanceSettings extends StatefulWidget {
|
||||||
|
const AppearanceSettings({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AppearanceSettings> createState() => _AppearanceSettingsState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AppearanceSettingsState extends State<AppearanceSettings> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SmoothCustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
SliverAppbar(title: Text("Appearance".tl)),
|
||||||
|
SelectSetting(
|
||||||
|
title: "Theme Mode".tl,
|
||||||
|
settingKey: "theme_mode",
|
||||||
|
optionTranslation: {
|
||||||
|
"system": "System".tl,
|
||||||
|
"light": "Light".tl,
|
||||||
|
"dark": "Dark".tl,
|
||||||
|
},
|
||||||
|
).toSliver(),
|
||||||
|
SelectSetting(
|
||||||
|
title: "Theme Color".tl,
|
||||||
|
settingKey: "color",
|
||||||
|
optionTranslation: {
|
||||||
|
"red": "Red".tl,
|
||||||
|
"pink": "Pink".tl,
|
||||||
|
"purple": "Purple".tl,
|
||||||
|
"green": "Green".tl,
|
||||||
|
"orange": "Orange".tl,
|
||||||
|
"blue": "Blue".tl,
|
||||||
|
},
|
||||||
|
onChanged: () async {
|
||||||
|
await App.init();
|
||||||
|
App.forceRebuild();
|
||||||
|
},
|
||||||
|
).toSliver(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
182
lib/pages/settings/explore_settings.dart
Normal file
182
lib/pages/settings/explore_settings.dart
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
part of 'settings_page.dart';
|
||||||
|
|
||||||
|
class ExploreSettings extends StatefulWidget {
|
||||||
|
const ExploreSettings({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ExploreSettings> createState() => _ExploreSettingsState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ExploreSettingsState extends State<ExploreSettings> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SmoothCustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
SliverAppbar(title: Text("Explore".tl)),
|
||||||
|
SelectSetting(
|
||||||
|
title: "Display mode of comic tile".tl,
|
||||||
|
settingKey: "comicDisplayMode",
|
||||||
|
optionTranslation: {
|
||||||
|
"detailed": "Detailed".tl,
|
||||||
|
"brief": "Brief".tl,
|
||||||
|
},
|
||||||
|
).toSliver(),
|
||||||
|
_SliderSetting(
|
||||||
|
title: "Size of comic tile".tl,
|
||||||
|
settingsIndex: "comicTileScale",
|
||||||
|
interval: 0.05,
|
||||||
|
min: 0.75,
|
||||||
|
max: 1.25,
|
||||||
|
).toSliver(),
|
||||||
|
_PopupWindowSetting(
|
||||||
|
title: "Explore Pages".tl,
|
||||||
|
builder: () {
|
||||||
|
var pages = <String, String>{};
|
||||||
|
for (var c in ComicSource.all()) {
|
||||||
|
for (var page in c.explorePages) {
|
||||||
|
pages[page.title] = page.title;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return _MultiPagesFilter(
|
||||||
|
title: "Explore Pages".tl,
|
||||||
|
settingsIndex: "explore_pages",
|
||||||
|
pages: pages,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
).toSliver(),
|
||||||
|
_PopupWindowSetting(
|
||||||
|
title: "Category Pages".tl,
|
||||||
|
builder: () {
|
||||||
|
var pages = <String, String>{};
|
||||||
|
for (var c in ComicSource.all()) {
|
||||||
|
if (c.categoryData != null) {
|
||||||
|
pages[c.categoryData!.key] = c.categoryData!.title;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return _MultiPagesFilter(
|
||||||
|
title: "Category Pages".tl,
|
||||||
|
settingsIndex: "categories",
|
||||||
|
pages: pages,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
).toSliver(),
|
||||||
|
_PopupWindowSetting(
|
||||||
|
title: "Explore Pages".tl,
|
||||||
|
builder: () {
|
||||||
|
var pages = <String, String>{};
|
||||||
|
for (var c in ComicSource.all()) {
|
||||||
|
if (c.favoriteData != null) {
|
||||||
|
pages[c.favoriteData!.key] = c.favoriteData!.title;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return _MultiPagesFilter(
|
||||||
|
title: "Network Favorite Pages".tl,
|
||||||
|
settingsIndex: "favorites",
|
||||||
|
pages: pages,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
).toSliver(),
|
||||||
|
_SwitchSetting(
|
||||||
|
title: "Show favorite status on comic tile".tl,
|
||||||
|
settingKey: "showFavoriteStatusOnTile",
|
||||||
|
).toSliver(),
|
||||||
|
_SwitchSetting(
|
||||||
|
title: "Show history on comic tile".tl,
|
||||||
|
settingKey: "showHistoryStatusOnTile",
|
||||||
|
).toSliver(),
|
||||||
|
_PopupWindowSetting(
|
||||||
|
title: "Keyword blocking".tl,
|
||||||
|
builder: () => const _ManageBlockingWordView(),
|
||||||
|
).toSliver(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ManageBlockingWordView extends StatefulWidget {
|
||||||
|
const _ManageBlockingWordView({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_ManageBlockingWordView> createState() =>
|
||||||
|
_ManageBlockingWordViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ManageBlockingWordViewState extends State<_ManageBlockingWordView> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
assert(appdata.settings["blockedWords"] is List);
|
||||||
|
return PopUpWidgetScaffold(
|
||||||
|
title: "Keyword blocking".tl,
|
||||||
|
tailing: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
onPressed: add,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
body: ListView.builder(
|
||||||
|
itemCount: appdata.settings["blockedWords"].length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
return ListTile(
|
||||||
|
title: Text(appdata.settings["blockedWords"][index]),
|
||||||
|
trailing: IconButton(
|
||||||
|
icon: const Icon(Icons.close),
|
||||||
|
onPressed: () {
|
||||||
|
appdata.settings["blockedWords"].removeAt(index);
|
||||||
|
appdata.saveData();
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void add() {
|
||||||
|
showDialog(
|
||||||
|
context: App.rootContext,
|
||||||
|
barrierColor: Colors.black.withOpacity(0.1),
|
||||||
|
builder: (context) {
|
||||||
|
var controller = TextEditingController();
|
||||||
|
String? error;
|
||||||
|
return StatefulBuilder(builder: (context, setState) {
|
||||||
|
return ContentDialog(
|
||||||
|
title: "Add keyword".tl,
|
||||||
|
content: TextField(
|
||||||
|
controller: controller,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
label: Text("Keyword".tl),
|
||||||
|
errorText: error,
|
||||||
|
),
|
||||||
|
onChanged: (s) {
|
||||||
|
if(error != null){
|
||||||
|
setState(() {
|
||||||
|
error = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
).paddingHorizontal(12),
|
||||||
|
actions: [
|
||||||
|
Button.filled(
|
||||||
|
onPressed: () {
|
||||||
|
if(appdata.settings["blockedWords"].contains(controller.text)){
|
||||||
|
setState(() {
|
||||||
|
error = "Keyword already exists".tl;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
appdata.settings["blockedWords"].add(controller.text);
|
||||||
|
appdata.saveData();
|
||||||
|
this.setState(() {});
|
||||||
|
context.pop();
|
||||||
|
},
|
||||||
|
child: Text("Add".tl),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
36
lib/pages/settings/local_favorites.dart
Normal file
36
lib/pages/settings/local_favorites.dart
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
part of 'settings_page.dart';
|
||||||
|
|
||||||
|
class LocalFavoritesSettings extends StatefulWidget {
|
||||||
|
const LocalFavoritesSettings({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<LocalFavoritesSettings> createState() => _LocalFavoritesSettingsState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LocalFavoritesSettingsState extends State<LocalFavoritesSettings> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SmoothCustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
SliverAppbar(title: Text("Local Favorites".tl)),
|
||||||
|
const SelectSetting(
|
||||||
|
title: "Add new favorite to",
|
||||||
|
settingKey: "newFavoriteAddTo",
|
||||||
|
optionTranslation: {
|
||||||
|
"start": "Start",
|
||||||
|
"end": "End",
|
||||||
|
},
|
||||||
|
).toSliver(),
|
||||||
|
const SelectSetting(
|
||||||
|
title: "Move favorite after read",
|
||||||
|
settingKey: "moveFavoriteAfterRead",
|
||||||
|
optionTranslation: {
|
||||||
|
"none": "None",
|
||||||
|
"end": "End",
|
||||||
|
"start": "Start",
|
||||||
|
},
|
||||||
|
).toSliver(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
236
lib/pages/settings/network.dart
Normal file
236
lib/pages/settings/network.dart
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
part of 'settings_page.dart';
|
||||||
|
|
||||||
|
class NetworkSettings extends StatefulWidget {
|
||||||
|
const NetworkSettings({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<NetworkSettings> createState() => _NetworkSettingsState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _NetworkSettingsState extends State<NetworkSettings> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SmoothCustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
SliverAppbar(title: Text("Network".tl)),
|
||||||
|
_PopupWindowSetting(
|
||||||
|
title: "Proxy".tl,
|
||||||
|
builder: () => const _ProxySettingView(),
|
||||||
|
).toSliver(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ProxySettingView extends StatefulWidget {
|
||||||
|
const _ProxySettingView();
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_ProxySettingView> createState() => _ProxySettingViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ProxySettingViewState extends State<_ProxySettingView> {
|
||||||
|
String type = '';
|
||||||
|
|
||||||
|
String host = '';
|
||||||
|
|
||||||
|
String port = '';
|
||||||
|
|
||||||
|
String username = '';
|
||||||
|
|
||||||
|
String password = '';
|
||||||
|
|
||||||
|
// USERNAME:PASSWORD@HOST:PORT
|
||||||
|
String toProxyStr() {
|
||||||
|
if(type == 'direct') {
|
||||||
|
return 'direct';
|
||||||
|
} else if(type == 'system') {
|
||||||
|
return 'system';
|
||||||
|
}
|
||||||
|
var res = '';
|
||||||
|
if(username.isNotEmpty) {
|
||||||
|
res += username;
|
||||||
|
if(password.isNotEmpty) {
|
||||||
|
res += ':$password';
|
||||||
|
}
|
||||||
|
res += '@';
|
||||||
|
}
|
||||||
|
res += host;
|
||||||
|
if(port.isNotEmpty) {
|
||||||
|
res += ':$port';
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
void parseProxyString(String proxy) {
|
||||||
|
if(proxy == 'direct') {
|
||||||
|
type = 'direct';
|
||||||
|
return;
|
||||||
|
} else if(proxy == 'system') {
|
||||||
|
type = 'system';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
type = 'manual';
|
||||||
|
var parts = proxy.split('@');
|
||||||
|
if(parts.length == 2) {
|
||||||
|
var auth = parts[0].split(':');
|
||||||
|
if(auth.length == 2) {
|
||||||
|
username = auth[0];
|
||||||
|
password = auth[1];
|
||||||
|
}
|
||||||
|
parts = parts[1].split(':');
|
||||||
|
if(parts.length == 2) {
|
||||||
|
host = parts[0];
|
||||||
|
port = parts[1];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
parts = proxy.split(':');
|
||||||
|
if(parts.length == 2) {
|
||||||
|
host = parts[0];
|
||||||
|
port = parts[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
var proxy = appdata.settings['proxy'];
|
||||||
|
parseProxyString(proxy);
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return PopUpWidgetScaffold(
|
||||||
|
title: "Proxy".tl,
|
||||||
|
body: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
RadioListTile<String>(
|
||||||
|
title: Text("Direct".tl),
|
||||||
|
value: 'direct',
|
||||||
|
groupValue: type,
|
||||||
|
onChanged: (v) {
|
||||||
|
setState(() {
|
||||||
|
type = v!;
|
||||||
|
});
|
||||||
|
appdata.settings['proxy'] = toProxyStr();
|
||||||
|
appdata.saveData();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
RadioListTile<String>(
|
||||||
|
title: Text("System".tl),
|
||||||
|
value: 'system',
|
||||||
|
groupValue: type,
|
||||||
|
onChanged: (v) {
|
||||||
|
setState(() {
|
||||||
|
type = v!;
|
||||||
|
});
|
||||||
|
appdata.settings['proxy'] = toProxyStr();
|
||||||
|
appdata.saveData();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
RadioListTile(
|
||||||
|
title: Text("Manual".tl),
|
||||||
|
value: 'manual',
|
||||||
|
groupValue: type,
|
||||||
|
onChanged: (v) {
|
||||||
|
setState(() {
|
||||||
|
type = v!;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if(type == 'manual') buildManualProxy(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
var formKey = GlobalKey<FormState>();
|
||||||
|
|
||||||
|
Widget buildManualProxy() {
|
||||||
|
return Form(
|
||||||
|
key: formKey,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
TextFormField(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
labelText: "Host".tl,
|
||||||
|
),
|
||||||
|
controller: TextEditingController(text: host),
|
||||||
|
onChanged: (v) {
|
||||||
|
host = v;
|
||||||
|
},
|
||||||
|
validator: (v) {
|
||||||
|
if(v?.isEmpty ?? false) {
|
||||||
|
return "Host cannot be empty".tl;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
TextFormField(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
labelText: "Port".tl,
|
||||||
|
),
|
||||||
|
controller: TextEditingController(text: port),
|
||||||
|
onChanged: (v) {
|
||||||
|
port = v;
|
||||||
|
},
|
||||||
|
validator: (v) {
|
||||||
|
if(v?.isEmpty ?? true) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if(int.tryParse(v!) == null) {
|
||||||
|
return "Port must be a number".tl;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
TextFormField(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
labelText: "Username".tl,
|
||||||
|
),
|
||||||
|
controller: TextEditingController(text: username),
|
||||||
|
onChanged: (v) {
|
||||||
|
username = v;
|
||||||
|
},
|
||||||
|
validator: (v) {
|
||||||
|
if((v?.isEmpty ?? false) && password.isNotEmpty) {
|
||||||
|
return "Username cannot be empty".tl;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
TextFormField(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
labelText: "Password".tl,
|
||||||
|
),
|
||||||
|
controller: TextEditingController(text: password),
|
||||||
|
onChanged: (v) {
|
||||||
|
password = v;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () {
|
||||||
|
if(formKey.currentState?.validate() ?? false) {
|
||||||
|
appdata.settings['proxy'] = toProxyStr();
|
||||||
|
appdata.saveData();
|
||||||
|
App.rootContext.pop();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Text("Save".tl),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
).paddingHorizontal(16).paddingTop(16);
|
||||||
|
}
|
||||||
|
}
|
@@ -14,7 +14,7 @@ class _ReaderSettingsState extends State<ReaderSettings> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SmoothCustomScrollView(
|
return SmoothCustomScrollView(
|
||||||
slivers: [
|
slivers: [
|
||||||
SliverAppbar(title: Text("Settings".tl)),
|
SliverAppbar(title: Text("Reading".tl)),
|
||||||
_SwitchSetting(
|
_SwitchSetting(
|
||||||
title: "Tap to turn Pages".tl,
|
title: "Tap to turn Pages".tl,
|
||||||
settingKey: "enableTapToTurnPages",
|
settingKey: "enableTapToTurnPages",
|
||||||
|
@@ -251,3 +251,225 @@ class _SliderSettingState extends State<_SliderSetting> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _PopupWindowSetting extends StatelessWidget {
|
||||||
|
const _PopupWindowSetting({required this.title, required this.builder});
|
||||||
|
|
||||||
|
final Widget Function() builder;
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ListTile(
|
||||||
|
title: Text(title),
|
||||||
|
trailing: const Icon(Icons.arrow_right),
|
||||||
|
onTap: () {
|
||||||
|
showPopUpWidget(App.rootContext, builder());
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MultiPagesFilter extends StatefulWidget {
|
||||||
|
const _MultiPagesFilter({
|
||||||
|
required this.title,
|
||||||
|
required this.settingsIndex,
|
||||||
|
required this.pages,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
final String settingsIndex;
|
||||||
|
|
||||||
|
// key - name
|
||||||
|
final Map<String, String> pages;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_MultiPagesFilter> createState() => _MultiPagesFilterState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MultiPagesFilterState extends State<_MultiPagesFilter> {
|
||||||
|
late List<String> keys;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
keys = List.from(appdata.settings[widget.settingsIndex]);
|
||||||
|
keys.remove("");
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
var reorderWidgetKey = UniqueKey();
|
||||||
|
var scrollController = ScrollController();
|
||||||
|
final _key = GlobalKey();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var tiles = keys.map((e) => buildItem(e)).toList();
|
||||||
|
|
||||||
|
var view = ReorderableBuilder(
|
||||||
|
key: reorderWidgetKey,
|
||||||
|
scrollController: scrollController,
|
||||||
|
longPressDelay: App.isDesktop
|
||||||
|
? const Duration(milliseconds: 100)
|
||||||
|
: const Duration(milliseconds: 500),
|
||||||
|
dragChildBoxDecoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
|
boxShadow: const [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black12,
|
||||||
|
blurRadius: 5,
|
||||||
|
offset: Offset(0, 2),
|
||||||
|
spreadRadius: 2)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onReorder: (reorderFunc) {
|
||||||
|
setState(() {
|
||||||
|
keys = List.from(reorderFunc(keys));
|
||||||
|
});
|
||||||
|
updateSetting();
|
||||||
|
},
|
||||||
|
children: tiles,
|
||||||
|
builder: (children) {
|
||||||
|
return GridView(
|
||||||
|
key: _key,
|
||||||
|
controller: scrollController,
|
||||||
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
crossAxisCount: 1,
|
||||||
|
mainAxisExtent: 48,
|
||||||
|
),
|
||||||
|
children: children,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return PopUpWidgetScaffold(
|
||||||
|
title: widget.title,
|
||||||
|
tailing: [
|
||||||
|
if (keys.length < widget.pages.length)
|
||||||
|
IconButton(onPressed: showAddDialog, icon: const Icon(Icons.add))
|
||||||
|
],
|
||||||
|
body: view,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildItem(String key) {
|
||||||
|
Widget removeButton = Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 8),
|
||||||
|
child: IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
keys.remove(key);
|
||||||
|
});
|
||||||
|
updateSetting();
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.delete)),
|
||||||
|
);
|
||||||
|
|
||||||
|
return ListTile(
|
||||||
|
title: Text(widget.pages[key] ?? "(Invalid) $key"),
|
||||||
|
key: Key(key),
|
||||||
|
trailing: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
removeButton,
|
||||||
|
const Icon(Icons.drag_handle),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void showAddDialog() {
|
||||||
|
var canAdd = <String, String>{};
|
||||||
|
widget.pages.forEach((key, value) {
|
||||||
|
if (!keys.contains(key)) {
|
||||||
|
canAdd[key] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) {
|
||||||
|
return SimpleDialog(
|
||||||
|
title: const Text("Add"),
|
||||||
|
children: canAdd.entries
|
||||||
|
.map((e) => InkWell(
|
||||||
|
child: ListTile(title: Text(e.value), key: Key(e.key)),
|
||||||
|
onTap: () {
|
||||||
|
context.pop();
|
||||||
|
setState(() {
|
||||||
|
keys.add(e.key);
|
||||||
|
});
|
||||||
|
updateSetting();
|
||||||
|
},
|
||||||
|
))
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateSetting() {
|
||||||
|
appdata.settings[widget.settingsIndex] = keys;
|
||||||
|
appdata.saveData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CallbackSetting extends StatelessWidget {
|
||||||
|
const _CallbackSetting({
|
||||||
|
required this.title,
|
||||||
|
required this.callback,
|
||||||
|
required this.actionTitle,
|
||||||
|
this.subtitle,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
final String? subtitle;
|
||||||
|
|
||||||
|
final VoidCallback callback;
|
||||||
|
|
||||||
|
final String actionTitle;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ListTile(
|
||||||
|
title: Text(title),
|
||||||
|
subtitle: subtitle == null ? null : Text(subtitle!),
|
||||||
|
trailing: FilledButton(
|
||||||
|
onPressed: callback,
|
||||||
|
child: Text(actionTitle),
|
||||||
|
).fixHeight(28),
|
||||||
|
onTap: callback,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SettingPartTitle extends StatelessWidget {
|
||||||
|
const _SettingPartTitle({required this.title, required this.icon});
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
final IconData icon;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SliverToBoxAdapter(
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.only(left: 16, top: 16, bottom: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border(
|
||||||
|
bottom: BorderSide(
|
||||||
|
color: context.colorScheme.onSurface.withOpacity(0.1),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(icon, size: 24),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(title, style: ts.s18),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -1,8 +1,361 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:file_picker/file_picker.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_reorderable_grid_view/widgets/reorderable_builder.dart';
|
||||||
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
import 'package:venera/components/components.dart';
|
import 'package:venera/components/components.dart';
|
||||||
import 'package:venera/foundation/app.dart';
|
import 'package:venera/foundation/app.dart';
|
||||||
import 'package:venera/foundation/appdata.dart';
|
import 'package:venera/foundation/appdata.dart';
|
||||||
|
import 'package:venera/foundation/cache_manager.dart';
|
||||||
|
import 'package:venera/foundation/comic_source/comic_source.dart';
|
||||||
|
import 'package:venera/foundation/consts.dart';
|
||||||
|
import 'package:venera/foundation/local.dart';
|
||||||
|
import 'package:venera/foundation/log.dart';
|
||||||
|
import 'package:venera/network/app_dio.dart';
|
||||||
|
import 'package:venera/utils/io.dart';
|
||||||
import 'package:venera/utils/translations.dart';
|
import 'package:venera/utils/translations.dart';
|
||||||
|
import 'package:yaml/yaml.dart';
|
||||||
|
|
||||||
part 'reader.dart';
|
part 'reader.dart';
|
||||||
|
part 'explore_settings.dart';
|
||||||
part 'setting_components.dart';
|
part 'setting_components.dart';
|
||||||
|
part 'appearance.dart';
|
||||||
|
part 'local_favorites.dart';
|
||||||
|
part 'app.dart';
|
||||||
|
part 'about.dart';
|
||||||
|
part 'network.dart';
|
||||||
|
|
||||||
|
class SettingsPage extends StatefulWidget {
|
||||||
|
const SettingsPage({this.initialPage = -1, super.key});
|
||||||
|
|
||||||
|
final int initialPage;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SettingsPage> createState() => _SettingsPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SettingsPageState extends State<SettingsPage> implements PopEntry {
|
||||||
|
int currentPage = -1;
|
||||||
|
|
||||||
|
ColorScheme get colors => Theme.of(context).colorScheme;
|
||||||
|
|
||||||
|
bool get enableTwoViews => context.width > changePoint;
|
||||||
|
|
||||||
|
final categories = <String>[
|
||||||
|
"Explore",
|
||||||
|
"Reading",
|
||||||
|
"Appearance",
|
||||||
|
"Local Favorites",
|
||||||
|
"APP",
|
||||||
|
"Network",
|
||||||
|
"About",
|
||||||
|
];
|
||||||
|
|
||||||
|
final icons = <IconData>[
|
||||||
|
Icons.explore,
|
||||||
|
Icons.book,
|
||||||
|
Icons.color_lens,
|
||||||
|
Icons.collections_bookmark_rounded,
|
||||||
|
Icons.apps,
|
||||||
|
Icons.public,
|
||||||
|
Icons.info
|
||||||
|
];
|
||||||
|
|
||||||
|
double offset = 0;
|
||||||
|
|
||||||
|
late final HorizontalDragGestureRecognizer gestureRecognizer;
|
||||||
|
|
||||||
|
ModalRoute? _route;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
final ModalRoute<dynamic>? nextRoute = ModalRoute.of(context);
|
||||||
|
if (nextRoute != _route) {
|
||||||
|
_route?.unregisterPopEntry(this);
|
||||||
|
_route = nextRoute;
|
||||||
|
_route?.registerPopEntry(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
currentPage = widget.initialPage;
|
||||||
|
gestureRecognizer = HorizontalDragGestureRecognizer(debugOwner: this)
|
||||||
|
..onUpdate = ((details) => setState(() => offset += details.delta.dx))
|
||||||
|
..onEnd = (details) async {
|
||||||
|
if (details.velocity.pixelsPerSecond.dx.abs() > 1 &&
|
||||||
|
details.velocity.pixelsPerSecond.dx >= 0) {
|
||||||
|
setState(() {
|
||||||
|
Future.delayed(const Duration(milliseconds: 300), () => offset = 0);
|
||||||
|
currentPage = -1;
|
||||||
|
});
|
||||||
|
} else if (offset > MediaQuery.of(context).size.width / 2) {
|
||||||
|
setState(() {
|
||||||
|
Future.delayed(const Duration(milliseconds: 300), () => offset = 0);
|
||||||
|
currentPage = -1;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
int i = 10;
|
||||||
|
while (offset != 0) {
|
||||||
|
setState(() {
|
||||||
|
offset -= i;
|
||||||
|
i *= 10;
|
||||||
|
if (offset < 0) {
|
||||||
|
offset = 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await Future.delayed(const Duration(milliseconds: 10));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
..onCancel = () async {
|
||||||
|
int i = 10;
|
||||||
|
while (offset != 0) {
|
||||||
|
setState(() {
|
||||||
|
offset -= i;
|
||||||
|
i *= 10;
|
||||||
|
if (offset < 0) {
|
||||||
|
offset = 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await Future.delayed(const Duration(milliseconds: 10));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
dispose() {
|
||||||
|
super.dispose();
|
||||||
|
gestureRecognizer.dispose();
|
||||||
|
_route?.unregisterPopEntry(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (currentPage != -1) {
|
||||||
|
canPop.value = false;
|
||||||
|
} else {
|
||||||
|
canPop.value = true;
|
||||||
|
}
|
||||||
|
return Material(
|
||||||
|
child: buildBody(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildBody() {
|
||||||
|
if (enableTwoViews) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 280,
|
||||||
|
height: double.infinity,
|
||||||
|
child: buildLeft(),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
height: double.infinity,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border(
|
||||||
|
left: BorderSide(
|
||||||
|
color: context.colorScheme.outlineVariant,
|
||||||
|
width: 0.6,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(child: buildRight())
|
||||||
|
],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
Positioned.fill(child: buildLeft()),
|
||||||
|
Positioned(
|
||||||
|
left: offset,
|
||||||
|
width: MediaQuery.of(context).size.width,
|
||||||
|
height: MediaQuery.of(context).size.height,
|
||||||
|
child: Listener(
|
||||||
|
onPointerDown: handlePointerDown,
|
||||||
|
child: AnimatedSwitcher(
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
reverseDuration: const Duration(milliseconds: 300),
|
||||||
|
switchInCurve: Curves.fastOutSlowIn,
|
||||||
|
switchOutCurve: Curves.fastOutSlowIn,
|
||||||
|
transitionBuilder: (child, animation) {
|
||||||
|
var tween = Tween<Offset>(
|
||||||
|
begin: const Offset(1, 0), end: const Offset(0, 0));
|
||||||
|
|
||||||
|
return SlideTransition(
|
||||||
|
position: tween.animate(animation),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: currentPage == -1
|
||||||
|
? const SizedBox(
|
||||||
|
key: Key("1"),
|
||||||
|
)
|
||||||
|
: buildRight(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void handlePointerDown(PointerDownEvent event) {
|
||||||
|
if (event.position.dx < 20) {
|
||||||
|
gestureRecognizer.addPointer(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildLeft() {
|
||||||
|
return Material(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
height: MediaQuery.of(context).padding.top,
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
height: 56,
|
||||||
|
child: Row(children: [
|
||||||
|
const SizedBox(
|
||||||
|
width: 8,
|
||||||
|
),
|
||||||
|
Tooltip(
|
||||||
|
message: "Back",
|
||||||
|
child: IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back),
|
||||||
|
onPressed: context.pop,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: 24,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"Settings".tl,
|
||||||
|
style: ts.s20,
|
||||||
|
)
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 4,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: buildCategories(),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildCategories() {
|
||||||
|
Widget buildItem(String name, int id) {
|
||||||
|
final bool selected = id == currentPage;
|
||||||
|
|
||||||
|
Widget content = AnimatedContainer(
|
||||||
|
key: ValueKey(id),
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
width: double.infinity,
|
||||||
|
height: 46,
|
||||||
|
padding: const EdgeInsets.fromLTRB(12, 0, 12, 0),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: selected ? colors.primaryContainer.withOpacity(0.36) : null,
|
||||||
|
border: Border(
|
||||||
|
left: BorderSide(
|
||||||
|
color: selected ? colors.primary : Colors.transparent,
|
||||||
|
width: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(children: [
|
||||||
|
Icon(icons[id]),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Text(
|
||||||
|
name,
|
||||||
|
style: ts.s16,
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
if (selected) const Icon(Icons.arrow_right)
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: enableTwoViews
|
||||||
|
? const EdgeInsets.fromLTRB(8, 0, 8, 0)
|
||||||
|
: EdgeInsets.zero,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () => setState(() => currentPage = id),
|
||||||
|
child: content,
|
||||||
|
).paddingVertical(4),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ListView.builder(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
itemCount: categories.length,
|
||||||
|
itemBuilder: (context, index) => buildItem(categories[index].tl, index),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildRight() {
|
||||||
|
final Widget body = switch (currentPage) {
|
||||||
|
-1 => const SizedBox(),
|
||||||
|
0 => const ExploreSettings(),
|
||||||
|
1 => const ReaderSettings(),
|
||||||
|
2 => const AppearanceSettings(),
|
||||||
|
3 => const LocalFavoritesSettings(),
|
||||||
|
4 => const AppSettings(),
|
||||||
|
5 => const NetworkSettings(),
|
||||||
|
6 => const AboutSettings(),
|
||||||
|
_ => throw UnimplementedError()
|
||||||
|
};
|
||||||
|
|
||||||
|
return Material(
|
||||||
|
child: body,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
var canPop = ValueNotifier(true);
|
||||||
|
|
||||||
|
@override
|
||||||
|
ValueListenable<bool> get canPopNotifier => canPop;
|
||||||
|
|
||||||
|
/*
|
||||||
|
flutter >=3.24.0 api
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onPopInvokedWithResult(bool didPop, result) {
|
||||||
|
if (currentPage != -1) {
|
||||||
|
setState(() {
|
||||||
|
currentPage = -1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onPopInvoked(bool didPop) {
|
||||||
|
if (currentPage != -1) {
|
||||||
|
setState(() {
|
||||||
|
currentPage = -1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
// flutter <3.24.0 api
|
||||||
|
@override
|
||||||
|
PopInvokedCallback? get onPopInvoked => (bool didPop) {
|
||||||
|
if (currentPage != -1) {
|
||||||
|
setState(() {
|
||||||
|
currentPage = -1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@@ -186,3 +186,15 @@ class Share {
|
|||||||
s.Share.share(text);
|
s.Share.share(text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String bytesToReadableString(int bytes) {
|
||||||
|
if (bytes < 1024) {
|
||||||
|
return "$bytes B";
|
||||||
|
} else if (bytes < 1024 * 1024) {
|
||||||
|
return "${(bytes / 1024).toStringAsFixed(2)} KB";
|
||||||
|
} else if (bytes < 1024 * 1024 * 1024) {
|
||||||
|
return "${(bytes / 1024 / 1024).toStringAsFixed(2)} MB";
|
||||||
|
} else {
|
||||||
|
return "${(bytes / 1024 / 1024 / 1024).toStringAsFixed(2)} GB";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
16
pubspec.lock
16
pubspec.lock
@@ -164,6 +164,14 @@ packages:
|
|||||||
url: "https://github.com/wgh136/flutter_qjs"
|
url: "https://github.com/wgh136/flutter_qjs"
|
||||||
source: git
|
source: git
|
||||||
version: "0.3.7"
|
version: "0.3.7"
|
||||||
|
flutter_reorderable_grid_view:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_reorderable_grid_view
|
||||||
|
sha256: "40abcc5bff228ebff119326502e7357ee6399956b60b80b17385e9770b7458c0"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.0.1"
|
||||||
flutter_test:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -597,6 +605,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.4"
|
version: "1.0.4"
|
||||||
|
yaml:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: yaml
|
||||||
|
sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.2"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.4.4 <4.0.0"
|
dart: ">=3.4.4 <4.0.0"
|
||||||
flutter: ">=3.22.3"
|
flutter: ">=3.22.3"
|
||||||
|
@@ -40,6 +40,8 @@ dependencies:
|
|||||||
url: https://github.com/venera-app/flutter.widgets
|
url: https://github.com/venera-app/flutter.widgets
|
||||||
ref: 09e756b1f1b04e6298318d99ec20a787fb360f59
|
ref: 09e756b1f1b04e6298318d99ec20a787fb360f59
|
||||||
path: packages/scrollable_positioned_list
|
path: packages/scrollable_positioned_list
|
||||||
|
flutter_reorderable_grid_view: 5.0.1
|
||||||
|
yaml: any
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
@@ -51,4 +53,5 @@ flutter:
|
|||||||
assets:
|
assets:
|
||||||
- assets/translation.json
|
- assets/translation.json
|
||||||
- assets/init.js
|
- assets/init.js
|
||||||
|
- assets/app_icon.png
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user