CH-J SM Mobilepamamahala ng server sa pamamagitan ng SSH
Menu
Nailathalang source

CH-J SM Mobile

I-browse ang mga direktoryo at file ng isang partikular na release ng application.

I-download ang source ZIP
CH-J / SM Mobile 175 file
sm-mobile-20261016T185235Z-8e93ecc1b59a Idinagdag: · Binuo:
files_page.dart Idinagdag: Hindi naitala ang petsa 25.5 KB · 800 linya
Code I-download ang file
1import 'dart:async';
2import 'dart:io';
3import 'dart:typed_data';
4import 'package:dartssh2/dartssh2.dart';
5import 'package:file_picker/file_picker.dart';
6import 'package:flutter/material.dart';
7import 'package:path_provider/path_provider.dart';
8import 'package:share_plus/share_plus.dart';
9import '../core/app_controller.dart';
10import '../core/failure.dart';
11import '../l10n/strings.dart';
12import '../models/server_profile.dart';
13import '../services/documents.dart';
14import '../services/editor/languages.dart';
15import '../services/editor/recents.dart';
16import '../services/editor/sources.dart';
17import '../services/sftp_service.dart';
18import '../services/vault.dart';
19import 'adaptive.dart';
20import 'common.dart';
21import 'editor/viewers.dart';
23enum FilesTab { remote, device, recent, favorites }
25/// Remote files over SFTP, files on the device, recent and favorite files.
26/// Files only move between the device and a server on an explicit action.
27class FilesPage extends StatefulWidget {
28 final AppController controller;
29 const FilesPage(this.controller, {super.key});
30 @override
31 State<FilesPage> createState() => _FilesPageState();
34class _FilesPageState extends State<FilesPage> {
35 late FilesTab tab = widget.controller.ssh.connected
36 ? FilesTab.remote
37 : FilesTab.device;
38 SftpService? service;
39 String path = '/';
40 List<SftpName> entries = [];
41 bool loading = false;
42 TransferToken? transfer;
43 int transferred = 0, total = 0;
45 // Device tab: files picked in this session and an opened folder.
46 final picked = <DeviceDocument>[];
47 final folders = <DeviceDocument>[];
48 List<DeviceDocument> folderEntries = [];
50 AppController get c => widget.controller;
52 @override
53 void initState() {
54 super.initState();
55 if (c.ssh.connected) unawaited(load(initial: true));
56 }
58 @override
59 void dispose() {
60 transfer?.cancel();
61 super.dispose();
62 }
64 Future<void> load({bool initial = false, String? directory}) async {
65 if (!c.ssh.connected) {
66 if (mounted) setState(() => loading = false);
67 return;
68 }
69 setState(() => loading = true);
70 try {
71 service ??= await c.sftp();
72 if (!mounted) return;
73 final next = initial
74 ? c.remoteDirectory ?? await service!.home()
75 : directory ?? path;
76 final result = await service!.list(next);
77 if (mounted) {
78 setState(() {
79 path = next;
80 entries = result;
81 });
82 c.remoteDirectory = next;
83 }
84 } catch (e) {
85 if (mounted) showFailure(context, e);
86 } finally {
87 if (mounted) setState(() => loading = false);
88 }
89 }
91 Future<void> act(Future<void> Function() action, {bool reload = true}) async {
92 try {
93 await action();
94 if (mounted && reload && tab == FilesTab.remote) await load();
95 } catch (e) {
96 if (mounted) showFailure(context, e);
97 }
98 }
100 Future<void> runTransfer(
101 Future<void> Function(TransferToken, void Function(int, int)) action,
102 ) async {
103 final token = TransferToken();
104 setState(() {
105 transfer = token;
106 transferred = 0;
107 total = 0;
108 });
109 try {
110 await action(token, (n, size) {
111 if (mounted) {
112 setState(() {
113 transferred = n;
114 total = size;
115 });
116 }
117 });
118 } finally {
119 if (mounted) setState(() => transfer = null);
120 }
121 }
123 Rect get _origin {
124 final o =
125 (context.findRenderObject() as RenderBox?)?.localToGlobal(
126 Offset.zero,
127 ) ??
128 Offset.zero;
129 return Rect.fromLTWH(o.dx, o.dy, 1, 1);
130 }
132 RemoteSource _remote(String file) => RemoteSource(
133 service: () => c.sftpFor(c.ssh.profile!.id),
134 profileId: c.ssh.profile!.id,
135 profileName: c.ssh.profile!.name,
136 path: file,
137 );
139 Future<void> _open(FileSource source) async {
140 await openSource(context, c, source);
141 if (mounted) setState(() {});
142 }
144 // ---------------------------------------------------------------------------
145 // Remote actions
147 Future<void> upload() => act(() async {
148 final file = await c.withSystemDialog(() => FilePicker.pickFile());
149 if (file == null || !mounted || !c.vault.unlocked) return;
150 final destination = childPath(path, file.name);
151 await runTransfer(
152 (token, progress) => service!.upload(
153 file.readAsByteStream(),
154 file.lengthSync() ?? 0,
155 destination,
156 token,
157 progress,
158 ),
159 );
160 });
162 /// Downloads into a private temporary copy and offers the share sheet.
163 Future<void> share(SftpName entry) => act(() async {
164 final tempRoot = await getTemporaryDirectory();
165 final dir = await Directory(
166 '${tempRoot.path}/sm-transfer-${newId()}',
167 ).create();
168 final local = File('${dir.path}/${safeName(entry.filename)}');
169 final origin = _origin;
170 try {
171 await runTransfer(
172 (token, progress) => service!.download(
173 childPath(path, entry.filename),
174 local,
175 token,
176 progress,
177 ),
178 );
179 if (!mounted || !c.vault.unlocked) return;
180 await c.withSystemDialog(
181 () => SharePlus.instance.share(
182 ShareParams(files: [XFile(local.path)], sharePositionOrigin: origin),
183 ),
184 );
185 } finally {
186 if (await dir.exists()) await dir.delete(recursive: true);
187 }
188 }, reload: false);
190 /// Copies a server file to a place the user picks on the device.
191 Future<void> saveToDevice(SftpName entry) => act(() async {
192 Uint8List? bytes;
193 await runTransfer((token, progress) async {
194 final (b, _) = await service!.readFile(
195 childPath(path, entry.filename),
196 maxBytes: maxViewBytes,
197 token: token,
198 progress: progress,
199 );
200 bytes = b;
201 });
202 if (bytes == null || !mounted) return;
203 final doc = await c.withSystemDialog(
204 () => c.documents.saveAs(
205 entry.filename,
206 bytes!,
207 mime: mimeOf(entry.filename),
208 ),
209 );
210 if (doc != null && mounted) {
211 ScaffoldMessenger.of(
212 context,
213 ).showSnackBar(SnackBar(content: Text(context.tr('savedToDevice'))));
214 }
215 }, reload: false);
217 Future<void> newRemoteFile() async {
218 final name = await askName(context, 'newFile');
219 if (name == null || !mounted) return;
220 await act(() async {
221 final target = childPath(path, name);
222 if (await service!.statOrNull(target) != null) {
223 throw const Failure('fileExists');
224 }
225 await service!.saveFile(target, Uint8List(0));
226 if (mounted) await _open(_remote(target));
227 });
228 }
230 // ---------------------------------------------------------------------------
231 // Device actions
233 Future<void> pickFiles() => act(() async {
234 final docs = await c.withSystemDialog(
235 () => c.documents.pick(multiple: true),
236 );
237 if (docs.isEmpty || !mounted) return;
238 setState(() {
239 for (final d in docs) {
240 picked.removeWhere((p) => p.id == d.id);
241 picked.insert(0, d);
242 }
243 });
244 if (docs.length == 1) await _open(DeviceSource(c.documents, docs.single));
245 }, reload: false);
247 Future<void> pickFolder() => act(() async {
248 final folder = await c.withSystemDialog(() => c.documents.pickFolder());
249 if (folder == null || !mounted) return;
250 folders
251 ..clear()
252 ..add(folder);
253 await _listFolder();
254 }, reload: false);
256 Future<void> _listFolder() async {
257 final list = await c.documents.list(folders.last);
258 list.sort(
259 (a, b) => a.directory == b.directory
260 ? a.name.toLowerCase().compareTo(b.name.toLowerCase())
261 : a.directory
262 ? -1
263 : 1,
264 );
265 if (mounted) setState(() => folderEntries = list);
266 }
268 Future<void> newDeviceFile() async {
269 final name = await askName(context, 'newFile', initial: 'untitled.txt');
270 if (name == null || !mounted) return;
271 await act(() async {
272 safeName(name);
273 if (folders.isNotEmpty) {
274 final doc = await c.documents.create(
275 folders.last,
276 name,
277 Uint8List(0),
278 mime: mimeOf(name),
279 );
280 await _listFolder();
281 if (mounted) await _open(DeviceSource.saved(c.documents, doc, 0));
282 } else {
283 // Saved to a place of the user's choice on the first save.
284 await _open(NewSource(name));
285 }
286 }, reload: false);
287 }
289 /// Copies a device file to the current server directory.
290 Future<void> uploadDocument(DeviceDocument doc) => act(() async {
291 if (!c.ssh.connected) throw const Failure('connectToSave');
292 final s = service ??= await c.sftp();
293 final dir = c.remoteDirectory ?? await s.home();
294 final target = childPath(dir, doc.name);
295 final bytes = await c.documents.read(doc);
296 if (await s.statOrNull(target) != null) {
297 if (!mounted) return;
298 final ok = await confirm(context, 'overwriteExisting', detail: target);
299 if (!ok) return;
300 }
301 await s.saveFile(target, bytes, force: true);
302 if (mounted) {
303 ScaffoldMessenger.of(context).showSnackBar(
304 SnackBar(content: Text('${context.tr('uploadedTo')} $target')),
305 );
306 }
307 }, reload: false);
309 // ---------------------------------------------------------------------------
310 // Recent and favorites
312 RecentFiles get recents => RecentFiles(c.vault);
314 RecentFile _entry(FileSource s) => RecentFile(
315 key: s.key,
316 kind: s.kind,
317 name: s.name,
318 location: s.location,
319 source: s.toJson(),
320 opened: DateTime.now(),
321 );
323 Future<void> toggleFavorite(FileSource s) => act(() async {
324 await recents.setFavorite(_entry(s), !recents.isFavorite(s.key));
325 if (mounted) setState(() {});
326 }, reload: false);
328 Future<void> openRecent(RecentFile r) => act(() async {
329 if (r.kind == 'remote' &&
330 (!c.ssh.connected || c.ssh.profile?.id != r.source['profileId'])) {
331 throw const Failure('connectToOpen');
332 }
333 await _open(
334 sourceFromJson(r.source, documents: c.documents, sftpFor: c.sftpFor),
335 );
336 }, reload: false);
338 Future<void> restoreDraft(Draft d) async {
339 final source = sourceFromJson(
340 Map<String, dynamic>.from(d.json['source'] as Map),
341 documents: c.documents,
342 sftpFor: c.sftpFor,
343 fallbackName: d.json['name'] as String? ?? 'untitled.txt',
344 );
345 // The restored draft lives on in the editor (and is stashed again if
346 // the Vault locks before it is saved); the others stay recoverable.
347 await act(
348 () => c.vault.keepDrafts([...c.vault.drafts]..remove(d)),
349 reload: false,
350 );
351 if (mounted) await openSource(context, c, source, draft: d);
352 if (mounted) setState(() {});
353 }
355 List<Draft> get drafts => c.vault.drafts;
357 Future<void> discardDrafts() async {
358 final ok = await confirm(context, 'discardDrafts');
359 if (!ok) return;
360 await act(() => c.vault.clearDrafts(), reload: false);
361 if (mounted) setState(() {});
362 }
364 // ---------------------------------------------------------------------------
365 // UI
367 @override
368 Widget build(BuildContext context) => MinHeight(
369 minHeight: 260,
370 child: Column(
371 children: [
372 Padding(
373 padding: const EdgeInsets.fromLTRB(12, 8, 12, 4),
374 child: SingleChildScrollView(
375 scrollDirection: Axis.horizontal,
376 child: SegmentedButton<FilesTab>(
377 showSelectedIcon: false,
378 segments: [
379 ButtonSegment(
380 value: FilesTab.remote,
381 icon: const Icon(Icons.dns_outlined, size: 18),
382 label: Text(context.tr('remoteFiles')),
383 ),
384 ButtonSegment(
385 value: FilesTab.device,
386 icon: const Icon(Icons.phone_iphone, size: 18),
387 label: Text(context.tr('deviceFiles')),
388 ),
389 ButtonSegment(
390 value: FilesTab.recent,
391 icon: const Icon(Icons.history, size: 18),
392 label: Text(context.tr('recentFiles')),
393 ),
394 ButtonSegment(
395 value: FilesTab.favorites,
396 icon: const Icon(Icons.star_outline, size: 18),
397 label: Text(context.tr('favoriteFiles')),
398 ),
399 ],
400 selected: {tab},
401 onSelectionChanged: transfer != null
402 ? null
403 : (v) {
404 setState(() => tab = v.first);
405 if (tab == FilesTab.remote && service == null) {
406 unawaited(load(initial: true));
407 }
408 },
409 ),
410 ),
411 ),
412 if (c.vault.unlocked && drafts.isNotEmpty) _draftsBanner(context),
413 if (transfer != null)
414 Padding(
415 padding: const EdgeInsets.symmetric(horizontal: 12),
416 child: Column(
417 children: [
418 LinearProgressIndicator(
419 value: total > 0 ? (transferred / total).clamp(0, 1) : null,
420 ),
421 Row(
422 children: [
423 Expanded(child: Text('$transferred / $total B')),
424 TextButton(
425 onPressed: transfer!.cancel,
426 child: Text(context.tr('cancel')),
427 ),
428 ],
429 ),
430 ],
431 ),
432 ),
433 Expanded(
434 child: switch (tab) {
435 FilesTab.remote => _remoteView(context),
436 FilesTab.device => _deviceView(context),
437 FilesTab.recent => _recentView(context, favorites: false),
438 FilesTab.favorites => _recentView(context, favorites: true),
439 },
440 ),
441 ],
442 ),
443 );
445 Widget _draftsBanner(BuildContext context) => Card(
446 margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
447 color: Theme.of(context).colorScheme.secondaryContainer,
448 child: Column(
449 crossAxisAlignment: CrossAxisAlignment.stretch,
450 children: [
451 ListTile(
452 leading: const Icon(Icons.restore_page_outlined),
453 title: Text(context.tr('draftsRecovered')),
454 trailing: TextButton(
455 onPressed: discardDrafts,
456 child: Text(context.tr('discard')),
457 ),
458 ),
459 for (final d in drafts)
460 ListTile(
461 dense: true,
462 title: Text(d.json['name'] as String? ?? '?'),
463 subtitle: Text(d.json['location'] as String? ?? ''),
464 trailing: const Icon(Icons.chevron_right),
465 onTap: () => restoreDraft(d),
466 ),
467 ],
468 ),
469 );
471 Widget _remoteView(BuildContext context) {
472 if (!c.ssh.connected) {
473 return const PageMessage('selectServer', icon: Icons.folder_outlined);
474 }
475 final busy = loading || transfer != null;
476 return Column(
477 children: [
478 Padding(
479 padding: const EdgeInsets.symmetric(horizontal: 12),
480 child: Row(
481 children: [
482 IconButton(
483 onPressed: busy
484 ? null
485 : () => load(directory: parentPath(path)),
486 tooltip: context.tr('parent'),
487 icon: const Icon(Icons.arrow_upward),
488 ),
489 // One line: a deep path must not grow the header; it scrolls.
490 Expanded(child: SelectableText(path, maxLines: 1)),
491 IconButton(
492 onPressed: busy ? null : () => load(),
493 tooltip: context.tr('refresh'),
494 icon: const Icon(Icons.refresh),
495 ),
496 PopupMenuButton<String>(
497 enabled: !busy,
498 onSelected: (action) async {
499 if (action == 'upload') await upload();
500 if (action == 'newFile') await newRemoteFile();
501 if (action == 'newFolder' && context.mounted) {
502 final name = await askName(context, 'newFolder');
503 if (name != null) {
504 await act(() => service!.mkdir(childPath(path, name)));
505 }
506 }
507 },
508 itemBuilder: (ctx) => ['upload', 'newFile', 'newFolder']
509 .map((a) => PopupMenuItem(value: a, child: Text(ctx.tr(a))))
510 .toList(),
511 ),
512 ],
513 ),
514 ),
515 if (loading) const LinearProgressIndicator(),
516 Expanded(
517 child: entries.isEmpty && !loading
518 ? const PageMessage('emptyDirectory')
519 : ListView.builder(
520 itemCount: entries.length,
521 itemBuilder: (context, i) => _remoteTile(context, entries[i]),
522 ),
523 ),
524 ],
525 );
526 }
528 Widget _remoteTile(BuildContext context, SftpName e) {
529 final attrs = e.attr;
530 final symlink = attrs.isSymbolicLink;
531 final file = childPath(path, e.filename);
532 final time = attrs.modifyTime == null
533 ? '—'
534 : DateTime.fromMillisecondsSinceEpoch(
535 attrs.modifyTime! * 1000,
536 ).toLocal().toString().substring(0, 16);
537 final favorite =
538 attrs.isFile && recents.isFavorite('remote:${c.ssh.profile!.id}:$file');
539 return ListTile(
540 leading: Icon(
541 symlink
542 ? Icons.link_off
543 : attrs.isDirectory
544 ? Icons.folder_outlined
545 : _icon(fileKind(e.filename)),
546 ),
547 title: Text(e.filename),
548 subtitle: Text(
549 symlink
550 ? context.tr('symlink')
551 : '${context.tr('size')}: ${attrs.size ?? '—'} B · '
552 '${context.tr('modified')}: $time',
553 ),
554 onTap: transfer != null || symlink
555 ? null
556 : attrs.isDirectory
557 ? () => load(directory: file)
558 : attrs.isFile
559 ? () => _open(_remote(file))
560 : null,
561 trailing: symlink
562 ? null
563 : PopupMenuButton<String>(
564 enabled: transfer == null && !loading,
565 onSelected: (action) async {
566 switch (action) {
567 case 'open':
568 await _open(_remote(file));
569 case 'saveToDevice':
570 await saveToDevice(e);
571 case 'download':
572 await share(e);
573 case 'favorite':
574 await toggleFavorite(_remote(file));
575 case 'delete':
576 if (context.mounted &&
577 await confirm(context, 'confirmDelete', detail: file)) {
578 await act(() => service!.remove(file));
579 }
580 case 'rename':
581 if (!context.mounted) return;
582 final name = await askName(
583 context,
584 'rename',
585 initial: e.filename,
586 );
587 if (name != null &&
588 context.mounted &&
589 await confirm(
590 context,
591 'confirmRename',
592 detail: '${e.filename} → $name',
593 )) {
594 await act(
595 () => service!.rename(file, childPath(path, name)),
596 );
597 }
598 }
599 },
600 itemBuilder: (ctx) => [
601 if (attrs.isFile) ...[
602 PopupMenuItem(value: 'open', child: Text(ctx.tr('open'))),
603 PopupMenuItem(
604 value: 'saveToDevice',
605 child: Text(ctx.tr('saveToDevice')),
606 ),
607 PopupMenuItem(
608 value: 'download',
609 child: Text(ctx.tr('shareFile')),
610 ),
611 PopupMenuItem(
612 value: 'favorite',
613 child: Text(
614 ctx.tr(favorite ? 'removeFavorite' : 'addFavorite'),
615 ),
616 ),
617 ],
618 PopupMenuItem(value: 'rename', child: Text(ctx.tr('rename'))),
619 PopupMenuItem(value: 'delete', child: Text(ctx.tr('delete'))),
620 ],
621 ),
622 );
623 }
625 static IconData _icon(FileKind k) => switch (k) {
626 FileKind.image || FileKind.svg => Icons.image_outlined,
627 FileKind.pdf => Icons.picture_as_pdf_outlined,
628 FileKind.office => Icons.description_outlined,
629 FileKind.archive => Icons.archive_outlined,
630 FileKind.audio => Icons.audiotrack_outlined,
631 FileKind.video => Icons.movie_outlined,
632 FileKind.certificate => Icons.verified_user_outlined,
633 FileKind.notebook => Icons.data_object,
634 _ => Icons.insert_drive_file_outlined,
635 };
637 Widget _deviceView(BuildContext context) => LayoutBuilder(
638 builder: (context, constraints) => ListView(
639 padding: readablePadding(constraints, maxWidth: 900, gutter: 12),
640 children: [
641 Text(
642 context.tr('deviceFilesNote'),
643 style: Theme.of(context).textTheme.bodySmall,
644 ),
645 const SizedBox(height: 8),
646 Wrap(
647 spacing: 8,
648 runSpacing: 8,
649 children: [
650 FilledButton.icon(
651 onPressed: pickFiles,
652 icon: const Icon(Icons.file_open_outlined),
653 label: Text(context.tr('openFiles')),
654 ),
655 OutlinedButton.icon(
656 onPressed: pickFolder,
657 icon: const Icon(Icons.folder_open_outlined),
658 label: Text(context.tr('openFolder')),
659 ),
660 OutlinedButton.icon(
661 onPressed: newDeviceFile,
662 icon: const Icon(Icons.note_add_outlined),
663 label: Text(context.tr('newFile')),
664 ),
665 ],
666 ),
667 if (folders.isNotEmpty) ...[
668 const SizedBox(height: 16),
669 Row(
670 children: [
671 if (folders.length > 1)
672 IconButton(
673 tooltip: context.tr('parent'),
674 onPressed: () async {
675 folders.removeLast();
676 await act(_listFolder, reload: false);
677 },
678 icon: const Icon(Icons.arrow_upward),
679 ),
680 Expanded(
681 child: Text(
682 folders.map((f) => f.name).join(' / '),
683 style: Theme.of(context).textTheme.titleSmall,
684 ),
685 ),
686 ],
687 ),
688 if (folderEntries.isEmpty)
689 Padding(
690 padding: const EdgeInsets.all(16),
691 child: Text(context.tr('emptyDirectory')),
692 ),
693 for (final d in folderEntries) _deviceTile(context, d),
694 ],
695 if (picked.isNotEmpty) ...[
696 const SizedBox(height: 16),
697 Text(
698 context.tr('openedFiles'),
699 style: Theme.of(context).textTheme.titleSmall,
700 ),
701 for (final d in picked) _deviceTile(context, d),
702 ],
703 ],
704 ),
705 );
707 Widget _deviceTile(BuildContext context, DeviceDocument d) {
708 final source = DeviceSource(c.documents, d);
709 final favorite = !d.directory && recents.isFavorite(source.key);
710 return ListTile(
711 contentPadding: EdgeInsets.zero,
712 leading: Icon(
713 d.directory ? Icons.folder_outlined : _icon(fileKind(d.name)),
714 ),
715 title: Text(d.name),
716 subtitle: Text(
717 [
718 if (d.size != null && !d.directory) '${d.size} B',
719 if (!d.writable && !d.directory) context.tr('readOnly'),
720 ].join(' · '),
721 ),
722 onTap: d.directory
723 ? () async {
724 folders.add(d);
725 await act(_listFolder, reload: false);
726 }
727 : () => _open(source),
728 trailing: d.directory
729 ? null
730 : PopupMenuButton<String>(
731 onSelected: (a) async {
732 switch (a) {
733 case 'open':
734 await _open(source);
735 case 'upload':
736 await uploadDocument(d);
737 case 'favorite':
738 await toggleFavorite(source);
739 }
740 },
741 itemBuilder: (ctx) => [
742 PopupMenuItem(value: 'open', child: Text(ctx.tr('open'))),
743 PopupMenuItem(
744 value: 'upload',
745 enabled: c.ssh.connected,
746 child: Text(ctx.tr('uploadToServer')),
747 ),
748 PopupMenuItem(
749 value: 'favorite',
750 child: Text(
751 ctx.tr(favorite ? 'removeFavorite' : 'addFavorite'),
752 ),
753 ),
754 ],
755 ),
756 );
757 }
759 Widget _recentView(BuildContext context, {required bool favorites}) {
760 final list = favorites ? recents.favorites : recents.recent;
761 if (list.isEmpty) {
762 return PageMessage(
763 favorites ? 'noFavorites' : 'noRecentFiles',
764 icon: favorites ? Icons.star_outline : Icons.history,
765 );
766 }
767 return ListView.builder(
768 itemCount: list.length,
769 itemBuilder: (context, i) {
770 final r = list[i];
771 return ListTile(
772 leading: Icon(
773 r.kind == 'remote' ? Icons.dns_outlined : Icons.phone_iphone,
774 ),
775 title: Text(r.name),
776 subtitle: Text(
777 r.location,
778 maxLines: 1,
779 overflow: TextOverflow.ellipsis,
780 ),
781 onTap: () => openRecent(r),
782 trailing: IconButton(
783 tooltip: context.tr(
784 favorites ? 'removeFavorite' : 'removeFromList',
785 ),
786 icon: Icon(favorites ? Icons.star : Icons.close),
787 onPressed: () => act(() async {
788 if (favorites) {
789 await recents.setFavorite(r, false);
790 } else {
791 await recents.remove(r.key);
792 }
793 if (mounted) setState(() {});
794 }, reload: false),
795 ),
796 );
797 },
798 );
799 }
SHA-2564b43bcda52d09fb4e451ed0570e602dc8445e399343c64fe597227c21551febe

CH-J Proprietary Software License 1.14 · Ibinibigay ang source sa ilalim ng CH-J Proprietary Software License 1.14. Hindi binabago ng pagiging available nito ang mga tuntunin ng lisensya at hindi ito nagbibigay ng karagdagang karapatan.

SHA-256 ng archive46e427a43e87af091004f36145590bda8fe90086dfb0f631afeb5029fb0b5655
Mga file