mirror of
https://github.com/HChaZZY/PiliPlus.git
synced 2025-12-20 09:06:36 +08:00
feat: InportExportDialog (#1048)
This commit is contained in:
committed by
GitHub
parent
08c3789321
commit
ed57697fdc
@@ -1,9 +1,9 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:PiliPlus/build_config.dart';
|
import 'package:PiliPlus/build_config.dart';
|
||||||
import 'package:PiliPlus/common/widgets/dialog/dialog.dart';
|
import 'package:PiliPlus/common/widgets/dialog/dialog.dart';
|
||||||
import 'package:PiliPlus/models/common/account_type.dart';
|
|
||||||
import 'package:PiliPlus/pages/mine/controller.dart';
|
import 'package:PiliPlus/pages/mine/controller.dart';
|
||||||
import 'package:PiliPlus/services/loggeer.dart';
|
import 'package:PiliPlus/services/loggeer.dart';
|
||||||
import 'package:PiliPlus/utils/accounts.dart';
|
import 'package:PiliPlus/utils/accounts.dart';
|
||||||
@@ -24,6 +24,9 @@ import 'package:get/get.dart';
|
|||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
|
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
|
import 'package:re_highlight/languages/json.dart';
|
||||||
|
import 'package:re_highlight/re_highlight.dart';
|
||||||
|
import 'package:re_highlight/styles/github.dart';
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
|
|
||||||
class AboutPage extends StatefulWidget {
|
class AboutPage extends StatefulWidget {
|
||||||
@@ -222,199 +225,33 @@ Commit Hash: ${BuildConfig.commitHash}''',
|
|||||||
ListTile(
|
ListTile(
|
||||||
title: const Text('导入/导出登录信息'),
|
title: const Text('导入/导出登录信息'),
|
||||||
leading: const Icon(Icons.import_export_outlined),
|
leading: const Icon(Icons.import_export_outlined),
|
||||||
onTap: () => showDialog(
|
onTap: () => showInportExportDialog<Map>(
|
||||||
context: context,
|
context,
|
||||||
builder: (context) => SimpleDialog(
|
title: '登录信息',
|
||||||
title: const Text('导入/导出登录信息'),
|
toJson: () => jsonEncode(Accounts.account.toMap()),
|
||||||
clipBehavior: Clip.hardEdge,
|
fromJson: (json) async {
|
||||||
children: [
|
final res = json.map(
|
||||||
ListTile(
|
(key, value) => MapEntry(key, LoginAccount.fromJson(value)),
|
||||||
dense: true,
|
|
||||||
title: const Text('导出', style: style),
|
|
||||||
onTap: () {
|
|
||||||
Get.back();
|
|
||||||
String res = jsonEncode(Accounts.account.toMap());
|
|
||||||
Utils.copyText(res);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
dense: true,
|
|
||||||
title: const Text('导入', style: style),
|
|
||||||
onTap: () async {
|
|
||||||
Get.back();
|
|
||||||
ClipboardData? data = await Clipboard.getData(
|
|
||||||
'text/plain',
|
|
||||||
);
|
);
|
||||||
if (data?.text?.isNotEmpty != true) {
|
await Accounts.account.putAll(res);
|
||||||
SmartDialog.showToast('剪贴板无数据');
|
await Accounts.refresh();
|
||||||
return;
|
MineController.anonymity.value = !Accounts.heartbeat.isLogin;
|
||||||
}
|
|
||||||
if (!context.mounted) return;
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (context) {
|
|
||||||
return AlertDialog(
|
|
||||||
title: const Text('是否导入以下登录信息?'),
|
|
||||||
content: SingleChildScrollView(
|
|
||||||
child: Text(data!.text!),
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: Get.back,
|
|
||||||
child: Text(
|
|
||||||
'取消',
|
|
||||||
style: TextStyle(color: outline),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
Get.back();
|
|
||||||
try {
|
|
||||||
final res = (jsonDecode(data.text!) as Map)
|
|
||||||
.map(
|
|
||||||
(key, value) => MapEntry(
|
|
||||||
key,
|
|
||||||
LoginAccount.fromJson(value),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
Accounts.account
|
|
||||||
.putAll(res)
|
|
||||||
.whenComplete(Accounts.refresh)
|
|
||||||
.whenComplete(() {
|
|
||||||
MineController.anonymity.value =
|
|
||||||
!Accounts.get(
|
|
||||||
AccountType.heartbeat,
|
|
||||||
).isLogin;
|
|
||||||
if (Accounts.main.isLogin) {
|
if (Accounts.main.isLogin) {
|
||||||
return LoginUtils.onLoginMain();
|
await LoginUtils.onLoginMain();
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
SmartDialog.showToast('导入失败:$e');
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: const Text('确定'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: const Text('导入/导出设置'),
|
title: const Text('导入/导出设置'),
|
||||||
dense: false,
|
dense: false,
|
||||||
leading: const Icon(Icons.import_export_outlined),
|
leading: const Icon(Icons.import_export_outlined),
|
||||||
onTap: () => showDialog(
|
onTap: () => showInportExportDialog(
|
||||||
context: context,
|
context,
|
||||||
builder: (context) {
|
title: '设置',
|
||||||
return SimpleDialog(
|
label: GStorage.setting.name,
|
||||||
clipBehavior: Clip.hardEdge,
|
toJson: GStorage.exportAllSettings,
|
||||||
title: const Text('导入/导出设置'),
|
fromJson: GStorage.importAllJsonSettings,
|
||||||
children: [
|
|
||||||
ListTile(
|
|
||||||
dense: true,
|
|
||||||
title: const Text('导出文件至本地', style: style),
|
|
||||||
onTap: () async {
|
|
||||||
Get.back();
|
|
||||||
final res = utf8.encode(GStorage.exportAllSettings());
|
|
||||||
final name =
|
|
||||||
'piliplus_settings_${context.isTablet ? 'pad' : 'phone'}_'
|
|
||||||
'${DateFormat('yyyyMMddHHmmss').format(DateTime.now())}.json';
|
|
||||||
try {
|
|
||||||
DocumentFileSavePlusPlatform.instance
|
|
||||||
.saveMultipleFiles(
|
|
||||||
dataList: [res],
|
|
||||||
fileNameList: [name],
|
|
||||||
mimeTypeList: [Headers.jsonContentType],
|
|
||||||
);
|
|
||||||
if (Platform.isAndroid) {
|
|
||||||
SmartDialog.showToast('已保存');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
SharePlus.instance.share(
|
|
||||||
ShareParams(
|
|
||||||
files: [
|
|
||||||
XFile.fromData(
|
|
||||||
res,
|
|
||||||
name: name,
|
|
||||||
mimeType: Headers.jsonContentType,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
sharePositionOrigin:
|
|
||||||
await Utils.sharePositionOrigin,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
dense: true,
|
|
||||||
title: const Text('导出设置至剪贴板', style: style),
|
|
||||||
onTap: () {
|
|
||||||
Get.back();
|
|
||||||
String data = GStorage.exportAllSettings();
|
|
||||||
Utils.copyText(data);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
dense: true,
|
|
||||||
title: const Text('从剪贴板导入设置', style: style),
|
|
||||||
onTap: () async {
|
|
||||||
Get.back();
|
|
||||||
ClipboardData? data = await Clipboard.getData(
|
|
||||||
'text/plain',
|
|
||||||
);
|
|
||||||
if (data == null ||
|
|
||||||
data.text == null ||
|
|
||||||
data.text!.isEmpty) {
|
|
||||||
SmartDialog.showToast('剪贴板无数据');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!context.mounted) return;
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (context) {
|
|
||||||
return AlertDialog(
|
|
||||||
title: const Text('是否导入如下设置?'),
|
|
||||||
content: SingleChildScrollView(
|
|
||||||
child: Text(data.text!),
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: Get.back,
|
|
||||||
child: Text(
|
|
||||||
'取消',
|
|
||||||
style: TextStyle(color: outline),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () async {
|
|
||||||
Get.back();
|
|
||||||
try {
|
|
||||||
await GStorage.importAllSettings(
|
|
||||||
data.text!,
|
|
||||||
);
|
|
||||||
SmartDialog.showToast('导入成功');
|
|
||||||
} catch (e) {
|
|
||||||
SmartDialog.showToast('导入失败:$e');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: const Text('确定'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
@@ -466,3 +303,128 @@ Commit Hash: ${BuildConfig.commitHash}''',
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> showInportExportDialog<T>(
|
||||||
|
BuildContext context, {
|
||||||
|
required String title,
|
||||||
|
String? label,
|
||||||
|
required String Function() toJson,
|
||||||
|
required FutureOr<void> Function(T json) fromJson,
|
||||||
|
}) => showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) {
|
||||||
|
const style = TextStyle(fontSize: 15);
|
||||||
|
return SimpleDialog(
|
||||||
|
clipBehavior: Clip.hardEdge,
|
||||||
|
title: Text('导入/导出$title'),
|
||||||
|
children: [
|
||||||
|
if (label != null)
|
||||||
|
ListTile(
|
||||||
|
dense: true,
|
||||||
|
title: const Text('导出文件至本地', style: style),
|
||||||
|
onTap: () async {
|
||||||
|
Get.back();
|
||||||
|
final res = utf8.encode(toJson());
|
||||||
|
final name =
|
||||||
|
'piliplus_${label}_${context.isTablet ? 'pad' : 'phone'}_'
|
||||||
|
'${DateFormat('yyyyMMddHHmmss').format(DateTime.now())}.json';
|
||||||
|
try {
|
||||||
|
DocumentFileSavePlusPlatform.instance.saveMultipleFiles(
|
||||||
|
dataList: [res],
|
||||||
|
fileNameList: [name],
|
||||||
|
mimeTypeList: const [Headers.jsonContentType],
|
||||||
|
);
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
SmartDialog.showToast('已保存');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
SharePlus.instance.share(
|
||||||
|
ShareParams(
|
||||||
|
files: [
|
||||||
|
XFile.fromData(
|
||||||
|
res,
|
||||||
|
name: name,
|
||||||
|
mimeType: Headers.jsonContentType,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
sharePositionOrigin: await Utils.sharePositionOrigin,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
dense: true,
|
||||||
|
title: Text('导出$title至剪贴板', style: style),
|
||||||
|
onTap: () {
|
||||||
|
Get.back();
|
||||||
|
Utils.copyText(toJson());
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
dense: true,
|
||||||
|
title: Text('从剪贴板导入$title', style: style),
|
||||||
|
onTap: () async {
|
||||||
|
Get.back();
|
||||||
|
ClipboardData? data = await Clipboard.getData(
|
||||||
|
'text/plain',
|
||||||
|
);
|
||||||
|
if (data?.text?.isNotEmpty != true) {
|
||||||
|
SmartDialog.showToast('剪贴板无数据');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!context.mounted) return;
|
||||||
|
final text = data!.text!;
|
||||||
|
late final T json;
|
||||||
|
late final String formatText;
|
||||||
|
try {
|
||||||
|
json = jsonDecode(text);
|
||||||
|
formatText = const JsonEncoder.withIndent(' ').convert(json);
|
||||||
|
} catch (e) {
|
||||||
|
SmartDialog.showToast('解析json失败:$e');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final renderer = TextSpanRenderer(const TextStyle(), githubTheme);
|
||||||
|
Highlight()
|
||||||
|
..registerLanguage('json', langJson)
|
||||||
|
..highlight(code: formatText, language: 'json').render(renderer);
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text('是否导入如下$title?'),
|
||||||
|
content: SingleChildScrollView(
|
||||||
|
child: Text.rich(renderer.span!),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: Get.back,
|
||||||
|
child: Text(
|
||||||
|
'取消',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.outline,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () async {
|
||||||
|
Get.back();
|
||||||
|
try {
|
||||||
|
await fromJson(json);
|
||||||
|
SmartDialog.showToast('导入成功');
|
||||||
|
} catch (e) {
|
||||||
|
SmartDialog.showToast('导入失败:$e');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: const Text('确定'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|||||||
@@ -150,7 +150,6 @@ class SSearchController extends GetxController
|
|||||||
historyList
|
historyList
|
||||||
..remove(controller.text)
|
..remove(controller.text)
|
||||||
..insert(0, controller.text);
|
..insert(0, controller.text);
|
||||||
GStorage.historyWord.put('cacheList', historyList);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
searchFocusNode.unfocus();
|
searchFocusNode.unfocus();
|
||||||
@@ -198,7 +197,6 @@ class SSearchController extends GetxController
|
|||||||
|
|
||||||
void onLongSelect(String word) {
|
void onLongSelect(String word) {
|
||||||
historyList.remove(word);
|
historyList.remove(word);
|
||||||
GStorage.historyWord.put('cacheList', historyList);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void onClearHistory() {
|
void onClearHistory() {
|
||||||
@@ -207,13 +205,13 @@ class SSearchController extends GetxController
|
|||||||
title: '确定清空搜索历史?',
|
title: '确定清空搜索历史?',
|
||||||
onConfirm: () {
|
onConfirm: () {
|
||||||
historyList.clear();
|
historyList.clear();
|
||||||
GStorage.historyWord.put('cacheList', []);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onClose() {
|
void onClose() {
|
||||||
|
GStorage.historyWord.put('cacheList', historyList);
|
||||||
subDispose();
|
subDispose();
|
||||||
searchFocusNode.dispose();
|
searchFocusNode.dispose();
|
||||||
controller.dispose();
|
controller.dispose();
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:PiliPlus/common/widgets/disabled_icon.dart';
|
import 'package:PiliPlus/common/widgets/disabled_icon.dart';
|
||||||
import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart';
|
import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart';
|
||||||
import 'package:PiliPlus/http/loading_state.dart';
|
import 'package:PiliPlus/http/loading_state.dart';
|
||||||
import 'package:PiliPlus/models_new/search/search_rcmd/data.dart';
|
import 'package:PiliPlus/models_new/search/search_rcmd/data.dart';
|
||||||
|
import 'package:PiliPlus/pages/about/view.dart' show showInportExportDialog;
|
||||||
import 'package:PiliPlus/pages/search/controller.dart';
|
import 'package:PiliPlus/pages/search/controller.dart';
|
||||||
import 'package:PiliPlus/pages/search/widgets/hot_keyword.dart';
|
import 'package:PiliPlus/pages/search/widgets/hot_keyword.dart';
|
||||||
import 'package:PiliPlus/pages/search/widgets/search_text.dart';
|
import 'package:PiliPlus/pages/search/widgets/search_text.dart';
|
||||||
@@ -331,13 +334,14 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
_exportHsitory(theme),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 34,
|
height: 34,
|
||||||
child: TextButton.icon(
|
child: TextButton.icon(
|
||||||
style: ButtonStyle(
|
style: const ButtonStyle(
|
||||||
padding: WidgetStateProperty.all(
|
padding: WidgetStatePropertyAll(
|
||||||
const EdgeInsets.symmetric(
|
EdgeInsets.symmetric(
|
||||||
horizontal: 10,
|
horizontal: 10,
|
||||||
vertical: 6,
|
vertical: 6,
|
||||||
),
|
),
|
||||||
@@ -380,6 +384,30 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _exportHsitory(ThemeData theme) => SizedBox(
|
||||||
|
width: 34,
|
||||||
|
height: 34,
|
||||||
|
child: IconButton(
|
||||||
|
iconSize: 22,
|
||||||
|
tooltip: '导入/导出历史记录',
|
||||||
|
icon: Icon(
|
||||||
|
Icons.import_export_outlined,
|
||||||
|
color: theme.colorScheme.onSurfaceVariant.withValues(alpha: 0.8),
|
||||||
|
),
|
||||||
|
style: IconButton.styleFrom(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
),
|
||||||
|
onPressed: () => showInportExportDialog<List>(
|
||||||
|
context,
|
||||||
|
title: '历史记录',
|
||||||
|
toJson: () => jsonEncode(_searchController.historyList),
|
||||||
|
fromJson: (json) {
|
||||||
|
_searchController.historyList.value = json.cast<String>();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
Icon historyIcon(ThemeData theme) => Icon(
|
Icon historyIcon(ThemeData theme) => Icon(
|
||||||
Icons.history,
|
Icons.history,
|
||||||
color: theme.colorScheme.onSurfaceVariant.withValues(alpha: 0.8),
|
color: theme.colorScheme.onSurfaceVariant.withValues(alpha: 0.8),
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import 'package:path_provider/path_provider.dart';
|
|||||||
|
|
||||||
class GStorage {
|
class GStorage {
|
||||||
static late final Box<UserInfoData> userInfo;
|
static late final Box<UserInfoData> userInfo;
|
||||||
static late final Box<dynamic> historyWord;
|
static late final Box<List<String>> historyWord;
|
||||||
static late final Box<dynamic> localCache;
|
static late final Box<dynamic> localCache;
|
||||||
static late final Box<dynamic> setting;
|
static late final Box<dynamic> setting;
|
||||||
static late final Box<dynamic> video;
|
static late final Box<dynamic> video;
|
||||||
@@ -41,7 +41,7 @@ class GStorage {
|
|||||||
// 设置
|
// 设置
|
||||||
setting = await Hive.openBox('setting');
|
setting = await Hive.openBox('setting');
|
||||||
// 搜索历史
|
// 搜索历史
|
||||||
historyWord = await Hive.openBox(
|
historyWord = await Hive.openBox<List<String>>(
|
||||||
'historyWord',
|
'historyWord',
|
||||||
compactionStrategy: (int entries, int deletedEntries) {
|
compactionStrategy: (int entries, int deletedEntries) {
|
||||||
return deletedEntries > 10;
|
return deletedEntries > 10;
|
||||||
@@ -60,8 +60,10 @@ class GStorage {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> importAllSettings(String data) async {
|
static Future<void> importAllSettings(String data) =>
|
||||||
final Map<String, dynamic> map = jsonDecode(data);
|
importAllJsonSettings(jsonDecode(data));
|
||||||
|
|
||||||
|
static Future<void> importAllJsonSettings(Map<String, dynamic> map) async {
|
||||||
await setting.clear();
|
await setting.clear();
|
||||||
await video.clear();
|
await video.clear();
|
||||||
await setting.putAll(map[setting.name]);
|
await setting.putAll(map[setting.name]);
|
||||||
|
|||||||
Reference in New Issue
Block a user