mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2026-01-31 13:25:10 +01:00
opt.: migrate fl_lib
This commit is contained in:
@@ -2,31 +2,16 @@ import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:computer/computer.dart';
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
import 'package:toolbox/core/extension/context/common.dart';
|
||||
import 'package:toolbox/core/extension/context/dialog.dart';
|
||||
import 'package:toolbox/core/extension/context/locale.dart';
|
||||
import 'package:toolbox/core/extension/context/snackbar.dart';
|
||||
import 'package:toolbox/core/extension/datetime.dart';
|
||||
import 'package:toolbox/core/utils/misc.dart';
|
||||
import 'package:toolbox/core/utils/sync/icloud.dart';
|
||||
import 'package:toolbox/core/utils/platform/base.dart';
|
||||
import 'package:toolbox/core/utils/share.dart';
|
||||
import 'package:toolbox/core/utils/sync/webdav.dart';
|
||||
import 'package:toolbox/data/model/app/backup.dart';
|
||||
import 'package:toolbox/data/model/server/server_private_info.dart';
|
||||
import 'package:toolbox/data/res/logger.dart';
|
||||
import 'package:toolbox/data/res/path.dart';
|
||||
import 'package:toolbox/data/res/store.dart';
|
||||
import 'package:toolbox/data/res/ui.dart';
|
||||
import 'package:toolbox/data/res/url.dart';
|
||||
import 'package:toolbox/view/widget/appbar.dart';
|
||||
import 'package:toolbox/view/widget/expand_tile.dart';
|
||||
import 'package:toolbox/view/widget/cardx.dart';
|
||||
import 'package:toolbox/view/widget/input_field.dart';
|
||||
import 'package:toolbox/view/widget/markdown.dart';
|
||||
import 'package:toolbox/view/widget/store_switch.dart';
|
||||
|
||||
class BackupPage extends StatelessWidget {
|
||||
BackupPage({super.key});
|
||||
@@ -82,11 +67,12 @@ class BackupPage extends StatelessWidget {
|
||||
final path = await Backup.backup();
|
||||
|
||||
/// Issue #188
|
||||
if (isWindows) {
|
||||
await Shares.text(await File(path).readAsString());
|
||||
} else {
|
||||
await Shares.files([path]);
|
||||
}
|
||||
final _ = switch (Pfs.type) {
|
||||
Pfs.windows =>
|
||||
await Process.run('explorer', ['/select,', path]),
|
||||
Pfs.linux => await Process.run('xdg-open', [path]),
|
||||
_ => await Pfs.sharePath(path),
|
||||
};
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
@@ -206,7 +192,7 @@ class BackupPage extends StatelessWidget {
|
||||
trailing: const Icon(Icons.save),
|
||||
onTap: () async {
|
||||
final path = await Backup.backup();
|
||||
Shares.copy(await File(path).readAsString());
|
||||
Pfs.copy(await File(path).readAsString());
|
||||
context.showSnackBar(l10n.success);
|
||||
},
|
||||
),
|
||||
@@ -232,25 +218,14 @@ class BackupPage extends StatelessWidget {
|
||||
),
|
||||
leading: const Icon(Icons.import_export),
|
||||
onTap: () => _onBulkImportServers(context),
|
||||
trailing: const Icon(Icons.keyboard_arrow_right),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onTapFileRestore(BuildContext context) async {
|
||||
final path = await pickOneFile();
|
||||
if (path == null) return;
|
||||
|
||||
final file = File(path);
|
||||
if (!await file.exists()) {
|
||||
context.showSnackBar(l10n.fileNotExist(path));
|
||||
return;
|
||||
}
|
||||
|
||||
final text = await file.readAsString();
|
||||
if (text.isEmpty) {
|
||||
context.showSnackBar(l10n.fieldMustNotEmpty);
|
||||
return;
|
||||
}
|
||||
final text = await Pfs.pickFileString();
|
||||
if (text == null) return;
|
||||
|
||||
try {
|
||||
final backup = await context.showLoadingDialog(
|
||||
@@ -262,7 +237,7 @@ class BackupPage extends StatelessWidget {
|
||||
}
|
||||
|
||||
await context.showRoundDialog(
|
||||
title: Text(l10n.restore),
|
||||
title: l10n.restore,
|
||||
child: Text(l10n.askContinue(
|
||||
'${l10n.restore} ${l10n.backup}(${backup.date})',
|
||||
)),
|
||||
@@ -296,7 +271,7 @@ class BackupPage extends StatelessWidget {
|
||||
}
|
||||
|
||||
final fileName = await context.showRoundDialog<String>(
|
||||
title: Text(l10n.restore),
|
||||
title: l10n.restore,
|
||||
child: SizedBox(
|
||||
width: 300,
|
||||
height: 300,
|
||||
@@ -329,7 +304,7 @@ class BackupPage extends StatelessWidget {
|
||||
webdavLoading.value = false;
|
||||
return;
|
||||
}
|
||||
final dlFile = await File('${await Paths.doc}/$fileName').readAsString();
|
||||
final dlFile = await File('${Paths.doc}/$fileName').readAsString();
|
||||
final dlBak = await Computer.shared.start(Backup.fromJsonString, dlFile);
|
||||
await dlBak.restore(force: true);
|
||||
webdavLoading.value = false;
|
||||
@@ -337,7 +312,7 @@ class BackupPage extends StatelessWidget {
|
||||
|
||||
Future<void> _onTapWebdavUp(BuildContext context) async {
|
||||
webdavLoading.value = true;
|
||||
final bakName = '${DateTime.now().numStr}-${Paths.bakName}';
|
||||
final bakName = '${DateTime.now().ymdhms()}-${Paths.bakName}';
|
||||
await Backup.backup(bakName);
|
||||
final uploadResult = await Webdav.upload(relativePath: bakName);
|
||||
if (uploadResult != null) {
|
||||
@@ -359,7 +334,7 @@ class BackupPage extends StatelessWidget {
|
||||
text: Stores.setting.webdavPwd.fetch(),
|
||||
);
|
||||
final result = await context.showRoundDialog<bool>(
|
||||
title: const Text('WebDAV'),
|
||||
title: 'WebDAV',
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
@@ -401,7 +376,7 @@ class BackupPage extends StatelessWidget {
|
||||
}
|
||||
|
||||
void _onTapClipboardRestore(BuildContext context) async {
|
||||
final text = await Shares.paste();
|
||||
final text = await Pfs.paste();
|
||||
if (text == null || text.isEmpty) {
|
||||
context.showSnackBar(l10n.fieldMustNotEmpty);
|
||||
return;
|
||||
@@ -418,7 +393,7 @@ class BackupPage extends StatelessWidget {
|
||||
}
|
||||
|
||||
await context.showRoundDialog(
|
||||
title: Text(l10n.restore),
|
||||
title: l10n.restore,
|
||||
child: Text(l10n.askContinue(
|
||||
'${l10n.restore} ${l10n.backup}(${backup.date})',
|
||||
)),
|
||||
@@ -443,26 +418,8 @@ class BackupPage extends StatelessWidget {
|
||||
}
|
||||
|
||||
void _onBulkImportServers(BuildContext context) async {
|
||||
final path = await pickOneFile();
|
||||
if (path == null) return;
|
||||
|
||||
final file = File(path);
|
||||
if (!await file.exists()) {
|
||||
context.showRoundDialog(
|
||||
title: Text(l10n.error),
|
||||
child: Text(l10n.fileNotExist(path)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final text = await file.readAsString();
|
||||
if (text.isEmpty) {
|
||||
context.showRoundDialog(
|
||||
title: Text(l10n.error),
|
||||
child: Text(l10n.fieldMustNotEmpty),
|
||||
);
|
||||
return;
|
||||
}
|
||||
final text = await Pfs.pickFileString();
|
||||
if (text == null) return;
|
||||
|
||||
try {
|
||||
final spis = await context.showLoadingDialog(
|
||||
@@ -472,7 +429,7 @@ class BackupPage extends StatelessWidget {
|
||||
}, text.trim()),
|
||||
);
|
||||
final sure = await context.showRoundDialog<bool>(
|
||||
title: Text(l10n.import),
|
||||
title: l10n.import,
|
||||
child: Text(l10n.askContinue('${spis.length} ${l10n.server}')),
|
||||
actions: [
|
||||
TextButton(
|
||||
@@ -493,7 +450,7 @@ class BackupPage extends StatelessWidget {
|
||||
}
|
||||
} catch (e) {
|
||||
context.showRoundDialog(
|
||||
title: Text(l10n.error),
|
||||
title: l10n.error,
|
||||
child: Text(e.toString()),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,27 +1,20 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:toolbox/core/extension/context/common.dart';
|
||||
import 'package:toolbox/core/extension/context/dialog.dart';
|
||||
import 'package:toolbox/core/extension/context/locale.dart';
|
||||
import 'package:toolbox/core/extension/context/snackbar.dart';
|
||||
import 'package:toolbox/core/extension/stringx.dart';
|
||||
import 'package:toolbox/core/route.dart';
|
||||
import 'package:toolbox/data/model/app/menu/base.dart';
|
||||
import 'package:toolbox/data/model/app/menu/container.dart';
|
||||
import 'package:toolbox/data/model/container/image.dart';
|
||||
import 'package:toolbox/data/model/container/type.dart';
|
||||
import 'package:toolbox/data/res/store.dart';
|
||||
import 'package:toolbox/view/widget/expand_tile.dart';
|
||||
import 'package:toolbox/view/widget/input_field.dart';
|
||||
|
||||
import '../../data/model/container/ps.dart';
|
||||
import '../../data/model/server/server_private_info.dart';
|
||||
import '../../data/provider/container.dart';
|
||||
import '../../data/res/ui.dart';
|
||||
import '../widget/appbar.dart';
|
||||
import '../widget/popup_menu.dart';
|
||||
import '../widget/cardx.dart';
|
||||
import '../widget/two_line_text.dart';
|
||||
|
||||
class ContainerPage extends StatefulWidget {
|
||||
@@ -267,7 +260,8 @@ class _ContainerPageState extends State<ContainerPage> {
|
||||
|
||||
Widget _buildMoreBtn(ContainerPs dItem) {
|
||||
return PopupMenu(
|
||||
items: ContainerMenu.items(dItem.running).map((e) => e.widget).toList(),
|
||||
items: ContainerMenu.items(dItem.running),
|
||||
builder: (e) => PopMenu.build(e, e.icon, e.toStr),
|
||||
onSelected: (item) => _onTapMoreBtn(item, dItem),
|
||||
);
|
||||
}
|
||||
@@ -331,7 +325,7 @@ class _ContainerPageState extends State<ContainerPage> {
|
||||
final nameCtrl = TextEditingController();
|
||||
final argsCtrl = TextEditingController();
|
||||
await context.showRoundDialog(
|
||||
title: Text(l10n.newContainer),
|
||||
title: l10n.newContainer,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
@@ -380,7 +374,7 @@ class _ContainerPageState extends State<ContainerPage> {
|
||||
|
||||
Future<void> _showAddCmdPreview(String cmd) async {
|
||||
await context.showRoundDialog(
|
||||
title: Text(l10n.preview),
|
||||
title: l10n.preview,
|
||||
child: Text(cmd),
|
||||
actions: [
|
||||
TextButton(
|
||||
@@ -422,7 +416,7 @@ class _ContainerPageState extends State<ContainerPage> {
|
||||
final host = Stores.container.fetch(id);
|
||||
final ctrl = TextEditingController(text: host);
|
||||
await context.showRoundDialog(
|
||||
title: Text(l10n.dockerEditHost),
|
||||
title: l10n.dockerEditHost,
|
||||
child: Input(
|
||||
maxLines: 2,
|
||||
controller: ctrl,
|
||||
@@ -446,7 +440,7 @@ class _ContainerPageState extends State<ContainerPage> {
|
||||
|
||||
void _showImageRmDialog(ContainerImg e) {
|
||||
context.showRoundDialog(
|
||||
title: Text(l10n.attention),
|
||||
title: l10n.attention,
|
||||
child: Text(l10n.askContinue('${l10n.delete} Image(${e.repository})')),
|
||||
actions: [
|
||||
TextButton(
|
||||
@@ -477,7 +471,7 @@ class _ContainerPageState extends State<ContainerPage> {
|
||||
case ContainerMenu.rm:
|
||||
var force = false;
|
||||
context.showRoundDialog(
|
||||
title: Text(l10n.attention),
|
||||
title: l10n.attention,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
@@ -510,7 +504,7 @@ class _ContainerPageState extends State<ContainerPage> {
|
||||
);
|
||||
if (result != null) {
|
||||
context.showRoundDialog(
|
||||
title: Text(l10n.error),
|
||||
title: l10n.error,
|
||||
child: Text(result.message ?? l10n.unknownError),
|
||||
);
|
||||
}
|
||||
@@ -526,7 +520,7 @@ class _ContainerPageState extends State<ContainerPage> {
|
||||
);
|
||||
if (result != null) {
|
||||
context.showRoundDialog(
|
||||
title: Text(l10n.error),
|
||||
title: l10n.error,
|
||||
child: Text(result.message ?? l10n.unknownError),
|
||||
);
|
||||
}
|
||||
@@ -537,7 +531,7 @@ class _ContainerPageState extends State<ContainerPage> {
|
||||
);
|
||||
if (result != null) {
|
||||
context.showRoundDialog(
|
||||
title: Text(l10n.error),
|
||||
title: l10n.error,
|
||||
child: Text(result.message ?? l10n.unknownError),
|
||||
);
|
||||
}
|
||||
@@ -548,19 +542,19 @@ class _ContainerPageState extends State<ContainerPage> {
|
||||
);
|
||||
if (result != null) {
|
||||
context.showRoundDialog(
|
||||
title: Text(l10n.error),
|
||||
title: l10n.error,
|
||||
child: Text(result.message ?? l10n.unknownError),
|
||||
);
|
||||
}
|
||||
break;
|
||||
case ContainerMenu.logs:
|
||||
AppRoute.ssh(
|
||||
AppRoutes.ssh(
|
||||
spi: widget.spi,
|
||||
initCmd: 'docker logs -f --tail 100 ${dItem.id}',
|
||||
).go(context);
|
||||
break;
|
||||
case ContainerMenu.terminal:
|
||||
AppRoute.ssh(
|
||||
AppRoutes.ssh(
|
||||
spi: widget.spi,
|
||||
initCmd: 'docker exec -it ${dItem.id} sh',
|
||||
).go(context);
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:toolbox/core/extension/context/common.dart';
|
||||
import 'package:toolbox/data/provider/debug.dart';
|
||||
import 'package:toolbox/data/res/provider.dart';
|
||||
|
||||
import '../widget/appbar.dart';
|
||||
|
||||
class DebugPage extends StatelessWidget {
|
||||
const DebugPage({super.key});
|
||||
|
||||
@@ -3,18 +3,16 @@ import 'dart:io';
|
||||
|
||||
import 'package:code_text_field/code_text_field.dart';
|
||||
import 'package:computer/computer.dart';
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_highlight/theme_map.dart';
|
||||
import 'package:flutter_highlight/themes/a11y-light.dart';
|
||||
import 'package:flutter_highlight/themes/monokai.dart';
|
||||
import 'package:toolbox/core/extension/context/common.dart';
|
||||
import 'package:toolbox/core/extension/context/dialog.dart';
|
||||
import 'package:toolbox/core/extension/context/locale.dart';
|
||||
import 'package:toolbox/core/utils/misc.dart';
|
||||
import 'package:toolbox/data/res/highlight.dart';
|
||||
import 'package:toolbox/data/res/store.dart';
|
||||
|
||||
import '../widget/appbar.dart';
|
||||
import '../widget/two_line_text.dart';
|
||||
|
||||
class EditorPage extends StatefulWidget {
|
||||
|
||||
@@ -1,38 +1,24 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:after_layout/after_layout.dart';
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:icons_plus/icons_plus.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:toolbox/core/channel/bg_run.dart';
|
||||
import 'package:toolbox/core/channel/home_widget.dart';
|
||||
import 'package:toolbox/core/extension/build.dart';
|
||||
import 'package:toolbox/core/extension/context/common.dart';
|
||||
import 'package:toolbox/core/extension/context/dialog.dart';
|
||||
import 'package:toolbox/core/extension/context/locale.dart';
|
||||
import 'package:toolbox/core/persistant_store.dart';
|
||||
import 'package:toolbox/core/route.dart';
|
||||
import 'package:toolbox/core/update.dart';
|
||||
import 'package:toolbox/core/utils/platform/auth.dart';
|
||||
import 'package:toolbox/core/utils/platform/base.dart';
|
||||
import 'package:toolbox/core/utils/platform/perm.dart';
|
||||
import 'package:toolbox/core/utils/ui.dart';
|
||||
import 'package:toolbox/data/model/app/github_id.dart';
|
||||
import 'package:toolbox/data/model/app/tab.dart';
|
||||
import 'package:toolbox/data/res/build_data.dart';
|
||||
import 'package:toolbox/data/res/github_id.dart';
|
||||
import 'package:toolbox/data/res/logger.dart';
|
||||
import 'package:toolbox/data/res/misc.dart';
|
||||
import 'package:toolbox/data/res/provider.dart';
|
||||
import 'package:toolbox/data/res/store.dart';
|
||||
import 'package:toolbox/data/res/ui.dart';
|
||||
import 'package:toolbox/data/res/url.dart';
|
||||
import 'package:toolbox/view/widget/appbar.dart';
|
||||
import 'package:toolbox/view/widget/cardx.dart';
|
||||
import 'package:toolbox/view/widget/markdown.dart';
|
||||
import 'package:toolbox/view/widget/val_builder.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
|
||||
part 'appbar.dart';
|
||||
@@ -100,7 +86,9 @@ class _HomePageState extends State<HomePage>
|
||||
switch (state) {
|
||||
case AppLifecycleState.resumed:
|
||||
if (_shouldAuth) {
|
||||
BioAuth.go().then((_) => _shouldAuth = false);
|
||||
if (Stores.setting.useBioAuth.fetch()) {
|
||||
BioAuth.go().then((_) => _shouldAuth = false);
|
||||
}
|
||||
}
|
||||
if (!Pros.server.isAutoRefreshOn) {
|
||||
Pros.server.startAutoRefresh();
|
||||
@@ -112,9 +100,9 @@ class _HomePageState extends State<HomePage>
|
||||
// Keep running in background on Android device
|
||||
if (isAndroid && Stores.setting.bgRun.fetch()) {
|
||||
// Keep this if statement single
|
||||
if (Pros.app.moveBg) {
|
||||
BgRunMC.moveToBg();
|
||||
}
|
||||
// if (Pros.app.moveBg) {
|
||||
// BgRunMC.moveToBg();
|
||||
// }
|
||||
} else {
|
||||
//Pros.server.setDisconnected();
|
||||
Pros.server.stopAutoRefresh();
|
||||
@@ -152,7 +140,7 @@ class _HomePageState extends State<HomePage>
|
||||
IconButton(
|
||||
icon: const Icon(Icons.developer_mode, size: 21),
|
||||
tooltip: l10n.debug,
|
||||
onPressed: () => AppRoute.debug().go(context),
|
||||
onPressed: () => AppRoutes.debug().go(context),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -239,7 +227,7 @@ class _HomePageState extends State<HomePage>
|
||||
_buildIcon(),
|
||||
TextButton(
|
||||
onPressed: () => context.showRoundDialog(
|
||||
title: const Text(BuildDataX.versionStr),
|
||||
title: BuildDataX.versionStr,
|
||||
child: const Text(
|
||||
'${BuildData.buildAt}\nFlutter ${BuildData.engine}'),
|
||||
),
|
||||
@@ -264,23 +252,23 @@ class _HomePageState extends State<HomePage>
|
||||
ListTile(
|
||||
leading: const Icon(Icons.settings),
|
||||
title: Text(l10n.setting),
|
||||
onTap: () => AppRoute.settings().go(context),
|
||||
onTap: () => AppRoutes.settings().go(context),
|
||||
onLongPress: _onLongPressSetting,
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.vpn_key),
|
||||
title: Text(l10n.privateKey),
|
||||
onTap: () => AppRoute.keyList().go(context),
|
||||
onTap: () => AppRoutes.keyList().go(context),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(BoxIcons.bxs_file_blank),
|
||||
title: Text(l10n.files),
|
||||
onTap: () => AppRoute.localStorage().go(context),
|
||||
onTap: () => AppRoutes.localStorage().go(context),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(MingCute.file_import_fill),
|
||||
title: Text(l10n.backup),
|
||||
onTap: () => AppRoute.backup().go(context),
|
||||
onTap: () => AppRoutes.backup().go(context),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(OctIcons.feed_discussion),
|
||||
@@ -294,7 +282,7 @@ class _HomePageState extends State<HomePage>
|
||||
|
||||
void _showAboutDialog() {
|
||||
context.showRoundDialog(
|
||||
title: Text(l10n.about),
|
||||
title: l10n.about,
|
||||
child: _buildAboutContent(),
|
||||
actions: [
|
||||
TextButton(
|
||||
@@ -348,15 +336,19 @@ ${GithubIds.participants.map((e) => '[$e](${e.url})').join(' ')}
|
||||
@override
|
||||
Future<void> afterFirstLayout(BuildContext context) async {
|
||||
// Auth required for first launch
|
||||
BioAuth.go();
|
||||
if (Stores.setting.useBioAuth.fetch()) BioAuth.go();
|
||||
|
||||
_reqNotiPerm();
|
||||
|
||||
if (Stores.setting.autoCheckAppUpdate.fetch()) {
|
||||
doUpdate(context);
|
||||
AppUpdateIface.doUpdate(
|
||||
build: BuildData.build,
|
||||
url: '${Urls.cdnBase}/update.json',
|
||||
context: context,
|
||||
updateL10n: l10n.update,
|
||||
);
|
||||
}
|
||||
HomeWidgetMC.update();
|
||||
await GetIt.I.allReady();
|
||||
await Pros.server.load();
|
||||
await Pros.server.refresh();
|
||||
}
|
||||
@@ -369,7 +361,7 @@ ${GithubIds.participants.map((e) => '[$e](${e.url})').join(' ')}
|
||||
final noNotiPerm = Stores.setting.noNotiPerm;
|
||||
if (noNotiPerm.fetch()) return;
|
||||
context.showRoundDialog(
|
||||
title: Text(l10n.error),
|
||||
title: l10n.error,
|
||||
child: Text(l10n.noNotiPerm),
|
||||
actions: [
|
||||
TextButton(
|
||||
@@ -390,7 +382,7 @@ ${GithubIds.participants.map((e) => '[$e](${e.url})').join(' ')}
|
||||
|
||||
/// Encode [map] to String with indent `\t`
|
||||
final text = Miscs.jsonEncoder.convert(map);
|
||||
final result = await AppRoute.editor(
|
||||
final result = await AppRoutes.editor(
|
||||
text: text,
|
||||
langCode: 'json',
|
||||
title: l10n.setting,
|
||||
@@ -408,7 +400,7 @@ ${GithubIds.participants.map((e) => '[$e](${e.url})').join(' ')}
|
||||
}
|
||||
} catch (e, trace) {
|
||||
context.showRoundDialog(
|
||||
title: Text(l10n.error),
|
||||
title: l10n.error,
|
||||
child: Text('${l10n.save}:\n$e'),
|
||||
);
|
||||
Loggers.app.warning('Update json settings failed', e, trace);
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:toolbox/core/extension/context/locale.dart';
|
||||
import 'package:toolbox/core/extension/context/snackbar.dart';
|
||||
import 'package:toolbox/core/route.dart';
|
||||
import 'package:toolbox/data/model/server/server_private_info.dart';
|
||||
import 'package:toolbox/view/widget/appbar.dart';
|
||||
import 'package:toolbox/view/widget/input_field.dart';
|
||||
|
||||
class IPerfPage extends StatefulWidget {
|
||||
final ServerPrivateInfo spi;
|
||||
@@ -38,7 +36,7 @@ class _IPerfPageState extends State<IPerfPage> {
|
||||
context.showSnackBar(l10n.fieldMustNotEmpty);
|
||||
return;
|
||||
}
|
||||
AppRoute.ssh(
|
||||
AppRoutes.ssh(
|
||||
spi: widget.spi,
|
||||
initCmd: 'iperf -c ${_hostCtrl.text} -p ${_portCtrl.text}',
|
||||
).go(context);
|
||||
|
||||
@@ -1,19 +1,11 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:toolbox/core/extension/context/common.dart';
|
||||
import 'package:toolbox/core/extension/context/dialog.dart';
|
||||
import 'package:toolbox/core/extension/context/locale.dart';
|
||||
import 'package:toolbox/core/extension/context/snackbar.dart';
|
||||
import 'package:toolbox/core/extension/uint8list.dart';
|
||||
import 'package:toolbox/core/utils/share.dart';
|
||||
import 'package:toolbox/data/res/provider.dart';
|
||||
|
||||
import '../../data/model/server/ping_result.dart';
|
||||
import '../../data/res/color.dart';
|
||||
import '../../data/res/ui.dart';
|
||||
import '../widget/input_field.dart';
|
||||
import '../widget/cardx.dart';
|
||||
|
||||
/// Only permit ipv4 / ipv6 / domain chars
|
||||
final targetReg = RegExp(r'[a-zA-Z0-9\.-_:]+');
|
||||
@@ -61,7 +53,7 @@ class _PingPageState extends State<PingPage>
|
||||
heroTag: 'ping',
|
||||
onPressed: () {
|
||||
context.showRoundDialog(
|
||||
title: Text(l10n.choose),
|
||||
title: l10n.choose,
|
||||
child: Input(
|
||||
autoFocus: true,
|
||||
controller: _textEditingController,
|
||||
@@ -84,11 +76,11 @@ class _PingPageState extends State<PingPage>
|
||||
await doPing();
|
||||
} catch (e) {
|
||||
context.showRoundDialog(
|
||||
title: Text(l10n.error),
|
||||
title: l10n.error,
|
||||
child: Text(e.toString()),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Shares.copy(e.toString()),
|
||||
onPressed: () => Pfs.copy(e.toString()),
|
||||
child: Text(l10n.copy),
|
||||
),
|
||||
],
|
||||
@@ -125,7 +117,7 @@ class _PingPageState extends State<PingPage>
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: primaryColor,
|
||||
color: UIs.primaryColor,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
@@ -136,7 +128,7 @@ class _PingPageState extends State<PingPage>
|
||||
'${l10n.pingAvg}${result.statistic?.avg?.toStringAsFixed(2) ?? l10n.unknown} $ms',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: primaryColor,
|
||||
color: UIs.primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,23 +1,15 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:computer/computer.dart';
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:toolbox/core/extension/context/common.dart';
|
||||
import 'package:toolbox/core/extension/context/dialog.dart';
|
||||
import 'package:toolbox/core/extension/context/locale.dart';
|
||||
import 'package:toolbox/core/extension/context/snackbar.dart';
|
||||
import 'package:toolbox/core/extension/numx.dart';
|
||||
import 'package:toolbox/core/utils/misc.dart';
|
||||
import 'package:toolbox/data/res/misc.dart';
|
||||
import 'package:toolbox/data/res/provider.dart';
|
||||
import 'package:toolbox/view/widget/input_field.dart';
|
||||
import 'package:toolbox/view/widget/val_builder.dart';
|
||||
|
||||
import '../../../core/utils/server.dart';
|
||||
import '../../../data/model/server/private_key_info.dart';
|
||||
import '../../../data/res/ui.dart';
|
||||
import '../../widget/appbar.dart';
|
||||
|
||||
const _format = 'text/plain';
|
||||
|
||||
@@ -91,7 +83,7 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage> {
|
||||
tooltip: l10n.delete,
|
||||
onPressed: () {
|
||||
context.showRoundDialog(
|
||||
title: Text(l10n.attention),
|
||||
title: l10n.attention,
|
||||
child: Text(l10n.askContinue(
|
||||
'${l10n.delete} ${l10n.privateKey}(${widget.pki!.id})',
|
||||
)),
|
||||
@@ -181,7 +173,7 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage> {
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
final path = await pickOneFile();
|
||||
final path = await Pfs.pickFilePath();
|
||||
if (path == null) return;
|
||||
|
||||
final file = File(path);
|
||||
|
||||
@@ -1,21 +1,15 @@
|
||||
import 'dart:io';
|
||||
import 'dart:async';
|
||||
import 'package:after_layout/after_layout.dart';
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:toolbox/core/extension/context/common.dart';
|
||||
import 'package:toolbox/core/extension/context/dialog.dart';
|
||||
import 'package:toolbox/core/extension/context/locale.dart';
|
||||
import 'package:toolbox/core/utils/platform/base.dart';
|
||||
import 'package:toolbox/core/utils/platform/path.dart';
|
||||
import 'package:toolbox/data/res/store.dart';
|
||||
|
||||
import '../../../core/route.dart';
|
||||
import '../../../data/model/server/private_key_info.dart';
|
||||
import '../../../data/provider/private_key.dart';
|
||||
import '../../../data/res/ui.dart';
|
||||
import '../../widget/appbar.dart';
|
||||
import '../../widget/cardx.dart';
|
||||
|
||||
class PrivateKeysListPage extends StatefulWidget {
|
||||
const PrivateKeysListPage({super.key});
|
||||
@@ -35,7 +29,7 @@ class _PrivateKeyListState extends State<PrivateKeysListPage>
|
||||
body: _buildBody(),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
child: const Icon(Icons.add),
|
||||
onPressed: () => AppRoute.keyEdit().go(context),
|
||||
onPressed: () => AppRoutes.keyEdit().go(context),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -64,7 +58,7 @@ class _PrivateKeyListState extends State<PrivateKeysListPage>
|
||||
),
|
||||
title: Text(item.id),
|
||||
subtitle: Text(item.type ?? l10n.unknown, style: UIs.textGrey),
|
||||
onTap: () => AppRoute.keyEdit(pki: item).go(context),
|
||||
onTap: () => AppRoutes.keyEdit(pki: item).go(context),
|
||||
trailing: const Icon(Icons.edit),
|
||||
),
|
||||
);
|
||||
@@ -77,22 +71,22 @@ class _PrivateKeyListState extends State<PrivateKeysListPage>
|
||||
void autoAddSystemPriavteKey() {
|
||||
// Only trigger on desktop platform and no private key saved
|
||||
if (isDesktop && Stores.snippet.box.keys.isEmpty) {
|
||||
final home = getHomeDir();
|
||||
final home = Pfs.homeDir;
|
||||
if (home == null) return;
|
||||
final idRsaFile = File(joinPath(home, '.ssh/id_rsa'));
|
||||
final idRsaFile = File(home.joinPath('.ssh/id_rsa'));
|
||||
if (!idRsaFile.existsSync()) return;
|
||||
final sysPk = PrivateKeyInfo(
|
||||
id: 'system',
|
||||
key: idRsaFile.readAsStringSync(),
|
||||
);
|
||||
context.showRoundDialog(
|
||||
title: Text(l10n.attention),
|
||||
title: l10n.attention,
|
||||
child: Text(l10n.addSystemPrivateKeyTip),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
context.pop();
|
||||
AppRoute.keyEdit(pki: sysPk).go(context);
|
||||
AppRoutes.keyEdit(pki: sysPk).go(context);
|
||||
},
|
||||
child: Text(l10n.ok),
|
||||
),
|
||||
|
||||
@@ -1,21 +1,14 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:dartssh2/dartssh2.dart';
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:toolbox/core/extension/context/common.dart';
|
||||
import 'package:toolbox/core/extension/context/dialog.dart';
|
||||
import 'package:toolbox/core/extension/context/locale.dart';
|
||||
import 'package:toolbox/core/extension/context/snackbar.dart';
|
||||
import 'package:toolbox/core/extension/uint8list.dart';
|
||||
import 'package:toolbox/core/utils/share.dart';
|
||||
import 'package:toolbox/data/res/store.dart';
|
||||
|
||||
import '../../data/model/app/shell_func.dart';
|
||||
import '../../data/model/server/proc.dart';
|
||||
import '../../data/model/server/server_private_info.dart';
|
||||
import '../../data/res/ui.dart';
|
||||
import '../widget/appbar.dart';
|
||||
import '../widget/cardx.dart';
|
||||
import '../widget/two_line_text.dart';
|
||||
|
||||
class ProcessPage extends StatefulWidget {
|
||||
@@ -107,11 +100,11 @@ class _ProcessPageState extends State<ProcessPage> {
|
||||
actions.add(IconButton(
|
||||
icon: const Icon(Icons.error),
|
||||
onPressed: () => context.showRoundDialog(
|
||||
title: Text(l10n.error),
|
||||
title: l10n.error,
|
||||
child: SingleChildScrollView(child: Text(_result.error!)),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Shares.copy(_result.error!),
|
||||
onPressed: () => Pfs.copy(_result.error!),
|
||||
child: Text(l10n.copy),
|
||||
),
|
||||
],
|
||||
@@ -160,7 +153,7 @@ class _ProcessPageState extends State<ProcessPage> {
|
||||
onTap: () => _lastFocusId = proc.pid,
|
||||
onLongPress: () {
|
||||
context.showRoundDialog(
|
||||
title: Text(l10n.attention),
|
||||
title: l10n.attention,
|
||||
child: Text(l10n.askContinue(
|
||||
'${l10n.stop} ${l10n.process}(${proc.pid})',
|
||||
)),
|
||||
|
||||
@@ -1,25 +1,14 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:toolbox/core/extension/context/common.dart';
|
||||
import 'package:toolbox/core/extension/context/dialog.dart';
|
||||
import 'package:toolbox/core/extension/context/locale.dart';
|
||||
import 'package:toolbox/core/extension/context/snackbar.dart';
|
||||
import 'package:toolbox/core/extension/numx.dart';
|
||||
import 'package:toolbox/core/extension/widget.dart';
|
||||
import 'package:toolbox/data/model/server/pve.dart';
|
||||
import 'package:toolbox/data/model/server/server_private_info.dart';
|
||||
import 'package:toolbox/data/provider/pve.dart';
|
||||
import 'package:toolbox/data/res/color.dart';
|
||||
import 'package:toolbox/data/res/store.dart';
|
||||
import 'package:toolbox/data/res/ui.dart';
|
||||
import 'package:toolbox/view/widget/appbar.dart';
|
||||
import 'package:toolbox/view/widget/icon_btn.dart';
|
||||
import 'package:toolbox/view/widget/kv_row.dart';
|
||||
import 'package:toolbox/view/widget/percent_circle.dart';
|
||||
import 'package:toolbox/view/widget/row.dart';
|
||||
import 'package:toolbox/view/widget/two_line_text.dart';
|
||||
import 'package:toolbox/view/widget/val_builder.dart';
|
||||
|
||||
final class PvePage extends StatefulWidget {
|
||||
final ServerPrivateInfo spi;
|
||||
@@ -156,7 +145,7 @@ final class _PvePageState extends State<PvePage> {
|
||||
}
|
||||
|
||||
Widget _buildNode(PveNode item) {
|
||||
final valueAnim = AlwaysStoppedAnimation(primaryColor);
|
||||
final valueAnim = AlwaysStoppedAnimation(UIs.primaryColor);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 13, horizontal: 13),
|
||||
child: Column(
|
||||
@@ -216,7 +205,7 @@ final class _PvePageState extends State<PvePage> {
|
||||
),
|
||||
],
|
||||
),
|
||||
).card;
|
||||
).cardx;
|
||||
}
|
||||
|
||||
Widget _buildQemu(PveQemu item) {
|
||||
@@ -224,7 +213,7 @@ final class _PvePageState extends State<PvePage> {
|
||||
return ListTile(
|
||||
title: Text(_wrapNodeName(item), style: UIs.text13Bold),
|
||||
trailing: _buildCtrlBtns(item),
|
||||
).card;
|
||||
).cardx;
|
||||
}
|
||||
final children = <Widget>[
|
||||
const SizedBox(height: 5),
|
||||
@@ -293,7 +282,7 @@ final class _PvePageState extends State<PvePage> {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: children,
|
||||
).card;
|
||||
).cardx;
|
||||
}
|
||||
|
||||
Widget _buildLxc(PveLxc item) {
|
||||
@@ -301,7 +290,7 @@ final class _PvePageState extends State<PvePage> {
|
||||
return ListTile(
|
||||
title: Text(_wrapNodeName(item), style: UIs.text13Bold),
|
||||
trailing: _buildCtrlBtns(item),
|
||||
).card;
|
||||
).cardx;
|
||||
}
|
||||
final children = <Widget>[
|
||||
const SizedBox(height: 5),
|
||||
@@ -370,7 +359,7 @@ final class _PvePageState extends State<PvePage> {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: children,
|
||||
).card;
|
||||
).cardx;
|
||||
}
|
||||
|
||||
Widget _buildStorage(PveStorage item) {
|
||||
@@ -392,14 +381,14 @@ final class _PvePageState extends State<PvePage> {
|
||||
KvRow(k: l10n.plugInType, v: item.plugintype),
|
||||
],
|
||||
),
|
||||
).card;
|
||||
).cardx;
|
||||
}
|
||||
|
||||
Widget _buildSdn(PveSdn item) {
|
||||
return ListTile(
|
||||
title: Text(_wrapNodeName(item)),
|
||||
trailing: Text(item.summary),
|
||||
).card;
|
||||
).cardx;
|
||||
}
|
||||
|
||||
Widget _buildCtrlBtns(PveCtrlIface item) {
|
||||
@@ -430,7 +419,7 @@ final class _PvePageState extends State<PvePage> {
|
||||
|
||||
void _onCtrl(PveCtrlFunc func, String action, PveCtrlIface item) async {
|
||||
final sure = await context.showRoundDialog<bool>(
|
||||
title: Text(l10n.attention),
|
||||
title: l10n.attention,
|
||||
child: Text(l10n.askContinue('$action ${item.id}')),
|
||||
actions: [
|
||||
TextButton(
|
||||
|
||||
@@ -109,7 +109,7 @@ Widget _buildLineChart(
|
||||
isCurved: curve,
|
||||
barWidth: 2,
|
||||
isStrokeCapRound: true,
|
||||
color: primaryColor,
|
||||
color: UIs.primaryColor,
|
||||
dotData: const FlDotData(show: false),
|
||||
belowBarData: BarAreaData(show: false),
|
||||
))
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import 'package:extended_image/extended_image.dart';
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
import 'package:icons_plus/icons_plus.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:toolbox/core/extension/context/common.dart';
|
||||
import 'package:toolbox/core/extension/context/dialog.dart';
|
||||
import 'package:toolbox/core/extension/context/locale.dart';
|
||||
import 'package:toolbox/core/extension/listx.dart';
|
||||
import 'package:toolbox/core/extension/stringx.dart';
|
||||
import 'package:toolbox/data/model/app/server_detail_card.dart';
|
||||
import 'package:toolbox/data/model/app/shell_func.dart';
|
||||
import 'package:toolbox/data/model/server/battery.dart';
|
||||
@@ -21,20 +18,11 @@ import 'package:toolbox/data/model/server/sensors.dart';
|
||||
import 'package:toolbox/data/model/server/server_private_info.dart';
|
||||
import 'package:toolbox/data/model/server/system.dart';
|
||||
import 'package:toolbox/data/res/store.dart';
|
||||
import 'package:toolbox/view/widget/expand_tile.dart';
|
||||
import 'package:toolbox/view/widget/kv_row.dart';
|
||||
import 'package:toolbox/view/widget/markdown.dart';
|
||||
import 'package:toolbox/view/widget/server_func_btns.dart';
|
||||
import 'package:toolbox/view/widget/val_builder.dart';
|
||||
|
||||
import '../../../../core/extension/numx.dart';
|
||||
import '../../../../core/route.dart';
|
||||
import '../../../../data/model/server/server.dart';
|
||||
import '../../../../data/provider/server.dart';
|
||||
import '../../../../data/res/color.dart';
|
||||
import '../../../../data/res/ui.dart';
|
||||
import '../../../widget/appbar.dart';
|
||||
import '../../../widget/cardx.dart';
|
||||
|
||||
part 'misc.dart';
|
||||
|
||||
@@ -139,7 +127,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit),
|
||||
onPressed: () async {
|
||||
final delete = await AppRoute.serverEdit(spi: si.spi).go(context);
|
||||
final delete = await AppRoutes.serverEdit(spi: si.spi).go(context);
|
||||
if (delete == true) {
|
||||
context.pop();
|
||||
}
|
||||
@@ -322,8 +310,8 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
||||
return LinearProgressIndicator(
|
||||
value: percentWithinOne,
|
||||
minHeight: 7,
|
||||
backgroundColor: DynamicColors.progress.resolve(context),
|
||||
color: primaryColor,
|
||||
backgroundColor: UIs.halfAlpha,
|
||||
color: UIs.primaryColor,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -452,7 +440,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
||||
return processes.length * 47.0;
|
||||
}();
|
||||
context.showRoundDialog(
|
||||
title: Text(item.name),
|
||||
title: item.name,
|
||||
child: SizedBox(
|
||||
width: double.maxFinite,
|
||||
height: height,
|
||||
@@ -494,10 +482,8 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
||||
trailing: InkWell(
|
||||
onTap: () {
|
||||
context.showRoundDialog(
|
||||
title: SizedBox(
|
||||
width: 377,
|
||||
child: Text('${process.pid}', maxLines: 1),
|
||||
),
|
||||
title: '${process.pid}',
|
||||
titleMaxLines: 1,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -573,8 +559,8 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
||||
CircularProgressIndicator(
|
||||
value: disk.usedPercent / 100,
|
||||
strokeWidth: 5,
|
||||
backgroundColor: DynamicColors.progress.resolve(context),
|
||||
color: primaryColor,
|
||||
backgroundColor: UIs.halfAlpha,
|
||||
color: UIs.primaryColor,
|
||||
),
|
||||
Text('${disk.usedPercent}%', style: UIs.text12Grey)
|
||||
],
|
||||
@@ -770,7 +756,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
context.showRoundDialog(
|
||||
title: Text(si.device),
|
||||
title: si.device,
|
||||
child: SingleChildScrollView(
|
||||
child: SimpleMarkdown(
|
||||
data: si.toMarkdown,
|
||||
@@ -819,7 +805,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
||||
subtitle: Text(addr, style: UIs.textGrey),
|
||||
leading: const Icon(FontAwesome.server_solid, size: 17),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () => AppRoute.pve(spi: widget.spi).go(context),
|
||||
onTap: () => AppRoutes.pve(spi: widget.spi).go(context),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -847,7 +833,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
context.showRoundDialog(
|
||||
title: Text(cmd.key),
|
||||
title: cmd.key,
|
||||
child: SingleChildScrollView(
|
||||
child: Text(cmd.value, style: UIs.text13Grey),
|
||||
),
|
||||
|
||||
@@ -1,29 +1,19 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:icons_plus/icons_plus.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:toolbox/core/extension/context/common.dart';
|
||||
import 'package:toolbox/core/extension/context/dialog.dart';
|
||||
import 'package:toolbox/core/extension/context/locale.dart';
|
||||
import 'package:toolbox/core/extension/context/snackbar.dart';
|
||||
import 'package:toolbox/core/extension/stringx.dart';
|
||||
import 'package:toolbox/core/extension/widget.dart';
|
||||
import 'package:toolbox/core/utils/ui.dart';
|
||||
import 'package:toolbox/data/model/app/shell_func.dart';
|
||||
import 'package:toolbox/data/model/server/custom.dart';
|
||||
import 'package:toolbox/data/model/server/wol_cfg.dart';
|
||||
import 'package:toolbox/data/res/provider.dart';
|
||||
import 'package:toolbox/view/widget/expand_tile.dart';
|
||||
|
||||
import '../../../core/route.dart';
|
||||
import '../../../data/model/server/server_private_info.dart';
|
||||
import '../../../data/provider/private_key.dart';
|
||||
import '../../../data/res/ui.dart';
|
||||
import '../../widget/appbar.dart';
|
||||
import '../../widget/input_field.dart';
|
||||
import '../../widget/cardx.dart';
|
||||
import '../../widget/tag.dart';
|
||||
|
||||
class ServerEditPage extends StatefulWidget {
|
||||
const ServerEditPage({super.key, this.spi});
|
||||
@@ -163,7 +153,7 @@ class _ServerEditPageState extends State<ServerEditPage> {
|
||||
onPressed: () {
|
||||
var delScripts = false;
|
||||
context.showRoundDialog(
|
||||
title: Text(l10n.attention),
|
||||
title: l10n.attention,
|
||||
child: StatefulBuilder(builder: (ctx, setState) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -224,7 +214,7 @@ class _ServerEditPageState extends State<ServerEditPage> {
|
||||
icon: BoxIcons.bx_rename,
|
||||
obscureText: false,
|
||||
autoCorrect: true,
|
||||
suggestiion: true,
|
||||
suggestion: true,
|
||||
),
|
||||
Input(
|
||||
controller: _ipController,
|
||||
@@ -266,6 +256,9 @@ class _ServerEditPageState extends State<ServerEditPage> {
|
||||
onChanged: (p0) => _tags = p0,
|
||||
allTags: [...Pros.server.tags.value],
|
||||
onRenameTag: Pros.server.renameTag,
|
||||
renameL10n: l10n.rename,
|
||||
tagL10n: l10n.tag,
|
||||
addL10n: l10n.add,
|
||||
),
|
||||
ListTile(
|
||||
title: Text(l10n.autoConnect),
|
||||
@@ -367,7 +360,7 @@ class _ServerEditPageState extends State<ServerEditPage> {
|
||||
padding: EdgeInsets.only(right: 13),
|
||||
child: Icon(Icons.add),
|
||||
),
|
||||
onTap: () => AppRoute.keyEdit().go(context),
|
||||
onTap: () => AppRoutes.keyEdit().go(context),
|
||||
),
|
||||
);
|
||||
return CardX(
|
||||
@@ -440,7 +433,7 @@ class _ServerEditPageState extends State<ServerEditPage> {
|
||||
},
|
||||
),
|
||||
),
|
||||
).card,
|
||||
).cardx,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -464,7 +457,7 @@ class _ServerEditPageState extends State<ServerEditPage> {
|
||||
title: Text(l10n.doc),
|
||||
trailing: const Icon(Icons.open_in_new, size: 17),
|
||||
onTap: () => openUrl(l10n.customCmdDocUrl),
|
||||
).card,
|
||||
).cardx,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -479,7 +472,7 @@ class _ServerEditPageState extends State<ServerEditPage> {
|
||||
),
|
||||
title: Text(l10n.about),
|
||||
subtitle: Text(l10n.wolTip, style: UIs.text12Grey),
|
||||
).card,
|
||||
).cardx,
|
||||
Input(
|
||||
controller: _wolMacCtrl,
|
||||
type: TextInputType.text,
|
||||
@@ -564,7 +557,7 @@ class _ServerEditPageState extends State<ServerEditPage> {
|
||||
}
|
||||
if (_keyIdx.value == null && _passwordController.text.isEmpty) {
|
||||
final cancel = await context.showRoundDialog<bool>(
|
||||
title: Text(l10n.attention),
|
||||
title: l10n.attention,
|
||||
child: Text(l10n.askContinue(l10n.useNoPwd)),
|
||||
actions: [
|
||||
TextButton(
|
||||
|
||||
@@ -2,26 +2,16 @@ import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:after_layout/after_layout.dart';
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:icons_plus/icons_plus.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:toolbox/core/extension/context/common.dart';
|
||||
import 'package:toolbox/core/extension/context/dialog.dart';
|
||||
import 'package:toolbox/core/extension/context/locale.dart';
|
||||
import 'package:toolbox/core/extension/listx.dart';
|
||||
import 'package:toolbox/core/extension/media_queryx.dart';
|
||||
import 'package:toolbox/core/extension/numx.dart';
|
||||
import 'package:toolbox/core/extension/ssh_client.dart';
|
||||
import 'package:toolbox/core/utils/share.dart';
|
||||
import 'package:toolbox/data/model/app/shell_func.dart';
|
||||
import 'package:toolbox/data/model/server/try_limiter.dart';
|
||||
import 'package:toolbox/data/res/color.dart';
|
||||
import 'package:toolbox/data/res/provider.dart';
|
||||
import 'package:toolbox/data/res/store.dart';
|
||||
import 'package:toolbox/view/widget/auto_hide.dart';
|
||||
import 'package:toolbox/view/widget/icon_text_btn.dart';
|
||||
import 'package:toolbox/view/widget/markdown.dart';
|
||||
import 'package:toolbox/view/widget/percent_circle.dart';
|
||||
|
||||
import '../../../core/route.dart';
|
||||
@@ -29,10 +19,7 @@ import '../../../data/model/app/net_view.dart';
|
||||
import '../../../data/model/server/server.dart';
|
||||
import '../../../data/model/server/server_private_info.dart';
|
||||
import '../../../data/provider/server.dart';
|
||||
import '../../../data/res/ui.dart';
|
||||
import '../../widget/cardx.dart';
|
||||
import '../../widget/server_func_btns.dart';
|
||||
import '../../widget/tag.dart';
|
||||
|
||||
class ServerPage extends StatefulWidget {
|
||||
const ServerPage({super.key});
|
||||
@@ -79,7 +66,7 @@ class _ServerPageState extends State<ServerPage>
|
||||
_media = MediaQuery.of(context);
|
||||
_updateOffset();
|
||||
_updateTextScaler();
|
||||
_useDoubleColumn = _media.useDoubleColumn &&
|
||||
_useDoubleColumn = _media.size.width > 639 &&
|
||||
Stores.setting.doubleColumnServersPage.fetch();
|
||||
}
|
||||
|
||||
@@ -115,7 +102,7 @@ class _ServerPageState extends State<ServerPage>
|
||||
controller: _scrollController,
|
||||
child: FloatingActionButton(
|
||||
heroTag: 'addServer',
|
||||
onPressed: () => AppRoute.serverEdit().go(context),
|
||||
onPressed: () => AppRoutes.serverEdit().go(context),
|
||||
tooltip: l10n.addAServer,
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
@@ -137,7 +124,7 @@ class _ServerPageState extends State<ServerPage>
|
||||
top: 0,
|
||||
left: 0,
|
||||
child: IconButton(
|
||||
onPressed: () => AppRoute.settings().go(context),
|
||||
onPressed: () => AppRoutes.settings().go(context),
|
||||
icon: const Icon(Icons.settings, color: Colors.grey),
|
||||
),
|
||||
),
|
||||
@@ -227,6 +214,7 @@ class _ServerPageState extends State<ServerPage>
|
||||
_tag = p0;
|
||||
}),
|
||||
initTag: _tag,
|
||||
allL10n: l10n.all,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -285,9 +273,9 @@ class _ServerPageState extends State<ServerPage>
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
if (srv.canViewDetails) {
|
||||
AppRoute.serverDetail(spi: srv.spi).go(context);
|
||||
AppRoutes.serverDetail(spi: srv.spi).go(context);
|
||||
} else {
|
||||
AppRoute.serverEdit(spi: srv.spi).go(context);
|
||||
AppRoutes.serverEdit(spi: srv.spi).go(context);
|
||||
}
|
||||
},
|
||||
onLongPress: () {
|
||||
@@ -298,7 +286,7 @@ class _ServerPageState extends State<ServerPage>
|
||||
flip: !cardStatus.value.flip,
|
||||
);
|
||||
} else {
|
||||
AppRoute.serverEdit(spi: srv.spi).go(context);
|
||||
AppRoutes.serverEdit(spi: srv.spi).go(context);
|
||||
}
|
||||
},
|
||||
child: Padding(
|
||||
@@ -343,7 +331,7 @@ class _ServerPageState extends State<ServerPage>
|
||||
height: _calcCardHeight(srv.conn, cardStatus.value.flip),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: children,
|
||||
),
|
||||
@@ -363,7 +351,7 @@ class _ServerPageState extends State<ServerPage>
|
||||
func: () async {
|
||||
if (Stores.setting.showSuspendTip.fetch()) {
|
||||
await context.showRoundDialog(
|
||||
title: Text(l10n.attention),
|
||||
title: l10n.attention,
|
||||
child: Text(l10n.suspendTip),
|
||||
);
|
||||
Stores.setting.showSuspendTip.put(false);
|
||||
@@ -407,7 +395,7 @@ class _ServerPageState extends State<ServerPage>
|
||||
text: l10n.reboot,
|
||||
),
|
||||
IconTextBtn(
|
||||
onPressed: () => AppRoute.serverEdit(spi: srv.spi).go(context),
|
||||
onPressed: () => AppRoutes.serverEdit(spi: srv.spi).go(context),
|
||||
icon: Icons.edit,
|
||||
text: l10n.edit,
|
||||
)
|
||||
@@ -482,7 +470,7 @@ class _ServerPageState extends State<ServerPage>
|
||||
height: 19,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 3,
|
||||
valueColor: AlwaysStoppedAnimation(primaryColor),
|
||||
valueColor: AlwaysStoppedAnimation(UIs.primaryColor),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -549,11 +537,11 @@ ${ss.err?.solution ?? l10n.unknown}
|
||||
${ss.err?.message ?? l10n.unknownError}
|
||||
''';
|
||||
context.showRoundDialog(
|
||||
title: Text(l10n.error),
|
||||
title: l10n.error,
|
||||
child: SingleChildScrollView(child: SimpleMarkdown(data: md)),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Shares.copy(md),
|
||||
onPressed: () => Pfs.copy(md),
|
||||
child: Text(l10n.copy),
|
||||
)
|
||||
],
|
||||
@@ -649,7 +637,6 @@ ${ss.err?.message ?? l10n.unknownError}
|
||||
|
||||
@override
|
||||
Future<void> afterFirstLayout(BuildContext context) async {
|
||||
await GetIt.I.allReady();
|
||||
await Pros.server.load();
|
||||
Pros.server.startAutoRefresh();
|
||||
}
|
||||
@@ -682,7 +669,7 @@ ${ss.err?.message ?? l10n.unknownError}
|
||||
required String name,
|
||||
}) {
|
||||
context.showRoundDialog(
|
||||
title: Text(l10n.attention),
|
||||
title: l10n.attention,
|
||||
child: Text(l10n.askContinue('$typ ${l10n.server}($name)')),
|
||||
actions: [
|
||||
TextButton(
|
||||
|
||||
@@ -1,42 +1,22 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_highlight/theme_map.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:icons_plus/icons_plus.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:toolbox/core/build_mode.dart';
|
||||
import 'package:toolbox/core/extension/colorx.dart';
|
||||
import 'package:toolbox/core/extension/context/common.dart';
|
||||
import 'package:toolbox/core/extension/context/locale.dart';
|
||||
import 'package:toolbox/core/extension/context/snackbar.dart';
|
||||
import 'package:toolbox/core/extension/locale.dart';
|
||||
import 'package:toolbox/core/extension/context/dialog.dart';
|
||||
import 'package:toolbox/core/extension/stringx.dart';
|
||||
import 'package:toolbox/core/utils/function.dart';
|
||||
import 'package:toolbox/core/utils/platform/base.dart';
|
||||
import 'package:toolbox/data/res/provider.dart';
|
||||
import 'package:toolbox/data/res/rebuild.dart';
|
||||
import 'package:toolbox/data/res/store.dart';
|
||||
import 'package:toolbox/view/widget/expand_tile.dart';
|
||||
import 'package:toolbox/view/widget/markdown.dart';
|
||||
import 'package:toolbox/view/widget/val_builder.dart';
|
||||
import 'package:toolbox/data/res/url.dart';
|
||||
|
||||
import '../../../core/persistant_store.dart';
|
||||
import '../../../core/route.dart';
|
||||
import '../../../core/utils/misc.dart';
|
||||
import '../../../core/update.dart';
|
||||
import '../../../data/model/app/net_view.dart';
|
||||
import '../../../data/provider/app.dart';
|
||||
import '../../../data/res/build_data.dart';
|
||||
import '../../../data/res/color.dart';
|
||||
import '../../../data/res/path.dart';
|
||||
import '../../../data/res/ui.dart';
|
||||
import '../../widget/color_picker.dart';
|
||||
import '../../widget/appbar.dart';
|
||||
import '../../widget/input_field.dart';
|
||||
import '../../widget/cardx.dart';
|
||||
import '../../widget/store_switch.dart';
|
||||
|
||||
const _kIconSize = 23.0;
|
||||
|
||||
@@ -59,7 +39,7 @@ class _SettingPageState extends State<SettingPage> {
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete),
|
||||
onPressed: () => context.showRoundDialog(
|
||||
title: Text(l10n.attention),
|
||||
title: l10n.attention,
|
||||
child: SimpleMarkdown(
|
||||
data: l10n.askContinue(
|
||||
'${l10n.delete} **${l10n.all}** ${l10n.setting}',
|
||||
@@ -220,7 +200,13 @@ class _SettingPageState extends State<SettingPage> {
|
||||
},
|
||||
),
|
||||
onTap: () => Funcs.throttle(
|
||||
() => doUpdate(context, force: BuildMode.isDebug),
|
||||
() => AppUpdateIface.doUpdate(
|
||||
context: context,
|
||||
build: BuildData.build,
|
||||
url: Urls.updateCfg,
|
||||
force: BuildMode.isDebug,
|
||||
updateL10n: l10n.update,
|
||||
),
|
||||
),
|
||||
trailing: StoreSwitch(prop: _setting.autoCheckAppUpdate),
|
||||
);
|
||||
@@ -237,6 +223,7 @@ class _SettingPageState extends State<SettingPage> {
|
||||
),
|
||||
onTap: () async {
|
||||
final val = await context.showPickSingleDialog(
|
||||
title: l10n.setting,
|
||||
items: List.generate(10, (idx) => idx == 1 ? null : idx),
|
||||
initial: _setting.serverStatusUpdateInterval.fetch(),
|
||||
name: (p0) => p0 == 0 ? l10n.manual : '$p0 ${l10n.second}',
|
||||
@@ -260,12 +247,12 @@ class _SettingPageState extends State<SettingPage> {
|
||||
leading: const Icon(Icons.colorize),
|
||||
title: Text(l10n.primaryColorSeed),
|
||||
trailing: ClipOval(
|
||||
child: Container(color: primaryColor, height: 27, width: 27),
|
||||
child: Container(color: UIs.primaryColor, height: 27, width: 27),
|
||||
),
|
||||
onTap: () async {
|
||||
final ctrl = TextEditingController(text: primaryColor.toHex);
|
||||
final ctrl = TextEditingController(text: UIs.primaryColor.toHex);
|
||||
await context.showRoundDialog(
|
||||
title: Text(l10n.primaryColorSeed),
|
||||
title: l10n.primaryColorSeed,
|
||||
child: StatefulBuilder(builder: (context, setState) {
|
||||
final children = <Widget>[
|
||||
/// Plugin [dynamic_color] is not supported on iOS
|
||||
@@ -316,7 +303,7 @@ class _SettingPageState extends State<SettingPage> {
|
||||
}
|
||||
// Change [primaryColor] first, then change [_selectedColorValue],
|
||||
// So the [ValueBuilder] will be triggered with the new value
|
||||
primaryColor = color;
|
||||
UIs.colorSeed = color;
|
||||
_setting.primaryColor.put(color.value);
|
||||
context.pop();
|
||||
context.pop();
|
||||
@@ -376,6 +363,7 @@ class _SettingPageState extends State<SettingPage> {
|
||||
style: UIs.textGrey),
|
||||
onTap: () async {
|
||||
final selected = await context.showPickSingleDialog(
|
||||
title: l10n.maxRetryCount,
|
||||
items: List.generate(10, (index) => index),
|
||||
name: (p0) => '$p0 ${l10n.times}',
|
||||
initial: val,
|
||||
@@ -400,6 +388,7 @@ class _SettingPageState extends State<SettingPage> {
|
||||
title: Text(l10n.themeMode),
|
||||
onTap: () async {
|
||||
final selected = await context.showPickSingleDialog(
|
||||
title: l10n.themeMode,
|
||||
items: List.generate(len + 2, (index) => index),
|
||||
name: (p0) => _buildThemeModeStr(p0),
|
||||
initial: _setting.themeMode.fetch(),
|
||||
@@ -445,7 +434,7 @@ class _SettingPageState extends State<SettingPage> {
|
||||
),
|
||||
onTap: () {
|
||||
context.showRoundDialog(
|
||||
title: Text(l10n.font),
|
||||
title: l10n.font,
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () async => await _pickFontFile(),
|
||||
@@ -466,23 +455,22 @@ class _SettingPageState extends State<SettingPage> {
|
||||
}
|
||||
|
||||
Future<void> _pickFontFile() async {
|
||||
final path = await pickOneFile();
|
||||
if (path != null) {
|
||||
// iOS can't copy file to app dir, so we need to use the original path
|
||||
if (isIOS) {
|
||||
_setting.fontPath.put(path);
|
||||
} else {
|
||||
final fontFile = File(path);
|
||||
final newPath = '${await Paths.font}/${path.split('/').last}';
|
||||
await fontFile.copy(newPath);
|
||||
_setting.fontPath.put(newPath);
|
||||
}
|
||||
|
||||
context.pop();
|
||||
RebuildNodes.app.rebuild();
|
||||
return;
|
||||
final path = await Pfs.pickFilePath();
|
||||
if (path == null) return;
|
||||
|
||||
// iOS can't copy file to app dir, so we need to use the original path
|
||||
if (isIOS) {
|
||||
_setting.fontPath.put(path);
|
||||
} else {
|
||||
final fontFile = File(path);
|
||||
final newPath = '${Paths.fontPath}/${path.split('/').last}';
|
||||
await fontFile.copy(newPath);
|
||||
_setting.fontPath.put(newPath);
|
||||
}
|
||||
context.showSnackBar(l10n.failed);
|
||||
|
||||
context.pop();
|
||||
RebuildNodes.app.rebuild();
|
||||
return;
|
||||
}
|
||||
|
||||
Widget _buildTermFontSize() {
|
||||
@@ -544,6 +532,7 @@ class _SettingPageState extends State<SettingPage> {
|
||||
title: Text(l10n.language),
|
||||
onTap: () async {
|
||||
final selected = await context.showPickSingleDialog(
|
||||
title: l10n.language,
|
||||
items: S.supportedLocales,
|
||||
name: (p0) => p0.code,
|
||||
initial: _setting.locale.fetch().toLocale,
|
||||
@@ -583,6 +572,7 @@ class _SettingPageState extends State<SettingPage> {
|
||||
),
|
||||
onTap: () async {
|
||||
final selected = await context.showPickSingleDialog(
|
||||
title: l10n.theme,
|
||||
items: themeMap.keys.toList(),
|
||||
name: (p0) => p0,
|
||||
initial: _setting.editorTheme.fetch(),
|
||||
@@ -604,6 +594,7 @@ class _SettingPageState extends State<SettingPage> {
|
||||
),
|
||||
onTap: () async {
|
||||
final selected = await context.showPickSingleDialog(
|
||||
title: l10n.theme,
|
||||
items: themeMap.keys.toList(),
|
||||
name: (p0) => p0,
|
||||
initial: _setting.editorDarkTheme.fetch(),
|
||||
@@ -687,7 +678,7 @@ class _SettingPageState extends State<SettingPage> {
|
||||
leading: const Icon(BoxIcons.bxs_keyboard),
|
||||
title: Text(l10n.editVirtKeys),
|
||||
trailing: const Icon(Icons.keyboard_arrow_right),
|
||||
onTap: () => AppRoute.sshVirtKeySetting().go(context),
|
||||
onTap: () => AppRoutes.sshVirtKeySetting().go(context),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -731,6 +722,7 @@ class _SettingPageState extends State<SettingPage> {
|
||||
),
|
||||
onTap: () async {
|
||||
final selected = await context.showPickSingleDialog(
|
||||
title: l10n.netViewType,
|
||||
items: NetViewType.values,
|
||||
name: (p0) => p0.toStr,
|
||||
initial: _setting.netViewType.fetch(),
|
||||
@@ -749,7 +741,7 @@ class _SettingPageState extends State<SettingPage> {
|
||||
trailing: const Icon(Icons.keyboard_arrow_right),
|
||||
onTap: () async {
|
||||
context.showRoundDialog<List<String>>(
|
||||
title: Text(l10n.choose),
|
||||
title: l10n.choose,
|
||||
child: SingleChildScrollView(
|
||||
child: StatefulBuilder(builder: (ctx, setState) {
|
||||
final keys = Stores.server.box.keys.toList();
|
||||
@@ -761,7 +753,7 @@ class _SettingPageState extends State<SettingPage> {
|
||||
title: Text(name ?? e),
|
||||
subtitle: name != null ? Text(e) : null,
|
||||
onTap: () => context.showRoundDialog(
|
||||
title: Text(l10n.attention),
|
||||
title: l10n.attention,
|
||||
child: Text(l10n.askContinue(
|
||||
'${l10n.delete} ${l10n.server}($e)',
|
||||
)),
|
||||
@@ -806,7 +798,7 @@ class _SettingPageState extends State<SettingPage> {
|
||||
),
|
||||
),
|
||||
onTap: () => context.showRoundDialog(
|
||||
title: Text(l10n.textScaler),
|
||||
title: l10n.textScaler,
|
||||
child: Input(
|
||||
autoFocus: true,
|
||||
type: TextInputType.number,
|
||||
@@ -859,7 +851,7 @@ class _SettingPageState extends State<SettingPage> {
|
||||
return ListTile(
|
||||
title: Text(l10n.sequence),
|
||||
trailing: const Icon(Icons.keyboard_arrow_right),
|
||||
onTap: () => AppRoute.serverFuncBtnsOrder().go(context),
|
||||
onTap: () => AppRoutes.serverFuncBtnsOrder().go(context),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -868,7 +860,7 @@ class _SettingPageState extends State<SettingPage> {
|
||||
leading: const Icon(OctIcons.sort_desc, size: _kIconSize),
|
||||
title: Text(l10n.serverOrder),
|
||||
trailing: const Icon(Icons.keyboard_arrow_right),
|
||||
onTap: () => AppRoute.serverOrder().go(context),
|
||||
onTap: () => AppRoutes.serverOrder().go(context),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -877,7 +869,7 @@ class _SettingPageState extends State<SettingPage> {
|
||||
leading: const Icon(OctIcons.sort_desc, size: _kIconSize),
|
||||
title: Text(l10n.serverDetailOrder),
|
||||
trailing: const Icon(Icons.keyboard_arrow_right),
|
||||
onTap: () => AppRoute.serverDetailOrder().go(context),
|
||||
onTap: () => AppRoutes.serverDetailOrder().go(context),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -903,7 +895,7 @@ class _SettingPageState extends State<SettingPage> {
|
||||
final fontSize = double.tryParse(ctrller.text);
|
||||
if (fontSize == null) {
|
||||
context.showRoundDialog(
|
||||
title: Text(l10n.failed),
|
||||
title: l10n.failed,
|
||||
child: Text('Parsed failed: ${ctrller.text}'),
|
||||
);
|
||||
return;
|
||||
@@ -912,7 +904,7 @@ class _SettingPageState extends State<SettingPage> {
|
||||
}
|
||||
|
||||
context.showRoundDialog(
|
||||
title: Text(l10n.fontSize),
|
||||
title: l10n.fontSize,
|
||||
child: Input(
|
||||
controller: ctrller,
|
||||
autoFocus: true,
|
||||
@@ -947,15 +939,15 @@ class _SettingPageState extends State<SettingPage> {
|
||||
}
|
||||
|
||||
Widget? _buildPlatformSetting() {
|
||||
final func = switch (OS.type) {
|
||||
OS.android => AppRoute.androidSettings().go,
|
||||
OS.ios => AppRoute.iosSettings().go,
|
||||
final func = switch (Pfs.type) {
|
||||
Pfs.android => AppRoutes.androidSettings().go,
|
||||
Pfs.ios => AppRoutes.iosSettings().go,
|
||||
_ => null,
|
||||
};
|
||||
if (func == null) return null;
|
||||
return ListTile(
|
||||
leading: const Icon(Icons.phone_android),
|
||||
title: Text('${OS.type} ${l10n.setting}'),
|
||||
title: Text('${Pfs.type} ${l10n.setting}'),
|
||||
trailing: const Icon(Icons.keyboard_arrow_right),
|
||||
onTap: () => func(context),
|
||||
);
|
||||
@@ -1061,6 +1053,7 @@ class _SettingPageState extends State<SettingPage> {
|
||||
),
|
||||
onTap: () async {
|
||||
final selected = await context.showPickSingleDialog(
|
||||
title: l10n.theme,
|
||||
items: List.generate(3, (index) => index),
|
||||
name: (p0) => index2Str(p0),
|
||||
initial: _setting.termTheme.fetch(),
|
||||
@@ -1153,7 +1146,7 @@ class _SettingPageState extends State<SettingPage> {
|
||||
void onSave(String url) {
|
||||
if (url.isEmpty || !url.startsWith('http')) {
|
||||
context.showRoundDialog(
|
||||
title: Text(l10n.failed),
|
||||
title: l10n.failed,
|
||||
child: Text('${l10n.invalid} URL'),
|
||||
actions: [
|
||||
TextButton(
|
||||
@@ -1169,14 +1162,15 @@ class _SettingPageState extends State<SettingPage> {
|
||||
}
|
||||
|
||||
return ListTile(
|
||||
leading: const Icon(Icons.image),
|
||||
title: Text('Logo ${l10n.addr}'),
|
||||
subtitle: SimpleMarkdown(data: '${l10n.view} ${l10n.doc}'),
|
||||
subtitle: SimpleMarkdown(data: '[${l10n.doc}](${Urls.appWiki})'),
|
||||
trailing: const Icon(Icons.keyboard_arrow_right),
|
||||
onTap: () {
|
||||
final ctrl =
|
||||
TextEditingController(text: _setting.serverLogoUrl.fetch());
|
||||
context.showRoundDialog(
|
||||
title: Text('Logo ${l10n.addr}'),
|
||||
title: 'Logo ${l10n.addr}',
|
||||
child: Input(
|
||||
controller: ctrl,
|
||||
autoFocus: true,
|
||||
|
||||
@@ -1,19 +1,11 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:toolbox/core/extension/context/common.dart';
|
||||
import 'package:toolbox/core/extension/context/dialog.dart';
|
||||
import 'package:toolbox/core/extension/context/locale.dart';
|
||||
import 'package:toolbox/core/extension/context/snackbar.dart';
|
||||
import 'package:toolbox/core/utils/platform/auth.dart';
|
||||
import 'package:toolbox/data/res/store.dart';
|
||||
import 'package:toolbox/data/res/ui.dart';
|
||||
import 'package:toolbox/view/page/setting/platform/platform_pub.dart';
|
||||
import 'package:toolbox/view/widget/appbar.dart';
|
||||
import 'package:toolbox/view/widget/input_field.dart';
|
||||
import 'package:toolbox/view/widget/cardx.dart';
|
||||
import 'package:toolbox/view/widget/store_switch.dart';
|
||||
|
||||
class AndroidSettingsPage extends StatefulWidget {
|
||||
const AndroidSettingsPage({super.key});
|
||||
@@ -88,7 +80,7 @@ class _AndroidSettingsPageState extends State<AndroidSettingsPage> {
|
||||
});
|
||||
final ctrl = TextEditingController(text: json.encode(data));
|
||||
context.showRoundDialog(
|
||||
title: Text(l10n.homeWidgetUrlConfig),
|
||||
title: l10n.homeWidgetUrlConfig,
|
||||
child: Input(
|
||||
autoFocus: true,
|
||||
controller: ctrl,
|
||||
|
||||
@@ -1,22 +1,13 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:toolbox/core/extension/context/dialog.dart';
|
||||
import 'package:toolbox/core/extension/context/locale.dart';
|
||||
import 'package:toolbox/core/extension/context/snackbar.dart';
|
||||
import 'package:toolbox/core/route.dart';
|
||||
import 'package:toolbox/core/utils/misc.dart';
|
||||
import 'package:toolbox/core/utils/platform/auth.dart';
|
||||
import 'package:toolbox/core/utils/share.dart';
|
||||
import 'package:toolbox/data/res/logger.dart';
|
||||
import 'package:toolbox/data/res/misc.dart';
|
||||
import 'package:toolbox/data/res/store.dart';
|
||||
import 'package:toolbox/data/res/ui.dart';
|
||||
import 'package:toolbox/view/page/setting/platform/platform_pub.dart';
|
||||
import 'package:toolbox/view/widget/appbar.dart';
|
||||
import 'package:toolbox/view/widget/future_widget.dart';
|
||||
import 'package:toolbox/view/widget/cardx.dart';
|
||||
import 'package:toolbox/view/widget/store_switch.dart';
|
||||
import 'package:watch_connectivity/watch_connectivity.dart';
|
||||
|
||||
class IOSSettingsPage extends StatefulWidget {
|
||||
@@ -59,7 +50,7 @@ class _IOSSettingsPageState extends State<IOSSettingsPage> {
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: () {
|
||||
if (_pushToken.value != null) {
|
||||
Shares.copy(_pushToken.value!);
|
||||
Pfs.copy(_pushToken.value!);
|
||||
context.showSnackBar(l10n.success);
|
||||
} else {
|
||||
context.showSnackBar(l10n.getPushTokenFailed);
|
||||
@@ -126,7 +117,7 @@ class _IOSSettingsPageState extends State<IOSSettingsPage> {
|
||||
void _onTapWatchApp(Map<String, dynamic> map) async {
|
||||
/// Encode [map] to String with indent `\t`
|
||||
final text = Miscs.jsonEncoder.convert(map);
|
||||
final result = await AppRoute.editor(
|
||||
final result = await AppRoutes.editor(
|
||||
text: text,
|
||||
langCode: 'json',
|
||||
title: 'Watch app',
|
||||
@@ -139,7 +130,7 @@ class _IOSSettingsPageState extends State<IOSSettingsPage> {
|
||||
await wc.updateApplicationContext(newCtx);
|
||||
} catch (e, trace) {
|
||||
context.showRoundDialog(
|
||||
title: Text(l10n.error),
|
||||
title: l10n.error,
|
||||
child: Text('${l10n.save}:\n$e'),
|
||||
);
|
||||
Loggers.app.warning('Update watch config failed', e, trace);
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:toolbox/core/extension/context/locale.dart';
|
||||
import 'package:toolbox/core/utils/platform/auth.dart';
|
||||
import 'package:toolbox/data/res/store.dart';
|
||||
import 'package:toolbox/data/res/ui.dart';
|
||||
import 'package:toolbox/view/widget/future_widget.dart';
|
||||
import 'package:toolbox/view/widget/store_switch.dart';
|
||||
|
||||
abstract final class PlatformPublicSettings {
|
||||
static Widget buildBioAuth() {
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:toolbox/core/extension/context/locale.dart';
|
||||
import 'package:toolbox/core/extension/context/snackbar.dart';
|
||||
import 'package:toolbox/data/model/app/server_detail_card.dart';
|
||||
import 'package:toolbox/data/res/store.dart';
|
||||
import 'package:toolbox/view/widget/val_builder.dart';
|
||||
|
||||
import '../../../../core/extension/order.dart';
|
||||
import '../../../widget/appbar.dart';
|
||||
import '../../../widget/cardx.dart';
|
||||
|
||||
class ServerDetailOrderPage extends StatefulWidget {
|
||||
const ServerDetailOrderPage({super.key});
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:toolbox/core/extension/context/locale.dart';
|
||||
import 'package:toolbox/core/extension/context/snackbar.dart';
|
||||
import 'package:toolbox/data/model/app/menu/server_func.dart';
|
||||
import 'package:toolbox/data/res/store.dart';
|
||||
import 'package:toolbox/data/res/ui.dart';
|
||||
import 'package:toolbox/view/widget/val_builder.dart';
|
||||
|
||||
import '../../../../core/extension/order.dart';
|
||||
import '../../../widget/appbar.dart';
|
||||
import '../../../widget/cardx.dart';
|
||||
|
||||
class ServerFuncBtnsOrderPage extends StatefulWidget {
|
||||
const ServerFuncBtnsOrderPage({super.key});
|
||||
@@ -50,7 +44,7 @@ class _ServerDetailOrderPageState extends State<ServerFuncBtnsOrderPage> {
|
||||
title: RichText(
|
||||
text: TextSpan(
|
||||
children: [
|
||||
WidgetSpan(child: funcBtn.icon(2)),
|
||||
WidgetSpan(child: Icon(funcBtn.icon)),
|
||||
const WidgetSpan(child: UIs.width7),
|
||||
TextSpan(text: funcBtn.toStr, style: UIs.textGrey),
|
||||
],
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:toolbox/core/extension/context/locale.dart';
|
||||
import 'package:toolbox/core/extension/order.dart';
|
||||
import 'package:toolbox/data/res/provider.dart';
|
||||
import 'package:toolbox/data/res/store.dart';
|
||||
import 'package:toolbox/data/res/ui.dart';
|
||||
import 'package:toolbox/view/widget/cardx.dart';
|
||||
|
||||
import '../../../widget/appbar.dart';
|
||||
|
||||
class ServerOrderPage extends StatefulWidget {
|
||||
const ServerOrderPage({super.key});
|
||||
|
||||
@@ -1,17 +1,8 @@
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:toolbox/core/extension/context/locale.dart';
|
||||
import 'package:toolbox/core/extension/context/snackbar.dart';
|
||||
import 'package:toolbox/core/extension/order.dart';
|
||||
import 'package:toolbox/core/extension/widget.dart';
|
||||
import 'package:toolbox/core/utils/platform/base.dart';
|
||||
import 'package:toolbox/data/model/ssh/virtual_key.dart';
|
||||
import 'package:toolbox/data/res/store.dart';
|
||||
import 'package:toolbox/data/res/ui.dart';
|
||||
import 'package:toolbox/view/widget/cardx.dart';
|
||||
import 'package:toolbox/view/widget/store_switch.dart';
|
||||
import 'package:toolbox/view/widget/val_builder.dart';
|
||||
|
||||
import '../../../widget/appbar.dart';
|
||||
|
||||
class SSHVirtKeySettingPage extends StatefulWidget {
|
||||
const SSHVirtKeySettingPage({super.key});
|
||||
@@ -33,7 +24,7 @@ class _SSHVirtKeySettingPageState extends State<SSHVirtKeySettingPage> {
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(7),
|
||||
child: _buildOneLineVirtKey().card,
|
||||
child: _buildOneLineVirtKey().cardx,
|
||||
),
|
||||
Expanded(child: _buildBody()),
|
||||
],
|
||||
|
||||
@@ -1,20 +1,11 @@
|
||||
import 'package:after_layout/after_layout.dart';
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
import 'package:toolbox/core/extension/context/common.dart';
|
||||
import 'package:toolbox/core/extension/context/dialog.dart';
|
||||
import 'package:toolbox/core/extension/context/locale.dart';
|
||||
import 'package:toolbox/core/extension/context/snackbar.dart';
|
||||
import 'package:toolbox/data/res/provider.dart';
|
||||
import 'package:toolbox/view/widget/cardx.dart';
|
||||
import 'package:toolbox/view/widget/input_field.dart';
|
||||
import 'package:toolbox/view/widget/markdown.dart';
|
||||
import 'package:toolbox/view/widget/val_builder.dart';
|
||||
|
||||
import '../../../data/model/server/snippet.dart';
|
||||
import '../../../data/res/ui.dart';
|
||||
import '../../widget/appbar.dart';
|
||||
import '../../widget/tag.dart';
|
||||
|
||||
class SnippetEditPage extends StatefulWidget {
|
||||
const SnippetEditPage({super.key, this.snippet});
|
||||
@@ -62,7 +53,7 @@ class _SnippetEditPageState extends State<SnippetEditPage>
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
context.showRoundDialog(
|
||||
title: Text(l10n.attention),
|
||||
title: l10n.attention,
|
||||
child: Text(l10n.askContinue(
|
||||
'${l10n.delete} ${l10n.snippet}(${widget.snippet!.name})',
|
||||
)),
|
||||
@@ -145,6 +136,9 @@ class _SnippetEditPageState extends State<SnippetEditPage>
|
||||
onRenameTag: (old, n) => setState(() {
|
||||
Pros.snippet.renameTag(old, n);
|
||||
}),
|
||||
renameL10n: l10n.rename,
|
||||
tagL10n: l10n.tag,
|
||||
addL10n: l10n.add,
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -182,6 +176,7 @@ class _SnippetEditPageState extends State<SnippetEditPage>
|
||||
onTap: () async {
|
||||
vals.removeWhere((e) => !Pros.server.serverOrder.contains(e));
|
||||
final serverIds = await context.showPickDialog(
|
||||
title: l10n.autoRun,
|
||||
items: Pros.server.serverOrder,
|
||||
initial: vals,
|
||||
);
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:toolbox/core/extension/context/locale.dart';
|
||||
import 'package:toolbox/core/extension/order.dart';
|
||||
import 'package:toolbox/data/res/store.dart';
|
||||
|
||||
import '../../../data/model/server/snippet.dart';
|
||||
import '../../../data/res/ui.dart';
|
||||
import '../../widget/tag.dart';
|
||||
import '/core/route.dart';
|
||||
import '/data/provider/snippet.dart';
|
||||
import '../../widget/cardx.dart';
|
||||
|
||||
class SnippetListPage extends StatefulWidget {
|
||||
const SnippetListPage({super.key});
|
||||
@@ -36,7 +33,7 @@ class _SnippetListPageState extends State<SnippetListPage> {
|
||||
floatingActionButton: FloatingActionButton(
|
||||
heroTag: 'snippetAdd',
|
||||
child: const Icon(Icons.add),
|
||||
onPressed: () => AppRoute.snippetEdit().go(context),
|
||||
onPressed: () => AppRoutes.snippetEdit().go(context),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -72,6 +69,7 @@ class _SnippetListPageState extends State<SnippetListPage> {
|
||||
onTagChanged: (tag) => setState(() => _tag = tag),
|
||||
initTag: _tag,
|
||||
width: _media.size.width,
|
||||
allL10n: l10n.all,
|
||||
),
|
||||
footer: UIs.height77,
|
||||
buildDefaultDragHandles: false,
|
||||
@@ -108,7 +106,7 @@ class _SnippetListPageState extends State<SnippetListPage> {
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () =>
|
||||
AppRoute.snippetEdit(snippet: snippet).go(context),
|
||||
AppRoutes.snippetEdit(snippet: snippet).go(context),
|
||||
icon: const Icon(Icons.edit),
|
||||
),
|
||||
],
|
||||
@@ -128,7 +126,7 @@ class _SnippetListPageState extends State<SnippetListPage> {
|
||||
// final ids = servers.map((e) => e.spi.id).toList();
|
||||
// final results = await Pros.server.runSnippetsMulti(ids, snippet);
|
||||
// if (results.isNotEmpty) {
|
||||
// AppRoute.snippetResult(results: results).go(context);
|
||||
// AppRoutes.snippetResult(results: results).go(context);
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:toolbox/core/extension/context/locale.dart';
|
||||
import 'package:toolbox/data/model/server/snippet.dart';
|
||||
import 'package:toolbox/data/res/ui.dart';
|
||||
import 'package:toolbox/view/widget/cardx.dart';
|
||||
import 'package:toolbox/view/widget/appbar.dart';
|
||||
import 'package:toolbox/view/widget/expand_tile.dart';
|
||||
|
||||
class SnippetResultPage extends StatelessWidget {
|
||||
final List<SnippetResult?> results;
|
||||
|
||||
@@ -2,22 +2,18 @@ import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:dartssh2/dartssh2.dart';
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_background_service/flutter_background_service.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:toolbox/core/extension/context/common.dart';
|
||||
import 'package:toolbox/core/extension/context/dialog.dart';
|
||||
import 'package:toolbox/core/extension/context/locale.dart';
|
||||
import 'package:toolbox/core/utils/ssh_auth.dart';
|
||||
import 'package:toolbox/core/utils/platform/base.dart';
|
||||
import 'package:toolbox/core/utils/server.dart';
|
||||
import 'package:toolbox/core/utils/share.dart';
|
||||
import 'package:toolbox/data/model/server/snippet.dart';
|
||||
import 'package:toolbox/data/provider/virtual_keyboard.dart';
|
||||
import 'package:toolbox/data/res/provider.dart';
|
||||
import 'package:toolbox/data/res/store.dart';
|
||||
import 'package:toolbox/view/widget/appbar.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
import 'package:xterm/core.dart';
|
||||
import 'package:xterm/ui.dart' hide TerminalThemes;
|
||||
@@ -26,7 +22,6 @@ import '../../../core/route.dart';
|
||||
import '../../../core/utils/misc.dart';
|
||||
import '../../../data/model/server/server_private_info.dart';
|
||||
import '../../../data/model/ssh/virtual_key.dart';
|
||||
import '../../../data/res/color.dart';
|
||||
import '../../../data/res/terminal.dart';
|
||||
|
||||
const _echoPWD = 'echo \$PWD';
|
||||
@@ -104,7 +99,7 @@ class _SSHPageState extends State<SSHPage> with AutomaticKeepAliveClientMixin {
|
||||
_media = MediaQuery.of(context);
|
||||
|
||||
_terminalTheme = _isDark ? TerminalThemes.dark : TerminalThemes.light;
|
||||
_terminalTheme = _terminalTheme.copyWith(selectionCursor: primaryColor);
|
||||
_terminalTheme = _terminalTheme.copyWith(selectionCursor: UIs.primaryColor);
|
||||
|
||||
// Because the virtual keyboard only displayed on mobile devices
|
||||
if (isMobile) {
|
||||
@@ -231,7 +226,7 @@ class _SSHPageState extends State<SSHPage> with AutomaticKeepAliveClientMixin {
|
||||
item.text,
|
||||
style: TextStyle(
|
||||
color: selected
|
||||
? primaryColor
|
||||
? UIs.primaryColor
|
||||
: (_isDark ? Colors.white : Colors.black),
|
||||
fontSize: 15,
|
||||
),
|
||||
@@ -298,13 +293,14 @@ class _SSHPageState extends State<SSHPage> with AutomaticKeepAliveClientMixin {
|
||||
case VirtualKeyFunc.clipboard:
|
||||
final selected = terminalSelected;
|
||||
if (selected != null) {
|
||||
Shares.copy(selected);
|
||||
Pfs.copy(selected);
|
||||
} else {
|
||||
_paste();
|
||||
}
|
||||
break;
|
||||
case VirtualKeyFunc.snippet:
|
||||
final snippets = await context.showPickWithTagDialog<Snippet>(
|
||||
title: l10n.snippet,
|
||||
tags: Pros.snippet.tags,
|
||||
itemsBuilder: (e) {
|
||||
if (e == null) return Pros.snippet.snippets;
|
||||
@@ -313,6 +309,7 @@ class _SSHPageState extends State<SSHPage> with AutomaticKeepAliveClientMixin {
|
||||
.toList();
|
||||
},
|
||||
name: (e) => e.name,
|
||||
all: l10n.all,
|
||||
);
|
||||
if (snippets == null || snippets.isEmpty) return;
|
||||
|
||||
@@ -333,12 +330,12 @@ class _SSHPageState extends State<SSHPage> with AutomaticKeepAliveClientMixin {
|
||||
final initPath = cmds[idx + 1].toString();
|
||||
if (initPath.isEmpty || !initPath.startsWith('/')) {
|
||||
context.showRoundDialog(
|
||||
title: Text(l10n.error),
|
||||
title: l10n.error,
|
||||
child: const Text('Failed to get current path'),
|
||||
);
|
||||
return;
|
||||
}
|
||||
AppRoute.sftp(spi: widget.spi, initPath: initPath).go(context);
|
||||
AppRoutes.sftp(spi: widget.spi, initPath: initPath).go(context);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -525,7 +522,7 @@ class _SSHPageState extends State<SSHPage> with AutomaticKeepAliveClientMixin {
|
||||
Future<void> _showHelp() async {
|
||||
if (!Stores.setting.sshTermHelpShown.fetch()) {
|
||||
await context.showRoundDialog(
|
||||
title: Text(l10n.doc),
|
||||
title: l10n.doc,
|
||||
child: Text(l10n.sshTermHelp),
|
||||
actions: [
|
||||
TextButton(
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:toolbox/core/extension/context/common.dart';
|
||||
import 'package:toolbox/core/extension/context/dialog.dart';
|
||||
import 'package:toolbox/core/extension/context/locale.dart';
|
||||
import 'package:toolbox/core/route.dart';
|
||||
import 'package:toolbox/data/model/server/server_private_info.dart';
|
||||
import 'package:toolbox/data/provider/server.dart';
|
||||
import 'package:toolbox/data/res/provider.dart';
|
||||
import 'package:toolbox/data/res/ui.dart';
|
||||
import 'package:toolbox/view/page/ssh/page.dart';
|
||||
import 'package:toolbox/view/widget/cardx.dart';
|
||||
|
||||
class SSHTabPage extends StatefulWidget {
|
||||
const SSHTabPage({super.key});
|
||||
@@ -48,7 +45,7 @@ class _SSHTabPageState extends State<SSHTabPage>
|
||||
if (_fabRN.value != 0) return const SizedBox();
|
||||
return FloatingActionButton(
|
||||
heroTag: 'sshAddServer',
|
||||
onPressed: () => AppRoute.serverEdit().go(context),
|
||||
onPressed: () => AppRoutes.serverEdit().go(context),
|
||||
tooltip: l10n.addAServer,
|
||||
child: const Icon(Icons.add),
|
||||
);
|
||||
@@ -70,7 +67,7 @@ class _SSHTabPageState extends State<SSHTabPage>
|
||||
icon: const Icon(Icons.close, size: 17),
|
||||
onPressed: () async {
|
||||
final confirm = await context.showRoundDialog<bool>(
|
||||
title: Text(l10n.attention),
|
||||
title: l10n.attention,
|
||||
child: Text('${l10n.close} SSH ${l10n.conn}($e) ?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
|
||||
@@ -1,28 +1,17 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:toolbox/core/extension/context/common.dart';
|
||||
import 'package:toolbox/core/extension/context/dialog.dart';
|
||||
import 'package:toolbox/core/extension/context/locale.dart';
|
||||
import 'package:toolbox/core/extension/context/snackbar.dart';
|
||||
import 'package:toolbox/core/utils/share.dart';
|
||||
import 'package:toolbox/data/model/server/server_private_info.dart';
|
||||
import 'package:toolbox/data/model/sftp/req.dart';
|
||||
import 'package:toolbox/data/res/misc.dart';
|
||||
import 'package:toolbox/data/res/provider.dart';
|
||||
import 'package:toolbox/view/widget/input_field.dart';
|
||||
import 'package:toolbox/view/widget/omit_start_text.dart';
|
||||
import 'package:toolbox/view/widget/cardx.dart';
|
||||
import 'package:toolbox/view/widget/val_builder.dart';
|
||||
|
||||
import '../../../core/extension/numx.dart';
|
||||
import '../../../core/route.dart';
|
||||
import '../../../core/utils/misc.dart';
|
||||
import '../../../data/model/app/path_with_prefix.dart';
|
||||
import '../../../data/res/path.dart';
|
||||
import '../../../data/res/ui.dart';
|
||||
import '../../widget/appbar.dart';
|
||||
import '../../widget/fade_in.dart';
|
||||
|
||||
class LocalStoragePage extends StatefulWidget {
|
||||
final bool isPickFile;
|
||||
@@ -50,10 +39,8 @@ class _LocalStoragePageState extends State<LocalStoragePage> {
|
||||
_path = LocalPath(widget.initDir!);
|
||||
});
|
||||
} else {
|
||||
Paths.sftp.then((dir) {
|
||||
setState(() {
|
||||
_path = LocalPath(dir);
|
||||
});
|
||||
setState(() {
|
||||
_path = LocalPath(Paths.file);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -75,7 +62,7 @@ class _LocalStoragePageState extends State<LocalStoragePage> {
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.downloading),
|
||||
onPressed: () => AppRoute.sftpMission().go(context),
|
||||
onPressed: () => AppRoutes.sftpMission().go(context),
|
||||
),
|
||||
ValBuilder<_SortType>(
|
||||
listenable: _sortType,
|
||||
@@ -145,10 +132,10 @@ class _LocalStoragePageState extends State<LocalStoragePage> {
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
final path = await pickOneFile();
|
||||
final path = await Pfs.pickFilePath();
|
||||
if (path == null) return;
|
||||
final name = getFileName(path) ?? 'imported';
|
||||
await File(path).copy(pathJoin(_path!.path, name));
|
||||
await File(path).copy(_path!.path.joinPath(name));
|
||||
setState(() {});
|
||||
},
|
||||
icon: const Icon(Icons.add),
|
||||
@@ -237,7 +224,7 @@ class _LocalStoragePageState extends State<LocalStoragePage> {
|
||||
final fileName = file.path.split('/').last;
|
||||
if (widget.isPickFile) {
|
||||
await context.showRoundDialog(
|
||||
title: Text(l10n.pickFile),
|
||||
title: l10n.pickFile,
|
||||
child: Text(fileName),
|
||||
actions: [
|
||||
TextButton(
|
||||
@@ -262,12 +249,12 @@ class _LocalStoragePageState extends State<LocalStoragePage> {
|
||||
final stat = await file.stat();
|
||||
if (stat.size > Miscs.editorMaxSize) {
|
||||
context.showRoundDialog(
|
||||
title: Text(l10n.attention),
|
||||
title: l10n.attention,
|
||||
child: Text(l10n.fileTooLarge(fileName, stat.size, '1m')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
final result = await AppRoute.editor(
|
||||
final result = await AppRoutes.editor(
|
||||
path: file.absolute.path,
|
||||
).go<bool>(context);
|
||||
if (result == true) {
|
||||
@@ -299,6 +286,7 @@ class _LocalStoragePageState extends State<LocalStoragePage> {
|
||||
context.pop();
|
||||
|
||||
final spi = await context.showPickSingleDialog<ServerPrivateInfo>(
|
||||
title: l10n.choose,
|
||||
items: Pros.server.serverOrder
|
||||
.map((e) => Pros.server.pick(id: e)?.spi)
|
||||
.toList(),
|
||||
@@ -306,7 +294,7 @@ class _LocalStoragePageState extends State<LocalStoragePage> {
|
||||
);
|
||||
if (spi == null) return;
|
||||
|
||||
final remotePath = await AppRoute.sftp(
|
||||
final remotePath = await AppRoutes.sftp(
|
||||
spi: spi,
|
||||
isSelect: true,
|
||||
).go<String>(context);
|
||||
@@ -327,7 +315,7 @@ class _LocalStoragePageState extends State<LocalStoragePage> {
|
||||
leading: const Icon(Icons.open_in_new),
|
||||
title: Text(l10n.open),
|
||||
onTap: () {
|
||||
Shares.files([file.absolute.path]);
|
||||
Pfs.sharePath(file.absolute.path);
|
||||
},
|
||||
),
|
||||
],
|
||||
@@ -338,7 +326,7 @@ class _LocalStoragePageState extends State<LocalStoragePage> {
|
||||
void _showRenameDialog(FileSystemEntity file) {
|
||||
final fileName = file.path.split('/').last;
|
||||
context.showRoundDialog(
|
||||
title: Text(l10n.rename),
|
||||
title: l10n.rename,
|
||||
child: Input(
|
||||
autoFocus: true,
|
||||
controller: TextEditingController(text: fileName),
|
||||
@@ -361,7 +349,7 @@ class _LocalStoragePageState extends State<LocalStoragePage> {
|
||||
void _showDeleteDialog(FileSystemEntity file) {
|
||||
final fileName = file.path.split('/').last;
|
||||
context.showRoundDialog(
|
||||
title: Text(l10n.delete),
|
||||
title: l10n.delete,
|
||||
child: Text(l10n.askContinue('${l10n.delete} $fileName')),
|
||||
actions: [
|
||||
TextButton(
|
||||
|
||||
@@ -2,36 +2,21 @@ import 'dart:async';
|
||||
|
||||
import 'package:after_layout/after_layout.dart';
|
||||
import 'package:dartssh2/dartssh2.dart';
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:toolbox/core/extension/context/common.dart';
|
||||
import 'package:toolbox/core/extension/context/dialog.dart';
|
||||
import 'package:toolbox/core/extension/context/locale.dart';
|
||||
import 'package:toolbox/core/extension/context/snackbar.dart';
|
||||
import 'package:toolbox/core/extension/sftpfile.dart';
|
||||
import 'package:toolbox/core/utils/comparator.dart';
|
||||
import 'package:toolbox/core/utils/platform/base.dart';
|
||||
import 'package:toolbox/data/res/color.dart';
|
||||
import 'package:toolbox/data/res/logger.dart';
|
||||
import 'package:toolbox/data/res/misc.dart';
|
||||
import 'package:toolbox/data/res/provider.dart';
|
||||
import 'package:toolbox/data/res/store.dart';
|
||||
import 'package:toolbox/view/widget/omit_start_text.dart';
|
||||
import 'package:toolbox/view/widget/cardx.dart';
|
||||
import 'package:toolbox/view/widget/search.dart';
|
||||
import 'package:toolbox/view/widget/val_builder.dart';
|
||||
|
||||
import '../../../core/extension/numx.dart';
|
||||
import '../../../core/route.dart';
|
||||
import '../../../core/utils/misc.dart';
|
||||
import '../../../data/model/server/server_private_info.dart';
|
||||
import '../../../data/model/sftp/absolute_path.dart';
|
||||
import '../../../data/model/sftp/browser_status.dart';
|
||||
import '../../../data/model/sftp/req.dart';
|
||||
import '../../../data/res/path.dart';
|
||||
import '../../../data/res/ui.dart';
|
||||
import '../../widget/appbar.dart';
|
||||
import '../../widget/fade_in.dart';
|
||||
import '../../widget/input_field.dart';
|
||||
import '../../widget/two_line_text.dart';
|
||||
|
||||
class SftpPage extends StatefulWidget {
|
||||
@@ -72,7 +57,7 @@ class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.downloading),
|
||||
onPressed: () => AppRoute.sftpMission().go(context),
|
||||
onPressed: () => AppRoutes.sftpMission().go(context),
|
||||
),
|
||||
ValBuilder(
|
||||
listenable: _sortOption,
|
||||
@@ -96,7 +81,7 @@ class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
|
||||
: name,
|
||||
style: TextStyle(
|
||||
color: type == currentSelectedOption.sortBy
|
||||
? primaryColor
|
||||
? UIs.primaryColor
|
||||
: null,
|
||||
fontWeight: type == currentSelectedOption.sortBy
|
||||
? FontWeight.bold
|
||||
@@ -210,10 +195,10 @@ class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
|
||||
final path = await () async {
|
||||
switch (idx) {
|
||||
case 0:
|
||||
return await AppRoute.localStorage(isPickFile: true)
|
||||
return await AppRoutes.localStorage(isPickFile: true)
|
||||
.go<String>(context);
|
||||
case 1:
|
||||
return await pickOneFile();
|
||||
return await Pfs.pickFilePath();
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@@ -263,7 +248,7 @@ class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
|
||||
padding: const EdgeInsets.all(0),
|
||||
onPressed: () async {
|
||||
final p = await context.showRoundDialog<String>(
|
||||
title: Text(l10n.goto),
|
||||
title: l10n.goto,
|
||||
child: Autocomplete<String>(
|
||||
optionsBuilder: (val) {
|
||||
if (!Stores.setting.recordHistory.fetch()) {
|
||||
@@ -441,7 +426,7 @@ class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
|
||||
Pros.sftp.add(req, completer: completer);
|
||||
await context.showLoadingDialog(fn: () => completer.future);
|
||||
|
||||
final result = await AppRoute.editor(path: localPath).go<bool>(context);
|
||||
final result = await AppRoutes.editor(path: localPath).go<bool>(context);
|
||||
if (result != null && result) {
|
||||
Pros.sftp
|
||||
.add(SftpReq(req.spi, remotePath, localPath, SftpReqType.upload));
|
||||
@@ -451,7 +436,7 @@ class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
|
||||
|
||||
void _download(SftpName name) {
|
||||
context.showRoundDialog(
|
||||
title: Text(l10n.attention),
|
||||
title: l10n.attention,
|
||||
child: Text('${l10n.dl2Local(name.filename)}\n${l10n.keepForeground}'),
|
||||
actions: [
|
||||
TextButton(
|
||||
@@ -493,7 +478,7 @@ class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
|
||||
}();
|
||||
context.showRoundDialog(
|
||||
child: Text(text),
|
||||
title: Text(l10n.attention),
|
||||
title: l10n.attention,
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => context.pop(),
|
||||
@@ -515,7 +500,7 @@ class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
|
||||
});
|
||||
} catch (e) {
|
||||
context.showRoundDialog(
|
||||
title: Text(l10n.error),
|
||||
title: l10n.error,
|
||||
child: Text(e.toString()),
|
||||
actions: [
|
||||
TextButton(
|
||||
@@ -538,7 +523,7 @@ class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
|
||||
context.pop();
|
||||
final textController = TextEditingController();
|
||||
context.showRoundDialog(
|
||||
title: Text(l10n.createFolder),
|
||||
title: l10n.createFolder,
|
||||
child: Input(
|
||||
autoFocus: true,
|
||||
icon: Icons.folder,
|
||||
@@ -579,7 +564,7 @@ class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
|
||||
context.pop();
|
||||
final textController = TextEditingController();
|
||||
context.showRoundDialog(
|
||||
title: Text(l10n.createFile),
|
||||
title: l10n.createFile,
|
||||
child: Input(
|
||||
autoFocus: true,
|
||||
icon: Icons.insert_drive_file,
|
||||
@@ -591,7 +576,7 @@ class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
|
||||
onPressed: () async {
|
||||
if (textController.text.isEmpty) {
|
||||
context.showRoundDialog(
|
||||
title: Text(l10n.attention),
|
||||
title: l10n.attention,
|
||||
child: Text(l10n.fieldMustNotEmpty),
|
||||
actions: [
|
||||
TextButton(
|
||||
@@ -618,7 +603,7 @@ class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
|
||||
context.pop();
|
||||
final textController = TextEditingController(text: file.filename);
|
||||
context.showRoundDialog(
|
||||
title: Text(l10n.rename),
|
||||
title: l10n.rename,
|
||||
child: Input(
|
||||
autoFocus: true,
|
||||
icon: Icons.abc,
|
||||
@@ -631,7 +616,7 @@ class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
|
||||
onPressed: () async {
|
||||
if (textController.text.isEmpty) {
|
||||
context.showRoundDialog(
|
||||
title: Text(l10n.attention),
|
||||
title: l10n.attention,
|
||||
child: Text(l10n.fieldMustNotEmpty),
|
||||
actions: [
|
||||
TextButton(
|
||||
@@ -658,7 +643,7 @@ class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
|
||||
final cmd = _getDecompressCmd(absPath);
|
||||
if (cmd == null) {
|
||||
context.showRoundDialog(
|
||||
title: Text(l10n.error),
|
||||
title: l10n.error,
|
||||
child: Text('Unsupport file: ${name.filename}'),
|
||||
actions: [
|
||||
TextButton(
|
||||
@@ -675,11 +660,12 @@ class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
|
||||
|
||||
String _getRemotePath(SftpName name) {
|
||||
final prePath = _status.path!.path;
|
||||
return pathJoin(prePath, name.filename);
|
||||
// Only support Linux as remote now, so the seperator is '/'
|
||||
return prePath.joinPath(name.filename, seperator: '/');
|
||||
}
|
||||
|
||||
Future<String> _getLocalPath(String remotePath) async {
|
||||
return '${await Paths.sftp}$remotePath';
|
||||
return Paths.file.joinPath(remotePath);
|
||||
}
|
||||
|
||||
/// Only return true if the path is changed
|
||||
@@ -728,7 +714,7 @@ class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
|
||||
Future.delayed(
|
||||
const Duration(milliseconds: 177),
|
||||
() => context.showRoundDialog(
|
||||
title: Text(l10n.error),
|
||||
title: l10n.error,
|
||||
child: Text(e.toString()),
|
||||
actions: [
|
||||
TextButton(
|
||||
|
||||
@@ -1,19 +1,12 @@
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:toolbox/core/extension/context/common.dart';
|
||||
import 'package:toolbox/core/extension/context/locale.dart';
|
||||
import 'package:toolbox/core/extension/datetime.dart';
|
||||
import 'package:toolbox/core/extension/context/dialog.dart';
|
||||
import 'package:toolbox/core/route.dart';
|
||||
import 'package:toolbox/core/utils/share.dart';
|
||||
import 'package:toolbox/data/res/provider.dart';
|
||||
|
||||
import '../../../core/extension/numx.dart';
|
||||
import '../../../data/model/sftp/req.dart';
|
||||
import '../../../data/provider/sftp.dart';
|
||||
import '../../../data/res/ui.dart';
|
||||
import '../../widget/appbar.dart';
|
||||
import '../../widget/cardx.dart';
|
||||
|
||||
class SftpMissionPage extends StatefulWidget {
|
||||
const SftpMissionPage({super.key});
|
||||
@@ -59,7 +52,7 @@ class _SftpMissionPageState extends State<SftpMissionPage> {
|
||||
subtitle: l10n.error,
|
||||
trailing: IconButton(
|
||||
onPressed: () => context.showRoundDialog(
|
||||
title: Text(l10n.error),
|
||||
title: l10n.error,
|
||||
child: Text(err.toString()),
|
||||
),
|
||||
icon: const Icon(Icons.error),
|
||||
@@ -82,11 +75,11 @@ class _SftpMissionPageState extends State<SftpMissionPage> {
|
||||
onPressed: () {
|
||||
final idx = status.req.localPath.lastIndexOf('/');
|
||||
final dir = status.req.localPath.substring(0, idx);
|
||||
AppRoute.localStorage(initDir: dir).go(context);
|
||||
AppRoutes.localStorage(initDir: dir).go(context);
|
||||
},
|
||||
icon: const Icon(Icons.file_open)),
|
||||
IconButton(
|
||||
onPressed: () => Shares.files([status.req.localPath]),
|
||||
onPressed: () => Pfs.sharePath(status.req.localPath),
|
||||
icon: const Icon(Icons.open_in_new),
|
||||
)
|
||||
],
|
||||
@@ -118,7 +111,7 @@ class _SftpMissionPageState extends State<SftpMissionPage> {
|
||||
subtitle: l10n.unknown,
|
||||
trailing: IconButton(
|
||||
onPressed: () => context.showRoundDialog(
|
||||
title: Text(l10n.error),
|
||||
title: l10n.error,
|
||||
child: Text((status.error ?? l10n.unknown).toString()),
|
||||
),
|
||||
icon: const Icon(Icons.error),
|
||||
@@ -150,7 +143,7 @@ class _SftpMissionPageState extends State<SftpMissionPage> {
|
||||
Widget _buildDelete(String name, int id) {
|
||||
return IconButton(
|
||||
onPressed: () => context.showRoundDialog(
|
||||
title: Text(l10n.attention),
|
||||
title: l10n.attention,
|
||||
child: Text(l10n.askContinue(
|
||||
'${l10n.delete} ${l10n.mission}($name)',
|
||||
)),
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:toolbox/data/res/store.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
/// System status bar height
|
||||
static double? barHeight;
|
||||
static bool drawTitlebar = false;
|
||||
|
||||
const CustomAppBar({
|
||||
super.key,
|
||||
this.title,
|
||||
this.actions,
|
||||
this.centerTitle = true,
|
||||
this.leading,
|
||||
this.backgroundColor,
|
||||
});
|
||||
|
||||
final Widget? title;
|
||||
final List<Widget>? actions;
|
||||
final bool? centerTitle;
|
||||
final Widget? leading;
|
||||
final Color? backgroundColor;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bar = AppBar(
|
||||
key: key,
|
||||
title: title,
|
||||
actions: actions,
|
||||
centerTitle: centerTitle,
|
||||
leading: leading,
|
||||
backgroundColor: backgroundColor,
|
||||
toolbarHeight: (barHeight ?? 0) + kToolbarHeight,
|
||||
);
|
||||
if (!drawTitlebar) return bar;
|
||||
return Stack(
|
||||
children: [
|
||||
bar,
|
||||
Positioned(
|
||||
right: 0,
|
||||
top: 0,
|
||||
child: GestureDetector(
|
||||
onVerticalDragStart: (_) {
|
||||
windowManager.startDragging();
|
||||
},
|
||||
onHorizontalDragStart: (_) {
|
||||
windowManager.startDragging();
|
||||
},
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Transform.translate(
|
||||
offset: const Offset(0, -3.5),
|
||||
child: const Icon(Icons.minimize, size: 13),
|
||||
),
|
||||
onPressed: () => windowManager.minimize(),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.crop_square, size: 13),
|
||||
onPressed: () async {
|
||||
if (await windowManager.isMaximized()) {
|
||||
windowManager.unmaximize();
|
||||
} else {
|
||||
windowManager.maximize();
|
||||
}
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close, size: 14),
|
||||
onPressed: () => windowManager.close(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
static Future<void> updateTitlebarHeight() async {
|
||||
switch (Platform.operatingSystem) {
|
||||
case 'macos':
|
||||
barHeight = 27;
|
||||
break;
|
||||
case 'linux' || 'windows':
|
||||
if (!Stores.setting.hideTitleBar.fetch()) break;
|
||||
barHeight = 37;
|
||||
drawTitlebar = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Size get preferredSize => Size.fromHeight((barHeight ?? 0) + kToolbarHeight);
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
final class AutoHide extends StatefulWidget {
|
||||
final Widget child;
|
||||
final ScrollController controller;
|
||||
final AxisDirection direction;
|
||||
final double offset;
|
||||
|
||||
const AutoHide({
|
||||
super.key,
|
||||
required this.child,
|
||||
required this.controller,
|
||||
required this.direction,
|
||||
this.offset = 55,
|
||||
});
|
||||
|
||||
@override
|
||||
State<AutoHide> createState() => AutoHideState();
|
||||
}
|
||||
|
||||
final class AutoHideState extends State<AutoHide> {
|
||||
bool _visible = true;
|
||||
bool _isScrolling = false;
|
||||
Timer? _timer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.controller.addListener(_scrollListener);
|
||||
_setupTimer();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.controller.removeListener(_scrollListener);
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void show() {
|
||||
debugPrint('show');
|
||||
if (_visible) return;
|
||||
setState(() {
|
||||
_visible = true;
|
||||
});
|
||||
_setupTimer();
|
||||
}
|
||||
|
||||
void _setupTimer() {
|
||||
_timer?.cancel();
|
||||
_timer = Timer.periodic(const Duration(seconds: 3), (_) {
|
||||
if (_isScrolling) return;
|
||||
if (!_visible) return;
|
||||
final canScroll =
|
||||
widget.controller.positions.any((e) => e.maxScrollExtent >= 0);
|
||||
if (!canScroll) return;
|
||||
|
||||
setState(() {
|
||||
_visible = false;
|
||||
});
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
});
|
||||
}
|
||||
|
||||
void _scrollListener() {
|
||||
if (_isScrolling) return;
|
||||
_isScrolling = true;
|
||||
|
||||
if (!_visible) {
|
||||
setState(() {
|
||||
_visible = true;
|
||||
});
|
||||
_setupTimer();
|
||||
}
|
||||
|
||||
_isScrolling = false;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedContainer(
|
||||
duration: Durations.medium1,
|
||||
curve: Curves.easeInOutCubic,
|
||||
transform: _transform,
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
|
||||
Matrix4? get _transform {
|
||||
switch (widget.direction) {
|
||||
case AxisDirection.down:
|
||||
return _visible
|
||||
? Matrix4.identity()
|
||||
: Matrix4.translationValues(0, widget.offset, 0);
|
||||
case AxisDirection.up:
|
||||
return _visible
|
||||
? Matrix4.identity()
|
||||
: Matrix4.translationValues(0, -widget.offset, 0);
|
||||
case AxisDirection.left:
|
||||
return _visible
|
||||
? Matrix4.identity()
|
||||
: Matrix4.translationValues(-widget.offset, 0, 0);
|
||||
case AxisDirection.right:
|
||||
return _visible
|
||||
? Matrix4.identity()
|
||||
: Matrix4.translationValues(widget.offset, 0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CardX extends StatelessWidget {
|
||||
const CardX({super.key, required this.child, this.color});
|
||||
|
||||
final Widget child;
|
||||
final Color? color;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
key: key,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
color: color,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(17)),
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import 'package:choice/selection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ChoiceChipX<T> extends StatelessWidget {
|
||||
const ChoiceChipX({
|
||||
super.key,
|
||||
required this.label,
|
||||
required this.state,
|
||||
required this.value,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final ChoiceController<T> state;
|
||||
final T value;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 5),
|
||||
child: ChoiceChip(
|
||||
label: Text(label),
|
||||
side: BorderSide.none,
|
||||
showCheckmark: true,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 8),
|
||||
labelPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 0),
|
||||
selected: state.selected(value),
|
||||
onSelected: state.onSelected(value),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
enum _ColorPropType {
|
||||
r,
|
||||
g,
|
||||
b,
|
||||
}
|
||||
|
||||
class ColorPicker extends StatefulWidget {
|
||||
final Color color;
|
||||
final ValueChanged<Color> onColorChanged;
|
||||
|
||||
const ColorPicker({
|
||||
super.key,
|
||||
required this.color,
|
||||
required this.onColorChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
_ColorPickerState createState() => _ColorPickerState();
|
||||
}
|
||||
|
||||
class _ColorPickerState extends State<ColorPicker> {
|
||||
late int _r = widget.color.red;
|
||||
late int _g = widget.color.green;
|
||||
late int _b = widget.color.blue;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
_buildProgress(_ColorPropType.r, 'R', _r.toDouble()),
|
||||
_buildProgress(_ColorPropType.g, 'G', _g.toDouble()),
|
||||
_buildProgress(_ColorPropType.b, 'B', _b.toDouble()),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProgress(_ColorPropType type, String title, double value) {
|
||||
return Row(
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Slider(
|
||||
value: value,
|
||||
onChanged: (v) {
|
||||
setState(() {
|
||||
switch (type) {
|
||||
case _ColorPropType.r:
|
||||
_r = v.toInt();
|
||||
break;
|
||||
case _ColorPropType.g:
|
||||
_g = v.toInt();
|
||||
break;
|
||||
case _ColorPropType.b:
|
||||
_b = v.toInt();
|
||||
break;
|
||||
}
|
||||
});
|
||||
widget.onColorChanged(Color.fromARGB(255, _r, _g, _b));
|
||||
},
|
||||
min: 0,
|
||||
max: 255,
|
||||
divisions: 255,
|
||||
label: value.toInt().toString(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:toolbox/core/extension/context/locale.dart';
|
||||
|
||||
final class CountDownBtn extends StatefulWidget {
|
||||
final int seconds;
|
||||
final String text;
|
||||
final Color? afterColor;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const CountDownBtn({
|
||||
super.key,
|
||||
required this.onTap,
|
||||
this.seconds = 3,
|
||||
this.text = 'Go',
|
||||
this.afterColor,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CountDownBtn> createState() => _CountDownBtnState();
|
||||
}
|
||||
|
||||
final class _CountDownBtnState extends State<CountDownBtn> {
|
||||
late int _seconds = widget.seconds;
|
||||
Timer? _timer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_startCountDown();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
bool get isCounting => _seconds > 0;
|
||||
|
||||
void _startCountDown() {
|
||||
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
if (!isCounting) {
|
||||
_timer?.cancel();
|
||||
}
|
||||
setState(() {
|
||||
_seconds--;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextButton(
|
||||
onPressed: () {
|
||||
if (isCounting) return;
|
||||
widget.onTap();
|
||||
},
|
||||
child: Text(
|
||||
isCounting ? '$_seconds${l10n.second}' : widget.text,
|
||||
style: TextStyle(
|
||||
color: _seconds > 0 ? Colors.grey : widget.afterColor,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
const _shape = Border();
|
||||
|
||||
class ExpandTile extends ExpansionTile {
|
||||
const ExpandTile({
|
||||
super.key,
|
||||
super.leading,
|
||||
required super.title,
|
||||
super.children,
|
||||
super.subtitle,
|
||||
super.initiallyExpanded,
|
||||
super.tilePadding,
|
||||
super.childrenPadding,
|
||||
super.trailing,
|
||||
super.controller,
|
||||
}) : super(shape: _shape, collapsedShape: _shape);
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// 渐隐渐显实现
|
||||
class FadeIn extends StatefulWidget {
|
||||
final Widget child;
|
||||
final Duration duration;
|
||||
|
||||
const FadeIn({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.duration = const Duration(milliseconds: 477),
|
||||
});
|
||||
|
||||
@override
|
||||
_MyFadeInState createState() => _MyFadeInState();
|
||||
}
|
||||
|
||||
class _MyFadeInState extends State<FadeIn> with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _animation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: widget.duration,
|
||||
);
|
||||
_animation = Tween(
|
||||
begin: 0.0,
|
||||
end: 1.0,
|
||||
).animate(_controller);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_controller.forward();
|
||||
return FadeTransition(
|
||||
opacity: _animation,
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:toolbox/data/res/ui.dart';
|
||||
|
||||
class FutureWidget<T> extends StatelessWidget {
|
||||
final Future<T> future;
|
||||
final Widget loading;
|
||||
final Widget Function(Object? error, StackTrace? trace) error;
|
||||
final Widget Function(T? data) success;
|
||||
final Widget Function(AsyncSnapshot<Object?> snapshot)? active;
|
||||
|
||||
const FutureWidget({
|
||||
super.key,
|
||||
required this.future,
|
||||
this.loading = UIs.placeholder,
|
||||
required this.error,
|
||||
required this.success,
|
||||
this.active,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder<T>(
|
||||
future: future,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
return error(snapshot.error, snapshot.stackTrace);
|
||||
}
|
||||
switch (snapshot.connectionState) {
|
||||
case ConnectionState.none:
|
||||
case ConnectionState.waiting:
|
||||
return loading;
|
||||
case ConnectionState.active:
|
||||
if (active != null) {
|
||||
return active!(snapshot);
|
||||
}
|
||||
return loading;
|
||||
case ConnectionState.done:
|
||||
return success(snapshot.data);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
final class IconBtn extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final double size;
|
||||
final Color? color;
|
||||
final void Function() onTap;
|
||||
|
||||
const IconBtn({
|
||||
super.key,
|
||||
required this.icon,
|
||||
required this.onTap,
|
||||
this.size = 17,
|
||||
this.color,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(17),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Icon(icon, size: size, color: color),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:toolbox/data/res/ui.dart';
|
||||
|
||||
final class IconTextBtn extends StatelessWidget {
|
||||
final String text;
|
||||
final IconData icon;
|
||||
final VoidCallback? onPressed;
|
||||
final Orientation orientation;
|
||||
|
||||
const IconTextBtn({
|
||||
super.key,
|
||||
required this.text,
|
||||
required this.icon,
|
||||
this.onPressed,
|
||||
this.orientation = Orientation.portrait,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IconButton(
|
||||
onPressed: onPressed,
|
||||
tooltip: text,
|
||||
icon: orientation == Orientation.landscape
|
||||
? Row(
|
||||
children: [
|
||||
Icon(icon),
|
||||
UIs.width7,
|
||||
Text(text, style: UIs.text13Grey),
|
||||
],
|
||||
)
|
||||
: Column(
|
||||
children: [
|
||||
Icon(icon),
|
||||
UIs.height7,
|
||||
Text(text, style: UIs.text13Grey),
|
||||
],
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'cardx.dart';
|
||||
|
||||
class Input extends StatefulWidget {
|
||||
final TextEditingController? controller;
|
||||
final int maxLines;
|
||||
final int? minLines;
|
||||
final String? hint;
|
||||
final String? label;
|
||||
final void Function(String)? onSubmitted;
|
||||
final void Function(String)? onChanged;
|
||||
final bool obscureText;
|
||||
final IconData? icon;
|
||||
final TextInputType? type;
|
||||
final FocusNode? node;
|
||||
final bool autoCorrect;
|
||||
final bool suggestiion;
|
||||
final String? errorText;
|
||||
final Widget? prefix;
|
||||
final bool autoFocus;
|
||||
final void Function(bool)? onViewPwdTap;
|
||||
|
||||
const Input({
|
||||
super.key,
|
||||
this.controller,
|
||||
this.maxLines = 1,
|
||||
this.minLines,
|
||||
this.hint,
|
||||
this.label,
|
||||
this.onSubmitted,
|
||||
this.onChanged,
|
||||
this.obscureText = false,
|
||||
this.icon,
|
||||
this.type,
|
||||
this.node,
|
||||
this.autoCorrect = false,
|
||||
this.suggestiion = false,
|
||||
this.errorText,
|
||||
this.prefix,
|
||||
this.autoFocus = false,
|
||||
this.onViewPwdTap,
|
||||
});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _InputState();
|
||||
}
|
||||
|
||||
class _InputState extends State<Input> {
|
||||
bool _obscureText = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_obscureText = widget.obscureText;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CardX(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||
child: TextField(
|
||||
controller: widget.controller,
|
||||
maxLines: widget.maxLines,
|
||||
minLines: widget.minLines,
|
||||
obscureText: _obscureText,
|
||||
decoration: InputDecoration(
|
||||
hintText: widget.hint,
|
||||
labelText: widget.label,
|
||||
errorText: widget.errorText,
|
||||
border: InputBorder.none,
|
||||
prefixIcon: widget.icon == null ? null : Icon(widget.icon),
|
||||
prefix: widget.prefix,
|
||||
suffixIcon: widget.obscureText
|
||||
? IconButton(
|
||||
icon: Icon(
|
||||
_obscureText ? Icons.visibility : Icons.visibility_off,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_obscureText = !_obscureText;
|
||||
});
|
||||
if (widget.onViewPwdTap != null) {
|
||||
widget.onViewPwdTap?.call(_obscureText);
|
||||
}
|
||||
},
|
||||
)
|
||||
: null,
|
||||
),
|
||||
keyboardType: widget.type,
|
||||
focusNode: widget.node,
|
||||
autocorrect: widget.autoCorrect,
|
||||
enableSuggestions: widget.suggestiion,
|
||||
autofocus: widget.autoFocus,
|
||||
onSubmitted: widget.onSubmitted,
|
||||
onChanged: widget.onChanged,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:toolbox/data/res/ui.dart';
|
||||
|
||||
final class KvRow extends StatelessWidget {
|
||||
final String k;
|
||||
final String v;
|
||||
final void Function()? onTap;
|
||||
final Widget? Function()? kBuilder;
|
||||
final Widget? Function()? vBuilder;
|
||||
|
||||
const KvRow({
|
||||
super.key,
|
||||
required this.k,
|
||||
required this.v,
|
||||
this.onTap,
|
||||
this.kBuilder,
|
||||
this.vBuilder,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 3),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
kBuilder?.call() ?? Text(k, style: UIs.text12),
|
||||
UIs.width7,
|
||||
vBuilder?.call() ??
|
||||
Text(
|
||||
v,
|
||||
style: UIs.text11Grey,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
if (onTap != null) UIs.width7,
|
||||
if (onTap != null) const Icon(Icons.keyboard_arrow_right, size: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
import 'package:toolbox/core/extension/context/locale.dart';
|
||||
import 'package:toolbox/core/extension/context/snackbar.dart';
|
||||
import 'package:toolbox/core/utils/ui.dart';
|
||||
import 'package:toolbox/data/res/color.dart';
|
||||
|
||||
final class SimpleMarkdown extends StatelessWidget {
|
||||
const SimpleMarkdown({
|
||||
super.key,
|
||||
required this.data,
|
||||
this.styleSheet,
|
||||
});
|
||||
|
||||
final String data;
|
||||
final MarkdownStyleSheet? styleSheet;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MarkdownBody(
|
||||
data: data,
|
||||
onTapLink: (text, href, title) {
|
||||
if (href != null && href.isNotEmpty) {
|
||||
openUrl(href);
|
||||
return;
|
||||
}
|
||||
context.showSnackBar(l10n.failed);
|
||||
},
|
||||
styleSheet: styleSheet?.copyWith(
|
||||
a: TextStyle(color: primaryColor),
|
||||
) ??
|
||||
MarkdownStyleSheet(
|
||||
a: TextStyle(color: primaryColor),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:circle_chart/circle_chart.dart';
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:toolbox/data/res/color.dart';
|
||||
|
||||
final class PercentCircle extends StatelessWidget {
|
||||
final double percent;
|
||||
@@ -23,7 +23,7 @@ final class PercentCircle extends StatelessWidget {
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
CircleChart(
|
||||
progressColor: primaryColor,
|
||||
progressColor: UIs.primaryColor,
|
||||
progressNumber: percent,
|
||||
maxNumber: 100,
|
||||
width: 57,
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:toolbox/data/res/ui.dart';
|
||||
|
||||
class PopupMenu<T> extends StatelessWidget {
|
||||
final List<PopupMenuEntry<T>> items;
|
||||
final List<T> items;
|
||||
final Widget Function(T) builder;
|
||||
final void Function(T) onSelected;
|
||||
final Widget child;
|
||||
final EdgeInsetsGeometry padding;
|
||||
@@ -11,6 +12,7 @@ class PopupMenu<T> extends StatelessWidget {
|
||||
const PopupMenu({
|
||||
super.key,
|
||||
required this.items,
|
||||
required this.builder,
|
||||
required this.onSelected,
|
||||
this.child = UIs.popMenuChild,
|
||||
this.padding = const EdgeInsets.all(7),
|
||||
@@ -20,7 +22,9 @@ class PopupMenu<T> extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PopupMenuButton<T>(
|
||||
itemBuilder: (_) => items,
|
||||
itemBuilder: (_) => items
|
||||
.map((e) => PopupMenuItem(value: e, child: builder(e)))
|
||||
.toList(),
|
||||
onSelected: onSelected,
|
||||
initialValue: initialValue,
|
||||
padding: padding,
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
final class AvgWidthRow extends StatelessWidget {
|
||||
final List<Widget> children;
|
||||
final double? width;
|
||||
final double padding;
|
||||
|
||||
const AvgWidthRow({
|
||||
super.key,
|
||||
required this.children,
|
||||
this.width,
|
||||
this.padding = 0,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final width =
|
||||
((this.width ?? MediaQuery.of(context).size.width) - padding) /
|
||||
children.length;
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: children.map((e) => SizedBox(width: width, child: e)).toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:toolbox/core/extension/context/common.dart';
|
||||
import 'package:toolbox/core/extension/context/locale.dart';
|
||||
import 'package:toolbox/data/res/ui.dart';
|
||||
import 'package:toolbox/view/widget/future_widget.dart';
|
||||
|
||||
final class SearchPage<T> extends SearchDelegate<T> {
|
||||
final Future<List<T>> Function(String) future;
|
||||
final Widget Function(BuildContext, T) builder;
|
||||
final EdgeInsetsGeometry? padding;
|
||||
final Duration throttleInterval;
|
||||
|
||||
List<T> _cache = [];
|
||||
DateTime? _lastSearch;
|
||||
|
||||
SearchPage({
|
||||
required this.future,
|
||||
required this.builder,
|
||||
this.padding,
|
||||
this.throttleInterval = const Duration(milliseconds: 200),
|
||||
});
|
||||
|
||||
@override
|
||||
List<Widget>? buildActions(BuildContext context) {
|
||||
return [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
query = '';
|
||||
},
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
Widget? buildLeading(BuildContext context) {
|
||||
return IconButton(
|
||||
onPressed: context.pop,
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildResults(BuildContext context) {
|
||||
return _buildList(context);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildSuggestions(BuildContext context) {
|
||||
return _buildList(context);
|
||||
}
|
||||
|
||||
Widget _buildList(BuildContext context) {
|
||||
return FutureWidget(
|
||||
future: _search(query),
|
||||
loading: const Center(child: UIs.centerSizedLoading),
|
||||
error: (error, trace) {
|
||||
return Center(
|
||||
child: Text('$error\n$trace'),
|
||||
);
|
||||
},
|
||||
success: (list) {
|
||||
if (list == null || list.isEmpty) {
|
||||
return Center(child: Text(l10n.noResult));
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
padding: padding,
|
||||
itemCount: list.length,
|
||||
itemBuilder: (_, index) => builder(context, list[index]),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<T>> _search(String query) async {
|
||||
final lastSearch = _lastSearch;
|
||||
if (lastSearch != null) {
|
||||
final now = DateTime.now();
|
||||
if (now.difference(lastSearch) < throttleInterval) {
|
||||
return _cache;
|
||||
}
|
||||
}
|
||||
_cache = await future(query);
|
||||
return _cache;
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,17 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:toolbox/core/extension/context/common.dart';
|
||||
import 'package:toolbox/core/extension/context/dialog.dart';
|
||||
import 'package:toolbox/core/extension/context/locale.dart';
|
||||
import 'package:toolbox/core/extension/context/snackbar.dart';
|
||||
import 'package:toolbox/core/extension/ssh_client.dart';
|
||||
import 'package:toolbox/core/extension/uint8list.dart';
|
||||
import 'package:toolbox/core/utils/platform/base.dart';
|
||||
import 'package:toolbox/core/utils/platform/path.dart';
|
||||
import 'package:toolbox/data/model/app/menu/base.dart';
|
||||
import 'package:toolbox/data/model/app/menu/server_func.dart';
|
||||
import 'package:toolbox/data/model/app/shell_func.dart';
|
||||
import 'package:toolbox/data/model/pkg/manager.dart';
|
||||
import 'package:toolbox/data/model/server/dist.dart';
|
||||
import 'package:toolbox/data/model/server/snippet.dart';
|
||||
import 'package:toolbox/data/res/path.dart';
|
||||
import 'package:toolbox/data/res/provider.dart';
|
||||
import 'package:toolbox/data/res/store.dart';
|
||||
import 'package:toolbox/data/res/ui.dart';
|
||||
import 'package:toolbox/view/widget/count_down_btn.dart';
|
||||
|
||||
import '../../core/route.dart';
|
||||
import '../../core/utils/server.dart';
|
||||
@@ -37,18 +30,8 @@ class ServerFuncBtnsTopRight extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PopupMenu<ServerFuncBtn>(
|
||||
items: ServerFuncBtn.values
|
||||
.map((e) => PopupMenuItem<ServerFuncBtn>(
|
||||
value: e,
|
||||
child: Row(
|
||||
children: [
|
||||
e.icon(),
|
||||
const SizedBox(width: 10),
|
||||
Text(e.toStr),
|
||||
],
|
||||
),
|
||||
))
|
||||
.toList(),
|
||||
items: ServerFuncBtn.values,
|
||||
builder: (e) => PopMenu.build(e, e.icon, e.toStr),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
onSelected: (val) => _onTapMoreBtns(val, spi, context),
|
||||
);
|
||||
@@ -87,7 +70,7 @@ class ServerFuncBtns extends StatelessWidget {
|
||||
onPressed: () => _onTapMoreBtns(e, spi, context),
|
||||
padding: EdgeInsets.zero,
|
||||
tooltip: e.toStr,
|
||||
icon: e.icon(),
|
||||
icon: Icon(e.icon, size: 15),
|
||||
)
|
||||
: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 13),
|
||||
@@ -97,7 +80,7 @@ class ServerFuncBtns extends StatelessWidget {
|
||||
IconButton(
|
||||
onPressed: () => _onTapMoreBtns(e, spi, context),
|
||||
padding: EdgeInsets.zero,
|
||||
icon: e.icon(),
|
||||
icon: Icon(e.icon, size: 17),
|
||||
),
|
||||
Text(e.toStr, style: UIs.text11Grey)
|
||||
],
|
||||
@@ -119,7 +102,7 @@ void _onTapMoreBtns(
|
||||
_onPkg(context, spi);
|
||||
break;
|
||||
case ServerFuncBtn.sftp:
|
||||
AppRoute.sftp(spi: spi).checkGo(
|
||||
AppRoutes.sftp(spi: spi).checkGo(
|
||||
context: context,
|
||||
check: () => _checkClient(context, spi.id),
|
||||
);
|
||||
@@ -130,6 +113,7 @@ void _onTapMoreBtns(
|
||||
return;
|
||||
}
|
||||
final snippets = await context.showPickWithTagDialog<Snippet>(
|
||||
title: l10n.snippet,
|
||||
tags: Pros.snippet.tags,
|
||||
itemsBuilder: (e) {
|
||||
if (e == null) return Pros.snippet.snippets;
|
||||
@@ -138,23 +122,24 @@ void _onTapMoreBtns(
|
||||
.toList();
|
||||
},
|
||||
name: (e) => e.name,
|
||||
all: l10n.all,
|
||||
);
|
||||
if (snippets == null || snippets.isEmpty) return;
|
||||
final snippet = snippets.firstOrNull;
|
||||
if (snippet == null) return;
|
||||
AppRoute.ssh(spi: spi, initCmd: snippet.fmtWith(spi)).checkGo(
|
||||
AppRoutes.ssh(spi: spi, initCmd: snippet.fmtWith(spi)).checkGo(
|
||||
context: context,
|
||||
check: () => _checkClient(context, spi.id),
|
||||
);
|
||||
break;
|
||||
case ServerFuncBtn.container:
|
||||
AppRoute.docker(spi: spi).checkGo(
|
||||
AppRoutes.docker(spi: spi).checkGo(
|
||||
context: context,
|
||||
check: () => _checkClient(context, spi.id),
|
||||
);
|
||||
break;
|
||||
case ServerFuncBtn.process:
|
||||
AppRoute.process(spi: spi).checkGo(
|
||||
AppRoutes.process(spi: spi).checkGo(
|
||||
context: context,
|
||||
check: () => _checkClient(context, spi.id),
|
||||
);
|
||||
@@ -163,7 +148,7 @@ void _onTapMoreBtns(
|
||||
_gotoSSH(spi, context);
|
||||
break;
|
||||
case ServerFuncBtn.iperf:
|
||||
AppRoute.iperf(spi: spi).checkGo(
|
||||
AppRoutes.iperf(spi: spi).checkGo(
|
||||
context: context,
|
||||
check: () => _checkClient(context, spi.id),
|
||||
);
|
||||
@@ -174,7 +159,7 @@ void _onTapMoreBtns(
|
||||
void _gotoSSH(ServerPrivateInfo spi, BuildContext context) async {
|
||||
// run built-in ssh on macOS due to incompatibility
|
||||
if (isMobile || isMacOS) {
|
||||
AppRoute.ssh(spi: spi).go(context);
|
||||
AppRoutes.ssh(spi: spi).go(context);
|
||||
return;
|
||||
}
|
||||
final extraArgs = <String>[];
|
||||
@@ -186,7 +171,7 @@ void _gotoSSH(ServerPrivateInfo spi, BuildContext context) async {
|
||||
final tempKeyFileName = 'srvbox_pk_${spi.keyId}';
|
||||
|
||||
/// For security reason, save the private key file to app doc path
|
||||
return joinPath(await Paths.doc, tempKeyFileName);
|
||||
return Paths.doc.joinPath(tempKeyFileName);
|
||||
}();
|
||||
final file = File(path);
|
||||
final shouldGenKey = spi.keyId != null;
|
||||
@@ -199,12 +184,12 @@ void _gotoSSH(ServerPrivateInfo spi, BuildContext context) async {
|
||||
}
|
||||
|
||||
final sshCommand = ["ssh", "${spi.user}@${spi.ip}"] + extraArgs;
|
||||
final system = OS.type;
|
||||
final system = Pfs.type;
|
||||
switch (system) {
|
||||
case OS.windows:
|
||||
case Pfs.windows:
|
||||
await Process.start("cmd", ["/c", "start"] + sshCommand);
|
||||
break;
|
||||
case OS.linux:
|
||||
case Pfs.linux:
|
||||
await Process.start("x-terminal-emulator", ["-e"] + sshCommand);
|
||||
break;
|
||||
default:
|
||||
@@ -282,7 +267,7 @@ Future<void> _onPkg(BuildContext context, ServerPrivateInfo spi) async {
|
||||
|
||||
// Confirm upgrade
|
||||
final gotoUpgrade = await context.showRoundDialog<bool>(
|
||||
title: Text(l10n.attention),
|
||||
title: l10n.attention,
|
||||
child: SingleChildScrollView(
|
||||
child: Text(
|
||||
'${l10n.pkgUpgradeTip}\n${l10n.foundNUpdate(upgradeable.length)}\n\n$upgradeCmd'),
|
||||
@@ -298,7 +283,7 @@ Future<void> _onPkg(BuildContext context, ServerPrivateInfo spi) async {
|
||||
|
||||
if (gotoUpgrade != true) return;
|
||||
|
||||
AppRoute.ssh(spi: spi, initCmd: upgradeCmd).checkGo(
|
||||
AppRoutes.ssh(spi: spi, initCmd: upgradeCmd).checkGo(
|
||||
context: context,
|
||||
check: () => _checkClient(context, spi.id),
|
||||
);
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:toolbox/view/widget/val_builder.dart';
|
||||
|
||||
import '../../core/persistant_store.dart';
|
||||
|
||||
class StoreSwitch extends StatelessWidget {
|
||||
final StorePropertyBase<bool> prop;
|
||||
|
||||
/// Exec before make change, after validator.
|
||||
final FutureOr<void> Function(bool)? callback;
|
||||
|
||||
/// If return false, the switch will not change.
|
||||
final bool Function(bool)? validator;
|
||||
|
||||
const StoreSwitch({
|
||||
super.key,
|
||||
required this.prop,
|
||||
this.callback,
|
||||
this.validator,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ValBuilder(
|
||||
listenable: prop.listenable(),
|
||||
builder: (value) {
|
||||
return Switch(
|
||||
value: value,
|
||||
onChanged: (value) async {
|
||||
if (validator?.call(value) == false) return;
|
||||
await callback?.call(value);
|
||||
prop.put(value);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,254 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:toolbox/core/extension/context/common.dart';
|
||||
import 'package:toolbox/core/extension/context/dialog.dart';
|
||||
import 'package:toolbox/core/extension/context/locale.dart';
|
||||
import 'package:toolbox/data/res/ui.dart';
|
||||
import 'package:toolbox/view/widget/input_field.dart';
|
||||
import 'package:toolbox/view/widget/cardx.dart';
|
||||
import 'package:toolbox/view/widget/val_builder.dart';
|
||||
|
||||
import '../../data/res/color.dart';
|
||||
|
||||
const _kTagBtnHeight = 31.0;
|
||||
|
||||
class TagBtn extends StatelessWidget {
|
||||
final String content;
|
||||
final void Function() onTap;
|
||||
final bool isEnable;
|
||||
|
||||
const TagBtn({
|
||||
super.key,
|
||||
required this.onTap,
|
||||
required this.isEnable,
|
||||
required this.content,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _wrap(
|
||||
Text(
|
||||
content,
|
||||
textAlign: TextAlign.center,
|
||||
style: isEnable ? UIs.text13 : UIs.text13Grey,
|
||||
),
|
||||
onTap: onTap,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TagEditor extends StatefulWidget {
|
||||
final List<String> tags;
|
||||
final void Function(List<String>)? onChanged;
|
||||
final void Function(String old, String new_)? onRenameTag;
|
||||
final List<String> allTags;
|
||||
|
||||
const TagEditor({
|
||||
super.key,
|
||||
required this.tags,
|
||||
this.onChanged,
|
||||
this.onRenameTag,
|
||||
this.allTags = const <String>[],
|
||||
});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _TagEditorState();
|
||||
}
|
||||
|
||||
class _TagEditorState extends State<TagEditor> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CardX(
|
||||
child: ListTile(
|
||||
// Align the place of TextField.prefixIcon
|
||||
leading: const Padding(
|
||||
padding: EdgeInsets.only(left: 10),
|
||||
child: Icon(Icons.tag),
|
||||
),
|
||||
title: _buildTags(widget.tags),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.add),
|
||||
onPressed: () => _showAddTagDialog(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTags(List<String> tags) {
|
||||
final suggestions = widget.allTags.where((e) => !tags.contains(e)).toList();
|
||||
final suggestionLen = suggestions.length;
|
||||
|
||||
/// Add vertical divider if suggestions.length > 0
|
||||
final counts = tags.length + suggestionLen + (suggestionLen == 0 ? 0 : 1);
|
||||
if (counts == 0) return Text(l10n.tag);
|
||||
return ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: _kTagBtnHeight),
|
||||
child: ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemBuilder: (context, index) {
|
||||
if (index < tags.length) {
|
||||
return _buildTagItem(tags[index]);
|
||||
} else if (index > tags.length) {
|
||||
return _buildTagItem(
|
||||
suggestions[index - tags.length - 1],
|
||||
isAdd: true,
|
||||
);
|
||||
}
|
||||
return const VerticalDivider();
|
||||
},
|
||||
itemCount: counts,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTagItem(String tag, {bool isAdd = false}) {
|
||||
return _wrap(
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'#$tag',
|
||||
textAlign: TextAlign.center,
|
||||
style: isAdd ? UIs.text13Grey : UIs.text13,
|
||||
),
|
||||
const SizedBox(width: 4.0),
|
||||
Icon(
|
||||
isAdd ? Icons.add_circle : Icons.cancel,
|
||||
size: 13.7,
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
if (isAdd) {
|
||||
widget.tags.add(tag);
|
||||
} else {
|
||||
widget.tags.remove(tag);
|
||||
}
|
||||
widget.onChanged?.call(widget.tags);
|
||||
setState(() {});
|
||||
},
|
||||
onLongPress: () => _showRenameDialog(tag),
|
||||
);
|
||||
}
|
||||
|
||||
void _showAddTagDialog() {
|
||||
final textEditingController = TextEditingController();
|
||||
context.showRoundDialog(
|
||||
title: Text(l10n.add),
|
||||
child: Input(
|
||||
autoFocus: true,
|
||||
icon: Icons.tag,
|
||||
controller: textEditingController,
|
||||
hint: l10n.tag,
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
final tag = textEditingController.text;
|
||||
widget.tags.add(tag.trim());
|
||||
widget.onChanged?.call(widget.tags);
|
||||
context.pop();
|
||||
},
|
||||
child: Text(l10n.add),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _showRenameDialog(String tag) {
|
||||
final textEditingController = TextEditingController(text: tag);
|
||||
context.showRoundDialog(
|
||||
title: Text(l10n.rename),
|
||||
child: Input(
|
||||
autoFocus: true,
|
||||
icon: Icons.abc,
|
||||
controller: textEditingController,
|
||||
hint: l10n.tag,
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
final newTag = textEditingController.text.trim();
|
||||
if (newTag.isEmpty) return;
|
||||
widget.onRenameTag?.call(tag, newTag);
|
||||
context.pop();
|
||||
setState(() {});
|
||||
},
|
||||
child: Text(l10n.rename),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TagSwitcher extends StatelessWidget implements PreferredSizeWidget {
|
||||
final ValueNotifier<List<String>> tags;
|
||||
final double width;
|
||||
final void Function(String?) onTagChanged;
|
||||
final String? initTag;
|
||||
|
||||
const TagSwitcher({
|
||||
super.key,
|
||||
required this.tags,
|
||||
required this.width,
|
||||
required this.onTagChanged,
|
||||
this.initTag,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ValBuilder(
|
||||
listenable: tags,
|
||||
builder: (vals) {
|
||||
if (vals.isEmpty) return UIs.placeholder;
|
||||
final items = <String?>[null, ...vals];
|
||||
return Container(
|
||||
height: _kTagBtnHeight,
|
||||
width: width,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 7),
|
||||
alignment: Alignment.center,
|
||||
color: Colors.transparent,
|
||||
child: ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemBuilder: (context, index) {
|
||||
final item = items[index];
|
||||
return TagBtn(
|
||||
content: item == null ? l10n.all : '#$item',
|
||||
isEnable: initTag == item,
|
||||
onTap: () => onTagChanged(item),
|
||||
);
|
||||
},
|
||||
itemCount: items.length,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Size get preferredSize => const Size.fromHeight(_kTagBtnHeight);
|
||||
}
|
||||
|
||||
Widget _wrap(
|
||||
Widget child, {
|
||||
void Function()? onTap,
|
||||
void Function()? onLongPress,
|
||||
}) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(3),
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(20.0)),
|
||||
child: Material(
|
||||
color: primaryColor.withAlpha(20),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
onLongPress: onLongPress,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(11.7, 2.7, 11.7, 0),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:toolbox/data/res/ui.dart';
|
||||
|
||||
class TwoLineText extends StatelessWidget {
|
||||
const TwoLineText({super.key, required this.up, required this.down});
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:toolbox/data/res/color.dart';
|
||||
|
||||
import '../../core/utils/ui.dart';
|
||||
|
||||
final _reg = RegExp(
|
||||
r"(https?|ftp|file)://[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]*");
|
||||
|
||||
class UrlText extends StatelessWidget {
|
||||
final String text;
|
||||
final String? replace;
|
||||
final TextAlign? textAlign;
|
||||
final TextStyle style;
|
||||
|
||||
const UrlText({
|
||||
super.key,
|
||||
required this.text,
|
||||
this.replace,
|
||||
this.textAlign,
|
||||
this.style = const TextStyle(),
|
||||
});
|
||||
|
||||
List<InlineSpan> _buildTextSpans(Color c) {
|
||||
final widgets = <InlineSpan>[];
|
||||
int start = 0;
|
||||
|
||||
for (final match in _reg.allMatches(text)) {
|
||||
final group0 = match.group(0);
|
||||
if (group0 != null && group0.isNotEmpty) {
|
||||
if (start != match.start) {
|
||||
widgets.add(
|
||||
TextSpan(
|
||||
text: text.substring(start, match.start),
|
||||
style: style.copyWith(color: c),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
widgets.add(_LinkTextSpan(
|
||||
replace: replace,
|
||||
text: group0,
|
||||
style: style.copyWith(color: primaryColor),
|
||||
));
|
||||
start = match.end;
|
||||
}
|
||||
}
|
||||
|
||||
if (start < text.length) {
|
||||
widgets.add(
|
||||
TextSpan(
|
||||
text: text.substring(start),
|
||||
style: style.copyWith(color: c),
|
||||
),
|
||||
);
|
||||
}
|
||||
return widgets;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RichText(
|
||||
textAlign: textAlign ?? TextAlign.start,
|
||||
text: TextSpan(
|
||||
children: _buildTextSpans(DynamicColors.content.resolve(context)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LinkTextSpan extends TextSpan {
|
||||
_LinkTextSpan({super.style, required String text, String? replace})
|
||||
: super(
|
||||
text: replace ?? text,
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
openUrl(text);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
final class ValBuilder<T> extends ValueListenableBuilder<T> {
|
||||
ValBuilder({
|
||||
super.key,
|
||||
required ValueListenable<T> listenable,
|
||||
required Widget Function(T) builder,
|
||||
}) : super(
|
||||
valueListenable: listenable,
|
||||
builder: (_, val, __) => builder(val),
|
||||
);
|
||||
}
|
||||
|
||||
final class ValChildBuilder<T> extends ValueListenableBuilder<T> {
|
||||
ValChildBuilder({
|
||||
super.key,
|
||||
required ValueListenable<T> listenable,
|
||||
required Widget Function(T, Widget?) builder,
|
||||
super.child,
|
||||
}) : super(
|
||||
valueListenable: listenable,
|
||||
builder: (_, val, child) => builder(val, child),
|
||||
);
|
||||
}
|
||||
|
||||
final class ListenBuilder extends ListenableBuilder {
|
||||
ListenBuilder({
|
||||
super.key,
|
||||
required super.listenable,
|
||||
required Widget Function() builder,
|
||||
}) : super(
|
||||
builder: (_, __) => builder(),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user