Move all pages to subdirectory

This commit is contained in:
2024-05-28 01:16:11 +00:00
committed by GitHub
parent b1fdf4cf9f
commit 1918a45bd2
11 changed files with 44 additions and 47 deletions

View File

@@ -0,0 +1,232 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:go_router/go_router.dart';
import 'package:logging/logging.dart';
import '../globals.dart';
import 'login.dart';
final _log = Logger("CreateRootUserPage");
class CreateRootUserPage extends StatefulWidget {
const CreateRootUserPage({super.key});
static const String routeName = '/create_root_user';
@override
State<CreateRootUserPage> createState() => _CreateRootUserPage();
}
class _CreateRootUserPage extends State<CreateRootUserPage>
with ThemeModeWidget {
final _formKey = GlobalKey<FormState>();
bool _createdUser = false;
String _username = "";
String _password = "";
bool _passwordVisible = false;
bool _isValid = false;
bool _skipCreateRootUser = false;
bool _isCreated = false;
@override
void initState() {
super.initState();
_createdUser = false;
_username = "";
_password = "";
_passwordVisible = false;
_isValid = false;
try {
_skipCreateRootUser = prefs.getBool("skipCreateRootUser") ?? false;
} catch (e) {
_log.warning("Failed to get skipCreateRootUser:", e);
_skipCreateRootUser = false;
}
_isCreated = false;
}
Future<bool> _createRootUser(String username, String password) async {
if (!_createdUser) {
final re = await api.createUser(username, password);
if (re.ok) {
final id = re.unwrap();
_createdUser = true;
_log.info("New user's id: $id");
if (id != 0) {
_log.warning("The new user is not root user.");
}
} else if (re.status == 403 || re.status == 2) {
final e = re.unwrapErr();
_log.warning("Failed to create root user:", e);
return false;
} else {
throw re.unwrapErr();
}
}
return await login(username, password);
}
static bool _checkIsValid(String username, String password) {
return (username.isNotEmpty && password.isNotEmpty);
}
void _usernameChanged(String value) {
bool isValid = _checkIsValid(value, _password);
setState(() {
_username = value;
_isValid = isValid;
});
}
void _passwordChanged(String value) {
bool isValid = _checkIsValid(_username, value);
setState(() {
_password = value;
_isValid = isValid;
});
}
void _passwordVisibleChanged() {
setState(() {
_passwordVisible = !_passwordVisible;
});
}
@override
Widget build(BuildContext context) {
tryInitApi(context);
var actions = [buildThemeModeIcon(context)];
if (_skipCreateRootUser) {
actions.add(buildMoreVertSettingsButon(context));
}
return Scaffold(
appBar: AppBar(
leading: _skipCreateRootUser
? IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
context.canPop() ? context.pop() : context.go("/");
},
)
: null,
title: Text(AppLocalizations.of(context)!.createRootUser),
actions: actions,
),
body: Container(
padding: MediaQuery.of(context).size.width > 810
? const EdgeInsets.symmetric(horizontal: 100)
: null,
child: Form(
key: _formKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.symmetric(vertical: 8),
child: TextFormField(
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: AppLocalizations.of(context)!.username,
),
initialValue: _username,
onChanged: _usernameChanged,
)),
Container(
padding: const EdgeInsets.symmetric(vertical: 8),
child: TextFormField(
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: AppLocalizations.of(context)!.password,
suffixIcon: IconButton(
icon: Icon(
_passwordVisible
? Icons.visibility
: Icons.visibility_off,
color: Theme.of(context).primaryColorDark,
),
onPressed: _passwordVisibleChanged,
),
),
initialValue: _password,
onChanged: _passwordChanged,
obscureText: !_passwordVisible,
)),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
ElevatedButton(
onPressed: () => {
prefs
.setBool("skipCreateRootUser", true)
.then((re) {
if (!re) {
_log.warning(
"Failed to set skipCreateRootUser.");
} else {
context.canPop()
? context.pop()
: context.go("/");
}
}).catchError((e) {
_log.warning(
"Failed to set skipCreateRootUser:", e);
})
},
child: Text(AppLocalizations.of(context)!.skip),
),
ElevatedButton(
onPressed: !_isCreated && _isValid
? () {
setState(() {
_isCreated = true;
});
_createRootUser(_username, _password)
.then((re) {
if (re) {
clearAllStates(context);
context.canPop()
? context.pop()
: context.go("/");
} else {
if (!_createdUser) {
context.canPop()
? context.pop()
: context.go("/");
return;
}
final snackBar = SnackBar(
content: Text(
AppLocalizations.of(context)!
.incorrectUserPassword));
ScaffoldMessenger.of(context)
.showSnackBar(snackBar);
setState(() {
_isCreated = false;
});
}
}).catchError((e) {
_log.severe(
"Failed to create root user:", e);
final isNetworkError = e is! (int, String);
final snackBar = SnackBar(
content: Text(isNetworkError
? AppLocalizations.of(context)!
.networkError
: AppLocalizations.of(context)!
.internalError));
ScaffoldMessenger.of(context)
.showSnackBar(snackBar);
setState(() {
_isCreated = false;
});
});
}
: null,
child: Text(AppLocalizations.of(context)!.create),
)
],
)
],
))));
}
}

164
lib/pages/galleries.dart Normal file
View File

