mirror of
https://github.com/wgh136/pixes.git
synced 2025-09-27 12:57:24 +00:00
738 lines
23 KiB
Dart
738 lines
23 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:fluent_ui/fluent_ui.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:pixes/appdata.dart';
|
|
import 'package:pixes/components/keyboard.dart';
|
|
import 'package:pixes/components/md.dart';
|
|
import 'package:pixes/components/message.dart';
|
|
import 'package:pixes/components/page_route.dart';
|
|
import 'package:pixes/components/title_bar.dart';
|
|
import 'package:pixes/foundation/app.dart';
|
|
import 'package:pixes/pages/main_page.dart';
|
|
import 'package:pixes/utils/io.dart';
|
|
import 'package:pixes/utils/translation.dart';
|
|
import 'package:url_launcher/url_launcher_string.dart';
|
|
|
|
import 'logs.dart';
|
|
|
|
class SettingsPage extends StatefulWidget {
|
|
const SettingsPage({super.key});
|
|
|
|
@override
|
|
State<SettingsPage> createState() => _SettingsPageState();
|
|
}
|
|
|
|
class _SettingsPageState extends State<SettingsPage> {
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return ScaffoldPage(
|
|
padding: EdgeInsets.zero,
|
|
content: CustomScrollView(
|
|
slivers: [
|
|
SliverTitleBar(title: "Settings".tl),
|
|
buildHeader("Account".tl),
|
|
buildAccount(),
|
|
buildHeader("Browse".tl),
|
|
buildBrowse(),
|
|
buildHeader("Download".tl),
|
|
buildDownload(),
|
|
buildHeader("Appearance".tl),
|
|
buildAppearance(),
|
|
buildHeader("About".tl),
|
|
buildAbout(),
|
|
SliverPadding(
|
|
padding: EdgeInsets.only(bottom: context.padding.bottom)),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget buildHeader(String text) {
|
|
return SliverToBoxAdapter(
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
|
child: Text(text,
|
|
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget buildItem({required String title, String? subtitle, Widget? action}) {
|
|
return Card(
|
|
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
|
padding: EdgeInsets.zero,
|
|
child: ListTile(
|
|
title: Text(title),
|
|
subtitle: subtitle == null ? null : Text(subtitle),
|
|
trailing: action,
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget buildAccount() {
|
|
return SliverToBoxAdapter(
|
|
child: Column(
|
|
children: [
|
|
buildItem(
|
|
title: "Logout".tl,
|
|
action: Button(
|
|
onPressed: () {
|
|
showDialog<String>(
|
|
context: App.rootNavigatorKey.currentContext!,
|
|
builder: (context) => ContentDialog(
|
|
title: Text('Logout'.tl),
|
|
content: Text('Are you sure you want to logout?'.tl),
|
|
actions: [
|
|
Button(
|
|
child: Text('Continue'.tl),
|
|
onPressed: () {
|
|
appdata.account = null;
|
|
appdata.writeData();
|
|
App.rootNavigatorKey.currentState!.pushAndRemoveUntil(
|
|
AppPageRoute(
|
|
builder: (context) => const MainPage()),
|
|
(route) => false);
|
|
},
|
|
),
|
|
FilledButton(
|
|
child: Text('Cancel'.tl),
|
|
onPressed: () => context.pop(),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
child: Text("Continue".tl).fixWidth(64),
|
|
),
|
|
),
|
|
buildItem(
|
|
title: "Account Settings".tl,
|
|
action: Button(
|
|
child: Text("Edit".tl).fixWidth(64),
|
|
onPressed: () {
|
|
launchUrlString("https://www.pixiv.net/setting_user.php");
|
|
},
|
|
)),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget buildDownload() {
|
|
return SliverToBoxAdapter(
|
|
child: Column(
|
|
children: [
|
|
buildItem(
|
|
title: "Download Path".tl,
|
|
subtitle: appdata.settings["downloadPath"],
|
|
action: Button(
|
|
child: Text("Manage".tl).fixWidth(64),
|
|
onPressed: () {
|
|
if (Platform.isIOS) {
|
|
showToast(context, message: "Unsupported platform".tl);
|
|
return;
|
|
}
|
|
context.to(() => _SetSingleFieldPage(
|
|
"Download Path".tl,
|
|
"downloadPath",
|
|
check: (text) {
|
|
if (!Directory(text).havePermission()) {
|
|
return "No permission".tl;
|
|
} else {
|
|
return null;
|
|
}
|
|
},
|
|
));
|
|
}),
|
|
),
|
|
buildItem(
|
|
title: "Subpath".tl,
|
|
subtitle: appdata.settings["downloadSubPath"],
|
|
action: Button(
|
|
child: Text("Manage".tl).fixWidth(64),
|
|
onPressed: () {
|
|
context.to(() => const _SetDownloadSubPathPage());
|
|
}),
|
|
),
|
|
buildItem(
|
|
title: "Max parallels".tl,
|
|
action: SizedBox(
|
|
width: 64,
|
|
height: 32,
|
|
child: NumberBox<int>(
|
|
value: appdata.settings["maxParallels"],
|
|
autofocus: false,
|
|
onChanged: (value) {
|
|
appdata.settings["maxParallels"] = value;
|
|
appdata.writeSettings();
|
|
},
|
|
clearButton: false,
|
|
mode: SpinButtonPlacementMode.none,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget buildAbout() {
|
|
return SliverToBoxAdapter(
|
|
child: Column(
|
|
children: [
|
|
buildItem(title: "Version", subtitle: App.version),
|
|
buildItem(
|
|
title: "Check for updates on startup".tl,
|
|
action: ToggleSwitch(
|
|
checked: appdata.settings["checkUpdate"],
|
|
onChanged: (value) {
|
|
setState(() {
|
|
appdata.settings["checkUpdate"] = value;
|
|
});
|
|
appdata.writeData();
|
|
})),
|
|
buildItem(
|
|
title: "Github",
|
|
action: IconButton(
|
|
icon: const Icon(
|
|
MdIcons.open_in_new,
|
|
size: 18,
|
|
),
|
|
onPressed: () =>
|
|
launchUrlString("https://github.com/wgh136/pixes"),
|
|
)),
|
|
buildItem(
|
|
title: "Telegram",
|
|
action: IconButton(
|
|
icon: const Icon(
|
|
MdIcons.open_in_new,
|
|
size: 18,
|
|
),
|
|
onPressed: () => launchUrlString("https://t.me/pica_group"),
|
|
)),
|
|
buildItem(
|
|
title: "Logs",
|
|
action: IconButton(
|
|
icon: const Icon(
|
|
MdIcons.open_in_new,
|
|
size: 18,
|
|
),
|
|
onPressed: () => context.to(() => const LogsPage()))),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget buildBrowse() {
|
|
return SliverToBoxAdapter(
|
|
child: Column(
|
|
children: [
|
|
buildItem(
|
|
title: "Initial Page".tl,
|
|
action: Button(
|
|
child: Text("Edit".tl).fixWidth(64),
|
|
onPressed: () {
|
|
context.to(() => const _SetInitialPageWidget());
|
|
},
|
|
)),
|
|
buildItem(
|
|
title: "Proxy".tl,
|
|
action: Button(
|
|
child: Text("Edit".tl).fixWidth(64),
|
|
onPressed: () {
|
|
context.to(() => _SetSingleFieldPage(
|
|
"Http ${"Proxy".tl}",
|
|
"proxy",
|
|
));
|
|
},
|
|
)),
|
|
buildItem(
|
|
title: "Block(Account)".tl,
|
|
action: Button(
|
|
child: Text("Edit".tl).fixWidth(64),
|
|
onPressed: () {
|
|
launchUrlString("https://www.pixiv.net/setting_mute.php");
|
|
},
|
|
)),
|
|
buildItem(
|
|
title: "Block(Local)".tl,
|
|
action: Button(
|
|
child: Text("Edit".tl).fixWidth(64),
|
|
onPressed: () {
|
|
context.to(() => const _BlockTagsPage());
|
|
},
|
|
)),
|
|
buildItem(
|
|
title: "Shortcuts".tl,
|
|
action: Button(
|
|
child: Text("Edit".tl).fixWidth(64),
|
|
onPressed: () {
|
|
context.to(() => const ShortcutsSettings());
|
|
},
|
|
)),
|
|
buildItem(
|
|
title: "Display the original image on the details page".tl,
|
|
action: ToggleSwitch(
|
|
checked: appdata.settings['showOriginalImage'],
|
|
onChanged: (value) {
|
|
setState(() {
|
|
appdata.settings['showOriginalImage'] = value;
|
|
});
|
|
appdata.writeData();
|
|
})),
|
|
buildItem(
|
|
title: "Emphasize artworks from following artists".tl,
|
|
subtitle: "The border of the artworks will be darker".tl,
|
|
action: ToggleSwitch(
|
|
checked:
|
|
appdata.settings['emphasizeArtworksFromFollowingArtists'],
|
|
onChanged: (value) {
|
|
setState(() {
|
|
appdata.settings[
|
|
'emphasizeArtworksFromFollowingArtists'] = value;
|
|
});
|
|
appdata.writeData();
|
|
})),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget buildAppearance() {
|
|
return SliverToBoxAdapter(
|
|
child: Column(
|
|
children: [
|
|
buildItem(
|
|
title: "Theme".tl,
|
|
action: DropDownButton(
|
|
title: Text(appdata.settings["theme"] ?? "System".tl),
|
|
items: [
|
|
MenuFlyoutItem(
|
|
text: Text("System".tl),
|
|
onPressed: () {
|
|
setState(() {
|
|
appdata.settings["theme"] = "System";
|
|
});
|
|
appdata.writeData();
|
|
StateController.findOrNull(tag: "MyApp")?.update();
|
|
}),
|
|
MenuFlyoutItem(
|
|
text: Text("light".tl),
|
|
onPressed: () {
|
|
setState(() {
|
|
appdata.settings["theme"] = "Light";
|
|
});
|
|
appdata.writeData();
|
|
StateController.findOrNull(tag: "MyApp")?.update();
|
|
}),
|
|
MenuFlyoutItem(
|
|
text: Text("dark".tl),
|
|
onPressed: () {
|
|
setState(() {
|
|
appdata.settings["theme"] = "Dark";
|
|
});
|
|
appdata.writeData();
|
|
StateController.findOrNull(tag: "MyApp")?.update();
|
|
}),
|
|
])),
|
|
buildItem(
|
|
title: "Language".tl,
|
|
action: DropDownButton(
|
|
title: Text(appdata.settings["language"] ?? "System"),
|
|
items: [
|
|
MenuFlyoutItem(
|
|
text: const Text("System"),
|
|
onPressed: () {
|
|
setState(() {
|
|
appdata.settings["language"] = "System";
|
|
});
|
|
appdata.writeData();
|
|
StateController.findOrNull(tag: "MyApp")?.update();
|
|
}),
|
|
MenuFlyoutItem(
|
|
text: const Text("English"),
|
|
onPressed: () {
|
|
setState(() {
|
|
appdata.settings["language"] = "English";
|
|
});
|
|
appdata.writeData();
|
|
StateController.findOrNull(tag: "MyApp")?.update();
|
|
}),
|
|
MenuFlyoutItem(
|
|
text: const Text("简体中文"),
|
|
onPressed: () {
|
|
setState(() {
|
|
appdata.settings["language"] = "简体中文";
|
|
});
|
|
appdata.writeData();
|
|
StateController.findOrNull(tag: "MyApp")?.update();
|
|
}),
|
|
MenuFlyoutItem(
|
|
text: const Text("繁體中文"),
|
|
onPressed: () {
|
|
setState(() {
|
|
appdata.settings["language"] = "繁體中文";
|
|
});
|
|
appdata.writeData();
|
|
StateController.findOrNull(tag: "MyApp")?.update();
|
|
}),
|
|
])),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _SetSingleFieldPage extends StatefulWidget {
|
|
const _SetSingleFieldPage(this.title, this.field, {this.check});
|
|
|
|
final String title;
|
|
|
|
final String field;
|
|
|
|
final String? Function(String)? check;
|
|
|
|
@override
|
|
State<_SetSingleFieldPage> createState() => _SetSingleFieldPageState();
|
|
}
|
|
|
|
class _SetSingleFieldPageState extends State<_SetSingleFieldPage> {
|
|
late final controller =
|
|
TextEditingController(text: appdata.settings[widget.field]);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
TitleBar(title: widget.title),
|
|
TextBox(
|
|
controller: controller,
|
|
).paddingHorizontal(16),
|
|
const SizedBox(
|
|
height: 8,
|
|
),
|
|
Button(
|
|
child: Text("Confirm".tl),
|
|
onPressed: () {
|
|
var text = controller.text;
|
|
var checkRes = widget.check?.call(text);
|
|
if (checkRes == null) {
|
|
appdata.settings[widget.field] = text;
|
|
appdata.writeData();
|
|
context.pop();
|
|
} else {
|
|
showToast(context, message: checkRes);
|
|
}
|
|
},
|
|
).toAlign(Alignment.centerRight).paddingRight(16),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class _SetDownloadSubPathPage extends StatefulWidget {
|
|
const _SetDownloadSubPathPage();
|
|
|
|
@override
|
|
State<_SetDownloadSubPathPage> createState() =>
|
|
__SetDownloadSubPathPageState();
|
|
}
|
|
|
|
class __SetDownloadSubPathPageState extends State<_SetDownloadSubPathPage> {
|
|
final controller =
|
|
TextEditingController(text: appdata.settings["downloadSubPath"]);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return SingleChildScrollView(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
TitleBar(title: "Download subpath".tl),
|
|
Text("Rule".tl)
|
|
.padding(const EdgeInsets.symmetric(vertical: 8, horizontal: 16)),
|
|
TextBox(
|
|
controller: controller,
|
|
).paddingHorizontal(16),
|
|
const SizedBox(
|
|
height: 8,
|
|
),
|
|
Button(
|
|
child: Text("Confirm".tl),
|
|
onPressed: () {
|
|
var text = controller.text;
|
|
if (check(text)) {
|
|
appdata.settings["downloadSubPath"] = text;
|
|
appdata.writeData();
|
|
context.pop();
|
|
} else {
|
|
showToast(context, message: "Invalid".tl);
|
|
}
|
|
},
|
|
).toAlign(Alignment.centerRight).paddingRight(16),
|
|
const SizedBox(
|
|
height: 16,
|
|
),
|
|
SelectableText(_instruction).paddingHorizontal(16)
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
bool check(String text) {
|
|
if (text.startsWith('/') || text.startsWith('\\')) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
String get _instruction => """
|
|
${"Edit the rule for where to save an image.".tl}
|
|
${"Note: The rule should include the filename.".tl}
|
|
|
|
${"Some keywords will be replaced by the following rule:".tl}
|
|
\${title} -> ${"Title of the work".tl}
|
|
\${author} -> ${"Name of the author".tl}
|
|
\${id} -> ${"Artwork ID".tl}
|
|
\${index} -> ${"Index of the image in the artwork".tl}
|
|
\${page} -> ${"Replace with '-p\${index}' if the work have more than one images, otherwise replace with blank.".tl}
|
|
\${ext} -> ${"File extension".tl}
|
|
\${AI} -> ${"Replace with 'AI' if the work was generated by AI, otherwise replace with blank".tl}
|
|
\${tag(*)} -> ${"Replace with * if the work have tag *, otherwise replace with blank.".tl}
|
|
|
|
${"Multiple path separators will be automatically replaced with a single".tl}
|
|
""";
|
|
}
|
|
|
|
class _BlockTagsPage extends StatefulWidget {
|
|
const _BlockTagsPage();
|
|
|
|
@override
|
|
State<_BlockTagsPage> createState() => __BlockTagsPageState();
|
|
}
|
|
|
|
class __BlockTagsPageState extends State<_BlockTagsPage> {
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Column(
|
|
children: [
|
|
TitleBar(
|
|
title: "Block".tl,
|
|
action: FilledButton(
|
|
child: Text("Add".tl),
|
|
onPressed: () {
|
|
var controller = TextEditingController();
|
|
|
|
void finish(BuildContext context) {
|
|
var text = controller.text;
|
|
if (text.isNotEmpty &&
|
|
!(appdata.settings["blockTags"] as List).contains(text)) {
|
|
setState(() {
|
|
appdata.settings["blockTags"].add(text);
|
|
});
|
|
appdata.writeSettings();
|
|
}
|
|
context.pop();
|
|
}
|
|
|
|
showDialog(
|
|
context: context,
|
|
barrierDismissible: true,
|
|
builder: (context) {
|
|
return ContentDialog(
|
|
title: Text("Add".tl),
|
|
content: SizedBox(
|
|
width: 300,
|
|
height: 32,
|
|
child: TextBox(
|
|
controller: controller,
|
|
onSubmitted: (v) => finish(context),
|
|
),
|
|
),
|
|
actions: [
|
|
FilledButton(
|
|
child: Text("Submit".tl),
|
|
onPressed: () {
|
|
finish(context);
|
|
})
|
|
],
|
|
);
|
|
});
|
|
},
|
|
),
|
|
),
|
|
Expanded(
|
|
child: ListView.builder(
|
|
itemCount: appdata.settings["blockTags"].length,
|
|
itemBuilder: (context, index) {
|
|
return Card(
|
|
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
|
padding: EdgeInsets.zero,
|
|
child: ListTile(
|
|
title: Text(appdata.settings["blockTags"][index]),
|
|
trailing: Button(
|
|
child: Text("Delete".tl),
|
|
onPressed: () {
|
|
setState(() {
|
|
(appdata.settings["blockTags"] as List).removeAt(index);
|
|
});
|
|
appdata.writeSettings();
|
|
},
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
)
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class ShortcutsSettings extends StatefulWidget {
|
|
const ShortcutsSettings({super.key});
|
|
|
|
@override
|
|
State<ShortcutsSettings> createState() => _ShortcutsSettingsState();
|
|
}
|
|
|
|
class _ShortcutsSettingsState extends State<ShortcutsSettings> {
|
|
int listening = -1;
|
|
|
|
KeyEventListenerState? listener;
|
|
|
|
@override
|
|
void initState() {
|
|
listener = KeyEventListener.of(context);
|
|
super.initState();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
listener?.removeAll();
|
|
super.dispose();
|
|
}
|
|
|
|
final settings = <String>[
|
|
"Page down",
|
|
"Page up",
|
|
"Next work",
|
|
"Previous work",
|
|
"Add to favorites",
|
|
"Download",
|
|
"Follow the artist",
|
|
"Show comments",
|
|
"Show original image"
|
|
];
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return SingleChildScrollView(
|
|
child: Column(children: [
|
|
TitleBar(title: "Shortcuts".tl),
|
|
...settings.map((e) => buildItem(e, settings.indexOf(e)))
|
|
]),
|
|
);
|
|
}
|
|
|
|
Widget buildItem(String text, int index) {
|
|
var keyText = listening == index
|
|
? "Waiting..."
|
|
: LogicalKeyboardKey(appdata.settings['shortcuts'][index]).keyLabel;
|
|
return Card(
|
|
padding: EdgeInsets.zero,
|
|
margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 12),
|
|
child: ListTile(
|
|
title: Text(text.tl),
|
|
trailing: Button(
|
|
child: Text(keyText),
|
|
onPressed: () {
|
|
if (listening != -1) {
|
|
listener?.removeAll();
|
|
}
|
|
setState(() {
|
|
listening = index;
|
|
});
|
|
listener?.addHandler((key) {
|
|
if (key == LogicalKeyboardKey.escape) return;
|
|
setState(() {
|
|
appdata.settings['shortcuts'][index] = key.keyId;
|
|
listening = -1;
|
|
appdata.writeData();
|
|
});
|
|
Future.microtask(() => listener?.removeAll());
|
|
});
|
|
},
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _SetInitialPageWidget extends StatefulWidget {
|
|
const _SetInitialPageWidget();
|
|
|
|
@override
|
|
State<_SetInitialPageWidget> createState() => _SetInitialPageWidgetState();
|
|
}
|
|
|
|
class _SetInitialPageWidgetState extends State<_SetInitialPageWidget> {
|
|
int index = appdata.settings["initialPage"] ?? 4;
|
|
|
|
static const pageNames = [
|
|
"Search",
|
|
"Downloading",
|
|
"Downloaded",
|
|
"Explore",
|
|
"Bookmarks",
|
|
"Following",
|
|
"History",
|
|
"Ranking",
|
|
"Recommendation",
|
|
"Bookmarks",
|
|
"Ranking",
|
|
];
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return ScaffoldPage(
|
|
header: TitleBar(title: "Initial Page".tl),
|
|
content: ListView.builder(
|
|
itemCount: pageNames.length + 2,
|
|
itemBuilder: (context, index) {
|
|
if (index == 3) {
|
|
return Text('${"Illustrations".tl}/${"Manga".tl}').paddingHorizontal(16).paddingVertical(8);
|
|
} else if (index > 3) {
|
|
index--;
|
|
}
|
|
if (index == 8) {
|
|
return Text("Novel".tl).paddingHorizontal(16).paddingVertical(8);
|
|
} else if (index > 8) {
|
|
index--;
|
|
}
|
|
|
|
return Card(
|
|
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
|
padding: EdgeInsets.zero,
|
|
child: ListTile(
|
|
title: Text(pageNames[index].tl),
|
|
trailing: RadioButton(
|
|
checked: this.index - 1 == index,
|
|
onChanged: (value) {
|
|
setState(() {
|
|
this.index = index + 1;
|
|
appdata.settings["initialPage"] = index + 1;
|
|
appdata.writeData();
|
|
});
|
|
},
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|