mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2026-01-31 13:25:10 +01:00
feat. & fix.
- support backup & restore - fix when client.run empty return
This commit is contained in:
201
lib/view/page/backup.dart
Normal file
201
lib/view/page/backup.dart
Normal file
@@ -0,0 +1,201 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:toolbox/core/utils.dart';
|
||||
import 'package:toolbox/data/model/app/backup.dart';
|
||||
import 'package:toolbox/data/res/font_style.dart';
|
||||
import 'package:toolbox/data/store/private_key.dart';
|
||||
import 'package:toolbox/data/store/server.dart';
|
||||
import 'package:toolbox/data/store/setting.dart';
|
||||
import 'package:toolbox/data/store/snippet.dart';
|
||||
import 'package:toolbox/generated/l10n.dart';
|
||||
import 'package:toolbox/locator.dart';
|
||||
import 'package:toolbox/view/widget/round_rect_card.dart';
|
||||
|
||||
const backupFormatVersion = 1;
|
||||
|
||||
class BackupPage extends StatelessWidget {
|
||||
BackupPage({Key? key}) : super(key: key);
|
||||
|
||||
final setting = locator<SettingStore>();
|
||||
final server = locator<ServerStore>();
|
||||
final snippet = locator<SnippetStore>();
|
||||
final privateKey = locator<PrivateKeyStore>();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final media = MediaQuery.of(context);
|
||||
final s = S.of(context);
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(s.importAndExport, style: size18),
|
||||
),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(37),
|
||||
child: Text(
|
||||
s.backupTip,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 77,
|
||||
),
|
||||
_buildCard(s.import, Icons.download, media,
|
||||
() => _showImportDialog(context, s)),
|
||||
_buildCard(s.export, Icons.file_upload, media,
|
||||
() => _showExportDialog(context, s))
|
||||
],
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCard(String text, IconData icon, MediaQueryData media,
|
||||
FutureOr Function() onTap) {
|
||||
return RoundRectCard(InkWell(
|
||||
onTap: onTap,
|
||||
child: SizedBox(
|
||||
width: media.size.width * 0.77,
|
||||
height: media.size.height * 0.17,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(icon, size: media.size.height * 0.05),
|
||||
const SizedBox(height: 10),
|
||||
Text(text, style: TextStyle(fontSize: media.size.height * 0.02)),
|
||||
],
|
||||
),
|
||||
)));
|
||||
}
|
||||
|
||||
Future<void> _showExportDialog(BuildContext context, S s) async {
|
||||
final exportFieldController = TextEditingController()
|
||||
..text = _diyEncrtpt(json.encode(Backup(
|
||||
backupFormatVersion,
|
||||
DateTime.now().toString().split('.').first,
|
||||
server.fetch(),
|
||||
snippet.fetch(),
|
||||
privateKey.fetch(),
|
||||
setting.primaryColor.fetch() ?? Colors.pinkAccent.value,
|
||||
setting.serverStatusUpdateInterval.fetch() ?? 2,
|
||||
setting.launchPage.fetch() ?? 0,
|
||||
)));
|
||||
await showRoundDialog(
|
||||
context,
|
||||
s.export,
|
||||
TextField(
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'JSON',
|
||||
),
|
||||
maxLines: 7,
|
||||
controller: exportFieldController,
|
||||
),
|
||||
[
|
||||
TextButton(
|
||||
child: Text(s.copy),
|
||||
onPressed: () {
|
||||
Clipboard.setData(
|
||||
ClipboardData(text: exportFieldController.text));
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
Future<void> _showImportDialog(BuildContext context, S s) async {
|
||||
final importFieldController = TextEditingController();
|
||||
await showRoundDialog(
|
||||
context,
|
||||
s.import,
|
||||
TextField(
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'JSON',
|
||||
),
|
||||
maxLines: 3,
|
||||
controller: importFieldController,
|
||||
),
|
||||
[
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(s.cancel)),
|
||||
TextButton(
|
||||
onPressed: () async =>
|
||||
await _import(importFieldController.text.trim(), context, s),
|
||||
child: const Text('GO'),
|
||||
)
|
||||
]);
|
||||
}
|
||||
|
||||
Future<void> _import(String text, BuildContext context, S s) async {
|
||||
if (text.isEmpty) {
|
||||
showSnackBar(context, Text(s.fieldMustNotEmpty));
|
||||
return;
|
||||
}
|
||||
_importBackup(text, context, s);
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
Future<void> _importBackup(String raw, BuildContext context, S s) async {
|
||||
try {
|
||||
final backup = await compute(_decode, raw);
|
||||
if (backupFormatVersion != backup.version) {
|
||||
showSnackBar(context, Text(s.backupVersionNotMatch));
|
||||
return;
|
||||
}
|
||||
|
||||
await showRoundDialog(
|
||||
context, s.attention, Text(s.restoreSureWithDate(backup.date)), [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(s.cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
for (final s in backup.snippets) {
|
||||
snippet.put(s);
|
||||
}
|
||||
for (final s in backup.spis) {
|
||||
server.put(s);
|
||||
}
|
||||
for (final s in backup.keys) {
|
||||
privateKey.put(s);
|
||||
}
|
||||
setting.primaryColor.put(backup.primaryColor);
|
||||
setting.serverStatusUpdateInterval
|
||||
.put(backup.serverStatusUpdateInterval);
|
||||
setting.launchPage.put(backup.launchPage);
|
||||
Navigator.of(context).pop();
|
||||
showSnackBar(context, Text(s.restoreSuccess));
|
||||
},
|
||||
child: Text(s.ok),
|
||||
),
|
||||
]);
|
||||
} catch (e) {
|
||||
showSnackBar(context, Text(s.invalidJson));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Backup _decode(String raw) {
|
||||
final decrypted = _diyDecrypt(raw);
|
||||
return Backup.fromJson(json.decode(decrypted));
|
||||
}
|
||||
|
||||
String _diyEncrtpt(String raw) =>
|
||||
json.encode(raw.codeUnits.map((e) => e * 2 + 1).toList(growable: false));
|
||||
String _diyDecrypt(String raw) {
|
||||
final list = json.decode(raw);
|
||||
final sb = StringBuffer();
|
||||
for (final e in list) {
|
||||
sb.writeCharCode((e - 1) ~/ 2);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import 'package:toolbox/data/res/url.dart';
|
||||
import 'package:toolbox/data/store/setting.dart';
|
||||
import 'package:toolbox/generated/l10n.dart';
|
||||
import 'package:toolbox/locator.dart';
|
||||
import 'package:toolbox/view/page/backup.dart';
|
||||
import 'package:toolbox/view/page/convert.dart';
|
||||
import 'package:toolbox/view/page/debug.dart';
|
||||
import 'package:toolbox/view/page/ping.dart';
|
||||
@@ -240,6 +241,12 @@ class _MyHomePageState extends State<MyHomePage>
|
||||
AppRoute(const SFTPDownloadedPage(), 'snippet list')
|
||||
.go(context),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.import_export),
|
||||
title: Text(s.backup),
|
||||
onTap: () =>
|
||||
AppRoute(BackupPage(), 'backup page').go(context),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.snippet_folder),
|
||||
title: Text(s.snippet),
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:toolbox/core/route.dart';
|
||||
@@ -26,9 +25,6 @@ class SnippetListPage extends StatefulWidget {
|
||||
class _SnippetListPageState extends State<SnippetListPage> {
|
||||
late ServerPrivateInfo _selectedIndex;
|
||||
|
||||
final _importFieldController = TextEditingController();
|
||||
final _exportFieldController = TextEditingController();
|
||||
|
||||
final _textStyle = TextStyle(color: primaryColor);
|
||||
|
||||
late S s;
|
||||
@@ -44,12 +40,6 @@ class _SnippetListPageState extends State<SnippetListPage> {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(s.snippet, style: size18),
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () => _showImportExport(),
|
||||
tooltip: s.importAndExport,
|
||||
icon: const Icon(Icons.import_export)),
|
||||
],
|
||||
),
|
||||
body: _buildBody(),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
@@ -60,97 +50,6 @@ class _SnippetListPageState extends State<SnippetListPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showImportExport() async {
|
||||
await showRoundDialog(
|
||||
context,
|
||||
s.choose,
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(s.import),
|
||||
leading: const Icon(Icons.download),
|
||||
onTap: () => _showImportDialog(),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(s.export),
|
||||
leading: const Icon(Icons.file_upload),
|
||||
onTap: () => _showExportDialog(),
|
||||
),
|
||||
],
|
||||
),
|
||||
[]);
|
||||
}
|
||||
|
||||
Future<void> _showExportDialog() async {
|
||||
Navigator.of(context).pop();
|
||||
_exportFieldController.text = locator<SnippetProvider>().export;
|
||||
await showRoundDialog(
|
||||
context,
|
||||
s.export,
|
||||
TextField(
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'JSON',
|
||||
),
|
||||
maxLines: 3,
|
||||
controller: _exportFieldController,
|
||||
),
|
||||
[
|
||||
TextButton(
|
||||
child: Text(s.ok),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
Future<void> _showImportDialog() async {
|
||||
Navigator.of(context).pop();
|
||||
await showRoundDialog(
|
||||
context,
|
||||
s.import,
|
||||
TextField(
|
||||
decoration: InputDecoration(
|
||||
labelText: s.urlOrJson,
|
||||
),
|
||||
maxLines: 2,
|
||||
controller: _importFieldController,
|
||||
),
|
||||
[
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(s.cancel)),
|
||||
TextButton(
|
||||
onPressed: () async =>
|
||||
await _import(_importFieldController.text.trim()),
|
||||
child: const Text('GO'),
|
||||
)
|
||||
]);
|
||||
}
|
||||
|
||||
Future<void> _import(String text) async {
|
||||
if (text.isEmpty) {
|
||||
showSnackBar(context, Text(s.fieldMustNotEmpty));
|
||||
return;
|
||||
}
|
||||
final snippetProvider = locator<SnippetProvider>();
|
||||
if (text.startsWith('http')) {
|
||||
final resp = await Dio().get(text);
|
||||
if (resp.statusCode != 200) {
|
||||
showSnackBar(
|
||||
context, Text(s.httpFailedWithCode(resp.statusCode ?? '-1')));
|
||||
return;
|
||||
}
|
||||
for (final snippet in getSnippetList(resp.data)) {
|
||||
snippetProvider.add(snippet);
|
||||
}
|
||||
} else {
|
||||
for (final snippet in getSnippetList(text)) {
|
||||
snippetProvider.add(snippet);
|
||||
}
|
||||
}
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
Widget _buildBody() {
|
||||
return Consumer<SnippetProvider>(
|
||||
builder: (_, key, __) {
|
||||
|
||||
Reference in New Issue
Block a user