@@ -0,0 +1,164 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:go_router/go_router.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:logging/logging.dart';
import '../api/client.dart';
import '../api/gallery.dart';
import '../globals.dart';
import '../main.dart';
final _log = Logger("GalleriesPage");
class GalleriesPageExtra {
const GalleriesPageExtra({this.translatedTag});
final String? translatedTag;
}
class GalleriesPage extends StatefulWidget {
const GalleriesPage(
{super.key, this.sortByGid, this.uploader, this.tag, this.translatedTag});
final SortByGid? sortByGid;
final String? uploader;
final String? tag;
final String? translatedTag;
bool _stt(BuildContext context) =>
prefs.getBool("showTranslatedTag") ??
MainApp.of(context).lang.toLocale().languageCode == "zh";
String? preferredTag(BuildContext context) =>
_stt(context) ? translatedTag ?? tag : tag;
static const String routeName = '/galleries';
@override
State<GalleriesPage> createState() => _GalleriesPage();
}
class _GalleriesPage extends State<GalleriesPage>
with ThemeModeWidget, IsTopWidget2 {
static const int _pageSize = 20;
bool? _sortByGid;
SortByGid _sortByGid2 = SortByGid.none;
final PagingController<int, GMeta> _pagingController =
PagingController(firstPageKey: 0);
Future<void> _fetchPage(int pageKey) async {
try {
final list = (await api.listGalleries(
offset: pageKey,
limit: _pageSize,
sortByGid: _sortByGid,
uploader: widget.uploader,
tag: widget.tag))
.unwrap();
final isLastPage = list.length < _pageSize;
if (isLastPage) {
_pagingController.appendLastPage(list);
} else {
final nextPageKey = pageKey + list.length;
_pagingController.appendPage(list, nextPageKey);
}
} catch (e) {
_log.severe("Failed to load page with offset $pageKey:", e);
_pagingController.error = e;
}
}
@override
void initState() {
try {
_sortByGid2 = widget.sortByGid != null
? widget.sortByGid!
: SortByGid.values[prefs.getInt("sortByGid") ?? 0];
_sortByGid = _sortByGid2.toBool();
} catch (e) {
_log.warning("Failed to load sortByGid from prefs:", e);
}
_pagingController.addPageRequestListener((pageKey) {
_fetchPage(pageKey);
});
super.initState();
}
@override
Widget build(BuildContext context) {
tryInitApi(context);
final i18n = AppLocalizations.of(context)!;
final sortByGidMenu = DropdownMenu<SortByGid>(
initialSelection: _sortByGid2,
onSelected: (v) {
if (v != null) {
prefs.setInt("sortByGid", v!.index).then((re) {
if (!re) _log.warning("Failed to save sortByGid to prefs.");
}).catchError((error) {
_log.warning("Failed to save sortByGid to prefs:", error);
});
var queryParameters = {
"sortByGid": [v!.index.toString()],
"tag": [widget.tag ?? ""],
"uploader": [widget.uploader ?? ""],
};
queryParameters.removeWhere((k, v) => v[0].isEmpty);
context.pushReplacementNamed("/galleries",
queryParameters: queryParameters);
}
},
label: Text(i18n.sortByGid),
dropdownMenuEntries: [
DropdownMenuEntry(value: SortByGid.none, label: i18n.none),
DropdownMenuEntry(value: SortByGid.asc, label: i18n.asc),
DropdownMenuEntry(value: SortByGid.desc, label: i18n.desc),
],
leadingIcon: const Icon(Icons.sort),
);
final title = widget.uploader != null && widget.tag != null
? i18n.tagUploaderGalleries(
widget.preferredTag(context)!, widget.uploader!)
: widget.uploader != null
? i18n.uploaderGalleries(widget.uploader!)
: widget.tag != null
? i18n.tagGalleries(widget.preferredTag(context)!)
: i18n.galleries;
if (isTop(context)) {
setCurrentTitle(title, Theme.of(context).primaryColor.value);
}
return Scaffold(
appBar: AppBar(
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
context.canPop() ? context.pop() : context.go("/");
},
),
title: Text(title),
actions: [
PopupMenuButton(
icon: const Icon(Icons.sort),
itemBuilder: (context) =>
[PopupMenuItem(child: sortByGidMenu)]),
buildThemeModeIcon(context),
buildMoreVertSettingsButon(context),
]),
body: PagedListView<int, GMeta>(
physics: const AlwaysScrollableScrollPhysics(),
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate<GMeta>(
itemBuilder: (context, item, index) {
return ListTile(
title: Text(item.preferredTitle),
onTap: () {
context.push("/gallery/${item.gid}",
extra: GalleryPageExtra(title: item.preferredTitle));
},
);
}),
));
}
@override
void dispose() {
_pagingController.dispose();
super.dispose();
}
}

151
lib/pages/gallery.dart Normal file
View File

@@ -0,0 +1,151 @@
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:go_router/go_router.dart';
import 'package:logging/logging.dart';
import '../api/file.dart';
import '../api/gallery.dart';
import '../components/gallery_info.dart';
import '../globals.dart';
final _log = Logger("GalleryPage");
class GalleryPageExtra {
const GalleryPageExtra({this.title});
final String? title;
}
class GalleryPage extends StatefulWidget {
const GalleryPage(int gid, {super.key, this.title}) : _gid = gid;
final int _gid;
final String? title;
static const String routeName = '/gallery/:gid';
@override
State<GalleryPage> createState() => _GalleryPage();
// ignore: library_private_types_in_public_api
static _GalleryPage of(BuildContext context) =>
context.findAncestorStateOfType<_GalleryPage>()!;
// ignore: library_private_types_in_public_api
static _GalleryPage? maybeOf(BuildContext context) =>
context.findAncestorStateOfType<_GalleryPage>();
}
class _GalleryPage extends State<GalleryPage>
with ThemeModeWidget, IsTopWidget2 {
_GalleryPage();
int _gid = 0;
GalleryData? _data;
EhFiles? _files;
Object? _error;
CancelToken? _cancel;
CancelToken? _markAsNsfwCancel;
bool _isLoading = false;
bool? get isAllNsfw => _data?.isAllNsfw;
Future<void> markGalleryAsNsfw(bool isNsfw) async {
try {
_markAsNsfwCancel = CancelToken();
(await api.updateGalleryFileMeta(_gid,
isNsfw: isNsfw, cancel: _markAsNsfwCancel))
.unwrap();
if (!_markAsNsfwCancel!.isCancelled) {
_fetchData();
}
} catch (e) {
if (!_markAsNsfwCancel!.isCancelled) {
_log.warning("Failed to mark gallery $_gid:", e);
}
}
}
Future<void> _fetchData() async {
try {
_cancel = CancelToken();
_isLoading = true;
final data = (await api.getGallery(_gid)).unwrap();
_data = data;
final fileData = (await api.getFiles(
data.pages.map((e) => e.token).toList(),
cancel: _cancel))
.unwrap();
if (!_cancel!.isCancelled) {
setState(() {
_files = fileData;
_isLoading = false;
});
}
} catch (e) {
if (!_cancel!.isCancelled) {
_log.severe("Failed to load gallery $_gid:", e);
setState(() {
_error = e;
_isLoading = false;
});
}
}
}
@override
void initState() {
_gid = widget._gid;
_data = null;
_error = null;
super.initState();
}
@override
Widget build(BuildContext context) {
tryInitApi(context);
final isLoading = _data == null && _error == null;
if (isLoading && !_isLoading) _fetchData();
final i18n = AppLocalizations.of(context)!;
final title = isLoading
? i18n.loading
: _data != null
? _data!.meta.preferredTitle
: i18n.gallery;
if (isTop(context)) {
if (!kIsWeb || (_data != null && kIsWeb)) {
setCurrentTitle(title, Theme.of(context).primaryColor.value,
includePrefix: false);
} else if (kIsWeb && widget.title != null) {
// 设置预加载标题
// Chrome 和 Firefox 必须尽快设置标题以确保在历史记录菜单显示正确的标题
setCurrentTitle(widget.title!, Theme.of(context).primaryColor.value,
includePrefix: false);
}
}
return Scaffold(
appBar: _data == null
? AppBar(
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
context.canPop() ? context.pop() : context.go("/gallery");
},
),
title: Text(title),
actions: [
buildThemeModeIcon(context),
buildMoreVertSettingsButon(context),
],
)
: null,
body: isLoading
? const Center(child: CircularProgressIndicator())
: _data != null
? GalleryInfo(_data!, files: _files)
: Center(
child: Text("Error: $_error"),
));
}
@override
void dispose() {
_cancel?.cancel();
_markAsNsfwCancel?.cancel();
super.dispose();
}
}

108
lib/pages/home.dart Normal file
View File

