mirror of
https://github.com/lifegpc/eh_downloader_flutter.git
synced 2026-07-08 01:30:28 +08:00
Move all pages to subdirectory
This commit is contained in:
232
lib/pages/create_root_user.dart
Normal file
232
lib/pages/create_root_user.dart
Normal 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
164
lib/pages/galleries.dart
Normal 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
151
lib/pages/gallery.dart
Normal 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
108
lib/pages/home.dart
Normal 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
212
lib/pages/login.dart
Normal 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)),
|
||||
])))),
|
||||
);
|
||||
}
|
||||
}
|
||||
745
lib/pages/server_settings.dart
Normal file
745
lib/pages/server_settings.dart
Normal 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
141
lib/pages/set_server.dart
Normal 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
424
lib/pages/settings.dart
Normal 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
270
lib/pages/task_manager.dart
Normal 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))
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user