mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2025-12-18 07:44:26 +01:00
opt.: sftp home & back (#533)
This commit is contained in:
@@ -339,7 +339,7 @@ class BackupPage extends StatelessWidget {
|
||||
|
||||
Future<void> _onTapWebdavUp(BuildContext context) async {
|
||||
webdavLoading.value = true;
|
||||
final date = DateTime.now().ymdhms(ymdSep: "-", hmsSep: "-", sep: "-");
|
||||
final date = DateTime.now().ymdhms(ymdSep: '-', hmsSep: '-', sep: '-');
|
||||
final bakName = '$date-${Miscs.bakFileName}';
|
||||
try {
|
||||
await Backup.backup(bakName);
|
||||
|
||||
@@ -11,10 +11,10 @@ import 'package:server_box/data/model/container/image.dart';
|
||||
import 'package:server_box/data/model/container/type.dart';
|
||||
import 'package:server_box/data/res/store.dart';
|
||||
|
||||
import '../../data/model/container/ps.dart';
|
||||
import '../../data/model/server/server_private_info.dart';
|
||||
import '../../data/provider/container.dart';
|
||||
import '../widget/two_line_text.dart';
|
||||
import 'package:server_box/data/model/container/ps.dart';
|
||||
import 'package:server_box/data/model/server/server_private_info.dart';
|
||||
import 'package:server_box/data/provider/container.dart';
|
||||
import 'package:server_box/view/widget/two_line_text.dart';
|
||||
|
||||
class ContainerPage extends StatefulWidget {
|
||||
final ServerPrivateInfo spi;
|
||||
|
||||
@@ -12,7 +12,7 @@ import 'package:server_box/core/extension/context/locale.dart';
|
||||
import 'package:server_box/data/res/highlight.dart';
|
||||
import 'package:server_box/data/res/store.dart';
|
||||
|
||||
import '../widget/two_line_text.dart';
|
||||
import 'package:server_box/view/widget/two_line_text.dart';
|
||||
|
||||
class EditorPage extends StatefulWidget {
|
||||
/// If path is not null, then it's a file editor
|
||||
|
||||
@@ -5,7 +5,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:server_box/core/extension/context/locale.dart';
|
||||
import 'package:server_box/data/res/provider.dart';
|
||||
|
||||
import '../../data/model/server/ping_result.dart';
|
||||
import 'package:server_box/data/model/server/ping_result.dart';
|
||||
|
||||
/// Only permit ipv4 / ipv6 / domain chars
|
||||
final targetReg = RegExp(r'[a-zA-Z0-9\.-_:]+');
|
||||
|
||||
@@ -8,8 +8,8 @@ import 'package:server_box/core/extension/context/locale.dart';
|
||||
import 'package:server_box/data/res/misc.dart';
|
||||
import 'package:server_box/data/res/provider.dart';
|
||||
|
||||
import '../../../core/utils/server.dart';
|
||||
import '../../../data/model/server/private_key_info.dart';
|
||||
import 'package:server_box/core/utils/server.dart';
|
||||
import 'package:server_box/data/model/server/private_key_info.dart';
|
||||
|
||||
const _format = 'text/plain';
|
||||
|
||||
@@ -107,7 +107,7 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage> {
|
||||
}
|
||||
|
||||
String _standardizeLineSeparators(String value) {
|
||||
return value.replaceAll("\r\n", "\n").replaceAll("\r", "\n");
|
||||
return value.replaceAll('\r\n', '\n').replaceAll('\r', '\n');
|
||||
}
|
||||
|
||||
Widget _buildFAB() {
|
||||
|
||||
@@ -7,9 +7,9 @@ import 'package:provider/provider.dart';
|
||||
import 'package:server_box/core/extension/context/locale.dart';
|
||||
import 'package:server_box/data/res/store.dart';
|
||||
|
||||
import '../../../core/route.dart';
|
||||
import '../../../data/model/server/private_key_info.dart';
|
||||
import '../../../data/provider/private_key.dart';
|
||||
import 'package:server_box/core/route.dart';
|
||||
import 'package:server_box/data/model/server/private_key_info.dart';
|
||||
import 'package:server_box/data/provider/private_key.dart';
|
||||
|
||||
class PrivateKeysListPage extends StatefulWidget {
|
||||
const PrivateKeysListPage({super.key});
|
||||
|
||||
@@ -6,10 +6,10 @@ import 'package:flutter/material.dart';
|
||||
import 'package:server_box/core/extension/context/locale.dart';
|
||||
import 'package:server_box/data/res/store.dart';
|
||||
|
||||
import '../../data/model/app/shell_func.dart';
|
||||
import '../../data/model/server/proc.dart';
|
||||
import '../../data/model/server/server_private_info.dart';
|
||||
import '../widget/two_line_text.dart';
|
||||
import 'package:server_box/data/model/app/shell_func.dart';
|
||||
import 'package:server_box/data/model/server/proc.dart';
|
||||
import 'package:server_box/data/model/server/server_private_info.dart';
|
||||
import 'package:server_box/view/widget/two_line_text.dart';
|
||||
|
||||
class ProcessPage extends StatefulWidget {
|
||||
final ServerPrivateInfo spi;
|
||||
|
||||
@@ -20,9 +20,9 @@ import 'package:server_box/data/model/server/system.dart';
|
||||
import 'package:server_box/data/res/store.dart';
|
||||
import 'package:server_box/view/widget/server_func_btns.dart';
|
||||
|
||||
import '../../../../core/route.dart';
|
||||
import '../../../../data/model/server/server.dart';
|
||||
import '../../../../data/provider/server.dart';
|
||||
import 'package:server_box/core/route.dart';
|
||||
import 'package:server_box/data/model/server/server.dart';
|
||||
import 'package:server_box/data/provider/server.dart';
|
||||
|
||||
part 'misc.dart';
|
||||
|
||||
|
||||
@@ -9,9 +9,9 @@ import 'package:server_box/data/model/server/custom.dart';
|
||||
import 'package:server_box/data/model/server/wol_cfg.dart';
|
||||
import 'package:server_box/data/res/provider.dart';
|
||||
|
||||
import '../../../core/route.dart';
|
||||
import '../../../data/model/server/server_private_info.dart';
|
||||
import '../../../data/provider/private_key.dart';
|
||||
import 'package:server_box/core/route.dart';
|
||||
import 'package:server_box/data/model/server/server_private_info.dart';
|
||||
import 'package:server_box/data/provider/private_key.dart';
|
||||
|
||||
class ServerEditPage extends StatefulWidget {
|
||||
const ServerEditPage({super.key, this.spi});
|
||||
|
||||
@@ -13,12 +13,12 @@ import 'package:server_box/data/res/provider.dart';
|
||||
import 'package:server_box/data/res/store.dart';
|
||||
import 'package:server_box/view/widget/percent_circle.dart';
|
||||
|
||||
import '../../../core/route.dart';
|
||||
import '../../../data/model/app/net_view.dart';
|
||||
import '../../../data/model/server/server.dart';
|
||||
import '../../../data/model/server/server_private_info.dart';
|
||||
import '../../../data/provider/server.dart';
|
||||
import '../../widget/server_func_btns.dart';
|
||||
import 'package:server_box/core/route.dart';
|
||||
import 'package:server_box/data/model/app/net_view.dart';
|
||||
import 'package:server_box/data/model/server/server.dart';
|
||||
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';
|
||||
|
||||
class ServerPage extends StatefulWidget {
|
||||
const ServerPage({super.key});
|
||||
|
||||
@@ -10,9 +10,9 @@ import 'package:server_box/data/res/rebuild.dart';
|
||||
import 'package:server_box/data/res/store.dart';
|
||||
import 'package:server_box/data/res/url.dart';
|
||||
|
||||
import '../../../core/route.dart';
|
||||
import '../../../data/model/app/net_view.dart';
|
||||
import '../../../data/res/build_data.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';
|
||||
|
||||
const _kIconSize = 23.0;
|
||||
|
||||
@@ -1032,7 +1032,6 @@ class _SettingPageState extends State<SettingPage> {
|
||||
Widget _buildHideTitleBar() {
|
||||
return ListTile(
|
||||
title: Text(l10n.hideTitleBar),
|
||||
subtitle: Text(l10n.hideTitleBarTip, style: UIs.textGrey),
|
||||
trailing: StoreSwitch(prop: _setting.hideTitleBar),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@ import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:server_box/data/res/store.dart';
|
||||
|
||||
import '../../../data/model/server/snippet.dart';
|
||||
import '/core/route.dart';
|
||||
import '/data/provider/snippet.dart';
|
||||
import 'package:server_box/data/model/server/snippet.dart';
|
||||
import 'package:server_box/core/route.dart';
|
||||
import 'package:server_box/data/provider/snippet.dart';
|
||||
|
||||
class SnippetListPage extends StatefulWidget {
|
||||
const SnippetListPage({super.key});
|
||||
|
||||
@@ -18,10 +18,10 @@ import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
import 'package:xterm/core.dart';
|
||||
import 'package:xterm/ui.dart' hide TerminalThemes;
|
||||
|
||||
import '../../../core/route.dart';
|
||||
import '../../../data/model/server/server_private_info.dart';
|
||||
import '../../../data/model/ssh/virtual_key.dart';
|
||||
import '../../../data/res/terminal.dart';
|
||||
import 'package:server_box/core/route.dart';
|
||||
import 'package:server_box/data/model/server/server_private_info.dart';
|
||||
import 'package:server_box/data/model/ssh/virtual_key.dart';
|
||||
import 'package:server_box/data/res/terminal.dart';
|
||||
|
||||
const _echoPWD = 'echo \$PWD';
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@ import 'package:server_box/data/res/misc.dart';
|
||||
import 'package:server_box/data/res/provider.dart';
|
||||
import 'package:server_box/view/widget/omit_start_text.dart';
|
||||
|
||||
import '../../../core/route.dart';
|
||||
import '../../../data/model/app/path_with_prefix.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;
|
||||
|
||||
@@ -9,7 +9,6 @@ import 'package:server_box/core/extension/sftpfile.dart';
|
||||
import 'package:server_box/core/route.dart';
|
||||
import 'package:server_box/core/utils/comparator.dart';
|
||||
import 'package:server_box/data/model/server/server_private_info.dart';
|
||||
import 'package:server_box/data/model/sftp/absolute_path.dart';
|
||||
import 'package:server_box/data/model/sftp/browser_status.dart';
|
||||
import 'package:server_box/data/model/sftp/worker.dart';
|
||||
import 'package:server_box/data/res/misc.dart';
|
||||
@@ -38,111 +37,99 @@ class SftpPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
|
||||
final _status = SftpBrowserStatus();
|
||||
late final _client = widget.spi.server?.client;
|
||||
|
||||
final _sortOption =
|
||||
ValueNotifier(_SortOption(sortBy: _SortType.name, reversed: false));
|
||||
late final _status = SftpBrowserStatus(_client);
|
||||
late final _client = widget.spi.server!.client!;
|
||||
final _sortOption = _SortOption().vn;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final children = [
|
||||
Btn.icon(
|
||||
icon: const Icon(Icons.downloading),
|
||||
onTap: () => AppRoutes.sftpMission().go(context),
|
||||
),
|
||||
_buildSortMenu(),
|
||||
_buildSearchBtn(),
|
||||
];
|
||||
if (isDesktop) children.add(_buildRefreshBtn());
|
||||
|
||||
return Scaffold(
|
||||
appBar: CustomAppBar(
|
||||
leading: IconButton(
|
||||
icon: const BackButtonIcon(),
|
||||
onPressed: () {
|
||||
_status.path?.update('/');
|
||||
context.pop();
|
||||
},
|
||||
),
|
||||
title: TwoLineText(up: 'SFTP', down: widget.spi.name),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.downloading),
|
||||
onPressed: () => AppRoutes.sftpMission().go(context),
|
||||
),
|
||||
ValBuilder(
|
||||
listenable: _sortOption,
|
||||
builder: (value) {
|
||||
return PopupMenuButton<_SortType>(
|
||||
icon: const Icon(Icons.sort),
|
||||
itemBuilder: (context) {
|
||||
final currentSelectedOption = _sortOption.value;
|
||||
final options = [
|
||||
(_SortType.name, libL10n.name),
|
||||
(_SortType.size, l10n.size),
|
||||
(_SortType.time, l10n.time),
|
||||
];
|
||||
return options.map((r) {
|
||||
final (type, name) = r;
|
||||
return PopupMenuItem(
|
||||
value: type,
|
||||
child: Text(
|
||||
type == currentSelectedOption.sortBy
|
||||
? "$name (${currentSelectedOption.reversed ? '-' : '+'})"
|
||||
: name,
|
||||
style: TextStyle(
|
||||
color: type == currentSelectedOption.sortBy
|
||||
? UIs.primaryColor
|
||||
: null,
|
||||
fontWeight: type == currentSelectedOption.sortBy
|
||||
? FontWeight.bold
|
||||
: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList();
|
||||
},
|
||||
onSelected: (sortBy) {
|
||||
final oldValue = _sortOption.value;
|
||||
if (oldValue.sortBy == sortBy) {
|
||||
_sortOption.value = _SortOption(
|
||||
sortBy: sortBy, reversed: !oldValue.reversed);
|
||||
} else {
|
||||
_sortOption.value =
|
||||
_SortOption(sortBy: sortBy, reversed: false);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
actions: children,
|
||||
),
|
||||
body: _buildFileView(),
|
||||
bottomNavigationBar: _buildBottom(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSortMenu() {
|
||||
return ValBuilder(
|
||||
listenable: _sortOption,
|
||||
builder: (value) {
|
||||
return PopupMenuButton<_SortType>(
|
||||
icon: const Icon(Icons.sort),
|
||||
itemBuilder: (context) {
|
||||
final currentSelectedOption = _sortOption.value;
|
||||
final options = [
|
||||
(_SortType.name, libL10n.name),
|
||||
(_SortType.size, l10n.size),
|
||||
(_SortType.time, l10n.time),
|
||||
];
|
||||
return options.map((r) {
|
||||
final (type, name) = r;
|
||||
final selected = type == currentSelectedOption.sortBy;
|
||||
final title = selected
|
||||
? "$name (${currentSelectedOption.reversed ? '-' : '+'})"
|
||||
: name;
|
||||
return PopupMenuItem(
|
||||
value: type,
|
||||
child: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
color: selected ? UIs.primaryColor : null,
|
||||
fontWeight: selected ? FontWeight.bold : null,
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList();
|
||||
},
|
||||
onSelected: (sortBy) {
|
||||
final old = _sortOption.value;
|
||||
if (old.sortBy == sortBy) {
|
||||
_sortOption.value = old.copyWith(reversed: !old.reversed);
|
||||
} else {
|
||||
_sortOption.value = old.copyWith(sortBy: sortBy);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBottom() {
|
||||
final children = widget.isSelect
|
||||
? [
|
||||
IconButton(
|
||||
onPressed: () => context.pop(_status.path?.path),
|
||||
onPressed: () => context.pop(_status.path.path),
|
||||
icon: const Icon(Icons.done),
|
||||
),
|
||||
_buildSearchBtn(),
|
||||
]
|
||||
: [
|
||||
IconButton(
|
||||
padding: const EdgeInsets.all(0),
|
||||
onPressed: () async {
|
||||
await _backward();
|
||||
},
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
),
|
||||
_buildBackBtn(),
|
||||
_buildHomeBtn(),
|
||||
_buildAddBtn(),
|
||||
_buildGotoBtn(),
|
||||
_buildUploadBtn(),
|
||||
_buildSearchBtn(),
|
||||
];
|
||||
if (isDesktop) children.add(_buildRefreshBtn());
|
||||
return SafeArea(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.fromLTRB(11, 7, 11, 11),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
OmitStartText(_status.path?.path ?? '...'),
|
||||
OmitStartText(_status.path.path),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: children,
|
||||
@@ -153,169 +140,18 @@ class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSearchBtn() {
|
||||
return IconButton(
|
||||
onPressed: () async {
|
||||
Stream<SftpName> find(String query) async* {
|
||||
final fs = _status.files;
|
||||
if (fs == null) return;
|
||||
for (final f in fs) {
|
||||
if (f.filename.contains(query)) yield f;
|
||||
}
|
||||
}
|
||||
|
||||
final search = SearchPage(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 3),
|
||||
future: (q) => find(q).toList(),
|
||||
builder: (ctx, e) => _buildItem(e, beforeTap: () => ctx.pop()),
|
||||
);
|
||||
await showSearch(context: context, delegate: search);
|
||||
},
|
||||
icon: const Icon(Icons.search),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildUploadBtn() {
|
||||
return IconButton(
|
||||
onPressed: () async {
|
||||
final idx = await context.showRoundDialog(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.open_in_new),
|
||||
title: Text(l10n.system),
|
||||
onTap: () => context.pop(1),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.folder),
|
||||
title: Text(l10n.inner),
|
||||
onTap: () => context.pop(0),
|
||||
),
|
||||
],
|
||||
));
|
||||
final path = await () async {
|
||||
switch (idx) {
|
||||
case 0:
|
||||
return await AppRoutes.localStorage(isPickFile: true)
|
||||
.go<String>(context);
|
||||
case 1:
|
||||
return await Pfs.pickFilePath();
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}();
|
||||
if (path == null) {
|
||||
return;
|
||||
}
|
||||
final remoteDir = _status.path?.path;
|
||||
if (remoteDir == null) {
|
||||
context.showSnackBar('remote path is null');
|
||||
return;
|
||||
}
|
||||
final fileName = path.split(Platform.pathSeparator).lastOrNull;
|
||||
final remotePath = '$remoteDir/$fileName';
|
||||
Loggers.app.info('SFTP upload local: $path, remote: $remotePath');
|
||||
Pros.sftp.add(
|
||||
SftpReq(widget.spi, remotePath, path, SftpReqType.upload),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.upload_file),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAddBtn() {
|
||||
return IconButton(
|
||||
onPressed: () => context.showRoundDialog(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.folder),
|
||||
title: Text(libL10n.folder),
|
||||
onTap: _mkdir,
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.insert_drive_file),
|
||||
title: Text(libL10n.file),
|
||||
onTap: _newFile,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
icon: const Icon(Icons.add),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildGotoBtn() {
|
||||
return IconButton(
|
||||
padding: const EdgeInsets.all(0),
|
||||
onPressed: () async {
|
||||
final p = await context.showRoundDialog<String>(
|
||||
title: l10n.goto,
|
||||
child: Autocomplete<String>(
|
||||
optionsBuilder: (val) {
|
||||
if (!Stores.setting.recordHistory.fetch()) {
|
||||
return [];
|
||||
}
|
||||
return Stores.history.sftpGoPath.all.cast<String>().where(
|
||||
(element) => element.contains(val.text),
|
||||
);
|
||||
},
|
||||
fieldViewBuilder: (_, controller, node, __) {
|
||||
return Input(
|
||||
autoFocus: true,
|
||||
icon: Icons.abc,
|
||||
label: libL10n.path,
|
||||
node: node,
|
||||
controller: controller,
|
||||
suggestion: true,
|
||||
onSubmitted: (value) => context.pop(value),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
if (p == null || p.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
_status.path?.update(p);
|
||||
final suc = await _listDir() ?? false;
|
||||
if (suc && Stores.setting.recordHistory.fetch()) {
|
||||
Stores.history.sftpGoPath.add(p);
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.gps_fixed),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRefreshBtn() {
|
||||
return IconButton(
|
||||
onPressed: () => _listDir(),
|
||||
icon: const Icon(Icons.refresh),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFileView() {
|
||||
if (_status.files == null) {
|
||||
return UIs.centerLoading;
|
||||
}
|
||||
|
||||
if (_status.files!.isEmpty) {
|
||||
return const Center(
|
||||
child: Text('~'),
|
||||
);
|
||||
}
|
||||
if (_status.files.isEmpty) return Center(child: Text(libL10n.empty));
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: _listDir,
|
||||
child: FadeIn(
|
||||
key: Key(widget.spi.name + _status.path!.path),
|
||||
key: Key(widget.spi.name + _status.path.path),
|
||||
child: ValBuilder(
|
||||
listenable: _sortOption,
|
||||
builder: (sortOption) {
|
||||
final files = sortOption.sortBy.sort(
|
||||
_status.files!,
|
||||
_status.files,
|
||||
reversed: sortOption.reversed,
|
||||
);
|
||||
return ListView.builder(
|
||||
@@ -326,7 +162,6 @@ class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
|
||||
},
|
||||
),
|
||||
),
|
||||
onRefresh: () => _listDir(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -351,7 +186,7 @@ class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
|
||||
onTap: () {
|
||||
beforeTap?.call();
|
||||
if (isDir) {
|
||||
_status.path?.update(file.filename);
|
||||
_status.path.path = file.filename;
|
||||
_listDir();
|
||||
} else {
|
||||
_onItemPress(file, true);
|
||||
@@ -402,7 +237,7 @@ class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
|
||||
final permStr = newPerm.perm;
|
||||
if (ok == true && permStr != perm.perm) {
|
||||
await context.showLoadingDialog(fn: () async {
|
||||
await _client!.run('chmod $permStr "${_getRemotePath(file)}"');
|
||||
await _client.run('chmod $permStr "${_getRemotePath(file)}"');
|
||||
await _listDir();
|
||||
});
|
||||
}
|
||||
@@ -570,7 +405,7 @@ class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
|
||||
fn: () async {
|
||||
final remotePath = _getRemotePath(file);
|
||||
if (useRmr) {
|
||||
await _client!.run('rm -r "$remotePath"');
|
||||
await _client.run('rm -r "$remotePath"');
|
||||
} else if (file.attr.isDirectory) {
|
||||
await _status.client!.rmdir(remotePath);
|
||||
} else {
|
||||
@@ -606,7 +441,7 @@ class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
|
||||
|
||||
final (suc, err) = await context.showLoadingDialog(
|
||||
fn: () async {
|
||||
final dir = '${_status.path!.path}/$text';
|
||||
final dir = '${_status.path.path}/$text';
|
||||
await _status.client!.mkdir(dir);
|
||||
return true;
|
||||
},
|
||||
@@ -651,8 +486,8 @@ class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
|
||||
|
||||
final (suc, err) = await context.showLoadingDialog(
|
||||
fn: () async {
|
||||
final path = '${_status.path!.path}/$text';
|
||||
await _client!.run('touch "$path"');
|
||||
final path = '${_status.path.path}/$text';
|
||||
await _client.run('touch "$path"');
|
||||
return true;
|
||||
},
|
||||
);
|
||||
@@ -748,7 +583,7 @@ class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
|
||||
}
|
||||
|
||||
String _getRemotePath(SftpName name) {
|
||||
final prePath = _status.path!.path;
|
||||
final prePath = _status.path.path;
|
||||
// Only support Linux as remote now, so the seperator is '/'
|
||||
return prePath.joinPath(name.filename, seperator: '/');
|
||||
}
|
||||
@@ -761,8 +596,8 @@ class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
|
||||
Future<bool?> _listDir() async {
|
||||
final (ret, err) = await context.showLoadingDialog(
|
||||
fn: () async {
|
||||
_status.client ??= await _client?.sftp();
|
||||
final listPath = _status.path?.path ?? '/';
|
||||
_status.client ??= await _client.sftp();
|
||||
final listPath = _status.path.path;
|
||||
final fs = await _status.client?.listdir(listPath);
|
||||
if (fs == null) {
|
||||
return false;
|
||||
@@ -776,16 +611,16 @@ class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
|
||||
fs.removeAt(0);
|
||||
}
|
||||
|
||||
/// Issue #96
|
||||
/// Due to [WillPopScope] added in this page
|
||||
/// There is no need to keep '..' folder in listdir
|
||||
/// So remove it
|
||||
if (fs.isNotEmpty && fs.firstOrNull?.filename == '..') {
|
||||
if (fs.isNotEmpty &&
|
||||
fs.firstOrNull?.filename == '..' &&
|
||||
_status.path.path == '/') {
|
||||
fs.removeAt(0);
|
||||
}
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_status.files = fs;
|
||||
_status.files
|
||||
..clear()
|
||||
..addAll(fs);
|
||||
});
|
||||
|
||||
// Only update history when success
|
||||
@@ -803,11 +638,162 @@ class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
|
||||
}
|
||||
|
||||
Future<void> _backward() async {
|
||||
if (_status.path?.undo() ?? false) {
|
||||
if (_status.path.undo()) {
|
||||
await _listDir();
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildBackBtn() {
|
||||
return Btn.icon(
|
||||
onTap: _backward,
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSearchBtn() {
|
||||
return Btn.icon(
|
||||
onTap: () {
|
||||
Stream<SftpName> find(String query) async* {
|
||||
final fs = _status.files;
|
||||
for (final f in fs) {
|
||||
if (f.filename.contains(query)) yield f;
|
||||
}
|
||||
}
|
||||
|
||||
showSearch(
|
||||
context: context,
|
||||
delegate: SearchPage(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 3),
|
||||
future: (q) => find(q).toList(),
|
||||
builder: (ctx, e) => _buildItem(e, beforeTap: () => ctx.pop()),
|
||||
));
|
||||
},
|
||||
icon: const Icon(Icons.search),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildUploadBtn() {
|
||||
return Btn.icon(
|
||||
onTap: () async {
|
||||
final idx = await context.showRoundDialog(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Btn.tile(
|
||||
icon: const Icon(Icons.open_in_new),
|
||||
text: l10n.system,
|
||||
onTap: () => context.pop(1),
|
||||
),
|
||||
Btn.tile(
|
||||
icon: const Icon(Icons.folder),
|
||||
text: l10n.inner,
|
||||
onTap: () => context.pop(0),
|
||||
),
|
||||
],
|
||||
));
|
||||
final path = switch (idx) {
|
||||
0 =>
|
||||
await AppRoutes.localStorage(isPickFile: true).go<String>(context),
|
||||
1 => await Pfs.pickFilePath(),
|
||||
_ => null,
|
||||
};
|
||||
if (path == null) return;
|
||||
|
||||
final remoteDir = _status.path.path;
|
||||
final fileName = path.split(Platform.pathSeparator).lastOrNull;
|
||||
final remotePath = '$remoteDir/$fileName';
|
||||
Loggers.app.info('SFTP upload local: $path, remote: $remotePath');
|
||||
Pros.sftp.add(
|
||||
SftpReq(widget.spi, remotePath, path, SftpReqType.upload),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.upload_file),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAddBtn() {
|
||||
return Btn.icon(
|
||||
onTap: () => context.showRoundDialog(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Btn.tile(
|
||||
icon: const Icon(Icons.folder),
|
||||
text: libL10n.folder,
|
||||
onTap: _mkdir,
|
||||
),
|
||||
Btn.tile(
|
||||
icon: const Icon(Icons.insert_drive_file),
|
||||
text: libL10n.file,
|
||||
onTap: _newFile,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
icon: const Icon(Icons.add),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildGotoBtn() {
|
||||
return Btn.icon(
|
||||
onTap: () async {
|
||||
final p = await context.showRoundDialog<String>(
|
||||
title: l10n.goto,
|
||||
child: Autocomplete<String>(
|
||||
optionsBuilder: (val) {
|
||||
if (!Stores.setting.recordHistory.fetch()) {
|
||||
return [];
|
||||
}
|
||||
return Stores.history.sftpGoPath.all.cast<String>().where(
|
||||
(element) => element.contains(val.text),
|
||||
);
|
||||
},
|
||||
fieldViewBuilder: (_, controller, node, __) {
|
||||
return Input(
|
||||
autoFocus: true,
|
||||
icon: Icons.abc,
|
||||
label: libL10n.path,
|
||||
node: node,
|
||||
controller: controller,
|
||||
suggestion: true,
|
||||
onSubmitted: (value) => context.pop(value),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
if (p == null || p.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
_status.path.path = p;
|
||||
final suc = await _listDir() ?? false;
|
||||
if (suc && Stores.setting.recordHistory.fetch()) {
|
||||
Stores.history.sftpGoPath.add(p);
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.gps_fixed),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRefreshBtn() {
|
||||
return Btn.icon(
|
||||
onTap: _listDir,
|
||||
icon: const Icon(Icons.refresh),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHomeBtn() {
|
||||
return IconButton(
|
||||
onPressed: () {
|
||||
final user = widget.spi.user;
|
||||
_status.path.path = user != 'root' ? '/home/$user' : '/root';
|
||||
_listDir();
|
||||
},
|
||||
icon: const Icon(Icons.home),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<void> afterFirstLayout(BuildContext context) {
|
||||
var initPath = '/';
|
||||
@@ -817,7 +803,8 @@ class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
|
||||
initPath = history;
|
||||
}
|
||||
}
|
||||
_status.path = AbsolutePath(widget.initPath ?? initPath);
|
||||
|
||||
_status.path.path = widget.initPath ?? initPath;
|
||||
_listDir();
|
||||
}
|
||||
}
|
||||
@@ -947,5 +934,15 @@ class _SortOption {
|
||||
final _SortType sortBy;
|
||||
final bool reversed;
|
||||
|
||||
_SortOption({required this.sortBy, required this.reversed});
|
||||
_SortOption({this.sortBy = _SortType.name, this.reversed = false});
|
||||
|
||||
_SortOption copyWith({
|
||||
_SortType? sortBy,
|
||||
bool? reversed,
|
||||
}) {
|
||||
return _SortOption(
|
||||
sortBy: sortBy ?? this.sortBy,
|
||||
reversed: reversed ?? this.reversed,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user