mirror of
https://github.com/bggRGjQaUbCoE/PiliPlus.git
synced 2026-05-26 11:08:44 +00:00
improve export/import
Signed-off-by: dom <githubaccount56556@proton.me>
This commit is contained in:
278
lib/common/widgets/dialog/export_import.dart
Normal file
278
lib/common/widgets/dialog/export_import.dart
Normal file
@@ -0,0 +1,278 @@
|
||||
import 'dart:async' show FutureOr;
|
||||
import 'dart:convert' show utf8, jsonDecode;
|
||||
import 'dart:io' show File;
|
||||
|
||||
import 'package:PiliPlus/common/constants.dart' show StyleString;
|
||||
import 'package:PiliPlus/utils/extension/context_ext.dart';
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart' show Clipboard;
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get_core/src/get_main.dart';
|
||||
import 'package:get/get_navigation/src/extension_navigation.dart';
|
||||
import 'package:intl/intl.dart' show DateFormat;
|
||||
import 'package:re_highlight/languages/json.dart';
|
||||
import 'package:re_highlight/re_highlight.dart';
|
||||
import 'package:re_highlight/styles/base16/github.dart';
|
||||
import 'package:re_highlight/styles/github-dark.dart';
|
||||
|
||||
void exportToClipBoard({
|
||||
required ValueGetter<String> onExport,
|
||||
}) {
|
||||
Utils.copyText(onExport());
|
||||
}
|
||||
|
||||
void exportToLocalFile({
|
||||
required ValueGetter<String> onExport,
|
||||
required ValueGetter<String> localFileName,
|
||||
}) {
|
||||
final res = utf8.encode(onExport());
|
||||
Utils.saveBytes2File(
|
||||
name:
|
||||
'piliplus_${localFileName()}_'
|
||||
'${DateFormat('yyyyMMddHHmmss').format(DateTime.now())}.json',
|
||||
bytes: res,
|
||||
allowedExtensions: const ['json'],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> importFromClipBoard<T>(
|
||||
BuildContext context, {
|
||||
required String title,
|
||||
required ValueGetter<String> onExport,
|
||||
required FutureOr<void> Function(T json) onImport,
|
||||
bool showConfirmDialog = true,
|
||||
}) async {
|
||||
final 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 = Utils.jsonEncoder.convert(json);
|
||||
} catch (e) {
|
||||
SmartDialog.showToast('解析json失败:$e');
|
||||
return;
|
||||
}
|
||||
bool? executeImport;
|
||||
if (showConfirmDialog) {
|
||||
final highlight = Highlight()..registerLanguage('json', langJson);
|
||||
final result = highlight.highlight(
|
||||
code: formatText,
|
||||
language: 'json',
|
||||
);
|
||||
late TextSpanRenderer renderer;
|
||||
bool? isDarkMode;
|
||||
executeImport = await showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
final isDark = context.isDarkMode;
|
||||
if (isDark != isDarkMode) {
|
||||
isDarkMode = isDark;
|
||||
renderer = TextSpanRenderer(
|
||||
const TextStyle(),
|
||||
isDark ? githubDarkTheme : githubTheme,
|
||||
);
|
||||
result.render(renderer);
|
||||
}
|
||||
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: () => Get.back(result: true),
|
||||
child: const Text('确定'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
executeImport = true;
|
||||
}
|
||||
if (executeImport ?? false) {
|
||||
try {
|
||||
await onImport(json);
|
||||
SmartDialog.showToast('导入成功');
|
||||
} catch (e) {
|
||||
SmartDialog.showToast('导入失败:$e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> importFromLocalFile<T>({
|
||||
required FutureOr<void> Function(T json) onImport,
|
||||
}) async {
|
||||
final result = await FilePicker.pickFiles();
|
||||
if (result != null) {
|
||||
final path = result.files.first.path;
|
||||
if (path != null) {
|
||||
final data = await File(path).readAsString();
|
||||
late final T json;
|
||||
try {
|
||||
json = jsonDecode(data);
|
||||
} catch (e) {
|
||||
SmartDialog.showToast('解析json失败:$e');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await onImport(json);
|
||||
SmartDialog.showToast('导入成功');
|
||||
} catch (e) {
|
||||
SmartDialog.showToast('导入失败:$e');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void importFromInput<T>(
|
||||
BuildContext context, {
|
||||
required String title,
|
||||
required FutureOr<void> Function(T json) onImport,
|
||||
}) {
|
||||
final key = GlobalKey<FormFieldState<String>>();
|
||||
late T json;
|
||||
String? forceErrorText;
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text('输入$title'),
|
||||
constraints: StyleString.dialogFixedConstraints,
|
||||
content: TextFormField(
|
||||
key: key,
|
||||
minLines: 4,
|
||||
maxLines: 12,
|
||||
autofocus: true,
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
errorMaxLines: 3,
|
||||
),
|
||||
validator: (value) {
|
||||
if (forceErrorText != null) return forceErrorText;
|
||||
try {
|
||||
json = jsonDecode(value!) as T;
|
||||
return null;
|
||||
} catch (e) {
|
||||
if (e is FormatException) {}
|
||||
return '解析json失败:$e';
|
||||
}
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: Get.back,
|
||||
child: Text(
|
||||
'取消',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
if (key.currentState?.validate() == true) {
|
||||
try {
|
||||
await onImport(json);
|
||||
Get.back();
|
||||
SmartDialog.showToast('导入成功');
|
||||
return;
|
||||
} catch (e) {
|
||||
forceErrorText = '导入失败:$e';
|
||||
}
|
||||
key.currentState?.validate();
|
||||
forceErrorText = null;
|
||||
}
|
||||
},
|
||||
child: const Text('确定'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> showImportExportDialog<T>(
|
||||
BuildContext context, {
|
||||
required String title,
|
||||
String? label,
|
||||
required ValueGetter<String> onExport,
|
||||
required FutureOr<void> Function(T json) onImport,
|
||||
required ValueGetter<String> localFileName,
|
||||
}) => showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
const style = TextStyle(fontSize: 14);
|
||||
return SimpleDialog(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
title: Text('导入/导出$title'),
|
||||
children: [
|
||||
ListTile(
|
||||
dense: true,
|
||||
title: const Text('导出至剪贴板', style: style),
|
||||
onTap: () {
|
||||
Get.back();
|
||||
exportToClipBoard(onExport: onExport);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
dense: true,
|
||||
title: const Text('导出文件至本地', style: style),
|
||||
onTap: () {
|
||||
Get.back();
|
||||
exportToLocalFile(onExport: onExport, localFileName: localFileName);
|
||||
},
|
||||
),
|
||||
Divider(
|
||||
height: 1,
|
||||
color: ColorScheme.of(context).outline.withValues(alpha: 0.1),
|
||||
),
|
||||
ListTile(
|
||||
dense: true,
|
||||
title: const Text('输入', style: style),
|
||||
onTap: () {
|
||||
Get.back();
|
||||
importFromInput<T>(context, title: title, onImport: onImport);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
dense: true,
|
||||
title: const Text('从剪贴板导入', style: style),
|
||||
onTap: () {
|
||||
Get.back();
|
||||
importFromClipBoard<T>(
|
||||
context,
|
||||
title: title,
|
||||
onExport: onExport,
|
||||
onImport: onImport,
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
dense: true,
|
||||
title: const Text('从本地文件导入', style: style),
|
||||
onTap: () {
|
||||
Get.back();
|
||||
importFromLocalFile<T>(onImport: onImport);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -1,10 +1,10 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:PiliPlus/build_config.dart';
|
||||
import 'package:PiliPlus/common/constants.dart';
|
||||
import 'package:PiliPlus/common/widgets/dialog/dialog.dart';
|
||||
import 'package:PiliPlus/common/widgets/dialog/export_import.dart';
|
||||
import 'package:PiliPlus/common/widgets/flutter/list_tile.dart';
|
||||
import 'package:PiliPlus/pages/mine/controller.dart';
|
||||
import 'package:PiliPlus/services/logger.dart';
|
||||
@@ -21,15 +21,9 @@ import 'package:PiliPlus/utils/storage.dart';
|
||||
import 'package:PiliPlus/utils/update.dart';
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
import 'package:flutter/material.dart' hide ListTile;
|
||||
import 'package:flutter/services.dart' show Clipboard, ClipboardData;
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
|
||||
import 'package:re_highlight/languages/json.dart';
|
||||
import 'package:re_highlight/re_highlight.dart';
|
||||
import 'package:re_highlight/styles/github-dark.dart';
|
||||
import 'package:re_highlight/styles/github.dart';
|
||||
|
||||
class AboutPage extends StatefulWidget {
|
||||
const AboutPage({super.key, this.showAppBar = true});
|
||||
@@ -86,7 +80,7 @@ class _AboutPageState extends State<AboutPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
const style = TextStyle(fontSize: 15);
|
||||
const style = TextStyle(fontSize: 14);
|
||||
final outline = theme.colorScheme.outline;
|
||||
final subTitleStyle = TextStyle(fontSize: 13, color: outline);
|
||||
final showAppBar = widget.showAppBar;
|
||||
@@ -249,8 +243,10 @@ Commit Hash: ${BuildConfig.commitHash}''',
|
||||
onTap: () => showImportExportDialog<Map>(
|
||||
context,
|
||||
title: '登录信息',
|
||||
toJson: () => Utils.jsonEncoder.convert(Accounts.account.toMap()),
|
||||
fromJson: (json) async {
|
||||
localFileName: () => 'account',
|
||||
onExport: () =>
|
||||
Utils.jsonEncoder.convert(Accounts.account.toMap()),
|
||||
onImport: (json) async {
|
||||
final res = json.map(
|
||||
(key, value) => MapEntry(key, LoginAccount.fromJson(value)),
|
||||
);
|
||||
@@ -260,7 +256,6 @@ Commit Hash: ${BuildConfig.commitHash}''',
|
||||
if (Accounts.main.isLogin) {
|
||||
await LoginUtils.onLoginMain();
|
||||
}
|
||||
return true;
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -268,12 +263,14 @@ Commit Hash: ${BuildConfig.commitHash}''',
|
||||
title: const Text('导入/导出设置'),
|
||||
dense: false,
|
||||
leading: const Icon(Icons.import_export_outlined),
|
||||
onTap: () => showImportExportDialog(
|
||||
onTap: () => showImportExportDialog<Map<String, dynamic>>(
|
||||
context,
|
||||
title: '设置',
|
||||
localFileName: () =>
|
||||
'setting_${context.isTablet ? 'pad' : 'phone'}',
|
||||
label: GStorage.setting.name,
|
||||
toJson: GStorage.exportAllSettings,
|
||||
fromJson: GStorage.importAllJsonSettings,
|
||||
onExport: GStorage.exportAllSettings,
|
||||
onImport: GStorage.importAllJsonSettings,
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
@@ -302,15 +299,7 @@ Commit Hash: ${BuildConfig.commitHash}''',
|
||||
dense: true,
|
||||
onTap: () async {
|
||||
Get.back();
|
||||
await Future.wait([
|
||||
GStorage.userInfo.clear(),
|
||||
GStorage.setting.clear(),
|
||||
GStorage.localCache.clear(),
|
||||
GStorage.video.clear(),
|
||||
GStorage.historyWord.clear(),
|
||||
Accounts.clear(),
|
||||
GStorage.watchProgress.clear(),
|
||||
]);
|
||||
await GStorage.clear();
|
||||
SmartDialog.showToast('重置成功');
|
||||
},
|
||||
title: const Text('重置所有数据(含登录信息)', style: style),
|
||||
@@ -325,190 +314,3 @@ Commit Hash: ${BuildConfig.commitHash}''',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> showImportExportDialog<T>(
|
||||
BuildContext context, {
|
||||
required String title,
|
||||
String? label,
|
||||
required ValueGetter<String> toJson,
|
||||
required FutureOr<bool> 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: () {
|
||||
Get.back();
|
||||
final res = utf8.encode(toJson());
|
||||
final name =
|
||||
'piliplus_${label}_${context.isTablet ? 'pad' : 'phone'}_'
|
||||
'${DateFormat('yyyyMMddHHmmss').format(DateTime.now())}.json';
|
||||
Utils.saveBytes2File(
|
||||
name: name,
|
||||
bytes: res,
|
||||
allowedExtensions: const ['json'],
|
||||
);
|
||||
},
|
||||
),
|
||||
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 = Utils.jsonEncoder.convert(json);
|
||||
} catch (e) {
|
||||
SmartDialog.showToast('解析json失败:$e');
|
||||
return;
|
||||
}
|
||||
final highlight = Highlight()..registerLanguage('json', langJson);
|
||||
final result = highlight.highlight(
|
||||
code: formatText,
|
||||
language: 'json',
|
||||
);
|
||||
late TextSpanRenderer renderer;
|
||||
bool? isDarkMode;
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
final isDark = context.isDarkMode;
|
||||
if (isDark != isDarkMode) {
|
||||
isDarkMode = isDark;
|
||||
renderer = TextSpanRenderer(
|
||||
const TextStyle(),
|
||||
isDark ? githubDarkTheme : githubTheme,
|
||||
);
|
||||
result.render(renderer);
|
||||
}
|
||||
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 {
|
||||
if (await fromJson(json)) {
|
||||
SmartDialog.showToast('导入成功');
|
||||
}
|
||||
} catch (e) {
|
||||
SmartDialog.showToast('导入失败:$e');
|
||||
}
|
||||
},
|
||||
child: const Text('确定'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
dense: true,
|
||||
title: Text('输入$title', style: style),
|
||||
onTap: () {
|
||||
Get.back();
|
||||
final key = GlobalKey<FormFieldState<String>>();
|
||||
late T json;
|
||||
String? forceErrorText;
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text('输入$title'),
|
||||
constraints: StyleString.dialogFixedConstraints,
|
||||
content: TextFormField(
|
||||
key: key,
|
||||
minLines: 4,
|
||||
maxLines: 12,
|
||||
autofocus: true,
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
errorMaxLines: 3,
|
||||
),
|
||||
validator: (value) {
|
||||
if (forceErrorText != null) return forceErrorText;
|
||||
try {
|
||||
json = jsonDecode(value!) as T;
|
||||
return null;
|
||||
} catch (e) {
|
||||
if (e is FormatException) {}
|
||||
return '解析json失败:$e';
|
||||
}
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: Get.back,
|
||||
child: Text(
|
||||
'取消',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
if (key.currentState?.validate() == true) {
|
||||
try {
|
||||
if (await fromJson(json)) {
|
||||
Get.back();
|
||||
SmartDialog.showToast('导入成功');
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
forceErrorText = '导入失败:$e';
|
||||
}
|
||||
key.currentState?.validate();
|
||||
forceErrorText = null;
|
||||
}
|
||||
},
|
||||
child: const Text('确定'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import 'package:PiliPlus/common/widgets/dialog/dialog.dart';
|
||||
import 'package:PiliPlus/common/widgets/dialog/export_import.dart';
|
||||
import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart';
|
||||
import 'package:PiliPlus/common/widgets/view_sliver_safe_area.dart';
|
||||
import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart';
|
||||
import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart'
|
||||
show ReplyInfo;
|
||||
import 'package:PiliPlus/pages/video/reply/widgets/reply_item_grpc.dart';
|
||||
import 'package:PiliPlus/utils/app_scheme.dart';
|
||||
import 'package:PiliPlus/utils/id_utils.dart';
|
||||
@@ -9,9 +11,13 @@ import 'package:PiliPlus/utils/page_utils.dart';
|
||||
import 'package:PiliPlus/utils/reply_utils.dart';
|
||||
import 'package:PiliPlus/utils/storage.dart';
|
||||
import 'package:PiliPlus/utils/storage_pref.dart';
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
import 'package:PiliPlus/utils/waterfall.dart';
|
||||
import 'package:flutter/foundation.dart' show kDebugMode;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get_core/src/get_main.dart';
|
||||
import 'package:get/get_navigation/src/extension_navigation.dart';
|
||||
import 'package:get/get_rx/get_rx.dart';
|
||||
import 'package:waterfall_flow/waterfall_flow.dart';
|
||||
|
||||
class MyReply extends StatefulWidget {
|
||||
@@ -22,13 +28,19 @@ class MyReply extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _MyReplyState extends State<MyReply> with DynMixin {
|
||||
late final List<ReplyInfo> _replies;
|
||||
final List<ReplyInfo> _replies = <ReplyInfo>[];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_replies = GStorage.reply!.values.map(ReplyInfo.fromBuffer).toList()
|
||||
..sort((a, b) => b.ctime.compareTo(a.ctime)); // rpid not aligned
|
||||
_initReply();
|
||||
}
|
||||
|
||||
void _initReply() {
|
||||
_replies.assignAll(
|
||||
GStorage.reply!.values.map(ReplyInfo.fromBuffer).toList()
|
||||
..sort((a, b) => b.ctime.compareTo(a.ctime)), // rpid not aligned
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -36,24 +48,33 @@ class _MyReplyState extends State<MyReply> with DynMixin {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('我的评论'),
|
||||
actions: kDebugMode
|
||||
? [
|
||||
IconButton(
|
||||
tooltip: 'Clear',
|
||||
onPressed: () => showConfirmDialog(
|
||||
context: context,
|
||||
title: 'Clear Local Storage?',
|
||||
onConfirm: () {
|
||||
GStorage.reply!.clear();
|
||||
_replies.clear();
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
icon: const Icon(Icons.clear_all),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
]
|
||||
: null,
|
||||
actions: [
|
||||
if (kDebugMode)
|
||||
IconButton(
|
||||
tooltip: 'Clear',
|
||||
onPressed: () => showConfirmDialog(
|
||||
context: context,
|
||||
title: 'Clear Local Storage?',
|
||||
onConfirm: () {
|
||||
GStorage.reply!.clear();
|
||||
_replies.clear();
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
icon: const Icon(Icons.clear_all),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: '导出',
|
||||
onPressed: _showExportDialog,
|
||||
icon: const Icon(Icons.file_upload_outlined),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: '导入',
|
||||
onPressed: _showImportDialog,
|
||||
icon: const Icon(Icons.file_download_outlined),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
],
|
||||
),
|
||||
body: CustomScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
@@ -121,4 +142,89 @@ class _MyReplyState extends State<MyReply> with DynMixin {
|
||||
isManual: true,
|
||||
);
|
||||
}
|
||||
|
||||
String _onExport() {
|
||||
return Utils.jsonEncoder.convert(
|
||||
_replies.map((e) => e.toProto3Json()).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
void _showExportDialog() {
|
||||
const style = TextStyle(fontSize: 14);
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => SimpleDialog(
|
||||
clipBehavior: .hardEdge,
|
||||
contentPadding: const .symmetric(vertical: 12),
|
||||
children: [
|
||||
ListTile(
|
||||
dense: true,
|
||||
title: const Text('导出至剪贴板', style: style),
|
||||
onTap: () {
|
||||
Get.back();
|
||||
exportToClipBoard(onExport: _onExport);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
dense: true,
|
||||
title: const Text('导出文件至本地', style: style),
|
||||
onTap: () {
|
||||
Get.back();
|
||||
exportToLocalFile(
|
||||
onExport: _onExport,
|
||||
localFileName: () => 'reply',
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onImport(List<dynamic> list) async {
|
||||
await GStorage.reply!.putAll({
|
||||
for (var e in list)
|
||||
e['id'].toString(): (ReplyInfo.create()..mergeFromProto3Json(e))
|
||||
.writeToBuffer(),
|
||||
});
|
||||
if (mounted) {
|
||||
_initReply();
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
void _showImportDialog() {
|
||||
const style = TextStyle(fontSize: 14);
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => SimpleDialog(
|
||||
clipBehavior: .hardEdge,
|
||||
contentPadding: const .symmetric(vertical: 12),
|
||||
children: [
|
||||
ListTile(
|
||||
dense: true,
|
||||
title: const Text('从剪贴板导入', style: style),
|
||||
onTap: () {
|
||||
Get.back();
|
||||
importFromClipBoard<List<dynamic>>(
|
||||
context,
|
||||
title: '评论',
|
||||
onExport: _onExport,
|
||||
onImport: _onImport,
|
||||
showConfirmDialog: false,
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
dense: true,
|
||||
title: const Text('从本地文件导入', style: style),
|
||||
onTap: () {
|
||||
Get.back();
|
||||
importFromLocalFile<List<dynamic>>(onImport: _onImport);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:PiliPlus/common/widgets/dialog/export_import.dart';
|
||||
import 'package:PiliPlus/common/widgets/disabled_icon.dart';
|
||||
import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart';
|
||||
import 'package:PiliPlus/common/widgets/sliver_wrap.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 showImportExportDialog;
|
||||
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';
|
||||
@@ -424,12 +424,12 @@ class _SearchPageState extends State<SearchPage> {
|
||||
onPressed: () => showImportExportDialog<List>(
|
||||
context,
|
||||
title: '历史记录',
|
||||
toJson: () => jsonEncode(_searchController.historyList),
|
||||
fromJson: (json) {
|
||||
localFileName: () => 'search',
|
||||
onExport: () => jsonEncode(_searchController.historyList),
|
||||
onImport: (json) {
|
||||
final list = List<String>.from(json);
|
||||
_searchController.historyList.value = list;
|
||||
GStorage.historyWord.put('cacheList', list);
|
||||
return true;
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
@@ -87,12 +87,13 @@ abstract final class GStorage {
|
||||
static Future<void> importAllSettings(String data) =>
|
||||
importAllJsonSettings(jsonDecode(data));
|
||||
|
||||
static Future<bool> importAllJsonSettings(Map<String, dynamic> map) async {
|
||||
await Future.wait([
|
||||
static Future<List<void>> importAllJsonSettings(
|
||||
Map<String, dynamic> map,
|
||||
) {
|
||||
return Future.wait([
|
||||
setting.clear().then((_) => setting.putAll(map[setting.name])),
|
||||
video.clear().then((_) => video.putAll(map[video.name])),
|
||||
]);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void regAdapter() {
|
||||
@@ -107,8 +108,8 @@ abstract final class GStorage {
|
||||
..registerAdapter(RuleFilterAdapter());
|
||||
}
|
||||
|
||||
static Future<void> compact() async {
|
||||
await Future.wait([
|
||||
static Future<List<void>> compact() {
|
||||
return Future.wait([
|
||||
userInfo.compact(),
|
||||
historyWord.compact(),
|
||||
localCache.compact(),
|
||||
@@ -120,8 +121,8 @@ abstract final class GStorage {
|
||||
]);
|
||||
}
|
||||
|
||||
static Future<void> close() async {
|
||||
await Future.wait([
|
||||
static Future<List<void>> close() {
|
||||
return Future.wait([
|
||||
userInfo.close(),
|
||||
historyWord.close(),
|
||||
localCache.close(),
|
||||
@@ -133,6 +134,19 @@ abstract final class GStorage {
|
||||
]);
|
||||
}
|
||||
|
||||
static Future<List<void>> clear() {
|
||||
return Future.wait([
|
||||
userInfo.clear(),
|
||||
historyWord.clear(),
|
||||
localCache.clear(),
|
||||
setting.clear(),
|
||||
video.clear(),
|
||||
Accounts.clear(),
|
||||
watchProgress.clear(),
|
||||
?reply?.clear(),
|
||||
]);
|
||||
}
|
||||
|
||||
static int _intStrKeyComparator(dynamic k1, dynamic k2) {
|
||||
if (k1 is int) {
|
||||
if (k2 is int) {
|
||||
|
||||
Reference in New Issue
Block a user