@@ -0,0 +1,108 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:logging/logging.dart';
import '../globals.dart';
import '../main.dart';
final _log = Logger("HomePage");
class HomeDrawer extends StatelessWidget {
const HomeDrawer({super.key});
@override
Widget build(BuildContext context) {
return Drawer(
child: ListView(
children: <Widget>[
Row(
children: [
Expanded(child: Container()),
IconButton(
onPressed: () => Scaffold.of(context).closeDrawer(),
icon: const Icon(Icons.close))
],
),
ListTile(
leading: const Icon(Icons.collections),
title: Text(AppLocalizations.of(context)!.galleries),
onTap: () {
Scaffold.of(context).closeDrawer();
context.push("/galleries");
},
),
auth.isAdmin == true
? ListTile(
leading: const Icon(Icons.admin_panel_settings),
title: Text(AppLocalizations.of(context)!.serverSettings),
onTap: () {
Scaffold.of(context).closeDrawer();
context.push("/server_settings");
},
)
: Container(),
auth.canManageTasks == true
? ListTile(
leading: const Icon(Icons.task),
title: Text(AppLocalizations.of(context)!.taskManager),
onTap: () {
Scaffold.of(context).closeDrawer();
context.push("/task_manager");
},
)
: Container(),
ListTile(
leading: const Icon(Icons.settings),
title: Text(AppLocalizations.of(context)!.settings),
onTap: () {
Scaffold.of(context).closeDrawer();
context.push("/settings");
},
)
],
));
}
}
class HomePage extends HookWidget {
const HomePage({super.key});
static const String routeName = '/';
@override
Widget build(BuildContext context) {
tryInitApi(context);
var mode = useState(MainApp.of(context).themeMode);
mode.value = MainApp.of(context).themeMode;
setCurrentTitle("", Theme.of(context).primaryColor.value, usePrefix: true);
return Scaffold(
appBar: AppBar(
title: Text(AppLocalizations.of(context)!.titleBar),
actions: [
IconButton(
onPressed: () {
final n = themeModeNext(mode.value);
MainApp.of(context).changeThemeMode(n);
mode.value = n;
},
icon: Icon(mode.value == ThemeMode.system
? Icons.brightness_auto
: mode.value == ThemeMode.dark
? Icons.dark_mode
: Icons.light_mode)),
buildMoreVertSettingsButon(context),
],
),
drawer: const HomeDrawer(),
body: Center(
child: TextButton(
child: Text('Hello World!'),
onPressed: () {
context.push("/galleries");
},
),
),
);
}
}

212
lib/pages/login.dart Normal file
View File

@@ -0,0 +1,212 @@
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:go_router/go_router.dart';
import 'package:logging/logging.dart';
import '../globals.dart';
import '../platform/device.dart';
final _log = Logger("LoginPage");
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
static const String routeName = '/login';
@override
State<LoginPage> createState() => _LoginPageState();
}
Future<bool> login(String username, String password) async {
String baseUrl = api.baseUrl!;
final u = Uri.parse(baseUrl);
_log.info("Secure level: ${u.scheme}");
final re = await api.createToken(
username: username,
password: password,
setCookie: true,
httpOnly: true,
secure: u.scheme == 'https' || u.host == "localhost",
client: "flutter",
device: await device,
clientVersion: await clientVersion,
clientPlatform: clientPlatform);
if (re.ok) return true;
if (re.status == 4) return false;
throw re.unwrapErr();
}
class _LoginPageState extends State<LoginPage> with ThemeModeWidget {
final _formKey = GlobalKey<FormState>();
String _username = "";
String _password = "";
bool _passwordVisible = false;
bool _isValid = false;
bool _isLogin = false;
bool _checkAuth = false;
@override
void initState() {
super.initState();
_username = "";
_password = "";
_passwordVisible = false;
_isValid = false;
_isLogin = false;
}
static bool _checkIsValid(String username, String password) {
return (username.isNotEmpty && password.isNotEmpty);
}
void _usernameChanged(String value) {
bool isValid = _checkIsValid(value, _password);
setState(() {
_username = value;
_isValid = isValid;
});
}
void _passwordChanged(String value) {
bool isValid = _checkIsValid(_username, value);
setState(() {
_password = value;
_isValid = isValid;
});
}
void _passwordVisibleChanged() {
setState(() {
_passwordVisible = !_passwordVisible;
});
}
void _checkStatus(BuildContext build) {
if (auth.isAuthed) {
SchedulerBinding.instance.addPostFrameCallback((_) {
build.go("/");
});
return;
}
if (!auth.checked) {
if (_checkAuth) return;
_checkAuth = true;
auth.checkAuth().then((re) {
_checkAuth = false;
if (re) {
build.go("/");
}
}).catchError((e) {
_log.severe("Failed to check auth info:", e);
_checkAuth = false;
});
}
}
@override
Widget build(BuildContext context) {
tryInitApi(context);
_checkStatus(context);
return Scaffold(
appBar: AppBar(
leading: auth.user == null
? Container()
: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
context.canPop() ? context.pop() : context.go("/");
}),
title: Text(AppLocalizations.of(context)!.login),
actions: [
buildThemeModeIcon(context),
],
),
body: PopScope(
canPop: auth.user != null,
child: Container(
padding: MediaQuery.of(context).size.width > 810
? const EdgeInsets.symmetric(horizontal: 100)
: null,
child: Form(
key: _formKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.symmetric(vertical: 8),
child: TextFormField(
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText:
AppLocalizations.of(context)!.username,
),
initialValue: _username,
onChanged: _usernameChanged,
)),
Container(
padding: const EdgeInsets.symmetric(vertical: 8),
child: TextFormField(
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText:
AppLocalizations.of(context)!.password,
suffixIcon: IconButton(
icon: Icon(
_passwordVisible
? Icons.visibility
: Icons.visibility_off,
color: Theme.of(context).primaryColorDark,
),
onPressed: _passwordVisibleChanged,
),
),
initialValue: _password,
onChanged: _passwordChanged,
obscureText: !_passwordVisible,
)),
ElevatedButton(
onPressed: _isValid && !_isLogin
? () {
setState(() {
_isLogin = true;
});
login(_username, _password).then((re) {
if (re) {
clearAllStates(context);
context.canPop()
? context.pop()
: context.go("/");
} else {
final snackBar = SnackBar(
content: Text(
AppLocalizations.of(context)!
.incorrectUserPassword));
ScaffoldMessenger.of(context)
.showSnackBar(snackBar);
setState(() {
_isLogin = false;
});
}
}).catchError((e) {
_log.severe("Failed to login:", e);
final isNetworkError =
e is! (int, String);
final snackBar = SnackBar(
content: Text(isNetworkError
? AppLocalizations.of(context)!
.networkError
: AppLocalizations.of(context)!
.internalError));
ScaffoldMessenger.of(context)
.showSnackBar(snackBar);
setState(() {
_isLogin = false;
});
});
}
: null,
child: Text(AppLocalizations.of(context)!.login)),
])))),
);
}
}

View File

