import 'dart:typed_data'; import 'package:flutter/material.dart'; import '../../core/app_controller.dart'; import '../../core/failure.dart'; import '../../l10n/strings.dart'; import '../../services/editor/languages.dart'; import '../../services/editor/recents.dart'; import '../../services/editor/sources.dart'; import '../../services/sftp_service.dart'; import '../common.dart'; /// Remembers [source] in the encrypted list of recent files. Future recordRecent(AppController c, FileSource source) async { if (source.kind == 'new' || !c.vault.unlocked) return; try { await RecentFiles(c.vault).touch( RecentFile( key: source.key, kind: source.kind, name: source.name, location: source.location, source: source.toJson(), opened: DateTime.now(), ), ); } catch (_) { /* Recents are a convenience. */ } } /// Saves [bytes] under a new name: on the device (system save dialog) or on /// the connected server. Only ever on explicit user choice. Returns the new /// source, or null when cancelled. Future saveAsDialog( BuildContext context, AppController c, { required String name, required Uint8List bytes, String? target, }) async { final connected = c.ssh.connected; final where = target ?? await showModalBottomSheet( context: context, builder: (ctx) => SafeArea( child: Column( mainAxisSize: MainAxisSize.min, children: [ ListTile( title: Text( ctx.tr('saveAs'), style: Theme.of(ctx).textTheme.titleMedium, ), ), ListTile( leading: const Icon(Icons.phone_iphone), title: Text(ctx.tr('saveToDevice')), onTap: () => Navigator.pop(ctx, 'device'), ), ListTile( leading: const Icon(Icons.dns_outlined), enabled: connected, title: Text(ctx.tr('saveToServer')), subtitle: Text( connected ? c.ssh.profile!.name : ctx.tr('selectServer'), ), onTap: () => Navigator.pop(ctx, 'server'), ), ], ), ), ); if (where == null || !context.mounted) return null; try { if (where == 'device') { final doc = await c.withSystemDialog( () => c.documents.saveAs(name, bytes, mime: mimeOf(name)), ); if (doc == null) return null; return DeviceSource.saved(c.documents, doc, bytes.length); } final profile = c.ssh.profile!; final service = await c.sftp(); final dir = c.remoteDirectory ?? await service.home(); if (!context.mounted) return null; final path = await _askPath(context, childPath(dir, safeName(name))); if (path == null || !context.mounted) return null; safePath(path); final existing = await service.statOrNull(path); if (existing != null) { if (!context.mounted) return null; final ok = await confirm(context, 'overwriteExisting', detail: path); if (!ok) return null; } await service.saveFile(path, bytes, force: true); final source = RemoteSource( service: () => c.sftpFor(profile.id), profileId: profile.id, profileName: profile.name, path: path, ); await source.load(maxBytes: maxEditableBytes); return source; } on Failure catch (e) { if (e.code == 'atomicNotPossible' || e.code == 'ownerWouldChange') { if (context.mounted) showFailure(context, e); return null; } rethrow; } } Future _askPath(BuildContext context, String initial) async { final field = TextEditingController(text: initial); final result = await showDialog( context: context, builder: (ctx) => AlertDialog( title: Text(ctx.tr('saveToServer')), content: TextField( controller: field, autofocus: true, autocorrect: false, decoration: InputDecoration(labelText: ctx.tr('remotePath')), ), actions: [ TextButton( onPressed: () => Navigator.pop(ctx), child: Text(ctx.tr('cancel')), ), FilledButton( onPressed: () => Navigator.pop(ctx, field.text.trim()), child: Text(ctx.tr('save')), ), ], ), ); Future.delayed(const Duration(seconds: 1), field.dispose); return result; }