Publicerad källa
Ladda ner källa ZIP
CH-J SM Mobile
Bläddra i kataloger och filer för en specifik programutgåva.
CH-J / SM Mobile
134 filer
sm-mobile-20261016T050549Z-3678f2a05fa9
Tillagd: · Byggd:
1
import 'dart:io';2
import 'package:dartssh2/dartssh2.dart';3
import 'package:file_picker/file_picker.dart';4
import 'package:flutter/material.dart';5
import 'package:path_provider/path_provider.dart';6
import 'package:share_plus/share_plus.dart';7
import '../core/app_controller.dart';8
import '../l10n/strings.dart';9
import '../models/server_profile.dart';10
import '../services/sftp_service.dart';11
import 'common.dart';13
class FilesPage extends StatefulWidget {14
final AppController controller;15
const FilesPage(this.controller, {super.key});16
@override17
State<FilesPage> createState() => _FilesPageState();18
}20
class _FilesPageState extends State<FilesPage> {21
SftpService? service;22
String path = '/', error = '';23
List<SftpName> entries = [];24
bool loading = true;25
TransferToken? transfer;26
int transferred = 0, total = 0;27
@override28
void initState() {29
super.initState();30
load(initial: true);31
}33
@override34
void dispose() {35
transfer?.cancel();36
service?.client.close();37
super.dispose();38
}40
Future<void> load({bool initial = false, String? directory}) async {41
if (!widget.controller.ssh.connected) {42
if (mounted) setState(() => loading = false);43
return;44
}45
setState(() {46
loading = true;47
error = '';48
});49
try {50
if (service == null) {51
final sftp = await widget.controller.ssh.client.sftp();52
if (!mounted) {53
sftp.close();54
return;55
}56
service = SftpService(sftp);57
}58
final next = initial ? await service!.home() : directory ?? path;59
final result = await service!.list(next);60
if (mounted) {61
setState(() {62
path = next;63
entries = result;64
});65
}66
} catch (e) {67
if (mounted) {68
showFailure(context, e);69
}70
} finally {71
if (mounted) setState(() => loading = false);72
}73
}75
Future<void> act(Future<void> Function() action) async {76
try {77
await action();78
if (mounted) await load();79
} catch (e) {80
if (mounted) showFailure(context, e);81
}82
}84
Future<void> runTransfer(85
Future<void> Function(TransferToken, void Function(int, int)) action,86
) async {87
final token = TransferToken();88
setState(() {89
transfer = token;90
transferred = 0;91
total = 0;92
});93
try {94
await action(token, (n, size) {95
if (mounted) {96
setState(() {97
transferred = n;98
total = size;99
});100
}101
});102
} finally {103
if (mounted) setState(() => transfer = null);104
}105
}107
Future<void> upload() async {108
await act(() async {109
final picked = await widget.controller.withSystemDialog(110
() => FilePicker.pickFile(),111
);112
if (picked == null || !mounted || !widget.controller.vault.unlocked) {113
return;114
}115
final destination = childPath(path, picked.name);116
await runTransfer(117
(token, progress) => service!.upload(118
picked.readAsByteStream(),119
picked.lengthSync() ?? 0,120
destination,121
token,122
progress,123
),124
);125
});126
}128
Future<void> download(SftpName entry) async {129
final origin =130
(context.findRenderObject() as RenderBox?)?.localToGlobal(131
Offset.zero,132
) ??133
Offset.zero;134
await act(() async {135
final tempRoot = await getTemporaryDirectory();136
final dir = await Directory(137
'${tempRoot.path}/sm-transfer-${newId()}',138
).create();139
final local = File('${dir.path}/${safeName(entry.filename)}');140
try {141
await runTransfer(142
(token, progress) => service!.download(143
childPath(path, entry.filename),144
local,145
token,146
progress,147
),148
);149
if (!mounted || !widget.controller.vault.unlocked) return;150
await widget.controller.withSystemDialog(151
() => SharePlus.instance.share(152
ShareParams(153
files: [XFile(local.path)],154
sharePositionOrigin: Rect.fromLTWH(origin.dx, origin.dy, 1, 1),155
),156
),157
);158
} finally {159
if (await dir.exists()) await dir.delete(recursive: true);160
}161
});162
}164
@override165
Widget build(BuildContext context) {166
if (!widget.controller.ssh.connected) {167
return const PageMessage('selectServer', icon: Icons.folder_outlined);168
}169
return Column(170
children: [171
Padding(172
padding: const EdgeInsets.symmetric(horizontal: 12),173
child: Row(174
children: [175
IconButton(176
onPressed: loading || transfer != null177
? null178
: () => load(directory: parentPath(path)),179
tooltip: context.tr('parent'),180
icon: const Icon(Icons.arrow_upward),181
),182
Expanded(child: SelectableText(path)),183
IconButton(184
onPressed: loading || transfer != null ? null : () => load(),185
tooltip: context.tr('refresh'),186
icon: const Icon(Icons.refresh),187
),188
PopupMenuButton<String>(189
enabled: !loading && transfer == null,190
onSelected: (action) async {191
if (action == 'upload') await upload();192
if (action == 'newFolder' && context.mounted) {193
final name = await askName(context, 'newFolder');194
if (name != null) {195
await act(() => service!.mkdir(childPath(path, name)));196
}197
}198
},199
itemBuilder: (ctx) => ['upload', 'newFolder']200
.map((a) => PopupMenuItem(value: a, child: Text(ctx.tr(a))))201
.toList(),202
),203
],204
),205
),206
if (loading) const LinearProgressIndicator(),207
if (transfer != null)208
Padding(209
padding: const EdgeInsets.all(12),210
child: Column(211
children: [212
LinearProgressIndicator(213
value: total > 0 ? (transferred / total).clamp(0, 1) : null,214
),215
Row(216
children: [217
Expanded(child: Text('$transferred / $total B')),218
TextButton(219
onPressed: transfer!.cancel,220
child: Text(context.tr('cancel')),221
),222
],223
),224
],225
),226
),227
Expanded(228
child: entries.isEmpty && !loading229
? const PageMessage('emptyDirectory')230
: ListView.builder(231
itemCount: entries.length,232
itemBuilder: (context, i) {233
final e = entries[i], attrs = entries[i].attr;234
final symlink = attrs.isSymbolicLink;235
final time = attrs.modifyTime == null236
? '—'237
: DateTime.fromMillisecondsSinceEpoch(238
attrs.modifyTime! * 1000,239
).toLocal().toString();240
return ListTile(241
leading: Icon(242
symlink243
? Icons.link_off244
: attrs.isDirectory245
? Icons.folder_outlined246
: Icons.insert_drive_file_outlined,247
),248
title: Text(e.filename),249
subtitle: Text(250
symlink251
? context.tr('symlink')252
: '${context.tr('size')}: ${attrs.size ?? '—'} B · ${context.tr('modified')}: $time',253
),254
onTap: attrs.isDirectory && !symlink && transfer == null255
? () => load(directory: childPath(path, e.filename))256
: null,257
trailing: symlink258
? null259
: PopupMenuButton<String>(260
enabled: transfer == null && !loading,261
onSelected: (action) async {262
if (action == 'download') await download(e);263
if (action == 'delete' &&264
context.mounted &&265
await confirm(266
context,267
'confirmDelete',268
detail: childPath(path, e.filename),269
)) {270
await act(271
() => service!.remove(272
childPath(path, e.filename),273
),274
);275
}276
if (action == 'rename' && context.mounted) {277
final name = await askName(278
context,279
'rename',280
initial: e.filename,281
);282
if (name != null &&283
context.mounted &&284
await confirm(285
context,286
'confirmRename',287
detail: '${e.filename} → $name',288
)) {289
await act(290
() => service!.rename(291
childPath(path, e.filename),292
childPath(path, name),293
),294
);295
}296
}297
},298
itemBuilder: (ctx) =>299
[300
if (attrs.isFile) 'download',301
'rename',302
'delete',303
]304
.map(305
(a) => PopupMenuItem(306
value: a,307
child: Text(ctx.tr(a)),308
),309
)310
.toList(),311
),312
);313
},314
),315
),316
],317
);318
}319
}SHA-256
331add2cdd4ae32b9e5197c0570880a1c43974fa2f72c53c342c9d4a06e24fb7CH-J Proprietary Software License 1.14 · Källan finns under CH-J Proprietary Software License 1.14. Dess tillgänglighet ändrar inte licensvillkoren eller ger ytterligare rättigheter.
Arkiv SHA-256
c486a807e4828998b9c57ab0f2e760f7980b31965fe118e89d3e1a5a2b65be65