@@ -0,0 +1,745 @@
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:go_router/go_router.dart';
import 'package:logging/logging.dart';
import '../api/config.dart';
import '../components/labeled_checkbox.dart';
import '../components/number_field.dart';
import '../components/string_list_field.dart';
import '../components/string_map_field.dart';
import '../globals.dart';
import '../platform/ua.dart';
final _log = Logger("ServerSettingsPage");
class ServerSettingsPage extends StatefulWidget {
const ServerSettingsPage({super.key});
static get routeName => "/server_settings";
@override
State<ServerSettingsPage> createState() => _ServerSettingsPage();
}
class _ServerSettingsPage extends State<ServerSettingsPage>
with IsTopWidget2, ThemeModeWidget {
final _formKey = GlobalKey<FormState>();
late bool _isLoading;
late bool _isSaving;
late bool _changed;
late ScrollController _controller;
late ConfigOptional _now;
Config? _config;
Object? _error;
CancelToken? _cancel;
CancelToken? _saveCancel;
late TextEditingController _uaController;
late AppLocalizations i18n;
Future<void> _fetchData() async {
_cancel = CancelToken();
try {
final config = await api.getConfig(cancel: _cancel);
if (!_cancel!.isCancelled) {
setState(() {
_config = config;
_error = null;
});
}
} catch (e) {
if (!_cancel!.isCancelled) {
_log.warning("Error when fetching config:", e);
setState(() {
_error = e;
});
}
}
}
Future<void> _saveConfig() async {
if (_isSaving) return;
try {
_now.corsCredentialsHosts?.removeWhere((e) => e.isEmpty);
_now.meiliHosts?.removeWhere((k, v) => k.isEmpty || v.isEmpty);
_saveCancel = CancelToken();
setState(() {
_isSaving = true;
});
await api.updateConfig(_now, cancel: _saveCancel);
if (!_saveCancel!.isCancelled) {
setState(() {
_isSaving = false;
_now = ConfigOptional();
_changed = false;
_config = null;
});
}
} catch (e) {
if (!_saveCancel!.isCancelled) {
_log.warning("Error when saving config:", e);
setState(() {
_isSaving = false;
});
}
}
}
@override
void initState() {
super.initState();
_isLoading = false;
_isSaving = false;
_changed = false;
_controller = ScrollController();
_now = ConfigOptional();
if (kIsWeb) {
_uaController = TextEditingController();
}
}
@override
void dispose() {
_cancel?.cancel();
_formKey.currentState?.dispose();
_controller.dispose();
_saveCancel?.cancel();
if (kIsWeb) {
_uaController.dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
if (!tryInitApi(context)) {
return Container();
}
this.i18n = AppLocalizations.of(context)!;
final isLoading = _config == null && _error == null;
if (isLoading && !_isLoading) _fetchData();
final i18n = AppLocalizations.of(context)!;
final cs = Theme.of(context).colorScheme;
if (isTop(context)) {
setCurrentTitle(i18n.serverSettings, cs.primary.value);
}
return Scaffold(
appBar: isLoading
? AppBar(
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
context.canPop() ? context.pop() : context.go("/");
},
),
title: Text(i18n.serverSettings),
actions: [
buildThemeModeIcon(context),
buildMoreVertSettingsButon(context),
])
: null,
body: isLoading
? const Center(child: CircularProgressIndicator())
: _error != null
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SelectableText("Error $_error"),
ElevatedButton.icon(
onPressed: () {
_fetchData();
setState(() {
_error = null;
});
},
icon: const Icon(Icons.refresh),
label: Text(AppLocalizations.of(context)!.retry))
]))
: _buildForm(context));
}
String? urlOriginValidator(String? s) {
if (s == null || s.isEmpty) return null;
try {
final u = Uri.parse(s);
if (u.hasQuery ||
u.userInfo.isNotEmpty ||
u.hasFragment ||
!u.hasEmptyPath ||
!u.hasScheme) return i18n.invalidURLOrigin;
if (u.scheme != "http" && u.scheme != "https") {
return i18n.httpHttpsNeeded;
}
return null;
} catch (e) {
return i18n.invalidURL;
}
}
Widget _buildForm(BuildContext context) {
final i18n = AppLocalizations.of(context)!;
return Form(
key: _formKey,
child: CustomScrollView(
controller: _controller,
slivers: [
SliverAppBar(
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
context.canPop() ? context.pop() : context.go("/");
},
),
title: Text(i18n.serverSettings),
actions: [
buildThemeModeIcon(context),
buildMoreVertSettingsButon(context),
],
floating: true),
SliverList(
delegate: SliverChildListDelegate([
_buildCheckBox(context),
_buildTextBox(context),
_buildBottomBar(context),
])),
],
));
}
Widget _buildWithHorizontalPadding(BuildContext context, Widget child) {
return Container(
padding: MediaQuery.of(context).size.width > 810
? const EdgeInsets.symmetric(horizontal: 100)
: const EdgeInsets.symmetric(horizontal: 16),
child: child,
);
}
Widget _buildWithVecticalPadding(Widget child) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 8),
child: child,
);
}
Widget _buildCheckBox(BuildContext context) {
final i18n = AppLocalizations.of(context)!;
return _buildWithHorizontalPadding(
context,
Column(mainAxisSize: MainAxisSize.min, children: [
_buildWithVecticalPadding(LabeledCheckbox(
value: _now.ex ?? _config!.ex,
onChanged: (b) {
if (b != null) {
setState(() {
_now.ex = b;
_changed = true;
});
}
},
label: Text(i18n.useEx))),
_buildWithVecticalPadding(LabeledCheckbox(
value: _now.mpv ?? _config!.mpv,
onChanged: (b) {
if (b != null) {
setState(() {
_now.mpv = b;
_changed = true;
});
}
},
label: Text(i18n.mpv))),
_buildWithVecticalPadding(LabeledCheckbox(
value: _now.downloadOriginalImg ?? _config!.downloadOriginalImg,
onChanged: (b) {
if (b != null) {
setState(() {
_now.downloadOriginalImg = b;
_changed = true;
});
}
},
label: Text(i18n.downloadOriginalImg))),
_buildWithVecticalPadding(LabeledCheckbox(
value: _now.exportZipJpnTitle ?? _config!.exportZipJpnTitle,
onChanged: (b) {
if (b != null) {
setState(() {
_now.exportZipJpnTitle = b;
_changed = true;
});
}
},
label: Text(i18n.exportZipJpnTitle))),
_buildWithVecticalPadding(LabeledCheckbox(
value:
_now.removePreviousGallery ?? _config!.removePreviousGallery,
onChanged: (b) {
if (b != null) {
setState(() {
_now.removePreviousGallery = b;
_changed = true;
});
}
},
label: Text(i18n.removePreviousGallery))),
_buildWithVecticalPadding(LabeledCheckbox(
value: _now.redirectToFlutter ?? _config!.redirectToFlutter,
onChanged: (b) {
if (b != null) {
setState(() {
_now.redirectToFlutter = b;
_changed = true;
});
}
},
label: Text(i18n.redirectToFlutter))),
]));
}
Widget _buildTextBox(BuildContext context) {
final i18n = AppLocalizations.of(context)!;
if (kIsWeb) {
final t = _now.ua ?? _config!.ua ?? "";
if (_uaController.text != t) {
_uaController.text = t;
}
}
return _buildWithHorizontalPadding(
context,
Column(mainAxisSize: MainAxisSize.min, children: [
_buildWithVecticalPadding(TextFormField(
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText:
_config!.cookies ? i18n.enterNewCookies : i18n.enterCookies,
),
onChanged: (s) {
setState(() {
_now.cookies = s;
_changed = true;
});
},
)),
_buildWithVecticalPadding(TextFormField(
initialValue: _now.dbPath ?? _config!.dbPath,
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: i18n.serverDbPath,
helperText: auth.isDocker == true
? i18n.dockerHelper
: i18n.serverDbPathHelp,
),
onChanged: (s) {
setState(() {
_now.dbPath = s;
_changed = true;
});
})),
_buildWithVecticalPadding(TextFormField(
initialValue: kIsWeb ? null : _now.ua ?? _config!.ua,
controller: kIsWeb ? _uaController : null,
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: i18n.userAgent,
counter: kIsWeb
? TextButton(
onPressed: () {
setState(() {
_now.ua = oUA;
_changed = true;
});
},
child: Text(i18n.useBrowserUA))
: null,
),
onChanged: (s) {
setState(() {
_now.ua = s;
_changed = true;
});
})),
_buildWithVecticalPadding(TextFormField(
initialValue: _now.base ?? _config!.base,
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: i18n.downloadLocation,
helperText: auth.isDocker == true ? i18n.dockerHelper : null,
),
onChanged: (s) {
setState(() {
_now.base = s;
_changed = true;
});
},
)),
_buildWithVecticalPadding(NumberFormField(
min: 1,
initialValue: _now.maxTaskCount ?? _config!.maxTaskCount,
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: i18n.maxTaskCount,
),
onChanged: (s) {
if (s != null) {
setState(() {
_now.maxTaskCount = s;
_changed = true;
});
}
},
)),
_buildWithVecticalPadding(NumberFormField(
min: 1,
initialValue: _now.maxRetryCount ?? _config!.maxRetryCount,
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: i18n.maxRetryCount,
),
onChanged: (s) {
if (s != null) {
setState(() {
_now.maxRetryCount = s;
_changed = true;
});
}
},
)),
_buildWithVecticalPadding(NumberFormField(
min: 1,
initialValue:
_now.maxDownloadImgCount ?? _config!.maxDownloadImgCount,
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: i18n.maxDownloadImgCount,
),
onChanged: (s) {
if (s != null) {
setState(() {
_now.maxDownloadImgCount = s;
_changed = true;
});
}
},
)),
auth.isDocker == true
? Container()
: _buildWithVecticalPadding(NumberFormField(
min: 0,
max: 65535,
initialValue: _now.port ?? _config!.port,
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: i18n.listeningPort,
),
onChanged: (s) {
if (s != null) {
setState(() {
_now.port = s;
_changed = true;
});
}
},
)),
auth.isDocker == true
? Container()
: _buildWithVecticalPadding(TextFormField(
initialValue: _now.hostname ?? _config!.hostname,
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: i18n.listeningHostname,
),
onChanged: (s) {
setState(() {
_now.hostname = s;
_changed = true;
});
},
)),
_buildWithVecticalPadding(TextFormField(
initialValue: _now.meiliHost ?? _config!.meiliHost,
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: i18n.meiliHost,
),
onChanged: (s) {
setState(() {
_now.meiliHost = s;
_changed = true;
});
},
)),
_buildWithVecticalPadding(TextFormField(
initialValue: _now.meiliSearchApiKey ?? _config!.meiliSearchApiKey,
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: i18n.meiliSearchApiKey,
),
onChanged: (s) {
setState(() {
_now.meiliSearchApiKey = s;
_changed = true;
});
},
)),
_buildWithVecticalPadding(TextFormField(
initialValue: _now.meiliUpdateApiKey ?? _config!.meiliUpdateApiKey,
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: i18n.meiliUpdateApiKey,
),
onChanged: (s) {
setState(() {
_now.meiliUpdateApiKey = s;
_changed = true;
});
},
)),
_buildWithVecticalPadding(TextFormField(
initialValue: _now.ffmpegPath ?? _config!.ffmpegPath,
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: i18n.ffmpegPath,
helperText: auth.isDocker == true ? i18n.dockerHelper : null,
),
onChanged: (s) {
setState(() {
_now.ffmpegPath = s;
_changed = true;
});
},
)),
_buildWithVecticalPadding(DropdownButtonFormField<ThumbnailMethod>(
items: [
DropdownMenuItem(
value: ThumbnailMethod.ffmpegBinary,
child: Text(i18n.thumbnailMethod0)),
DropdownMenuItem(
value: ThumbnailMethod.ffmpegApi,
child: Text(i18n.thumbnailMethod1)),
],
onChanged: (v) {
if (v != null) {
setState(() {
_now.thumbnailMethod = v;
_changed = true;
});
}
},
value: _now.thumbnailMethod ?? _config!.thumbnailMethod,
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: i18n.thumbnailMethod,
))),
_buildWithVecticalPadding(TextFormField(
initialValue: _now.thumbnailDir ?? _config!.thumbnailDir,
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: i18n.thumbnailDir,
helperText: auth.isDocker == true ? i18n.dockerHelper : null,
),
onChanged: (s) {
setState(() {
_now.thumbnailDir = s;
_changed = true;
});
},
)),
_buildWithVecticalPadding(TextFormField(
initialValue: _now.imgVerifySecret ?? _config!.imgVerifySecret,
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: i18n.imgVerifySecret,
),
onChanged: (s) {
setState(() {
_now.imgVerifySecret = s;
_changed = true;
});
},
)),
StringMapFormField(
key: const ValueKey("meiliHosts"),
initialValue: _now.meiliHosts ?? _config!.meiliHosts,
keyDecoration: const InputDecoration(
border: OutlineInputBorder(),
),
valueDecoration: const InputDecoration(
border: OutlineInputBorder(),
),
padding: const EdgeInsets.symmetric(vertical: 8),
keyPadding: const EdgeInsets.only(right: 4),
valuePadding: const EdgeInsets.only(left: 4),
onChanged: (s) {
setState(() {
_now.meiliHosts = s;
_changed = true;
});
},
keyValidator: urlOriginValidator,
valueValidator: urlOriginValidator,
keyAutovalidateMode: AutovalidateMode.onUserInteraction,
valueAutovalidateMode: AutovalidateMode.onUserInteraction,
label: Text(i18n.meiliHosts),
constraints: const BoxConstraints(
maxHeight: 300,
),
helper: Text(i18n.meiliHostsHelp,
style: Theme.of(context).textTheme.bodySmall),
),
StringListFormField(
key: const ValueKey("corsCredentialsHosts"),
initialValue:
_now.corsCredentialsHosts ?? _config!.corsCredentialsHosts,
decoration: InputDecoration(
border: const OutlineInputBorder(),
hintText: i18n.corsCredentialsHostsHint,
),
padding: const EdgeInsets.symmetric(vertical: 8),
onChanged: (s) {
setState(() {
_now.corsCredentialsHosts = s;
_changed = true;
});
},
validator: urlOriginValidator,
autovalidateMode: AutovalidateMode.onUserInteraction,
label: Text(i18n.corsCredentialsHosts),
constraints: const BoxConstraints(
maxHeight: 300,
),
helper: Text(i18n.corsCredentialsHostsHelp,
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(color: Colors.red)),
),
_buildWithVecticalPadding(TextFormField(
initialValue: _now.flutterFrontend ?? _config!.flutterFrontend,
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: i18n.flutterFrontend,
helperText: auth.isDocker == true ? i18n.dockerHelper : null,
),
onChanged: (s) {
setState(() {
_now.flutterFrontend = s;
_changed = true;
});
},
)),
_buildWithVecticalPadding(NumberFormField(
min: 1,
initialValue: _now.fetchTimeout ?? _config!.fetchTimeout,
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: i18n.fetchTimeout,
suffixText: i18n.millisecond,
),
onChanged: (s) {
setState(() {
_now.fetchTimeout = s;
_changed = true;
});
},
)),
_buildWithVecticalPadding(NumberFormField(
min: 1,
initialValue: _now.downloadTimeout ?? _config!.downloadTimeout,
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: i18n.downloadTimeout,
suffixText: i18n.millisecond,
helperText: i18n.downloadTimeoutHelp,
helperMaxLines: 3,
),
onChanged: (s) {
setState(() {
_now.downloadTimeout = s;
_changed = true;
});
},
)),
_buildWithVecticalPadding(TextFormField(
initialValue: _now.ffprobePath ?? _config!.ffprobePath,
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: i18n.ffprobePath,
),
onChanged: (s) {
setState(() {
_now.ffprobePath = s;
_changed = true;
});
},
)),
_buildWithVecticalPadding(NumberFormField(
min: 1,
initialValue: _now.downloadTimeoutCheckInterval ??
_config!.downloadTimeoutCheckInterval,
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: i18n.downloadTimeoutCheckInterval,
suffixText: i18n.millisecond,
helperText: i18n.downloadTimeoutCheckIntervalHelp,
helperMaxLines: 3,
),
onChanged: (s) {
setState(() {
_now.downloadTimeoutCheckInterval = s;
_changed = true;
});
},
)),
_buildWithVecticalPadding(NumberFormField(
min: 1,
initialValue:
_now.ehMetadataCacheTime ?? _config!.ehMetadataCacheTime,
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: i18n.ehMetadataCacheTime,
suffixText: i18n.hour,
),
onChanged: (s) {
setState(() {
_now.ehMetadataCacheTime = s;
_changed = true;
});
},
)),
_buildWithVecticalPadding(TextFormField(
initialValue: _now.randomFileSecret ?? _config!.randomFileSecret,
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: i18n.randomFileSecret,
),
onChanged: (s) {
setState(() {
_now.randomFileSecret = s;
_changed = true;
});
},
)),
]));
}
Widget _buildBottomBar(BuildContext context) {
final i18n = AppLocalizations.of(context)!;
return _buildWithHorizontalPadding(
context,
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
_buildWithVecticalPadding(ElevatedButton.icon(
icon: const Icon(Icons.save),
label: Text(i18n.save),
onPressed: _changed
? () {
_saveConfig();
}
: null)),
],
));
}
}

