opt.: migrate fl_lib

This commit is contained in:
lollipopkit
2024-05-14 22:29:37 +08:00
parent 248430e5b0
commit 04dfede535
136 changed files with 686 additions and 3896 deletions

View File

@@ -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()),
);
}

View File

@@ -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);

View File

@@ -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});

View File

@@ -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 {

View File

@@ -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);

View File

@@ -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);

View File

@@ -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,
),
),
),

View File

@@ -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);

View File

@@ -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),
),

View File

@@ -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})',
)),

View File

@@ -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(

View File

@@ -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),
))

View File

@@ -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),
),

View File

@@ -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(

View File

@@ -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(

View File

@@ -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,

View File

@@ -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,

View File

@@ -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);

View File

@@ -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() {

View File

@@ -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});

View File

@@ -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),
],

View File

@@ -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});

View File

@@ -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()),
],

View File

@@ -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,
);

View File

@@ -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);
// }
// }
}

View File

@@ -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;

View File

@@ -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(

View File

@@ -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(

View File

@@ -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(

View File

@@ -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(

View File

@@ -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)',
)),

View File

@@ -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);
}

View File

@@ -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);
}
}
}

View File

@@ -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,
);
}
}

View File

@@ -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),
),
);
}
}

View File

@@ -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(),
),
),
],
);
}
}

View File

@@ -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,
),
),
);
}
}

View File

@@ -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);
}

View File

@@ -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,
);
}
}

View File

@@ -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);
}
},
);
}
}

View File

@@ -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),
),
);
}
}

View File

@@ -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),
],
));
}
}

View File

@@ -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,
),
),
);
}
}

View File

@@ -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),
],
),
),
);
}
}

View File

@@ -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),
),
);
}
}

View File

@@ -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,

View File

@@ -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,

View File

@@ -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(),
);
}
}

View File

@@ -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;
}
}

View File

@@ -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),
);

View File

@@ -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);
},
);
},
);
}
}

View File

@@ -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,
),
),
),
),
);
}

View File

@@ -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});

View File

@@ -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);
},
);
}

View File

@@ -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(),
);
}