mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2025-12-17 23:34:24 +01:00
390 lines
11 KiB
Dart
390 lines
11 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:fl_lib/fl_lib.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:server_box/core/extension/context/locale.dart';
|
|
import 'package:server_box/data/model/server/server_private_info.dart';
|
|
import 'package:server_box/data/model/sftp/worker.dart';
|
|
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 'package:server_box/core/route.dart';
|
|
import 'package:server_box/data/model/app/path_with_prefix.dart';
|
|
|
|
class LocalStoragePage extends StatefulWidget {
|
|
final bool isPickFile;
|
|
final String? initDir;
|
|
const LocalStoragePage({
|
|
super.key,
|
|
required this.isPickFile,
|
|
this.initDir,
|
|
});
|
|
|
|
@override
|
|
State<LocalStoragePage> createState() => _LocalStoragePageState();
|
|
}
|
|
|
|
class _LocalStoragePageState extends State<LocalStoragePage> {
|
|
LocalPath? _path;
|
|
|
|
final _sortType = ValueNotifier(_SortType.name);
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
if (widget.initDir != null) {
|
|
setState(() {
|
|
_path = LocalPath(widget.initDir!);
|
|
});
|
|
} else {
|
|
setState(() {
|
|
_path = LocalPath(Paths.file);
|
|
});
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: CustomAppBar(
|
|
leading: IconButton(
|
|
icon: const BackButtonIcon(),
|
|
onPressed: () {
|
|
if (_path != null) {
|
|
_path!.update('/');
|
|
}
|
|
context.pop();
|
|
},
|
|
),
|
|
title: Text(libL10n.file),
|
|
actions: [
|
|
IconButton(
|
|
icon: const Icon(Icons.downloading),
|
|
onPressed: () => AppRoutes.sftpMission().go(context),
|
|
),
|
|
ValBuilder<_SortType>(
|
|
listenable: _sortType,
|
|
builder: (value) {
|
|
return PopupMenuButton<_SortType>(
|
|
icon: const Icon(Icons.sort),
|
|
itemBuilder: (context) {
|
|
return [
|
|
PopupMenuItem(
|
|
value: _SortType.name,
|
|
child: Text(libL10n.name),
|
|
),
|
|
PopupMenuItem(
|
|
value: _SortType.size,
|
|
child: Text(l10n.size),
|
|
),
|
|
PopupMenuItem(
|
|
value: _SortType.time,
|
|
child: Text(l10n.time),
|
|
),
|
|
];
|
|
},
|
|
onSelected: (value) {
|
|
_sortType.value = value;
|
|
},
|
|
);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
body: FadeIn(
|
|
key: UniqueKey(),
|
|
child: ValBuilder(
|
|
listenable: _sortType,
|
|
builder: (val) {
|
|
return _buildBody();
|
|
},
|
|
),
|
|
),
|
|
bottomNavigationBar: SafeArea(child: _buildPath()),
|
|
);
|
|
}
|
|
|
|
Widget _buildPath() {
|
|
return Container(
|
|
padding: const EdgeInsets.fromLTRB(11, 7, 11, 11),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
OmitStartText(_path?.path ?? '...'),
|
|
_buildBtns(),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildBtns() {
|
|
return Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
|
children: [
|
|
IconButton(
|
|
onPressed: () {
|
|
_path?.update('..');
|
|
setState(() {});
|
|
},
|
|
icon: const Icon(Icons.arrow_back),
|
|
),
|
|
IconButton(
|
|
onPressed: () async {
|
|
final path = await Pfs.pickFilePath();
|
|
if (path == null) return;
|
|
final name = path.getFileName() ?? 'imported';
|
|
await File(path).copy(_path!.path.joinPath(name));
|
|
setState(() {});
|
|
},
|
|
icon: const Icon(Icons.add),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildBody() {
|
|
if (_path == null) {
|
|
return const Center(
|
|
child: CircularProgressIndicator(),
|
|
);
|
|
}
|
|
final dir = Directory(_path!.path);
|
|
final tempFiles = dir.listSync();
|
|
final files = _sortType.value.sort(tempFiles);
|
|
return ListView.builder(
|
|
itemCount: files.length,
|
|
padding: const EdgeInsets.symmetric(vertical: 3, horizontal: 7),
|
|
itemBuilder: (context, index) {
|
|
final file = files[index];
|
|
final fileName = file.path.split('/').last;
|
|
final stat = file.statSync();
|
|
final isDir = stat.type == FileSystemEntityType.directory;
|
|
|
|
return CardX(
|
|
child: ListTile(
|
|
leading: isDir
|
|
? const Icon(Icons.folder_open)
|
|
: const Icon(Icons.insert_drive_file),
|
|
title: Text(fileName),
|
|
subtitle:
|
|
isDir ? null : Text(stat.size.bytes2Str, style: UIs.textGrey),
|
|
trailing: Text(
|
|
stat.modified
|
|
.toString()
|
|
.substring(0, stat.modified.toString().length - 4),
|
|
style: UIs.textGrey,
|
|
),
|
|
onLongPress: () {
|
|
if (!isDir) return;
|
|
_showDirActionDialog(file);
|
|
},
|
|
onTap: () async {
|
|
if (!isDir) {
|
|
await _showFileActionDialog(file);
|
|
return;
|
|
}
|
|
_path!.update(fileName);
|
|
setState(() {});
|
|
},
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
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('/').last;
|
|
if (widget.isPickFile) {
|
|
await 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: [
|
|
ListTile(
|
|
leading: const Icon(Icons.edit),
|
|
title: Text(libL10n.edit),
|
|
onTap: () 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;
|
|
}
|
|
final result = await AppRoutes.editor(
|
|
path: file.absolute.path,
|
|
).go<bool>(context);
|
|
if (result == true) {
|
|
context.showSnackBar(l10n.saved);
|
|
setState(() {});
|
|
}
|
|
},
|
|
),
|
|
ListTile(
|
|
leading: const Icon(Icons.abc),
|
|
title: Text(libL10n.rename),
|
|
onTap: () {
|
|
context.pop();
|
|
_showRenameDialog(file);
|
|
},
|
|
),
|
|
ListTile(
|
|
leading: const Icon(Icons.delete),
|
|
title: Text(libL10n.delete),
|
|
onTap: () {
|
|
context.pop();
|
|
_showDeleteDialog(file);
|
|
},
|
|
),
|
|
ListTile(
|
|
leading: const Icon(Icons.upload),
|
|
title: Text(l10n.upload),
|
|
onTap: () async {
|
|
context.pop();
|
|
|
|
final spi = await context.showPickSingleDialog<ServerPrivateInfo>(
|
|
title: libL10n.select,
|
|
items: Pros.server.serverOrder
|
|
.map((e) => Pros.server.pick(id: e)?.spi)
|
|
.toList(),
|
|
name: (e) => e.name,
|
|
);
|
|
if (spi == null) return;
|
|
|
|
final remotePath = await AppRoutes.sftp(
|
|
spi: spi,
|
|
isSelect: true,
|
|
).go<String>(context);
|
|
if (remotePath == null) {
|
|
return;
|
|
}
|
|
|
|
Pros.sftp.add(SftpReq(
|
|
spi,
|
|
'$remotePath/$fileName',
|
|
file.absolute.path,
|
|
SftpReqType.upload,
|
|
));
|
|
context.showSnackBar(l10n.added2List);
|
|
},
|
|
),
|
|
ListTile(
|
|
leading: const Icon(Icons.open_in_new),
|
|
title: Text(libL10n.open),
|
|
onTap: () {
|
|
Pfs.share(path: file.absolute.path);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showRenameDialog(FileSystemEntity file) {
|
|
final fileName = file.path.split('/').last;
|
|
context.showRoundDialog(
|
|
title: libL10n.rename,
|
|
child: Input(
|
|
autoFocus: true,
|
|
controller: TextEditingController(text: fileName),
|
|
suggestion: true,
|
|
onSubmitted: (p0) {
|
|
context.pop();
|
|
final newPath = '${file.parent.path}/$p0';
|
|
try {
|
|
file.renameSync(newPath);
|
|
} catch (e) {
|
|
context.showSnackBar('${libL10n.fail}:\n$e');
|
|
return;
|
|
}
|
|
|
|
setState(() {});
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
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;
|
|
}
|
|
setState(() {});
|
|
},
|
|
).toList,
|
|
);
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|