141
lib/pages/set_server.dart Normal file
View File

@@ -0,0 +1,141 @@
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:go_router/go_router.dart';
import 'package:logging/logging.dart';
import '../globals.dart';
final _log = Logger("SetServerPage");
class SetServerPage extends StatefulWidget {
const SetServerPage({super.key});
static const String routeName = '/set_server';
@override
State<SetServerPage> createState() => _SetServerPageState();
}
class _SetServerPageState extends State<SetServerPage> with ThemeModeWidget {
String _serverUrl = "";
String _apiPath = "/api/";
bool _isValid = false;
final _formKey = GlobalKey<FormState>();
@override
void initState() {
super.initState();
String? baseUrl = prefs.getString('baseUrl');
if (baseUrl != null) {
try {
Uri url = Uri.parse(baseUrl);
_serverUrl = url.origin;
_apiPath = url.path;
_isValid = true;
} catch (e) {
_log.warning("Failed to parse baseUrl:", e);
}
}
}
static bool _checkIsValid(String serverUrl, String apiPath) {
try {
Uri url = Uri.parse(serverUrl + apiPath);
return url.isAbsolute;
} catch (e) {
return false;
}
}
void _serverUrlChanged(String value) {
bool isValid = _checkIsValid(value, _apiPath);
setState(() {
_serverUrl = value;
_isValid = isValid;
});
}
void _apiPathChanged(String value) {
bool isValid = _checkIsValid(_serverUrl, value);
setState(() {
_apiPath = value;
_isValid = isValid;
});
}
@override
Widget build(BuildContext context) {
bool? skipBaseUrl = const bool.fromEnvironment("skipBaseUrl");
if (skipBaseUrl == true) {
SchedulerBinding.instance.addPostFrameCallback((_) {
context.go("/");
});
}
final bool hasBaseUrl = prefs.getString('baseUrl') != null;
var actions = [
buildThemeModeIcon(context),
];
if (hasBaseUrl) actions.add(buildMoreVertSettingsButon(context));
return Scaffold(
appBar: AppBar(
title: Text(AppLocalizations.of(context)!.setServerUrl),
leading: hasBaseUrl
? IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
context.canPop() ? context.pop() : context.go("/");
},
)
: null,
actions: actions,
),
body: Container(
padding: MediaQuery.of(context).size.width > 810
? const EdgeInsets.symmetric(horizontal: 100)
: null,
child: Form(
key: _formKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.symmetric(vertical: 8),
child: TextFormField(
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: AppLocalizations.of(context)!.serverHost,
),
initialValue: _serverUrl,
onChanged: _serverUrlChanged,
)),
Container(
padding: const EdgeInsets.symmetric(vertical: 8),
child: TextFormField(
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: AppLocalizations.of(context)!.apiPath,
),
initialValue: _apiPath,
onChanged: _apiPathChanged,
)),
ElevatedButton(
onPressed: _isValid
? () {
prefs
.setString('baseUrl', _serverUrl + _apiPath)
.then((re) {
if (re) {
tryInitApi(context);
context.canPop()
? context.pop()
: context.go("/");
}
});
}
: null,
child: Text(AppLocalizations.of(context)!.save)),
],
))),
);
}
}

