From ed57697fdcc33b4ee67d6fc395b02ecf6d4283d4 Mon Sep 17 00:00:00 2001 From: My-Responsitories <107370289+My-Responsitories@users.noreply.github.com> Date: Mon, 18 Aug 2025 21:25:00 +0800 Subject: [PATCH] feat: InportExportDialog (#1048) --- lib/pages/about/view.dart | 338 ++++++++++++++----------------- lib/pages/search/controller.dart | 4 +- lib/pages/search/view.dart | 34 +++- lib/utils/storage.dart | 10 +- 4 files changed, 188 insertions(+), 198 deletions(-) diff --git a/lib/pages/about/view.dart b/lib/pages/about/view.dart index 5af069d8..97661dd0 100644 --- a/lib/pages/about/view.dart +++ b/lib/pages/about/view.dart @@ -1,9 +1,9 @@ +import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:PiliPlus/build_config.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/services/loggeer.dart'; import 'package:PiliPlus/utils/accounts.dart'; @@ -24,6 +24,9 @@ import 'package:get/get.dart'; import 'package:intl/intl.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.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'; class AboutPage extends StatefulWidget { @@ -222,199 +225,33 @@ Commit Hash: ${BuildConfig.commitHash}''', ListTile( title: const Text('导入/导出登录信息'), leading: const Icon(Icons.import_export_outlined), - onTap: () => showDialog( - context: context, - builder: (context) => SimpleDialog( - title: const Text('导入/导出登录信息'), - clipBehavior: Clip.hardEdge, - children: [ - ListTile( - 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) { - 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: () { - 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) { - return LoginUtils.onLoginMain(); - } - }); - } catch (e) { - SmartDialog.showToast('导入失败:$e'); - } - }, - child: const Text('确定'), - ), - ], - ); - }, - ); - }, - ), - ], - ), + onTap: () => showInportExportDialog( + context, + title: '登录信息', + toJson: () => jsonEncode(Accounts.account.toMap()), + fromJson: (json) async { + final res = json.map( + (key, value) => MapEntry(key, LoginAccount.fromJson(value)), + ); + await Accounts.account.putAll(res); + await Accounts.refresh(); + MineController.anonymity.value = !Accounts.heartbeat.isLogin; + if (Accounts.main.isLogin) { + await LoginUtils.onLoginMain(); + } + }, ), ), ListTile( title: const Text('导入/导出设置'), dense: false, leading: const Icon(Icons.import_export_outlined), - onTap: () => showDialog( - context: context, - builder: (context) { - return SimpleDialog( - clipBehavior: Clip.hardEdge, - title: const Text('导入/导出设置'), - 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('确定'), - ), - ], - ); - }, - ); - }, - ), - ], - ); - }, + onTap: () => showInportExportDialog( + context, + title: '设置', + label: GStorage.setting.name, + toJson: GStorage.exportAllSettings, + fromJson: GStorage.importAllJsonSettings, ), ), ListTile( @@ -466,3 +303,128 @@ Commit Hash: ${BuildConfig.commitHash}''', ); } } + +Future showInportExportDialog( + BuildContext context, { + required String title, + String? label, + required String Function() toJson, + required FutureOr 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('确定'), + ), + ], + ); + }, + ); + }, + ), + ], + ); + }, +); diff --git a/lib/pages/search/controller.dart b/lib/pages/search/controller.dart index fddcd681..1d09610b 100644 --- a/lib/pages/search/controller.dart +++ b/lib/pages/search/controller.dart @@ -150,7 +150,6 @@ class SSearchController extends GetxController historyList ..remove(controller.text) ..insert(0, controller.text); - GStorage.historyWord.put('cacheList', historyList); } searchFocusNode.unfocus(); @@ -198,7 +197,6 @@ class SSearchController extends GetxController void onLongSelect(String word) { historyList.remove(word); - GStorage.historyWord.put('cacheList', historyList); } void onClearHistory() { @@ -207,13 +205,13 @@ class SSearchController extends GetxController title: '确定清空搜索历史?', onConfirm: () { historyList.clear(); - GStorage.historyWord.put('cacheList', []); }, ); } @override void onClose() { + GStorage.historyWord.put('cacheList', historyList); subDispose(); searchFocusNode.dispose(); controller.dispose(); diff --git a/lib/pages/search/view.dart b/lib/pages/search/view.dart index 012146da..09620e72 100644 --- a/lib/pages/search/view.dart +++ b/lib/pages/search/view.dart @@ -1,7 +1,10 @@ +import 'dart:convert'; + import 'package:PiliPlus/common/widgets/disabled_icon.dart'; import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; import 'package:PiliPlus/http/loading_state.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/widgets/hot_keyword.dart'; import 'package:PiliPlus/pages/search/widgets/search_text.dart'; @@ -331,13 +334,14 @@ class _SearchPageState extends State { ); }, ), + _exportHsitory(theme), const Spacer(), SizedBox( height: 34, child: TextButton.icon( - style: ButtonStyle( - padding: WidgetStateProperty.all( - const EdgeInsets.symmetric( + style: const ButtonStyle( + padding: WidgetStatePropertyAll( + EdgeInsets.symmetric( horizontal: 10, vertical: 6, ), @@ -380,6 +384,30 @@ class _SearchPageState extends State { ); } + 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( + context, + title: '历史记录', + toJson: () => jsonEncode(_searchController.historyList), + fromJson: (json) { + _searchController.historyList.value = json.cast(); + }, + ), + ), + ); + Icon historyIcon(ThemeData theme) => Icon( Icons.history, color: theme.colorScheme.onSurfaceVariant.withValues(alpha: 0.8), diff --git a/lib/utils/storage.dart b/lib/utils/storage.dart index ec45c7ca..e3d88fb5 100644 --- a/lib/utils/storage.dart +++ b/lib/utils/storage.dart @@ -14,7 +14,7 @@ import 'package:path_provider/path_provider.dart'; class GStorage { static late final Box userInfo; - static late final Box historyWord; + static late final Box> historyWord; static late final Box localCache; static late final Box setting; static late final Box video; @@ -41,7 +41,7 @@ class GStorage { // 设置 setting = await Hive.openBox('setting'); // 搜索历史 - historyWord = await Hive.openBox( + historyWord = await Hive.openBox>( 'historyWord', compactionStrategy: (int entries, int deletedEntries) { return deletedEntries > 10; @@ -60,8 +60,10 @@ class GStorage { }); } - static Future importAllSettings(String data) async { - final Map map = jsonDecode(data); + static Future importAllSettings(String data) => + importAllJsonSettings(jsonDecode(data)); + + static Future importAllJsonSettings(Map map) async { await setting.clear(); await video.clear(); await setting.putAll(map[setting.name]);