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:
@@ -3,13 +3,11 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:server_box/data/model/server/private_key_info.dart';
|
import 'package:server_box/data/model/server/private_key_info.dart';
|
||||||
import 'package:server_box/data/model/server/server_private_info.dart';
|
import 'package:server_box/data/model/server/server_private_info.dart';
|
||||||
import 'package:server_box/data/res/store.dart';
|
import 'package:server_box/data/res/store.dart';
|
||||||
import 'package:server_box/view/page/backup.dart';
|
|
||||||
import 'package:server_box/view/page/container.dart';
|
import 'package:server_box/view/page/container.dart';
|
||||||
import 'package:server_box/view/page/home/home.dart';
|
import 'package:server_box/view/page/home/home.dart';
|
||||||
import 'package:server_box/view/page/iperf.dart';
|
import 'package:server_box/view/page/iperf.dart';
|
||||||
import 'package:server_box/view/page/ping.dart';
|
import 'package:server_box/view/page/ping.dart';
|
||||||
import 'package:server_box/view/page/private_key/edit.dart';
|
import 'package:server_box/view/page/private_key/edit.dart';
|
||||||
import 'package:server_box/view/page/private_key/list.dart';
|
|
||||||
import 'package:server_box/view/page/pve.dart';
|
import 'package:server_box/view/page/pve.dart';
|
||||||
import 'package:server_box/view/page/server/detail/view.dart';
|
import 'package:server_box/view/page/server/detail/view.dart';
|
||||||
import 'package:server_box/view/page/setting/platform/android.dart';
|
import 'package:server_box/view/page/setting/platform/android.dart';
|
||||||
@@ -18,17 +16,13 @@ import 'package:server_box/view/page/setting/seq/srv_func_seq.dart';
|
|||||||
import 'package:server_box/view/page/snippet/result.dart';
|
import 'package:server_box/view/page/snippet/result.dart';
|
||||||
import 'package:server_box/view/page/ssh/page.dart';
|
import 'package:server_box/view/page/ssh/page.dart';
|
||||||
import 'package:server_box/view/page/setting/seq/virt_key.dart';
|
import 'package:server_box/view/page/setting/seq/virt_key.dart';
|
||||||
import 'package:server_box/view/page/storage/local.dart';
|
|
||||||
|
|
||||||
import 'package:server_box/data/model/server/snippet.dart';
|
import 'package:server_box/data/model/server/snippet.dart';
|
||||||
import 'package:server_box/view/page/editor.dart';
|
import 'package:server_box/view/page/editor.dart';
|
||||||
import 'package:server_box/view/page/process.dart';
|
import 'package:server_box/view/page/process.dart';
|
||||||
import 'package:server_box/view/page/server/tab.dart';
|
import 'package:server_box/view/page/server/tab.dart';
|
||||||
import 'package:server_box/view/page/setting/entry.dart';
|
|
||||||
import 'package:server_box/view/page/setting/seq/srv_detail_seq.dart';
|
import 'package:server_box/view/page/setting/seq/srv_detail_seq.dart';
|
||||||
import 'package:server_box/view/page/setting/seq/srv_seq.dart';
|
import 'package:server_box/view/page/setting/seq/srv_seq.dart';
|
||||||
import 'package:server_box/view/page/snippet/edit.dart';
|
import 'package:server_box/view/page/snippet/edit.dart';
|
||||||
import 'package:server_box/view/page/snippet/list.dart';
|
|
||||||
import 'package:server_box/view/page/storage/sftp.dart';
|
import 'package:server_box/view/page/storage/sftp.dart';
|
||||||
import 'package:server_box/view/page/storage/sftp_mission.dart';
|
import 'package:server_box/view/page/storage/sftp_mission.dart';
|
||||||
|
|
||||||
@@ -72,10 +66,6 @@ class AppRoutes {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static AppRoutes keyList({Key? key}) {
|
|
||||||
return AppRoutes(PrivateKeysListPage(key: key), 'key_detail');
|
|
||||||
}
|
|
||||||
|
|
||||||
static AppRoutes snippetEdit({Key? key, Snippet? snippet}) {
|
static AppRoutes snippetEdit({Key? key, Snippet? snippet}) {
|
||||||
return AppRoutes(
|
return AppRoutes(
|
||||||
SnippetEditPage(snippet: snippet),
|
SnippetEditPage(snippet: snippet),
|
||||||
@@ -83,10 +73,6 @@ class AppRoutes {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static AppRoutes snippetList({Key? key}) {
|
|
||||||
return AppRoutes(SnippetListPage(key: key), 'snippet_detail');
|
|
||||||
}
|
|
||||||
|
|
||||||
static AppRoutes ssh({
|
static AppRoutes ssh({
|
||||||
Key? key,
|
Key? key,
|
||||||
required Spi spi,
|
required Spi spi,
|
||||||
@@ -108,17 +94,6 @@ class AppRoutes {
|
|||||||
return AppRoutes(SSHVirtKeySettingPage(key: key), 'ssh_virt_key_setting');
|
return AppRoutes(SSHVirtKeySettingPage(key: key), 'ssh_virt_key_setting');
|
||||||
}
|
}
|
||||||
|
|
||||||
static AppRoutes localStorage(
|
|
||||||
{Key? key, bool isPickFile = false, String? initDir}) {
|
|
||||||
return AppRoutes(
|
|
||||||
LocalStoragePage(
|
|
||||||
key: key,
|
|
||||||
isPickFile: isPickFile,
|
|
||||||
initDir: initDir,
|
|
||||||
),
|
|
||||||
'local_storage');
|
|
||||||
}
|
|
||||||
|
|
||||||
static AppRoutes sftpMission({Key? key}) {
|
static AppRoutes sftpMission({Key? key}) {
|
||||||
return AppRoutes(SftpMissionPage(key: key), 'sftp_mission');
|
return AppRoutes(SftpMissionPage(key: key), 'sftp_mission');
|
||||||
}
|
}
|
||||||
@@ -135,10 +110,6 @@ class AppRoutes {
|
|||||||
'sftp');
|
'sftp');
|
||||||
}
|
}
|
||||||
|
|
||||||
static AppRoutes backup({Key? key}) {
|
|
||||||
return AppRoutes(BackupPage(key: key), 'backup');
|
|
||||||
}
|
|
||||||
|
|
||||||
static AppRoutes docker({Key? key, required Spi spi}) {
|
static AppRoutes docker({Key? key, required Spi spi}) {
|
||||||
return AppRoutes(ContainerPage(key: key, spi: spi), 'docker');
|
return AppRoutes(ContainerPage(key: key, spi: spi), 'docker');
|
||||||
}
|
}
|
||||||
@@ -179,10 +150,6 @@ class AppRoutes {
|
|||||||
return AppRoutes(ProcessPage(key: key, spi: spi), 'process');
|
return AppRoutes(ProcessPage(key: key, spi: spi), 'process');
|
||||||
}
|
}
|
||||||
|
|
||||||
static AppRoutes settings({Key? key}) {
|
|
||||||
return AppRoutes(SettingPage(key: key), 'setting');
|
|
||||||
}
|
|
||||||
|
|
||||||
static AppRoutes serverOrder({Key? key}) {
|
static AppRoutes serverOrder({Key? key}) {
|
||||||
return AppRoutes(ServerOrderPage(key: key), 'server_order');
|
return AppRoutes(ServerOrderPage(key: key), 'server_order');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,62 @@
|
|||||||
|
import 'package:fl_lib/fl_lib.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:server_box/view/page/ping.dart';
|
import 'package:server_box/core/extension/context/locale.dart';
|
||||||
import 'package:server_box/view/page/server/tab.dart';
|
import 'package:server_box/view/page/server/tab.dart';
|
||||||
|
import 'package:server_box/view/page/setting/entry.dart';
|
||||||
import 'package:server_box/view/page/snippet/list.dart';
|
import 'package:server_box/view/page/snippet/list.dart';
|
||||||
import 'package:server_box/view/page/ssh/tab.dart';
|
import 'package:server_box/view/page/ssh/tab.dart';
|
||||||
|
import 'package:icons_plus/icons_plus.dart';
|
||||||
|
import 'package:server_box/view/page/storage/local.dart';
|
||||||
|
|
||||||
enum AppTab {
|
enum AppTab {
|
||||||
server,
|
server,
|
||||||
ssh,
|
ssh,
|
||||||
|
file,
|
||||||
snippet,
|
snippet,
|
||||||
ping,
|
settings,
|
||||||
;
|
;
|
||||||
|
|
||||||
Widget get page {
|
Widget get page {
|
||||||
switch (this) {
|
return switch (this) {
|
||||||
case server:
|
server => const ServerPage(),
|
||||||
return const ServerPage();
|
settings => const SettingsPage(),
|
||||||
case snippet:
|
ssh => const SSHTabPage(),
|
||||||
return const SnippetListPage();
|
file => const LocalFilePage(),
|
||||||
case ssh:
|
snippet => const SnippetListPage(),
|
||||||
return const SSHTabPage();
|
};
|
||||||
case ping:
|
}
|
||||||
return const PingPage();
|
|
||||||
}
|
NavigationDestination get navDestination {
|
||||||
|
return switch (this) {
|
||||||
|
server => NavigationDestination(
|
||||||
|
icon: const Icon(BoxIcons.bx_server),
|
||||||
|
label: l10n.server,
|
||||||
|
selectedIcon: const Icon(BoxIcons.bxs_server),
|
||||||
|
),
|
||||||
|
settings => NavigationDestination(
|
||||||
|
icon: const Icon(Icons.settings),
|
||||||
|
label: libL10n.setting,
|
||||||
|
selectedIcon: const Icon(Icons.settings),
|
||||||
|
),
|
||||||
|
ssh => const NavigationDestination(
|
||||||
|
icon: Icon(Icons.terminal_outlined),
|
||||||
|
label: 'SSH',
|
||||||
|
selectedIcon: Icon(Icons.terminal),
|
||||||
|
),
|
||||||
|
snippet => NavigationDestination(
|
||||||
|
icon: const Icon(Icons.code),
|
||||||
|
label: l10n.snippet,
|
||||||
|
selectedIcon: const Icon(Icons.code),
|
||||||
|
),
|
||||||
|
file => NavigationDestination(
|
||||||
|
icon: const Icon(Icons.folder_open),
|
||||||
|
label: libL10n.file,
|
||||||
|
selectedIcon: const Icon(Icons.folder),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<NavigationDestination> get navDestinations {
|
||||||
|
return AppTab.values.map((e) => e.navDestination).toList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -119,6 +119,9 @@ extension Spix on Spi {
|
|||||||
return (ip_, usr, port_);
|
return (ip_, usr, port_);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Just for showing the struct of the class.
|
||||||
|
///
|
||||||
|
/// **NOT** the default value.
|
||||||
static const example = Spi(
|
static const example = Spi(
|
||||||
name: 'name',
|
name: 'name',
|
||||||
ip: 'ip',
|
ip: 'ip',
|
||||||
|
|||||||
@@ -88,16 +88,15 @@ class ServerProvider extends Provider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void _updateTags() {
|
static void _updateTags() {
|
||||||
|
final tags = <String>{};
|
||||||
for (final s in servers.values) {
|
for (final s in servers.values) {
|
||||||
final tags = s.value.spi.tags;
|
final spiTags = s.value.spi.tags;
|
||||||
if (tags == null) continue;
|
if (spiTags == null) continue;
|
||||||
for (final t in tags) {
|
for (final t in spiTags) {
|
||||||
if (!_tags.value.contains(t)) {
|
tags.add(t);
|
||||||
_tags.value.add(t);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_tags.value = (_tags.value.toList()..sort()).toSet();
|
_tags.value = tags;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Server genServer(Spi spi) {
|
static Server genServer(Spi spi) {
|
||||||
|
|||||||
@@ -15,18 +15,23 @@ import 'package:server_box/data/res/store.dart';
|
|||||||
import 'package:icons_plus/icons_plus.dart';
|
import 'package:icons_plus/icons_plus.dart';
|
||||||
import 'package:server_box/data/store/no_backup.dart';
|
import 'package:server_box/data/store/no_backup.dart';
|
||||||
|
|
||||||
final icloudLoading = false.vn;
|
class BackupPage extends StatefulWidget {
|
||||||
final webdavLoading = false.vn;
|
|
||||||
|
|
||||||
final _noBak = NoBackupStore.instance;
|
|
||||||
|
|
||||||
class BackupPage extends StatelessWidget {
|
|
||||||
const BackupPage({super.key});
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
super.build(context);
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: CustomAppBar(title: Text(libL10n.backup)),
|
|
||||||
body: _buildBody(context),
|
body: _buildBody(context),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -477,4 +482,7 @@ class BackupPage extends StatelessWidget {
|
|||||||
Loggers.app.warning('Import servers failed', e, s);
|
Loggers.app.warning('Import servers failed', e, s);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get wantKeepAlive => true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,40 +1,20 @@
|
|||||||
part of 'home.dart';
|
part of 'home.dart';
|
||||||
|
|
||||||
final class _AppBar extends CustomAppBar {
|
final class _AppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||||
final ValueNotifier<int> selectIndex;
|
final double paddingTop;
|
||||||
final ValueNotifier<bool> landscape;
|
|
||||||
|
|
||||||
const _AppBar({
|
const _AppBar(this.paddingTop);
|
||||||
required this.selectIndex,
|
|
||||||
required this.landscape,
|
|
||||||
super.title,
|
|
||||||
super.actions,
|
|
||||||
super.centerTitle,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final placeholder = SizedBox(
|
return SizedBox(
|
||||||
height: CustomAppBar.sysStatusBarHeight ??
|
height: paddingTop,
|
||||||
0 + MediaQuery.of(context).padding.top,
|
child: isIOS
|
||||||
);
|
? const Center(child: Text(BuildData.name, style: UIs.text15Bold))
|
||||||
return selectIndex.listenVal(
|
: null,
|
||||||
(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);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Size get preferredSize => Size.fromHeight(paddingTop);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,10 @@
|
|||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:fl_lib/fl_lib.dart';
|
import 'package:fl_lib/fl_lib.dart';
|
||||||
import 'package:flutter/material.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/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/model/app/tab.dart';
|
||||||
import 'package:server_box/data/provider/app.dart';
|
import 'package:server_box/data/provider/app.dart';
|
||||||
import 'package:server_box/data/provider/server.dart';
|
import 'package:server_box/data/provider/server.dart';
|
||||||
import 'package:server_box/data/res/build_data.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/store.dart';
|
||||||
import 'package:server_box/data/res/url.dart';
|
import 'package:server_box/data/res/url.dart';
|
||||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||||
@@ -104,39 +97,10 @@ class _HomePageState extends State<HomePage>
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
super.build(context);
|
super.build(context);
|
||||||
AppProvider.ctx = 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(
|
return Scaffold(
|
||||||
drawer: _buildDrawer(),
|
appBar: _AppBar(sysPadding.top),
|
||||||
appBar: appBar,
|
|
||||||
body: PageView.builder(
|
body: PageView.builder(
|
||||||
controller: _pageController,
|
controller: _pageController,
|
||||||
itemCount: AppTab.values.length,
|
itemCount: AppTab.values.length,
|
||||||
@@ -184,133 +148,7 @@ class _HomePageState extends State<HomePage>
|
|||||||
labelBehavior: ls
|
labelBehavior: ls
|
||||||
? NavigationDestinationLabelBehavior.alwaysHide
|
? NavigationDestinationLabelBehavior.alwaysHide
|
||||||
: NavigationDestinationLabelBehavior.onlyShowSelected,
|
: NavigationDestinationLabelBehavior.onlyShowSelected,
|
||||||
destinations: [
|
destinations: AppTab.navDestinations,
|
||||||
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,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: CustomAppBar(
|
|
||||||
title: Text(l10n.privateKey),
|
|
||||||
),
|
|
||||||
body: _buildBody(),
|
body: _buildBody(),
|
||||||
floatingActionButton: FloatingActionButton(
|
floatingActionButton: FloatingActionButton(
|
||||||
child: const Icon(Icons.add),
|
child: const Icon(Icons.add),
|
||||||
|
|||||||
@@ -118,10 +118,10 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
|||||||
return CustomAppBar(
|
return CustomAppBar(
|
||||||
title: Text(si.spi.name),
|
title: Text(si.spi.name),
|
||||||
actions: [
|
actions: [
|
||||||
ShareBtn(
|
QrShareBtn(
|
||||||
data: si.spi.toJsonString(),
|
data: si.spi.toJsonString(),
|
||||||
tip: si.spi.name,
|
tip: si.spi.name,
|
||||||
tip2: '${libL10n.share} ${l10n.server} ~ ServerBox',
|
tip2: '${l10n.server} ~ ServerBox',
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.edit),
|
icon: const Icon(Icons.edit),
|
||||||
|
|||||||
@@ -610,13 +610,12 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
|||||||
envs: _env.value.isEmpty ? null : _env.value,
|
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) {
|
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);
|
ServerProvider.addServer(spi);
|
||||||
} else {
|
} else {
|
||||||
ServerProvider.updateServer(this.spi!, spi);
|
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/core/extension/ssh_client.dart';
|
||||||
import 'package:server_box/data/model/app/shell_func.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/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/data/res/store.dart';
|
||||||
import 'package:server_box/view/page/server/edit.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/view/widget/percent_circle.dart';
|
||||||
|
|
||||||
import 'package:server_box/core/route.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/data/provider/server.dart';
|
||||||
import 'package:server_box/view/widget/server_func_btns.dart';
|
import 'package:server_box/view/widget/server_func_btns.dart';
|
||||||
|
|
||||||
|
part 'top_bar.dart';
|
||||||
|
|
||||||
class ServerPage extends StatefulWidget {
|
class ServerPage extends StatefulWidget {
|
||||||
const ServerPage({super.key});
|
const ServerPage({super.key});
|
||||||
|
|
||||||
@@ -85,7 +89,7 @@ class _ServerPageState extends State<ServerPage>
|
|||||||
|
|
||||||
Widget _buildPortrait() {
|
Widget _buildPortrait() {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: TagSwitcher(
|
appBar: _TopBar(
|
||||||
tags: ServerProvider.tags,
|
tags: ServerProvider.tags,
|
||||||
onTagChanged: (p0) => _tag.value = p0,
|
onTagChanged: (p0) => _tag.value = p0,
|
||||||
initTag: _tag.value,
|
initTag: _tag.value,
|
||||||
@@ -130,7 +134,7 @@ class _ServerPageState extends State<ServerPage>
|
|||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
onPressed: () => AppRoutes.settings().go(context),
|
onPressed: () => SettingsPage.route.go(context),
|
||||||
icon: const Icon(Icons.settings, color: Colors.grey),
|
icon: const Icon(Icons.settings, color: Colors.grey),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -482,30 +486,18 @@ class _ServerPageState extends State<ServerPage>
|
|||||||
null,
|
null,
|
||||||
),
|
),
|
||||||
ServerConn.failed => (
|
ServerConn.failed => (
|
||||||
const Icon(
|
const Icon(Icons.refresh, size: 21, color: Colors.grey),
|
||||||
Icons.refresh,
|
|
||||||
size: 21,
|
|
||||||
color: Colors.grey,
|
|
||||||
),
|
|
||||||
() {
|
() {
|
||||||
TryLimiter.reset(s.spi.id);
|
TryLimiter.reset(s.spi.id);
|
||||||
ServerProvider.refresh(spi: s.spi);
|
ServerProvider.refresh(spi: s.spi);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
ServerConn.disconnected => (
|
ServerConn.disconnected => (
|
||||||
const Icon(
|
const Icon(MingCute.link_3_line, size: 19, color: Colors.grey),
|
||||||
MingCute.link_3_line,
|
|
||||||
size: 19,
|
|
||||||
color: Colors.grey,
|
|
||||||
),
|
|
||||||
() => ServerProvider.refresh(spi: s.spi)
|
() => ServerProvider.refresh(spi: s.spi)
|
||||||
),
|
),
|
||||||
ServerConn.finished => (
|
ServerConn.finished => (
|
||||||
const Icon(
|
const Icon(MingCute.unlink_2_line, size: 17, color: Colors.grey),
|
||||||
MingCute.unlink_2_line,
|
|
||||||
size: 17,
|
|
||||||
color: Colors.grey,
|
|
||||||
),
|
|
||||||
() => ServerProvider.closeServer(id: s.spi.id),
|
() => ServerProvider.closeServer(id: s.spi.id),
|
||||||
),
|
),
|
||||||
_ when Stores.setting.serverTabUseOldUI.fetch() => (
|
_ when Stores.setting.serverTabUseOldUI.fetch() => (
|
||||||
@@ -517,11 +509,7 @@ class _ServerPageState extends State<ServerPage>
|
|||||||
// Or the loading icon will be rescaled.
|
// Or the loading icon will be rescaled.
|
||||||
final wrapped = child is SizedBox
|
final wrapped = child is SizedBox
|
||||||
? child
|
? child
|
||||||
: SizedBox(
|
: SizedBox(height: _kCardHeightMin, width: 27, child: child);
|
||||||
height: _kCardHeightMin,
|
|
||||||
width: 27,
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
if (onTap == null) return wrapped.paddingOnly(left: 10);
|
if (onTap == null) return wrapped.paddingOnly(left: 10);
|
||||||
return InkWell(
|
return InkWell(
|
||||||
borderRadius: BorderRadius.circular(7),
|
borderRadius: BorderRadius.circular(7),
|
||||||
@@ -654,7 +642,7 @@ ${ss.err?.message ?? 'null'}
|
|||||||
|
|
||||||
List<String> _filterServers(List<String> order) {
|
List<String> _filterServers(List<String> order) {
|
||||||
final tag = _tag.value;
|
final tag = _tag.value;
|
||||||
if (tag == kDefaultTag) return order;
|
if (tag == TagSwitcher.kDefaultTag) return order;
|
||||||
return order.where((e) {
|
return order.where((e) {
|
||||||
final tags = ServerProvider.pick(id: e)?.value.spi.tags;
|
final tags = ServerProvider.pick(id: e)?.value.spi.tags;
|
||||||
if (tags == null) return false;
|
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 'dart:io';
|
||||||
|
|
||||||
import 'package:fl_lib/fl_lib.dart';
|
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:flutter_gen/gen_l10n/l10n.dart';
|
||||||
import 'package:icons_plus/icons_plus.dart';
|
import 'package:icons_plus/icons_plus.dart';
|
||||||
import 'package:server_box/core/extension/context/locale.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/rebuild.dart';
|
||||||
import 'package:server_box/data/res/store.dart';
|
import 'package:server_box/data/res/store.dart';
|
||||||
import 'package:server_box/data/res/url.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/core/route.dart';
|
||||||
import 'package:server_box/data/model/app/net_view.dart';
|
import 'package:server_box/data/model/app/net_view.dart';
|
||||||
import 'package:server_box/data/res/build_data.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;
|
const _kIconSize = 23.0;
|
||||||
|
|
||||||
class SettingPage extends StatefulWidget {
|
enum SettingsTabs {
|
||||||
const SettingPage({super.key});
|
app,
|
||||||
|
privateKey,
|
||||||
|
backup,
|
||||||
|
about,
|
||||||
|
;
|
||||||
|
|
||||||
@override
|
String get i18n => switch (this) {
|
||||||
State<SettingPage> createState() => _SettingPageState();
|
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> {
|
class SettingsPage extends StatefulWidget {
|
||||||
final _setting = Stores.setting;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: CustomAppBar(
|
appBar: TabBar(
|
||||||
title: Text(libL10n.setting),
|
controller: _tabCtrl,
|
||||||
actions: [
|
dividerHeight: 0,
|
||||||
IconButton(
|
tabAlignment: TabAlignment.center,
|
||||||
icon: const Icon(Icons.delete),
|
isScrollable: true,
|
||||||
onPressed: () => context.showRoundDialog(
|
tabs: SettingsTabs.values
|
||||||
title: libL10n.attention,
|
.map((e) => Tab(text: e.i18n))
|
||||||
child: SimpleMarkdown(
|
.toList(growable: false),
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
body: MultiList(
|
// actions: [
|
||||||
widthDivider: 2.3,
|
// IconButton(
|
||||||
thumbVisibility: true,
|
// icon: const Icon(Icons.delete),
|
||||||
children: [
|
// onPressed: () => context.showRoundDialog(
|
||||||
[const CenterGreyTitle('App'), _buildApp()],
|
// title: libL10n.attention,
|
||||||
[CenterGreyTitle(l10n.server), _buildServer()],
|
// child: SimpleMarkdown(
|
||||||
[
|
// data: libL10n.askContinue(
|
||||||
const CenterGreyTitle('SSH'),
|
// '${libL10n.delete} **${libL10n.all}** ${libL10n.setting}',
|
||||||
_buildSSH(),
|
// ),
|
||||||
const CenterGreyTitle('SFTP'),
|
// ),
|
||||||
_buildSFTP()
|
// actions: [
|
||||||
],
|
// CountDownBtn(
|
||||||
[
|
// onTap: () {
|
||||||
CenterGreyTitle(l10n.container),
|
// context.pop();
|
||||||
_buildContainer(),
|
// _setting.box.deleteAll(_setting.box.keys);
|
||||||
CenterGreyTitle(l10n.editor),
|
// context.showSnackBar(libL10n.success);
|
||||||
_buildEditor(),
|
// },
|
||||||
],
|
// afterColor: Colors.red,
|
||||||
|
// )
|
||||||
|
// ],
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// ],
|
||||||
|
body: TabBarView(controller: _tabCtrl, children: SettingsTabs.pages),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Fullscreen Mode is designed for old mobile phone which can be
|
final class AppAboutPage extends StatefulWidget {
|
||||||
/// used as a status screen.
|
const AppAboutPage({super.key});
|
||||||
if (isMobile) [CenterGreyTitle(l10n.fullScreen), _buildFullScreen()],
|
|
||||||
|
@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(),
|
_buildAppMore(),
|
||||||
];
|
];
|
||||||
|
|
||||||
return Column(
|
return Column(children: children.map((e) => e.cardx).toList());
|
||||||
children: children.map((e) => CardX(child: e)).toList(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildFullScreen() {
|
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();
|
State<SnippetListPage> createState() => _SnippetListPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SnippetListPageState extends State<SnippetListPage> {
|
class _SnippetListPageState extends State<SnippetListPage>
|
||||||
|
with AutomaticKeepAliveClientMixin {
|
||||||
final _tag = ''.vn;
|
final _tag = ''.vn;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
super.build(context);
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: TagSwitcher(
|
appBar: TagSwitcher(
|
||||||
tags: SnippetProvider.tags,
|
tags: SnippetProvider.tags,
|
||||||
@@ -43,7 +45,7 @@ class _SnippetListPageState extends State<SnippetListPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSnippetList(List<Snippet> snippets, String tag) {
|
Widget _buildSnippetList(List<Snippet> snippets, String tag) {
|
||||||
final filtered = tag == kDefaultTag
|
final filtered = tag == TagSwitcher.kDefaultTag
|
||||||
? snippets
|
? snippets
|
||||||
: snippets.where((e) => e.tags?.contains(tag) ?? false).toList();
|
: 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 {
|
// Future<void> _runSnippet(Snippet snippet) async {
|
||||||
// final servers = await context.showPickDialog<Server>(
|
// final servers = await context.showPickDialog<Server>(
|
||||||
// items: Pros.server.servers.toList(),
|
// items: Pros.server.servers.toList(),
|
||||||
|
|||||||
@@ -300,7 +300,9 @@ class SSHPageState extends State<SSHPage>
|
|||||||
title: l10n.snippet,
|
title: l10n.snippet,
|
||||||
tags: SnippetProvider.tags,
|
tags: SnippetProvider.tags,
|
||||||
itemsBuilder: (e) {
|
itemsBuilder: (e) {
|
||||||
if (e == kDefaultTag) return SnippetProvider.snippets.value;
|
if (e == TagSwitcher.kDefaultTag) {
|
||||||
|
return SnippetProvider.snippets.value;
|
||||||
|
}
|
||||||
return SnippetProvider.snippets.value
|
return SnippetProvider.snippets.value
|
||||||
.where((element) => element.tags?.contains(e) ?? false)
|
.where((element) => element.tags?.contains(e) ?? false)
|
||||||
.toList();
|
.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/server.dart';
|
||||||
import 'package:server_box/data/provider/sftp.dart';
|
import 'package:server_box/data/provider/sftp.dart';
|
||||||
import 'package:server_box/data/res/misc.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/core/route.dart';
|
||||||
import 'package:server_box/data/model/app/path_with_prefix.dart';
|
import 'package:server_box/data/model/app/path_with_prefix.dart';
|
||||||
|
|
||||||
class LocalStoragePage extends StatefulWidget {
|
final class LocalFilePageArgs {
|
||||||
final bool isPickFile;
|
final bool? isPickFile;
|
||||||
final String? initDir;
|
final String? initDir;
|
||||||
const LocalStoragePage({
|
const LocalFilePageArgs({
|
||||||
super.key,
|
this.isPickFile,
|
||||||
required this.isPickFile,
|
|
||||||
this.initDir,
|
this.initDir,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
|
||||||
State<LocalStoragePage> createState() => _LocalStoragePageState();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _LocalStoragePageState extends State<LocalStoragePage> {
|
class LocalFilePage extends StatefulWidget {
|
||||||
LocalPath? _path;
|
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
|
@override
|
||||||
void initState() {
|
State<LocalFilePage> createState() => _LocalFilePageState();
|
||||||
super.initState();
|
}
|
||||||
if (widget.initDir != null) {
|
|
||||||
setState(() {
|
class _LocalFilePageState extends State<LocalFilePage>
|
||||||
_path = LocalPath(widget.initDir!);
|
with AutomaticKeepAliveClientMixin {
|
||||||
});
|
late final _path = LocalPath(widget.args?.initDir ?? Paths.file);
|
||||||
} else {
|
final _sortType = _SortType.name.vn;
|
||||||
setState(() {
|
bool get isPickFile => widget.args?.isPickFile ?? false;
|
||||||
_path = LocalPath(Paths.file);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
super.build(context);
|
||||||
|
final title = _path.path.fileName ?? libL10n.file;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: CustomAppBar(
|
appBar: CustomAppBar(
|
||||||
leading: IconButton(
|
title: AnimatedSwitcher(
|
||||||
icon: const BackButtonIcon(),
|
duration: Durations.short3,
|
||||||
onPressed: () {
|
child: Text(title, key: ValueKey(title)),
|
||||||
if (_path != null) {
|
|
||||||
_path!.update('/');
|
|
||||||
}
|
|
||||||
context.pop();
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
title: Text(libL10n.file),
|
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
if (!isPickFile)
|
||||||
icon: const Icon(Icons.downloading),
|
IconButton(
|
||||||
onPressed: () => AppRoutes.sftpMission().go(context),
|
onPressed: () async {
|
||||||
),
|
final path = await Pfs.pickFilePath();
|
||||||
ValBuilder<_SortType>(
|
if (path == null) return;
|
||||||
listenable: _sortType,
|
final name = path.getFileName() ?? 'imported';
|
||||||
builder: (value) {
|
await File(path).copy(_path.path.joinPath(name));
|
||||||
return PopupMenuButton<_SortType>(
|
setState(() {});
|
||||||
icon: const Icon(Icons.sort),
|
},
|
||||||
itemBuilder: (context) {
|
icon: const Icon(Icons.add),
|
||||||
return [
|
),
|
||||||
PopupMenuItem(
|
if (!isPickFile) _buildMissionBtn(),
|
||||||
value: _SortType.name,
|
_buildSortBtn(),
|
||||||
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;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: FadeIn(
|
body: _sortType.listen(_buildBody),
|
||||||
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),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildBody() {
|
Widget _buildBody() {
|
||||||
if (_path == null) {
|
Future<List<(FileSystemEntity, FileStat)>> getEntities() async {
|
||||||
return const Center(
|
final files = await Directory(_path.path).list().toList();
|
||||||
child: CircularProgressIndicator(),
|
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(
|
return FutureWidget(
|
||||||
child: ListTile(
|
future: getEntities(),
|
||||||
leading: isDir
|
loading: UIs.placeholder,
|
||||||
? const Icon(Icons.folder_open)
|
success: (items_) {
|
||||||
: const Icon(Icons.insert_drive_file),
|
final items = items_ ?? [];
|
||||||
title: Text(fileName),
|
final len = _path.canBack ? items.length + 1 : items.length;
|
||||||
subtitle:
|
return ListView.builder(
|
||||||
isDir ? null : Text(stat.size.bytes2Str, style: UIs.textGrey),
|
itemCount: len,
|
||||||
trailing: Text(
|
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 13),
|
||||||
stat.modified
|
itemBuilder: (context, index) {
|
||||||
.toString()
|
if (index == 0 && _path.canBack) {
|
||||||
.substring(0, stat.modified.toString().length - 4),
|
return CardX(
|
||||||
style: UIs.textGrey,
|
child: ListTile(
|
||||||
),
|
leading: const Icon(Icons.arrow_back),
|
||||||
onLongPress: () {
|
title: const Text('..'),
|
||||||
if (!isDir) return;
|
onTap: () {
|
||||||
_showDirActionDialog(file);
|
_path.update('..');
|
||||||
},
|
setState(() {});
|
||||||
onTap: () async {
|
},
|
||||||
if (!isDir) {
|
),
|
||||||
await _showFileActionDialog(file);
|
);
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
_path!.update(fileName);
|
if (_path.canBack) index--;
|
||||||
setState(() {});
|
|
||||||
},
|
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 {
|
Future<void> _showFileActionDialog(FileSystemEntity file) async {
|
||||||
final fileName = file.path.split('/').last;
|
final fileName = file.path.split('/').last;
|
||||||
if (widget.isPickFile) {
|
if (isPickFile) {
|
||||||
await context.showRoundDialog(
|
await context.showRoundDialog(
|
||||||
title: libL10n.file,
|
title: libL10n.file,
|
||||||
child: Text(fileName),
|
child: Text(fileName),
|
||||||
@@ -324,25 +278,33 @@ class _LocalStoragePageState extends State<LocalStoragePage> {
|
|||||||
|
|
||||||
void _showRenameDialog(FileSystemEntity file) {
|
void _showRenameDialog(FileSystemEntity file) {
|
||||||
final fileName = file.path.split('/').last;
|
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(
|
context.showRoundDialog(
|
||||||
title: libL10n.rename,
|
title: libL10n.rename,
|
||||||
child: Input(
|
child: Input(
|
||||||
autoFocus: true,
|
autoFocus: true,
|
||||||
|
icon: Icons.abc,
|
||||||
|
label: libL10n.name,
|
||||||
controller: TextEditingController(text: fileName),
|
controller: TextEditingController(text: fileName),
|
||||||
suggestion: true,
|
suggestion: true,
|
||||||
onSubmitted: (p0) {
|
maxLines: 3,
|
||||||
context.pop();
|
onSubmitted: (p0) => onSubmit(),
|
||||||
final newPath = '${file.parent.path}/$p0';
|
|
||||||
try {
|
|
||||||
file.renameSync(newPath);
|
|
||||||
} catch (e) {
|
|
||||||
context.showSnackBar('${libL10n.fail}:\n$e');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setState(() {});
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
|
actions: Btn.ok(onTap: onSubmit).toList,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -365,6 +327,30 @@ class _LocalStoragePageState extends State<LocalStoragePage> {
|
|||||||
).toList,
|
).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 {
|
enum _SortType {
|
||||||
@@ -388,4 +374,29 @@ enum _SortType {
|
|||||||
}
|
}
|
||||||
return files;
|
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/provider/sftp.dart';
|
||||||
import 'package:server_box/data/res/misc.dart';
|
import 'package:server_box/data/res/misc.dart';
|
||||||
import 'package:server_box/data/res/store.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/omit_start_text.dart';
|
||||||
import 'package:server_box/view/widget/two_line_text.dart';
|
import 'package:server_box/view/widget/two_line_text.dart';
|
||||||
import 'package:server_box/view/widget/unix_perm.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) {
|
final path = switch (idx) {
|
||||||
0 =>
|
0 => await LocalFilePage.route.go(
|
||||||
await AppRoutes.localStorage(isPickFile: true).go<String>(context),
|
context,
|
||||||
|
args: const LocalFilePageArgs(isPickFile: true),
|
||||||
|
),
|
||||||
1 => await Pfs.pickFilePath(),
|
1 => await Pfs.pickFilePath(),
|
||||||
_ => null,
|
_ => null,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import 'package:fl_lib/fl_lib.dart';
|
import 'package:fl_lib/fl_lib.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:server_box/core/extension/context/locale.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/model/sftp/worker.dart';
|
||||||
import 'package:server_box/data/provider/sftp.dart';
|
import 'package:server_box/data/provider/sftp.dart';
|
||||||
|
import 'package:server_box/view/page/storage/local.dart';
|
||||||
|
|
||||||
class SftpMissionPage extends StatefulWidget {
|
class SftpMissionPage extends StatefulWidget {
|
||||||
const SftpMissionPage({super.key});
|
const SftpMissionPage({super.key});
|
||||||
@@ -115,7 +115,10 @@ class _SftpMissionPageState extends State<SftpMissionPage> {
|
|||||||
onPressed: () {
|
onPressed: () {
|
||||||
final idx = status.req.localPath.lastIndexOf('/');
|
final idx = status.req.localPath.lastIndexOf('/');
|
||||||
final dir = status.req.localPath.substring(0, idx);
|
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),
|
icon: const Icon(Icons.file_open),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -113,7 +113,9 @@ void _onTapMoreBtns(
|
|||||||
title: l10n.snippet,
|
title: l10n.snippet,
|
||||||
tags: SnippetProvider.tags,
|
tags: SnippetProvider.tags,
|
||||||
itemsBuilder: (e) {
|
itemsBuilder: (e) {
|
||||||
if (e == kDefaultTag) return SnippetProvider.snippets.value;
|
if (e == TagSwitcher.kDefaultTag) {
|
||||||
|
return SnippetProvider.snippets.value;
|
||||||
|
}
|
||||||
return SnippetProvider.snippets.value
|
return SnippetProvider.snippets.value
|
||||||
.where((element) => element.tags?.contains(e) ?? false)
|
.where((element) => element.tags?.contains(e) ?? false)
|
||||||
.toList();
|
.toList();
|
||||||
|
|||||||
@@ -470,8 +470,8 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: "v1.0.183"
|
ref: "v1.0.187"
|
||||||
resolved-ref: "1333b6269a4caa54c58324c66558e09a0525784b"
|
resolved-ref: "9c69e55fd227428935de8de938c265ab5c099451"
|
||||||
url: "https://github.com/lppcg/fl_lib"
|
url: "https://github.com/lppcg/fl_lib"
|
||||||
source: git
|
source: git
|
||||||
version: "0.0.1"
|
version: "0.0.1"
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ dependencies:
|
|||||||
fl_lib:
|
fl_lib:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/lppcg/fl_lib
|
url: https://github.com/lppcg/fl_lib
|
||||||
ref: v1.0.183
|
ref: v1.0.187
|
||||||
|
|
||||||
dependency_overrides:
|
dependency_overrides:
|
||||||
# dartssh2:
|
# dartssh2:
|
||||||
|
|||||||
Reference in New Issue
Block a user