424
lib/pages/settings.dart Normal file
View File

@@ -0,0 +1,424 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:go_router/go_router.dart';
import 'package:logging/logging.dart';
import '../globals.dart';
import '../main.dart';
import '../utils.dart';
import '../utils/filesize.dart';
final _log = Logger("SettingsPage");
class SettingsPage extends StatefulWidget {
const SettingsPage({super.key});
static const String routeName = '/settings';
@override
State<SettingsPage> createState() => _SettingsPage();
}
class _SettingsPage extends State<SettingsPage> with ThemeModeWidget {
bool _oriDisplayAd = false;
Lang _oriLang = Lang.system;
bool _oriPreventScreenCapture = false;
bool _oriShowNsfw = false;
bool _oriShowTranslatedTag = false;
bool _oriUseTitleJpn = false;
bool _oriDlUseAvgSpeed = false;
bool _oriEnableImageCache = false;
bool _displayAd = false;
Lang _lang = Lang.system;
bool _preventScreenCapture = false;
bool _showNsfw = false;
bool _showTranslatedTag = false;
bool _useTitleJpn = false;
bool _dlUseAvgSpeed = false;
bool _enableImageCache = false;
@override
void initState() {
super.initState();
try {
_oriLang = Lang.values[prefs.getInt("lang") ?? 0];
_lang = _oriLang;
} catch (e) {
_log.warning("Failed to get lang:", e);
_oriLang = Lang.system;
_lang = Lang.system;
}
try {
_oriUseTitleJpn = prefs.getBool("useTitleJpn") ?? false;
_useTitleJpn = _oriUseTitleJpn;
} catch (e) {
_log.warning("Failed to get useTitleJpn:", e);
_oriUseTitleJpn = false;
_useTitleJpn = false;
}
try {
_oriShowNsfw = prefs.getBool("showNsfw") ?? false;
_showNsfw = _oriShowNsfw;
} catch (e) {
_log.warning("Failed to get showNsfw:", e);
_oriShowNsfw = false;
_showNsfw = false;
}
try {
_oriDisplayAd = prefs.getBool("displayAd") ?? false;
_displayAd = _oriDisplayAd;
} catch (e) {
_log.warning("Failed to get displayAd:", e);
_oriDisplayAd = false;
_displayAd = false;
}
try {
_oriShowTranslatedTag = prefs.getBool("showTranslatedTag") ??
_oriLang.toLocale().languageCode == "zh";
_showTranslatedTag = _oriShowTranslatedTag;
} catch (e) {
_log.warning("Failed to get showTranslatedTag:", e);
_oriShowTranslatedTag = false;
_showTranslatedTag = false;
}
try {
_oriPreventScreenCapture = prefs.getBool("preventScreenCapture") ?? false;
_preventScreenCapture = _oriPreventScreenCapture;
} catch (e) {
_log.warning("Failed to get preventScreenCapture:", e);
_oriPreventScreenCapture = false;
_preventScreenCapture = false;
}
try {
_oriDlUseAvgSpeed = prefs.getBool("dlUseAvgSpeed") ?? false;
_dlUseAvgSpeed = _oriDlUseAvgSpeed;
} catch (e) {
_log.warning("Failed to get dlUseAvgSpeed:", e);
_oriDlUseAvgSpeed = false;
_dlUseAvgSpeed = false;
}
try {
_oriEnableImageCache = prefs.getBool("enableImageCache") ?? true;
_enableImageCache = _oriEnableImageCache;
} catch (e) {
_log.warning("Failed to get enableImageCache:", e);
_oriEnableImageCache = true;
_enableImageCache = true;
}
}
void fallback(BuildContext context) {
if (_oriLang != _lang) {
MainApp.of(context).changeLang(_oriLang);
}
}
void reset(BuildContext context) {
if (_lang != Lang.system) MainApp.of(context).changeLang(Lang.system);
setState(() {
_lang = Lang.system;
_useTitleJpn = false;
_showNsfw = false;
_displayAd = false;
_showTranslatedTag = _oriLang.toLocale().languageCode == "zh";
_preventScreenCapture = false;
_dlUseAvgSpeed = false;
_enableImageCache = true;
});
}
Future<bool> save() async {
bool re = true;
if (_lang != _oriLang && !await prefs.setInt("lang", _lang.index)) {
re = false;
_log.warning("Failed to save lang.");
} else {
_oriLang = _lang;
}
if (_oriUseTitleJpn != _useTitleJpn) {
if (!await prefs.setBool("useTitleJpn", _useTitleJpn)) {
re = false;
_log.warning("Failed to save useTitleJpn.");
} else {
_oriUseTitleJpn = _useTitleJpn;
}
}
if (_oriShowNsfw != _showNsfw) {
if (!await prefs.setBool("showNsfw", _showNsfw)) {
re = false;
_log.warning("Failed to save showNsfw.");
} else {
_oriShowNsfw = _showNsfw;
}
}
if (_oriDisplayAd != _displayAd) {
if (!await prefs.setBool("displayAd", _displayAd)) {
re = false;
_log.warning("Failed to save displayAd.");
} else {
_oriDisplayAd = _displayAd;
}
}
if (_oriShowTranslatedTag != _showTranslatedTag) {
if (!await prefs.setBool("showTranslatedTag", _showTranslatedTag)) {
re = false;
_log.warning("Failed to save showTranslatedTag.");
} else {
_oriShowTranslatedTag = _showTranslatedTag;
}
}
if (_oriPreventScreenCapture != _preventScreenCapture) {
if (!await prefs.setBool("preventScreenCapture", _preventScreenCapture)) {
re = false;
_log.warning("Failed to save preventScreenCapture.");
} else {
_oriPreventScreenCapture = _preventScreenCapture;
}
if (_preventScreenCapture) {
if (!await platformDisplay.enableProtect()) {
_log.warning("Failed to enable protect.");
}
} else {
if (!await platformDisplay.disableProtect()) {
_log.warning("Failed to disable protect.");
}
}
}
if (_dlUseAvgSpeed != _oriDlUseAvgSpeed) {
if (!await prefs.setBool("dlUseAvgSpeed", _dlUseAvgSpeed)) {
re = false;
_log.warning("Failed to save dlUseAvgSpeed.");
} else {
_oriDlUseAvgSpeed = _dlUseAvgSpeed;
}
}
if (_enableImageCache != _oriEnableImageCache) {
if (!await prefs.setBool("enableImageCache", _enableImageCache)) {
re = false;
_log.warning("Failed to save enableImageCache.");
} else {
_oriEnableImageCache = _enableImageCache;
}
}
return re;
}
Widget _buildCache(BuildContext context) {
final i18n = AppLocalizations.of(context)!;
final text = SelectableText(
"${i18n.cachedFileSize}${i18n.colon}${getFileSize(imageCaches.size)}");
final button = ElevatedButton(
onPressed: () {
imageCaches.updateSize(clear: true).then((_) {
setState(() {});
}).onError((e, _) {
_log.warning("Failed to update image cache size: $e");
return null;
});
},
child: Text(i18n.updateFileSize));
final cButton = Container(
padding: const EdgeInsets.only(left: 8),
child: ElevatedButton(
onPressed: () {
imageCaches.clear().then((_) {
setState(() {});
}).onError((e, _) {
_log.warning("Failed to clear image cache: $e");
return null;
});
},
child: Text(i18n.clearCaches)));
final maxWidth = MediaQuery.of(context).size.width;
final useTwoLine = maxWidth <= 500 ? true : false;
return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
CheckboxMenuButton(
value: _enableImageCache,
onChanged: (bool? value) {
if (value != null) {
setState(() {
_enableImageCache = value;
});
}
},
child: Text(i18n.enableImageCache),
),
Container(
padding: const EdgeInsets.only(left: 6, top: 4),
child: useTwoLine ? text : Row(children: [text, button, cButton])),
useTwoLine ? Row(children: [button, cButton]) : Container(),
]);
}
@override
Widget build(BuildContext context) {
final i18n = AppLocalizations.of(context)!;
setCurrentTitle(i18n.settings, Theme.of(context).primaryColor.value);
return Scaffold(
appBar: AppBar(
leading: IconButton(
onPressed: () {
fallback(context);
context.canPop() ? context.pop() : context.go("/");
},
icon: const Icon(Icons.arrow_back),
),
title: Text(i18n.settings),
actions: [
buildThemeModeIcon(context),
buildMoreVertSettingsButon(context),
],
),
body: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints:
BoxConstraints(minHeight: constraints.maxHeight),
child: Container(
padding: MediaQuery.of(context).size.width > 810
? const EdgeInsets.symmetric(horizontal: 100)
: null,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding:
const EdgeInsets.symmetric(vertical: 8),
child: DropdownMenu<Lang>(
initialSelection: _lang,
onSelected: (value) {
if (value != null) {
setState(() {
_lang = value;
});
MainApp.of(context).changeLang(_lang);
}
},
label: Text(i18n.lang),
dropdownMenuEntries: Lang.values
.map((e) => DropdownMenuEntry(
value: e,
label: e == Lang.system
? i18n.systemLang
: e.langName))
.toList(),
leadingIcon: const Icon(Icons.language),
)),
Container(
padding:
const EdgeInsets.symmetric(vertical: 8),
child: CheckboxMenuButton(
value: _useTitleJpn,
onChanged: (bool? value) {
if (value != null) {
setState(() {
_useTitleJpn = value;
});
}
},
child: Text(i18n.useTitleJpn),
)),
Container(
padding: const EdgeInsets.symmetric(vertical: 8),
child: CheckboxMenuButton(
value: _showNsfw,
onChanged: (bool? value) {
if (value != null) {
setState(() {
_showNsfw = value;
});
}
},
child: Text(i18n.showNsfw),
),
),
Container(
padding: const EdgeInsets.symmetric(vertical: 8),
child: CheckboxMenuButton(
value: _displayAd,
onChanged: (bool? value) {
if (value != null) {
setState(() {
_displayAd = value;
});
}
},
child: Text(i18n.displayAd),
),
),
Container(
padding: const EdgeInsets.symmetric(vertical: 8),
child: CheckboxMenuButton(
value: _showTranslatedTag,
onChanged: (bool? value) {
if (value != null) {
setState(() {
_showTranslatedTag = value;
});
}
},
child: Text(i18n.showTranslatedTag),
),
),
isAndroid || isWindows
? Container(
padding:
const EdgeInsets.symmetric(vertical: 8),
child: CheckboxMenuButton(
value: _preventScreenCapture,
onChanged: (bool? value) {
if (value != null) {
setState(() {
_preventScreenCapture = value;
});
}
},
child: Text(i18n.preventScreenCapture),
),
)
: Container(),
Container(
padding: const EdgeInsets.symmetric(vertical: 8),
child: CheckboxMenuButton(
value: _dlUseAvgSpeed,
onChanged: (bool? value) {
if (value != null) {
setState(() {
_dlUseAvgSpeed = value;
});
}
},
child: Text(i18n.dlUseAvgSpeed),
),
),
Container(
padding: const EdgeInsets.symmetric(vertical: 8),
child: _buildCache(context),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8),
child: ElevatedButton(
onPressed: () {
reset(context);
},
child: Text(i18n.reset))),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8),
child: ElevatedButton(
onPressed: () {
save();
},
child: Text(i18n.save),
))
],
),
],
))));
},
));
}
}

