mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2025-12-17 07:14:28 +01:00
opt.: redesigned settings page (#587)
This commit is contained in:
@@ -15,18 +15,23 @@ import 'package:server_box/data/res/store.dart';
|
||||
import 'package:icons_plus/icons_plus.dart';
|
||||
import 'package:server_box/data/store/no_backup.dart';
|
||||
|
||||
final icloudLoading = false.vn;
|
||||
final webdavLoading = false.vn;
|
||||
|
||||
final _noBak = NoBackupStore.instance;
|
||||
|
||||
class BackupPage extends StatelessWidget {
|
||||
class BackupPage extends StatefulWidget {
|
||||
const BackupPage({super.key});
|
||||
|
||||
@override
|
||||
State<BackupPage> createState() => _BackupPageState();
|
||||
}
|
||||
|
||||
final class _BackupPageState extends State<BackupPage>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
final _noBak = NoBackupStore.instance;
|
||||
final icloudLoading = false.vn;
|
||||
final webdavLoading = false.vn;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return Scaffold(
|
||||
appBar: CustomAppBar(title: Text(libL10n.backup)),
|
||||
body: _buildBody(context),
|
||||
);
|
||||
}
|
||||
@@ -477,4 +482,7 @@ class BackupPage extends StatelessWidget {
|
||||
Loggers.app.warning('Import servers failed', e, s);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
}
|
||||
|
||||
@@ -1,40 +1,20 @@
|
||||
part of 'home.dart';
|
||||
|
||||
final class _AppBar extends CustomAppBar {
|
||||
final ValueNotifier<int> selectIndex;
|
||||
final ValueNotifier<bool> landscape;
|
||||
final class _AppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
final double paddingTop;
|
||||
|
||||
const _AppBar({
|
||||
required this.selectIndex,
|
||||
required this.landscape,
|
||||
super.title,
|
||||
super.actions,
|
||||
super.centerTitle,
|
||||
});
|
||||
const _AppBar(this.paddingTop);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final placeholder = SizedBox(
|
||||
height: CustomAppBar.sysStatusBarHeight ??
|
||||
0 + MediaQuery.of(context).padding.top,
|
||||
);
|
||||
return selectIndex.listenVal(
|
||||
(idx) {
|
||||
if (isDesktop) return super.build(context);
|
||||
|
||||
if (idx == AppTab.ssh.index) {
|
||||
return placeholder;
|
||||
}
|
||||
|
||||
return ValBuilder(
|
||||
listenable: landscape,
|
||||
builder: (ls) {
|
||||
if (ls) return placeholder;
|
||||
|
||||
return super.build(context);
|
||||
},
|
||||
);
|
||||
},
|
||||
return SizedBox(
|
||||
height: paddingTop,
|
||||
child: isIOS
|
||||
? const Center(child: Text(BuildData.name, style: UIs.text15Bold))
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Size get preferredSize => Size.fromHeight(paddingTop);
|
||||
}
|
||||
|
||||
@@ -1,17 +1,10 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:icons_plus/icons_plus.dart';
|
||||
import 'package:server_box/core/channel/home_widget.dart';
|
||||
import 'package:server_box/core/extension/context/locale.dart';
|
||||
import 'package:server_box/core/route.dart';
|
||||
import 'package:server_box/data/model/app/tab.dart';
|
||||
import 'package:server_box/data/provider/app.dart';
|
||||
import 'package:server_box/data/provider/server.dart';
|
||||
import 'package:server_box/data/res/build_data.dart';
|
||||
import 'package:server_box/data/res/github_id.dart';
|
||||
import 'package:server_box/data/res/misc.dart';
|
||||
import 'package:server_box/data/res/store.dart';
|
||||
import 'package:server_box/data/res/url.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
@@ -104,39 +97,10 @@ class _HomePageState extends State<HomePage>
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
AppProvider.ctx = context;
|
||||
final sysPadding = MediaQuery.of(context).padding;
|
||||
|
||||
final appBar = _AppBar(
|
||||
selectIndex: _selectIndex,
|
||||
landscape: _isLandscape,
|
||||
centerTitle: false,
|
||||
title: const Text(BuildData.name),
|
||||
actions: <Widget>[
|
||||
ValBuilder(
|
||||
listenable: Stores.setting.serverStatusUpdateInterval.listenable(),
|
||||
builder: (interval) {
|
||||
if (interval != 0) return UIs.placeholder;
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.refresh),
|
||||
tooltip: 'Refresh',
|
||||
onPressed: () async {
|
||||
await ServerProvider.refresh();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.developer_mode, size: 21),
|
||||
tooltip: 'Debug',
|
||||
onPressed: () => DebugPage.route.go(
|
||||
context,
|
||||
args: const DebugPageArgs(title: 'Debug(${BuildData.build})'),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
return Scaffold(
|
||||
drawer: _buildDrawer(),
|
||||
appBar: appBar,
|
||||
appBar: _AppBar(sysPadding.top),
|
||||
body: PageView.builder(
|
||||
controller: _pageController,
|
||||
itemCount: AppTab.values.length,
|
||||
@@ -184,133 +148,7 @@ class _HomePageState extends State<HomePage>
|
||||
labelBehavior: ls
|
||||
? NavigationDestinationLabelBehavior.alwaysHide
|
||||
: NavigationDestinationLabelBehavior.onlyShowSelected,
|
||||
destinations: [
|
||||
NavigationDestination(
|
||||
icon: const Icon(BoxIcons.bx_server),
|
||||
label: l10n.server,
|
||||
selectedIcon: const Icon(BoxIcons.bxs_server),
|
||||
),
|
||||
const NavigationDestination(
|
||||
icon: Icon(Icons.terminal_outlined),
|
||||
label: 'SSH',
|
||||
selectedIcon: Icon(Icons.terminal),
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: const Icon(MingCute.file_code_line),
|
||||
label: l10n.snippet,
|
||||
selectedIcon: const Icon(MingCute.file_code_fill),
|
||||
),
|
||||
const NavigationDestination(
|
||||
icon: Icon(MingCute.planet_line),
|
||||
label: 'Ping',
|
||||
selectedIcon: Icon(MingCute.planet_fill),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDrawer() {
|
||||
return Drawer(
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_buildIcon(),
|
||||
const Text(
|
||||
'${BuildData.name}\nv${BuildData.build}',
|
||||
textAlign: TextAlign.center,
|
||||
style: UIs.text15,
|
||||
),
|
||||
const SizedBox(height: 37),
|
||||
_buildTiles(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTiles() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 17),
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.settings),
|
||||
title: Text(libL10n.setting),
|
||||
onTap: () => AppRoutes.settings().go(context),
|
||||
onLongPress: _onLongPressSetting,
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.vpn_key),
|
||||
title: Text(l10n.privateKey),
|
||||
onTap: () => AppRoutes.keyList().go(context),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(BoxIcons.bxs_file_blank),
|
||||
title: Text(libL10n.file),
|
||||
onTap: () => AppRoutes.localStorage().go(context),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(MingCute.file_import_fill),
|
||||
title: Text(libL10n.backup),
|
||||
onTap: () => AppRoutes.backup().go(context),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(OctIcons.feed_discussion),
|
||||
title: Text('${libL10n.about} & ${libL10n.feedback}'),
|
||||
onTap: _showAboutDialog,
|
||||
)
|
||||
].map((e) => CardX(child: e)).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showAboutDialog() {
|
||||
context.showRoundDialog(
|
||||
title: libL10n.about,
|
||||
child: _buildAboutContent(),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Urls.appWiki.launch(),
|
||||
child: const Text('Wiki'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Urls.appHelp.launch(),
|
||||
child: Text(libL10n.feedback),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => showLicensePage(context: context),
|
||||
child: Text(l10n.license),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAboutContent() {
|
||||
return SingleChildScrollView(
|
||||
child: SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.8,
|
||||
child: SimpleMarkdown(
|
||||
data: '''
|
||||
${l10n.madeWithLove('[lollipopkit](${Urls.myGithub})')}
|
||||
|
||||
#### Contributors
|
||||
${GithubIds.contributors.map((e) => '[$e](${e.url})').join(' ')}
|
||||
|
||||
#### Participants
|
||||
${GithubIds.participants.map((e) => '[$e](${e.url})').join(' ')}
|
||||
|
||||
#### My other apps
|
||||
- [GPT Box](https://github.com/lollipopkit/flutter_gpt_box)
|
||||
''',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildIcon() {
|
||||
return ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: 57, maxWidth: 57),
|
||||
child: UIs.appIcon,
|
||||
destinations: AppTab.navDestinations,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -366,35 +204,4 @@ ${GithubIds.participants.map((e) => '[$e](${e.url})').join(' ')}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onLongPressSetting() async {
|
||||
final map = Stores.setting.box.toJson(includeInternal: false);
|
||||
final keys = map.keys;
|
||||
|
||||
/// Encode [map] to String with indent `\t`
|
||||
final text = Miscs.jsonEncoder.convert(map);
|
||||
final result = await AppRoutes.editor(
|
||||
text: text,
|
||||
langCode: 'json',
|
||||
title: libL10n.setting,
|
||||
).go<String>(context);
|
||||
if (result == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final newSettings = json.decode(result) as Map<String, dynamic>;
|
||||
Stores.setting.box.putAll(newSettings);
|
||||
final newKeys = newSettings.keys;
|
||||
final removedKeys = keys.where((e) => !newKeys.contains(e));
|
||||
for (final key in removedKeys) {
|
||||
Stores.setting.box.delete(key);
|
||||
}
|
||||
} catch (e, trace) {
|
||||
context.showRoundDialog(
|
||||
title: libL10n.error,
|
||||
child: Text('${l10n.save}:\n$e'),
|
||||
);
|
||||
Loggers.app.warning('Update json settings failed', e, trace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,9 +22,6 @@ class _PrivateKeyListState extends State<PrivateKeysListPage>
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: CustomAppBar(
|
||||
title: Text(l10n.privateKey),
|
||||
),
|
||||
body: _buildBody(),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
child: const Icon(Icons.add),
|
||||
|
||||
@@ -118,10 +118,10 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
||||
return CustomAppBar(
|
||||
title: Text(si.spi.name),
|
||||
actions: [
|
||||
ShareBtn(
|
||||
QrShareBtn(
|
||||
data: si.spi.toJsonString(),
|
||||
tip: si.spi.name,
|
||||
tip2: '${libL10n.share} ${l10n.server} ~ ServerBox',
|
||||
tip2: '${l10n.server} ~ ServerBox',
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit),
|
||||
|
||||
@@ -610,13 +610,12 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
||||
envs: _env.value.isEmpty ? null : _env.value,
|
||||
);
|
||||
|
||||
final existsIds = ServerStore.instance.box.keys;
|
||||
if (existsIds.contains(spi.id)) {
|
||||
context.showSnackBar('${l10n.sameIdServerExist}: ${spi.id}');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.spi == null) {
|
||||
final existsIds = ServerStore.instance.box.keys;
|
||||
if (existsIds.contains(spi.id)) {
|
||||
context.showSnackBar('${l10n.sameIdServerExist}: ${spi.id}');
|
||||
return;
|
||||
}
|
||||
ServerProvider.addServer(spi);
|
||||
} else {
|
||||
ServerProvider.updateServer(this.spi!, spi);
|
||||
|
||||
@@ -8,8 +8,10 @@ import 'package:server_box/core/extension/context/locale.dart';
|
||||
import 'package:server_box/core/extension/ssh_client.dart';
|
||||
import 'package:server_box/data/model/app/shell_func.dart';
|
||||
import 'package:server_box/data/model/server/try_limiter.dart';
|
||||
import 'package:server_box/data/res/build_data.dart';
|
||||
import 'package:server_box/data/res/store.dart';
|
||||
import 'package:server_box/view/page/server/edit.dart';
|
||||
import 'package:server_box/view/page/setting/entry.dart';
|
||||
import 'package:server_box/view/widget/percent_circle.dart';
|
||||
|
||||
import 'package:server_box/core/route.dart';
|
||||
@@ -19,6 +21,8 @@ import 'package:server_box/data/model/server/server_private_info.dart';
|
||||
import 'package:server_box/data/provider/server.dart';
|
||||
import 'package:server_box/view/widget/server_func_btns.dart';
|
||||
|
||||
part 'top_bar.dart';
|
||||
|
||||
class ServerPage extends StatefulWidget {
|
||||
const ServerPage({super.key});
|
||||
|
||||
@@ -85,7 +89,7 @@ class _ServerPageState extends State<ServerPage>
|
||||
|
||||
Widget _buildPortrait() {
|
||||
return Scaffold(
|
||||
appBar: TagSwitcher(
|
||||
appBar: _TopBar(
|
||||
tags: ServerProvider.tags,
|
||||
onTagChanged: (p0) => _tag.value = p0,
|
||||
initTag: _tag.value,
|
||||
@@ -130,7 +134,7 @@ class _ServerPageState extends State<ServerPage>
|
||||
top: 0,
|
||||
left: 0,
|
||||
child: IconButton(
|
||||
onPressed: () => AppRoutes.settings().go(context),
|
||||
onPressed: () => SettingsPage.route.go(context),
|
||||
icon: const Icon(Icons.settings, color: Colors.grey),
|
||||
),
|
||||
),
|
||||
@@ -482,30 +486,18 @@ class _ServerPageState extends State<ServerPage>
|
||||
null,
|
||||
),
|
||||
ServerConn.failed => (
|
||||
const Icon(
|
||||
Icons.refresh,
|
||||
size: 21,
|
||||
color: Colors.grey,
|
||||
),
|
||||
const Icon(Icons.refresh, size: 21, color: Colors.grey),
|
||||
() {
|
||||
TryLimiter.reset(s.spi.id);
|
||||
ServerProvider.refresh(spi: s.spi);
|
||||
},
|
||||
),
|
||||
ServerConn.disconnected => (
|
||||
const Icon(
|
||||
MingCute.link_3_line,
|
||||
size: 19,
|
||||
color: Colors.grey,
|
||||
),
|
||||
const Icon(MingCute.link_3_line, size: 19, color: Colors.grey),
|
||||
() => ServerProvider.refresh(spi: s.spi)
|
||||
),
|
||||
ServerConn.finished => (
|
||||
const Icon(
|
||||
MingCute.unlink_2_line,
|
||||
size: 17,
|
||||
color: Colors.grey,
|
||||
),
|
||||
const Icon(MingCute.unlink_2_line, size: 17, color: Colors.grey),
|
||||
() => ServerProvider.closeServer(id: s.spi.id),
|
||||
),
|
||||
_ when Stores.setting.serverTabUseOldUI.fetch() => (
|
||||
@@ -517,11 +509,7 @@ class _ServerPageState extends State<ServerPage>
|
||||
// Or the loading icon will be rescaled.
|
||||
final wrapped = child is SizedBox
|
||||
? child
|
||||
: SizedBox(
|
||||
height: _kCardHeightMin,
|
||||
width: 27,
|
||||
child: child,
|
||||
);
|
||||
: SizedBox(height: _kCardHeightMin, width: 27, child: child);
|
||||
if (onTap == null) return wrapped.paddingOnly(left: 10);
|
||||
return InkWell(
|
||||
borderRadius: BorderRadius.circular(7),
|
||||
@@ -654,7 +642,7 @@ ${ss.err?.message ?? 'null'}
|
||||
|
||||
List<String> _filterServers(List<String> order) {
|
||||
final tag = _tag.value;
|
||||
if (tag == kDefaultTag) return order;
|
||||
if (tag == TagSwitcher.kDefaultTag) return order;
|
||||
return order.where((e) {
|
||||
final tags = ServerProvider.pick(id: e)?.value.spi.tags;
|
||||
if (tags == null) return false;
|
||||
|
||||
43
lib/view/page/server/top_bar.dart
Normal file
43
lib/view/page/server/top_bar.dart
Normal file
@@ -0,0 +1,43 @@
|
||||
part of 'tab.dart';
|
||||
|
||||
final class _TopBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
final ValueNotifier<Set<String>> tags;
|
||||
final void Function(String) onTagChanged;
|
||||
final String initTag;
|
||||
|
||||
const _TopBar({
|
||||
required this.initTag,
|
||||
required this.onTagChanged,
|
||||
required this.tags,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 17),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Center(
|
||||
child: Text(
|
||||
BuildData.name,
|
||||
style: TextStyle(fontSize: 20),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 30),
|
||||
TagSwitcher(
|
||||
tags: tags,
|
||||
onTagChanged: onTagChanged,
|
||||
initTag: initTag,
|
||||
singleLine: true,
|
||||
reversed: true,
|
||||
).expanded(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Size get preferredSize => const Size.fromHeight(TagSwitcher.kTagBtnHeight);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
@@ -6,6 +7,7 @@ import 'package:flutter_highlight/theme_map.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:icons_plus/icons_plus.dart';
|
||||
import 'package:server_box/core/extension/context/locale.dart';
|
||||
import 'package:server_box/data/res/github_id.dart';
|
||||
import 'package:server_box/data/res/rebuild.dart';
|
||||
import 'package:server_box/data/res/store.dart';
|
||||
import 'package:server_box/data/res/url.dart';
|
||||
@@ -13,70 +15,196 @@ import 'package:server_box/data/res/url.dart';
|
||||
import 'package:server_box/core/route.dart';
|
||||
import 'package:server_box/data/model/app/net_view.dart';
|
||||
import 'package:server_box/data/res/build_data.dart';
|
||||
import 'package:server_box/view/page/backup.dart';
|
||||
import 'package:server_box/view/page/private_key/list.dart';
|
||||
|
||||
const _kIconSize = 23.0;
|
||||
|
||||
class SettingPage extends StatefulWidget {
|
||||
const SettingPage({super.key});
|
||||
enum SettingsTabs {
|
||||
app,
|
||||
privateKey,
|
||||
backup,
|
||||
about,
|
||||
;
|
||||
|
||||
@override
|
||||
State<SettingPage> createState() => _SettingPageState();
|
||||
String get i18n => switch (this) {
|
||||
SettingsTabs.app => libL10n.app,
|
||||
SettingsTabs.privateKey => l10n.privateKey,
|
||||
SettingsTabs.backup => libL10n.backup,
|
||||
SettingsTabs.about => libL10n.about,
|
||||
};
|
||||
|
||||
Widget get page => switch (this) {
|
||||
SettingsTabs.app => const AppSettingsPage(),
|
||||
SettingsTabs.privateKey => const PrivateKeysListPage(),
|
||||
SettingsTabs.backup => const BackupPage(),
|
||||
SettingsTabs.about => const AppAboutPage(),
|
||||
};
|
||||
|
||||
static final List<Widget> pages =
|
||||
SettingsTabs.values.map((e) => e.page).toList();
|
||||
}
|
||||
|
||||
class _SettingPageState extends State<SettingPage> {
|
||||
final _setting = Stores.setting;
|
||||
class SettingsPage extends StatefulWidget {
|
||||
const SettingsPage({super.key});
|
||||
|
||||
static const route = AppRouteNoArg(page: SettingsPage.new, path: '/settings');
|
||||
|
||||
@override
|
||||
State<SettingsPage> createState() => _SettingsPageState();
|
||||
}
|
||||
|
||||
class _SettingsPageState extends State<SettingsPage>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final _tabCtrl =
|
||||
TabController(length: SettingsTabs.values.length, vsync: this);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: CustomAppBar(
|
||||
title: Text(libL10n.setting),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete),
|
||||
onPressed: () => context.showRoundDialog(
|
||||
title: libL10n.attention,
|
||||
child: SimpleMarkdown(
|
||||
data: libL10n.askContinue(
|
||||
'${libL10n.delete} **${libL10n.all}** ${libL10n.setting}',
|
||||
),
|
||||
),
|
||||
actions: Btn.ok(
|
||||
onTap: () {
|
||||
context.pop();
|
||||
_setting.box.deleteAll(_setting.box.keys);
|
||||
context.showSnackBar(libL10n.success);
|
||||
},
|
||||
red: true,
|
||||
).toList,
|
||||
),
|
||||
),
|
||||
],
|
||||
appBar: TabBar(
|
||||
controller: _tabCtrl,
|
||||
dividerHeight: 0,
|
||||
tabAlignment: TabAlignment.center,
|
||||
isScrollable: true,
|
||||
tabs: SettingsTabs.values
|
||||
.map((e) => Tab(text: e.i18n))
|
||||
.toList(growable: false),
|
||||
),
|
||||
body: MultiList(
|
||||
widthDivider: 2.3,
|
||||
thumbVisibility: true,
|
||||
children: [
|
||||
[const CenterGreyTitle('App'), _buildApp()],
|
||||
[CenterGreyTitle(l10n.server), _buildServer()],
|
||||
[
|
||||
const CenterGreyTitle('SSH'),
|
||||
_buildSSH(),
|
||||
const CenterGreyTitle('SFTP'),
|
||||
_buildSFTP()
|
||||
],
|
||||
[
|
||||
CenterGreyTitle(l10n.container),
|
||||
_buildContainer(),
|
||||
CenterGreyTitle(l10n.editor),
|
||||
_buildEditor(),
|
||||
],
|
||||
// actions: [
|
||||
// IconButton(
|
||||
// icon: const Icon(Icons.delete),
|
||||
// onPressed: () => context.showRoundDialog(
|
||||
// title: libL10n.attention,
|
||||
// child: SimpleMarkdown(
|
||||
// data: libL10n.askContinue(
|
||||
// '${libL10n.delete} **${libL10n.all}** ${libL10n.setting}',
|
||||
// ),
|
||||
// ),
|
||||
// actions: [
|
||||
// CountDownBtn(
|
||||
// onTap: () {
|
||||
// context.pop();
|
||||
// _setting.box.deleteAll(_setting.box.keys);
|
||||
// context.showSnackBar(libL10n.success);
|
||||
// },
|
||||
// afterColor: Colors.red,
|
||||
// )
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
body: TabBarView(controller: _tabCtrl, children: SettingsTabs.pages),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Fullscreen Mode is designed for old mobile phone which can be
|
||||
/// used as a status screen.
|
||||
if (isMobile) [CenterGreyTitle(l10n.fullScreen), _buildFullScreen()],
|
||||
final class AppAboutPage extends StatefulWidget {
|
||||
const AppAboutPage({super.key});
|
||||
|
||||
@override
|
||||
State<AppAboutPage> createState() => _AppAboutPageState();
|
||||
}
|
||||
|
||||
final class _AppAboutPageState extends State<AppAboutPage>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(13),
|
||||
children: [
|
||||
UIs.height13,
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: 47, maxWidth: 47),
|
||||
child: UIs.appIcon,
|
||||
),
|
||||
const Text(
|
||||
'${BuildData.name}\nv${BuildData.build}',
|
||||
textAlign: TextAlign.center,
|
||||
style: UIs.text15,
|
||||
),
|
||||
UIs.height13,
|
||||
SizedBox(
|
||||
height: 47,
|
||||
child: ListView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
children: <Widget>[
|
||||
Btn.elevated(
|
||||
icon: const Icon(Icons.edit_document),
|
||||
text: 'Wiki',
|
||||
onTap: Urls.appWiki.launch,
|
||||
),
|
||||
Btn.elevated(
|
||||
icon: const Icon(Icons.feedback),
|
||||
text: libL10n.feedback,
|
||||
onTap: Urls.appHelp.launch,
|
||||
),
|
||||
Btn.elevated(
|
||||
icon: const Icon(MingCute.question_fill),
|
||||
text: l10n.license,
|
||||
onTap: () => showLicensePage(context: context),
|
||||
),
|
||||
].joinWith(UIs.width13),
|
||||
),
|
||||
),
|
||||
UIs.height13,
|
||||
SimpleMarkdown(
|
||||
data: '''
|
||||
#### Contributors
|
||||
${GithubIds.contributors.map((e) => '[$e](${e.url})').join(' ')}
|
||||
|
||||
#### Participants
|
||||
${GithubIds.participants.map((e) => '[$e](${e.url})').join(' ')}
|
||||
|
||||
#### My other apps
|
||||
[GPT Box](https://github.com/lollipopkit/flutter_gpt_box)
|
||||
|
||||
${l10n.madeWithLove('[lollipopkit](${Urls.myGithub})')}
|
||||
''',
|
||||
).paddingAll(13).cardx,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
}
|
||||
|
||||
final class AppSettingsPage extends StatefulWidget {
|
||||
const AppSettingsPage({super.key});
|
||||
|
||||
@override
|
||||
State<AppSettingsPage> createState() => _AppSettingsPageState();
|
||||
}
|
||||
|
||||
final class _AppSettingsPageState extends State<AppSettingsPage> {
|
||||
final _setting = Stores.setting;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MultiList(
|
||||
thumbVisibility: true,
|
||||
children: [
|
||||
[const CenterGreyTitle('App'), _buildApp()],
|
||||
[CenterGreyTitle(l10n.server), _buildServer()],
|
||||
[
|
||||
const CenterGreyTitle('SSH'),
|
||||
_buildSSH(),
|
||||
const CenterGreyTitle('SFTP'),
|
||||
_buildSFTP()
|
||||
],
|
||||
),
|
||||
[
|
||||
CenterGreyTitle(l10n.container),
|
||||
_buildContainer(),
|
||||
CenterGreyTitle(l10n.editor),
|
||||
_buildEditor(),
|
||||
],
|
||||
|
||||
/// Fullscreen Mode is designed for old mobile phone which can be
|
||||
/// used as a status screen.
|
||||
if (isMobile) [CenterGreyTitle(l10n.fullScreen), _buildFullScreen()],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -94,9 +222,7 @@ class _SettingPageState extends State<SettingPage> {
|
||||
_buildAppMore(),
|
||||
];
|
||||
|
||||
return Column(
|
||||
children: children.map((e) => CardX(child: e)).toList(),
|
||||
);
|
||||
return Column(children: children.map((e) => e.cardx).toList());
|
||||
}
|
||||
|
||||
Widget _buildFullScreen() {
|
||||
@@ -1182,4 +1308,35 @@ class _SettingPageState extends State<SettingPage> {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onLongPressSetting() async {
|
||||
final map = Stores.setting.box.toJson(includeInternal: false);
|
||||
final keys = map.keys;
|
||||
|
||||
/// Encode [map] to String with indent `\t`
|
||||
final text = jsonIndentEncoder.convert(map);
|
||||
final result = await AppRoutes.editor(
|
||||
text: text,
|
||||
langCode: 'json',
|
||||
title: libL10n.setting,
|
||||
).go<String>(context);
|
||||
if (result == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final newSettings = json.decode(result) as Map<String, dynamic>;
|
||||
Stores.setting.box.putAll(newSettings);
|
||||
final newKeys = newSettings.keys;
|
||||
final removedKeys = keys.where((e) => !newKeys.contains(e));
|
||||
for (final key in removedKeys) {
|
||||
Stores.setting.box.delete(key);
|
||||
}
|
||||
} catch (e, trace) {
|
||||
context.showRoundDialog(
|
||||
title: libL10n.error,
|
||||
child: Text('${l10n.save}:\n$e'),
|
||||
);
|
||||
Loggers.app.warning('Update json settings failed', e, trace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,11 +13,13 @@ class SnippetListPage extends StatefulWidget {
|
||||
State<SnippetListPage> createState() => _SnippetListPageState();
|
||||
}
|
||||
|
||||
class _SnippetListPageState extends State<SnippetListPage> {
|
||||
class _SnippetListPageState extends State<SnippetListPage>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
final _tag = ''.vn;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return Scaffold(
|
||||
appBar: TagSwitcher(
|
||||
tags: SnippetProvider.tags,
|
||||
@@ -43,7 +45,7 @@ class _SnippetListPageState extends State<SnippetListPage> {
|
||||
}
|
||||
|
||||
Widget _buildSnippetList(List<Snippet> snippets, String tag) {
|
||||
final filtered = tag == kDefaultTag
|
||||
final filtered = tag == TagSwitcher.kDefaultTag
|
||||
? snippets
|
||||
: snippets.where((e) => e.tags?.contains(tag) ?? false).toList();
|
||||
|
||||
@@ -95,6 +97,9 @@ class _SnippetListPageState extends State<SnippetListPage> {
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
|
||||
// Future<void> _runSnippet(Snippet snippet) async {
|
||||
// final servers = await context.showPickDialog<Server>(
|
||||
// items: Pros.server.servers.toList(),
|
||||
|
||||
@@ -300,7 +300,9 @@ class SSHPageState extends State<SSHPage>
|
||||
title: l10n.snippet,
|
||||
tags: SnippetProvider.tags,
|
||||
itemsBuilder: (e) {
|
||||
if (e == kDefaultTag) return SnippetProvider.snippets.value;
|
||||
if (e == TagSwitcher.kDefaultTag) {
|
||||
return SnippetProvider.snippets.value;
|
||||
}
|
||||
return SnippetProvider.snippets.value
|
||||
.where((element) => element.tags?.contains(e) ?? false)
|
||||
.toList();
|
||||
|
||||
@@ -8,187 +8,141 @@ import 'package:server_box/data/model/sftp/worker.dart';
|
||||
import 'package:server_box/data/provider/server.dart';
|
||||
import 'package:server_box/data/provider/sftp.dart';
|
||||
import 'package:server_box/data/res/misc.dart';
|
||||
import 'package:server_box/view/widget/omit_start_text.dart';
|
||||
|
||||
import 'package:server_box/core/route.dart';
|
||||
import 'package:server_box/data/model/app/path_with_prefix.dart';
|
||||
|
||||
class LocalStoragePage extends StatefulWidget {
|
||||
final bool isPickFile;
|
||||
final class LocalFilePageArgs {
|
||||
final bool? isPickFile;
|
||||
final String? initDir;
|
||||
const LocalStoragePage({
|
||||
super.key,
|
||||
required this.isPickFile,
|
||||
const LocalFilePageArgs({
|
||||
this.isPickFile,
|
||||
this.initDir,
|
||||
});
|
||||
|
||||
@override
|
||||
State<LocalStoragePage> createState() => _LocalStoragePageState();
|
||||
}
|
||||
|
||||
class _LocalStoragePageState extends State<LocalStoragePage> {
|
||||
LocalPath? _path;
|
||||
class LocalFilePage extends StatefulWidget {
|
||||
final LocalFilePageArgs? args;
|
||||
|
||||
final _sortType = ValueNotifier(_SortType.name);
|
||||
const LocalFilePage({super.key, this.args});
|
||||
|
||||
static const route = AppRoute<String, LocalFilePageArgs>(
|
||||
page: LocalFilePage.new,
|
||||
path: '/local_file',
|
||||
);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.initDir != null) {
|
||||
setState(() {
|
||||
_path = LocalPath(widget.initDir!);
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
_path = LocalPath(Paths.file);
|
||||
});
|
||||
}
|
||||
}
|
||||
State<LocalFilePage> createState() => _LocalFilePageState();
|
||||
}
|
||||
|
||||
class _LocalFilePageState extends State<LocalFilePage>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
late final _path = LocalPath(widget.args?.initDir ?? Paths.file);
|
||||
final _sortType = _SortType.name.vn;
|
||||
bool get isPickFile => widget.args?.isPickFile ?? false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
final title = _path.path.fileName ?? libL10n.file;
|
||||
return Scaffold(
|
||||
appBar: CustomAppBar(
|
||||
leading: IconButton(
|
||||
icon: const BackButtonIcon(),
|
||||
onPressed: () {
|
||||
if (_path != null) {
|
||||
_path!.update('/');
|
||||
}
|
||||
context.pop();
|
||||
},
|
||||
title: AnimatedSwitcher(
|
||||
duration: Durations.short3,
|
||||
child: Text(title, key: ValueKey(title)),
|
||||
),
|
||||
title: Text(libL10n.file),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.downloading),
|
||||
onPressed: () => AppRoutes.sftpMission().go(context),
|
||||
),
|
||||
ValBuilder<_SortType>(
|
||||
listenable: _sortType,
|
||||
builder: (value) {
|
||||
return PopupMenuButton<_SortType>(
|
||||
icon: const Icon(Icons.sort),
|
||||
itemBuilder: (context) {
|
||||
return [
|
||||
PopupMenuItem(
|
||||
value: _SortType.name,
|
||||
child: Text(libL10n.name),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: _SortType.size,
|
||||
child: Text(l10n.size),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: _SortType.time,
|
||||
child: Text(l10n.time),
|
||||
),
|
||||
];
|
||||
},
|
||||
onSelected: (value) {
|
||||
_sortType.value = value;
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
if (!isPickFile)
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
final path = await Pfs.pickFilePath();
|
||||
if (path == null) return;
|
||||
final name = path.getFileName() ?? 'imported';
|
||||
await File(path).copy(_path.path.joinPath(name));
|
||||
setState(() {});
|
||||
},
|
||||
icon: const Icon(Icons.add),
|
||||
),
|
||||
if (!isPickFile) _buildMissionBtn(),
|
||||
_buildSortBtn(),
|
||||
],
|
||||
),
|
||||
body: FadeIn(
|
||||
key: UniqueKey(),
|
||||
child: ValBuilder(
|
||||
listenable: _sortType,
|
||||
builder: (val) {
|
||||
return _buildBody();
|
||||
},
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: SafeArea(child: _buildPath()),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPath() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.fromLTRB(11, 7, 11, 11),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
OmitStartText(_path?.path ?? '...'),
|
||||
_buildBtns(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBtns() {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
_path?.update('..');
|
||||
setState(() {});
|
||||
},
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
final path = await Pfs.pickFilePath();
|
||||
if (path == null) return;
|
||||
final name = path.getFileName() ?? 'imported';
|
||||
await File(path).copy(_path!.path.joinPath(name));
|
||||
setState(() {});
|
||||
},
|
||||
icon: const Icon(Icons.add),
|
||||
),
|
||||
],
|
||||
body: _sortType.listen(_buildBody),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody() {
|
||||
if (_path == null) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
Future<List<(FileSystemEntity, FileStat)>> getEntities() async {
|
||||
final files = await Directory(_path.path).list().toList();
|
||||
final sorted = _sortType.value.sort(files);
|
||||
final stats = await Future.wait(
|
||||
sorted.map((e) async => (e, await e.stat())),
|
||||
);
|
||||
return stats;
|
||||
}
|
||||
final dir = Directory(_path!.path);
|
||||
final tempFiles = dir.listSync();
|
||||
final files = _sortType.value.sort(tempFiles);
|
||||
return ListView.builder(
|
||||
itemCount: files.length,
|
||||
padding: const EdgeInsets.symmetric(vertical: 3, horizontal: 7),
|
||||
itemBuilder: (context, index) {
|
||||
final file = files[index];
|
||||
final fileName = file.path.split('/').last;
|
||||
final stat = file.statSync();
|
||||
final isDir = stat.type == FileSystemEntityType.directory;
|
||||
|
||||
return CardX(
|
||||
child: ListTile(
|
||||
leading: isDir
|
||||
? const Icon(Icons.folder_open)
|
||||
: const Icon(Icons.insert_drive_file),
|
||||
title: Text(fileName),
|
||||
subtitle:
|
||||
isDir ? null : Text(stat.size.bytes2Str, style: UIs.textGrey),
|
||||
trailing: Text(
|
||||
stat.modified
|
||||
.toString()
|
||||
.substring(0, stat.modified.toString().length - 4),
|
||||
style: UIs.textGrey,
|
||||
),
|
||||
onLongPress: () {
|
||||
if (!isDir) return;
|
||||
_showDirActionDialog(file);
|
||||
},
|
||||
onTap: () async {
|
||||
if (!isDir) {
|
||||
await _showFileActionDialog(file);
|
||||
return;
|
||||
}
|
||||
_path!.update(fileName);
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
return FutureWidget(
|
||||
future: getEntities(),
|
||||
loading: UIs.placeholder,
|
||||
success: (items_) {
|
||||
final items = items_ ?? [];
|
||||
final len = _path.canBack ? items.length + 1 : items.length;
|
||||
return ListView.builder(
|
||||
itemCount: len,
|
||||
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 13),
|
||||
itemBuilder: (context, index) {
|
||||
if (index == 0 && _path.canBack) {
|
||||
return CardX(
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.arrow_back),
|
||||
title: const Text('..'),
|
||||
onTap: () {
|
||||
_path.update('..');
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (_path.canBack) index--;
|
||||
|
||||
final item = items[index];
|
||||
final file = item.$1;
|
||||
final fileName = file.path.split('/').last;
|
||||
final stat = item.$2;
|
||||
final isDir = stat.type == FileSystemEntityType.directory;
|
||||
|
||||
return CardX(
|
||||
child: ListTile(
|
||||
leading: isDir
|
||||
? const Icon(Icons.folder_open)
|
||||
: const Icon(Icons.insert_drive_file),
|
||||
title: Text(fileName),
|
||||
subtitle: isDir
|
||||
? null
|
||||
: Text(stat.size.bytes2Str, style: UIs.textGrey),
|
||||
trailing: Text(
|
||||
stat.modified.ymdhms(),
|
||||
style: UIs.textGrey,
|
||||
),
|
||||
onLongPress: () {
|
||||
if (isDir) {
|
||||
_showDirActionDialog(file);
|
||||
return;
|
||||
}
|
||||
_showFileActionDialog(file);
|
||||
},
|
||||
onTap: () {
|
||||
if (!isDir) {
|
||||
_showFileActionDialog(file);
|
||||
return;
|
||||
}
|
||||
_path.update(fileName);
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -222,7 +176,7 @@ class _LocalStoragePageState extends State<LocalStoragePage> {
|
||||
|
||||
Future<void> _showFileActionDialog(FileSystemEntity file) async {
|
||||
final fileName = file.path.split('/').last;
|
||||
if (widget.isPickFile) {
|
||||
if (isPickFile) {
|
||||
await context.showRoundDialog(
|
||||
title: libL10n.file,
|
||||
child: Text(fileName),
|
||||
@@ -324,25 +278,33 @@ class _LocalStoragePageState extends State<LocalStoragePage> {
|
||||
|
||||
void _showRenameDialog(FileSystemEntity file) {
|
||||
final fileName = file.path.split('/').last;
|
||||
final ctrl = TextEditingController(text: fileName);
|
||||
void onSubmit() async {
|
||||
final newName = ctrl.text;
|
||||
if (newName.isEmpty) {
|
||||
context.showSnackBar(libL10n.empty);
|
||||
return;
|
||||
}
|
||||
|
||||
context.pop();
|
||||
final newPath = '${file.parent.path}/$newName';
|
||||
await context.showLoadingDialog(fn: () => file.rename(newPath));
|
||||
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
context.showRoundDialog(
|
||||
title: libL10n.rename,
|
||||
child: Input(
|
||||
autoFocus: true,
|
||||
icon: Icons.abc,
|
||||
label: libL10n.name,
|
||||
controller: TextEditingController(text: fileName),
|
||||
suggestion: true,
|
||||
onSubmitted: (p0) {
|
||||
context.pop();
|
||||
final newPath = '${file.parent.path}/$p0';
|
||||
try {
|
||||
file.renameSync(newPath);
|
||||
} catch (e) {
|
||||
context.showSnackBar('${libL10n.fail}:\n$e');
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {});
|
||||
},
|
||||
maxLines: 3,
|
||||
onSubmitted: (p0) => onSubmit(),
|
||||
),
|
||||
actions: Btn.ok(onTap: onSubmit).toList,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -365,6 +327,30 @@ class _LocalStoragePageState extends State<LocalStoragePage> {
|
||||
).toList,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMissionBtn() {
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.downloading),
|
||||
onPressed: () => AppRoutes.sftpMission().go(context),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSortBtn() {
|
||||
return _sortType.listenVal(
|
||||
(value) {
|
||||
return PopupMenuButton<_SortType>(
|
||||
icon: const Icon(Icons.sort),
|
||||
itemBuilder: (_) => _SortType.values.map((e) => e.menuItem).toList(),
|
||||
onSelected: (value) {
|
||||
_sortType.value = value;
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
}
|
||||
|
||||
enum _SortType {
|
||||
@@ -388,4 +374,29 @@ enum _SortType {
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
String get i18n => switch (this) {
|
||||
name => libL10n.name,
|
||||
size => l10n.size,
|
||||
time => l10n.time,
|
||||
};
|
||||
|
||||
IconData get icon => switch (this) {
|
||||
name => Icons.sort_by_alpha,
|
||||
size => Icons.sort,
|
||||
time => Icons.access_time,
|
||||
};
|
||||
|
||||
PopupMenuItem<_SortType> get menuItem {
|
||||
return PopupMenuItem(
|
||||
value: this,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
Icon(icon),
|
||||
Text(i18n),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import 'package:server_box/data/model/sftp/worker.dart';
|
||||
import 'package:server_box/data/provider/sftp.dart';
|
||||
import 'package:server_box/data/res/misc.dart';
|
||||
import 'package:server_box/data/res/store.dart';
|
||||
import 'package:server_box/view/page/storage/local.dart';
|
||||
import 'package:server_box/view/widget/omit_start_text.dart';
|
||||
import 'package:server_box/view/widget/two_line_text.dart';
|
||||
import 'package:server_box/view/widget/unix_perm.dart';
|
||||
@@ -691,8 +692,10 @@ class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
|
||||
],
|
||||
));
|
||||
final path = switch (idx) {
|
||||
0 =>
|
||||
await AppRoutes.localStorage(isPickFile: true).go<String>(context),
|
||||
0 => await LocalFilePage.route.go(
|
||||
context,
|
||||
args: const LocalFilePageArgs(isPickFile: true),
|
||||
),
|
||||
1 => await Pfs.pickFilePath(),
|
||||
_ => null,
|
||||
};
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:server_box/core/extension/context/locale.dart';
|
||||
import 'package:server_box/core/route.dart';
|
||||
import 'package:server_box/data/model/sftp/worker.dart';
|
||||
import 'package:server_box/data/provider/sftp.dart';
|
||||
import 'package:server_box/view/page/storage/local.dart';
|
||||
|
||||
class SftpMissionPage extends StatefulWidget {
|
||||
const SftpMissionPage({super.key});
|
||||
@@ -115,7 +115,10 @@ class _SftpMissionPageState extends State<SftpMissionPage> {
|
||||
onPressed: () {
|
||||
final idx = status.req.localPath.lastIndexOf('/');
|
||||
final dir = status.req.localPath.substring(0, idx);
|
||||
AppRoutes.localStorage(initDir: dir).go(context);
|
||||
LocalFilePage.route.go(
|
||||
context,
|
||||
args: LocalFilePageArgs(initDir: dir),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.file_open),
|
||||
),
|
||||
|
||||
@@ -113,7 +113,9 @@ void _onTapMoreBtns(
|
||||
title: l10n.snippet,
|
||||
tags: SnippetProvider.tags,
|
||||
itemsBuilder: (e) {
|
||||
if (e == kDefaultTag) return SnippetProvider.snippets.value;
|
||||
if (e == TagSwitcher.kDefaultTag) {
|
||||
return SnippetProvider.snippets.value;
|
||||
}
|
||||
return SnippetProvider.snippets.value
|
||||
.where((element) => element.tags?.contains(e) ?? false)
|
||||
.toList();
|
||||
|
||||
Reference in New Issue
Block a user