CH-J SM Mobilegestione server su SSH
Menu
Sorgente pubblicata

CH-J SM Mobile

Sfoglia directory e file per una specifica release di applicazioni.

Scarica sorgente ZIP
CH-J / SM Mobile 134 file
sm-mobile-20261016T050549Z-3678f2a05fa9 Aggiunto: · Costruito:
File
files_page.dart Aggiunto: Data non registrata 10,7 KB · 319 righe
Scarica file
1import 'dart:io';
2import 'package:dartssh2/dartssh2.dart';
3import 'package:file_picker/file_picker.dart';
4import 'package:flutter/material.dart';
5import 'package:path_provider/path_provider.dart';
6import 'package:share_plus/share_plus.dart';
7import '../core/app_controller.dart';
8import '../l10n/strings.dart';
9import '../models/server_profile.dart';
10import '../services/sftp_service.dart';
11import 'common.dart';
13class FilesPage extends StatefulWidget {
14 final AppController controller;
15 const FilesPage(this.controller, {super.key});
16 @override
17 State<FilesPage> createState() => _FilesPageState();
20class _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 @override
28 void initState() {
29 super.initState();
30 load(initial: true);
31 }
33 @override
34 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 @override
165 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 != null
177 ? null
178 : () => 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 && !loading
229 ? 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 == null
236 ? '—'
237 : DateTime.fromMillisecondsSinceEpoch(
238 attrs.modifyTime! * 1000,
239 ).toLocal().toString();
240 return ListTile(
241 leading: Icon(
242 symlink
243 ? Icons.link_off
244 : attrs.isDirectory
245 ? Icons.folder_outlined
246 : Icons.insert_drive_file_outlined,
247 ),
248 title: Text(e.filename),
249 subtitle: Text(
250 symlink
251 ? context.tr('symlink')
252 : '${context.tr('size')}: ${attrs.size ?? '—'} B · ${context.tr('modified')}: $time',
253 ),
254 onTap: attrs.isDirectory && !symlink && transfer == null
255 ? () => load(directory: childPath(path, e.filename))
256 : null,
257 trailing: symlink
258 ? null
259 : 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 }
SHA-256331add2cdd4ae32b9e5197c0570880a1c43974fa2f72c53c342c9d4a06e24fb7

CH-J Proprietary Software License 1.14 · La sorgente è fornita sotto CH-J Proprietary Software License 1.14. La sua disponibilità non modifica i termini di licenza o concede diritti aggiuntivi.

Archivio SHA-256c486a807e4828998b9c57ab0f2e760f7980b31965fe118e89d3e1a5a2b65be65