270
lib/pages/task_manager.dart Normal file
View File

@@ -0,0 +1,270 @@
import 'dart:ui';
import 'package:enum_flag/enum_flag.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:go_router/go_router.dart';
import '../api/task.dart';
import '../components/task.dart';
import '../globals.dart';
import '../platform/media_query.dart';
import '../utils.dart';
enum TaskStatusFilterFlag with EnumFlag {
wait,
running,
finished,
failed;
String localText(BuildContext context) {
final i18n = AppLocalizations.of(context)!;
switch (this) {
case TaskStatusFilterFlag.wait:
return i18n.waiting;
case TaskStatusFilterFlag.running:
return i18n.running;
case TaskStatusFilterFlag.finished:
return i18n.finished;
case TaskStatusFilterFlag.failed:
return i18n.failed;
}
}
}
const taskStatusFilterFlagAll = 15;
class TaskStatusFilter {
TaskStatusFilter({this.code = taskStatusFilterFlagAll});
int code;
bool has(TaskStatusFilterFlag flag) => code.hasFlag(flag);
bool get isAll => code == taskStatusFilterFlagAll;
void add(TaskStatusFilterFlag flag) {
code |= flag.value;
}
bool filter(TaskStatus status) {
if (isAll) return true;
switch (status) {
case TaskStatus.wait:
return has(TaskStatusFilterFlag.wait);
case TaskStatus.running:
return has(TaskStatusFilterFlag.running);
case TaskStatus.finished:
return has(TaskStatusFilterFlag.finished);
case TaskStatus.failed:
return has(TaskStatusFilterFlag.failed);
}
}
void remove(TaskStatusFilterFlag flag) {
code &= ~flag.value;
}
}
class TaskManagerPage extends StatefulWidget {
const TaskManagerPage({super.key});
static const String routeName = '/task_manager';
@override
State<TaskManagerPage> createState() => _TaskManagerPage();
}
class _TaskManagerPage extends State<TaskManagerPage>
with ThemeModeWidget, IsTopWidget2 {
late TaskStatusFilter _filter;
final GlobalKey<RefreshIndicatorState> _refreshIndicatorKey =
GlobalKey<RefreshIndicatorState>();
@override
void initState() {
_filter = TaskStatusFilter();
listener.on("task_list_changed", _onStateChanged);
super.initState();
}
@override
void dispose() {
listener.removeEventListener("task_list_changed", _onStateChanged);
super.dispose();
}
void _onStateChanged(dynamic _) {
setState(() {});
}
Widget _buildItem(BuildContext context, int index) {
final task = tasks.tasks[tasks.tasksList[index]];
if (task == null) {
return Container(key: ValueKey("unknown_$index"));
}
if (!_filter.filter(task.status)) {
return Container(key: ValueKey("filtered_task_${task.base.id}"));
}
return Padding(
padding: const EdgeInsets.all(8),
key: ValueKey("task_${task.base.id}"),
child: TaskView(task, index));
}
Widget _proxyDecorator(Widget child, int index, Animation<double> animation) {
return AnimatedBuilder(
animation: animation,
builder: (BuildContext context, Widget? child) {
final double animValue = Curves.easeInOut.transform(animation.value);
final double elevation = lerpDouble(0, 6, animValue)!;
return Material(
elevation: elevation,
child: child,
);
},
child: child,
);
}
void _onReorder(int oldIndex, int newIndex) {
setState(() {
if (oldIndex < newIndex) {
newIndex -= 1;
}
final task = tasks.tasksList.removeAt(oldIndex);
tasks.tasksList.insert(newIndex, task);
});
}
Widget _buildList(BuildContext context) {
return SliverReorderableList(
itemBuilder: _buildItem,
itemCount: tasks.tasksList.length,
onReorder: _onReorder,
proxyDecorator: _proxyDecorator);
}
Widget _buildChips() {
final i18n = AppLocalizations.of(context)!;
var list = <FilterChip>[
FilterChip(
label: Text(i18n.allTasks),
selected: _filter.isAll,
onSelected: (bool value) {
setState(() {
if (value) {
_filter.code = taskStatusFilterFlagAll;
} else {
_filter.code = 0;
}
});
},
)
];
for (var flag in TaskStatusFilterFlag.values) {
list.add(FilterChip(
label: Text(flag.localText(context)),
selected: _filter.has(flag),
onSelected: (bool value) {
setState(() {
if (value) {
_filter.add(flag);
} else {
_filter.remove(flag);
}
});
},
));
}
return SliverToBoxAdapter(
child: Wrap(
spacing: 5.0,
children: list,
));
}
Widget _buildView(BuildContext context) {
final i18n = AppLocalizations.of(context)!;
return CustomScrollView(
physics: const AlwaysScrollableScrollPhysics(),
slivers: <Widget>[
SliverAppBar(
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
context.canPop() ? context.pop() : context.go("/");
},
),
title: Text(i18n.taskManager),
actions: [
buildThemeModeIcon(context),
buildMoreVertSettingsButon(context),
],
floating: true,
),
_buildChips(),
_buildList(context),
],
);
}
Widget _buildAddMenu(BuildContext context) {
return PopupMenuButton(
icon: const Icon(Icons.add),
itemBuilder: (context) {
return [
PopupMenuItem(
value: TaskType.download,
child: Text(AppLocalizations.of(context)!.createDownloadTask),
)
];
},
onSelected: (TaskType type) {
if (type == TaskType.download) {
context.push("/dialog/new_download_task");
}
});
}
Widget _buildRefreshIcon(BuildContext context) {
final i18n = AppLocalizations.of(context)!;
return IconButton(
onPressed: () {
_refreshIndicatorKey.currentState?.show();
},
tooltip: i18n.refresh,
icon: const Icon(Icons.refresh));
}
Widget _buildIconList(BuildContext context) {
return Row(children: [
isDesktop || (kIsWeb && pointerIsMouse)
? _buildRefreshIcon(context)
: Container(),
_buildAddMenu(context),
]);
}
@override
Widget build(BuildContext context) {
if (!tryInitApi(context)) {
return Container();
}
final i18n = AppLocalizations.of(context)!;
if (isTop(context)) {
setCurrentTitle(i18n.taskManager, Theme.of(context).primaryColor.value);
}
final size = MediaQuery.of(context).size;
return Scaffold(
body: Stack(
children: <Widget>[
RefreshIndicator(
key: _refreshIndicatorKey,
onRefresh: () async {
return await tasks.refresh();
},
child: _buildView(context)),
Positioned(
bottom: size.height / 10,
right: size.width / 10,
child: _buildIconList(context))
],
),
);
}
}