mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2025-12-17 15:24:35 +01:00
417 lines
12 KiB
Dart
417 lines
12 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:fl_lib/fl_lib.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:server_box/core/extension/context/locale.dart';
|
|
import 'package:server_box/data/model/app/path_with_prefix.dart';
|
|
import 'package:server_box/data/model/server/server_private_info.dart';
|
|
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/data/store/setting.dart';
|
|
import 'package:server_box/view/page/storage/sftp.dart';
|
|
import 'package:server_box/view/page/storage/sftp_mission.dart';
|
|
|
|
final class LocalFilePageArgs {
|
|
final bool? isPickFile;
|
|
final String? initDir;
|
|
const LocalFilePageArgs({this.isPickFile, this.initDir});
|
|
}
|
|
|
|
class LocalFilePage extends ConsumerStatefulWidget {
|
|
final LocalFilePageArgs? args;
|
|
|
|
const LocalFilePage({super.key, this.args});
|
|
|
|
static const route = AppRoute<String, LocalFilePageArgs>(page: LocalFilePage.new, path: '/files/local');
|
|
|
|
@override
|
|
ConsumerState<LocalFilePage> createState() => _LocalFilePageState();
|
|
}
|
|
|
|
class _LocalFilePageState extends ConsumerState<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
|
|
void dispose() {
|
|
super.dispose();
|
|
_sortType.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
super.build(context);
|
|
final title = _path.path.fileNameGetter ?? libL10n.file;
|
|
return Scaffold(
|
|
appBar: CustomAppBar(
|
|
title: AnimatedSwitcher(
|
|
duration: Durations.short3,
|
|
child: Text(title, key: ValueKey(title)),
|
|
),
|
|
actions: [
|
|
if (!isPickFile)
|
|
IconButton(
|
|
onPressed: () async {
|
|
final path = await Pfs.pickFilePath();
|
|
if (path == null) return;
|
|
final name = path.getFileName() ?? 'imported';
|
|
final destinationDir = Directory(_path.path);
|
|
if (!await destinationDir.exists()) {
|
|
await destinationDir.create(recursive: true);
|
|
}
|
|
await File(path).copy(_path.path.joinPath(name));
|
|
setState(() {});
|
|
},
|
|
icon: const Icon(Icons.add),
|
|
),
|
|
if (!isMobile)
|
|
IconButton(
|
|
icon: const Icon(Icons.refresh),
|
|
tooltip: MaterialLocalizations.of(context).refreshIndicatorSemanticLabel,
|
|
onPressed: () => setState(() {}),
|
|
),
|
|
if (!isPickFile) _buildMissionBtn(),
|
|
_buildSortBtn(),
|
|
],
|
|
),
|
|
body: isMobile
|
|
? RefreshIndicator(
|
|
onRefresh: () async {
|
|
setState(() {});
|
|
},
|
|
child: _sortType.listen(_buildBody),
|
|
)
|
|
: _sortType.listen(_buildBody),
|
|
);
|
|
}
|
|
|
|
Widget _buildBody() {
|
|
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;
|
|
}
|
|
|
|
return FutureWidget(
|
|
future: getEntities(),
|
|
loading: UIs.placeholder,
|
|
success: (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 ListTile(
|
|
leading: const Icon(Icons.arrow_back),
|
|
title: const Text('..'),
|
|
onTap: () {
|
|
_path.update('..');
|
|
setState(() {});
|
|
},
|
|
).cardx;
|
|
}
|
|
|
|
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 _buildItem(file: file, fileName: fileName, stat: stat, isDir: isDir);
|
|
},
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildItem({
|
|
required FileSystemEntity file,
|
|
required String fileName,
|
|
required FileStat stat,
|
|
required bool isDir,
|
|
}) {
|
|
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(() {});
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildMissionBtn() {
|
|
return IconButton(
|
|
icon: const Icon(Icons.downloading),
|
|
onPressed: () => SftpMissionPage.route.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;
|
|
}
|
|
|
|
extension _Actions on _LocalFilePageState {
|
|
Future<void> _showDirActionDialog(FileSystemEntity file) async {
|
|
context.showRoundDialog(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
ListTile(
|
|
onTap: () {
|
|
context.pop();
|
|
_showRenameDialog(file);
|
|
},
|
|
title: Text(libL10n.rename),
|
|
leading: const Icon(Icons.abc),
|
|
),
|
|
ListTile(
|
|
onTap: () {
|
|
context.pop();
|
|
_showDeleteDialog(file);
|
|
},
|
|
title: Text(libL10n.delete),
|
|
leading: const Icon(Icons.delete),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<void> _showFileActionDialog(FileSystemEntity file) async {
|
|
final fileName = file.path.split('/').lastOrNull ?? '';
|
|
if (isPickFile) {
|
|
context.showRoundDialog(
|
|
title: libL10n.file,
|
|
child: Text(fileName),
|
|
actions: [
|
|
Btn.ok(
|
|
onTap: () {
|
|
context.pop();
|
|
context.pop(file.path);
|
|
},
|
|
),
|
|
],
|
|
);
|
|
return;
|
|
}
|
|
context.showRoundDialog(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
if (isMobile)
|
|
Btn.tile(
|
|
icon: const Icon(Icons.edit),
|
|
text: libL10n.edit,
|
|
onTap: () => _onTapEdit(file, fileName),
|
|
),
|
|
Btn.tile(
|
|
icon: const Icon(Icons.abc),
|
|
text: libL10n.rename,
|
|
onTap: () {
|
|
context.pop();
|
|
_showRenameDialog(file);
|
|
},
|
|
),
|
|
Btn.tile(
|
|
icon: const Icon(Icons.delete),
|
|
text: libL10n.delete,
|
|
onTap: () {
|
|
context.pop();
|
|
_showDeleteDialog(file);
|
|
},
|
|
),
|
|
Btn.tile(
|
|
icon: const Icon(Icons.upload),
|
|
text: l10n.upload,
|
|
onTap: () => _onTapUpload(file, fileName),
|
|
),
|
|
Btn.tile(
|
|
icon: const Icon(Icons.open_in_new),
|
|
text: libL10n.open,
|
|
onTap: () {
|
|
Pfs.sharePaths(paths: [file.absolute.path]);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showRenameDialog(FileSystemEntity file) {
|
|
final fileName = file.path.split(Pfs.seperator).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}${Pfs.seperator}$newName';
|
|
await context.showLoadingDialog(fn: () => file.rename(newPath));
|
|
|
|
setStateSafe(() {});
|
|
}
|
|
|
|
context.showRoundDialog(
|
|
title: libL10n.rename,
|
|
child: Input(
|
|
autoFocus: true,
|
|
icon: Icons.abc,
|
|
label: libL10n.name,
|
|
controller: ctrl,
|
|
suggestion: true,
|
|
maxLines: 3,
|
|
onSubmitted: (p0) => onSubmit(),
|
|
),
|
|
actions: Btn.ok(onTap: onSubmit).toList,
|
|
);
|
|
}
|
|
|
|
void _showDeleteDialog(FileSystemEntity file) {
|
|
final fileName = file.path.split('/').last;
|
|
context.showRoundDialog(
|
|
title: libL10n.delete,
|
|
child: Text(libL10n.askContinue('${libL10n.delete} $fileName')),
|
|
actions: Btn.ok(
|
|
onTap: () async {
|
|
context.pop();
|
|
try {
|
|
await file.delete(recursive: true);
|
|
} catch (e) {
|
|
context.showSnackBar('${libL10n.fail}:\n$e');
|
|
return;
|
|
}
|
|
setStateSafe(() {});
|
|
},
|
|
).toList,
|
|
);
|
|
}
|
|
}
|
|
|
|
extension _OnTapFile on _LocalFilePageState {
|
|
void _onTapEdit(FileSystemEntity file, String fileName) async {
|
|
context.pop();
|
|
final stat = await file.stat();
|
|
if (stat.size > Miscs.editorMaxSize) {
|
|
context.showRoundDialog(
|
|
title: libL10n.attention,
|
|
child: Text(l10n.fileTooLarge(fileName, stat.size, '1m')),
|
|
);
|
|
return;
|
|
}
|
|
|
|
await EditorPage.route.go(
|
|
context,
|
|
args: EditorPageArgs(
|
|
path: file.absolute.path,
|
|
onSave: (_) {
|
|
context.showSnackBar(l10n.saved);
|
|
setStateSafe(() {});
|
|
},
|
|
closeAfterSave: SettingStore.instance.closeAfterSave.fetch(),
|
|
softWrap: SettingStore.instance.editorSoftWrap.fetch(),
|
|
enableHighlight: SettingStore.instance.editorHighlight.fetch(),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _onTapUpload(FileSystemEntity file, String fileName) async {
|
|
context.pop();
|
|
|
|
final spi = await context.showPickSingleDialog<Spi>(
|
|
title: libL10n.select,
|
|
items: ref.read(serverNotifierProvider).servers.values.toList(),
|
|
display: (e) => e.name,
|
|
);
|
|
if (spi == null) return;
|
|
|
|
final args = SftpPageArgs(spi: spi, isSelect: true);
|
|
final remotePath = await SftpPage.route.go(context, args);
|
|
if (remotePath == null) {
|
|
return;
|
|
}
|
|
|
|
ref.read(sftpNotifierProvider.notifier).add(SftpReq(spi, '$remotePath/$fileName', file.absolute.path, SftpReqType.upload));
|
|
context.showSnackBar(l10n.added2List);
|
|
}
|
|
}
|
|
|
|
enum _SortType {
|
|
name,
|
|
size,
|
|
time;
|
|
|
|
List<FileSystemEntity> sort(List<FileSystemEntity> files) {
|
|
switch (this) {
|
|
case _SortType.name:
|
|
files.sort((a, b) => a.path.compareTo(b.path));
|
|
break;
|
|
case _SortType.size:
|
|
files.sort((a, b) => a.statSync().size.compareTo(b.statSync().size));
|
|
break;
|
|
case _SortType.time:
|
|
files.sort((a, b) => a.statSync().modified.compareTo(b.statSync().modified));
|
|
break;
|
|
}
|
|
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)]),
|
|
);
|
|
}
|
|
}
|