import 'dart:io'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:path/path.dart' as p; import '../l10n/app_localizations.dart'; import '../services/vault_service.dart'; /// First-run (and re-prompt) gate that asks the user to pick a vault root /// folder — the single folder under which all notebooks will live, like an /// Obsidian vault. /// /// On a successful, writable selection the chosen path is persisted via /// [VaultService] and [onVaultReady] is invoked so the host can proceed to the /// home screen. When [missing] is true the screen shows a "your vault folder is /// missing" message instead of the first-run copy (the saved folder no longer /// exists). class VaultSetupScreen extends StatefulWidget { const VaultSetupScreen({ super.key, required this.vaultService, required this.onVaultReady, this.missing = false, }); final VaultService vaultService; /// Called after a valid, writable vault root has been persisted. final VoidCallback onVaultReady; /// Whether a previously-chosen folder went missing (changes the copy). final bool missing; @override State createState() => _VaultSetupScreenState(); } class _VaultSetupScreenState extends State { bool _busy = false; String? _error; Future _pickFolder() async { final l = AppLocalizations.of(context); setState(() { _busy = true; _error = null; }); try { // getDirectoryPath is Desktop/Windows supported by file_picker. // lockParentWindow makes the native Windows dialog modal. final path = await FilePicker.platform.getDirectoryPath( dialogTitle: l.vaultSetupTitle, lockParentWindow: true, ); if (path == null) { // User cancelled the native dialog. if (mounted) setState(() => _busy = false); return; } if (!await _isWritable(path)) { if (mounted) { setState(() { _busy = false; _error = l.vaultNotWritable; }); } return; } await widget.vaultService.setVaultRoot(path); if (mounted) widget.onVaultReady(); } catch (e) { if (mounted) { setState(() { _busy = false; _error = l.vaultPickFailed(e.toString()); }); } } } /// Probe writability by creating and deleting a temp file in [path]. The /// native folder dialog can hand back a read-only location on Windows, so we /// validate before committing it as the vault. Future _isWritable(String path) async { final probe = File(p.join(path, '.badnote-write-probe')); try { await probe.writeAsString('ok', flush: true); await probe.delete(); return true; } catch (_) { return false; } } @override Widget build(BuildContext context) { final l = AppLocalizations.of(context); final colorScheme = Theme.of(context).colorScheme; final textTheme = Theme.of(context).textTheme; return Scaffold( body: Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 480), child: Padding( padding: const EdgeInsets.all(32), child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon( widget.missing ? Icons.folder_off_outlined : Icons.folder_special_outlined, size: 56, color: colorScheme.primary, ), const SizedBox(height: 24), Text( widget.missing ? l.vaultMissingTitle : l.vaultSetupHeadline, style: textTheme.headlineSmall ?.copyWith(fontWeight: FontWeight.bold), ), const SizedBox(height: 12), Text( widget.missing ? l.vaultMissingBody : l.vaultSetupBody, style: textTheme.bodyMedium ?.copyWith(color: colorScheme.onSurfaceVariant), ), if (_error != null) ...[ const SizedBox(height: 16), Text( _error!, style: textTheme.bodyMedium ?.copyWith(color: colorScheme.error), ), ], const SizedBox(height: 32), FilledButton.icon( onPressed: _busy ? null : _pickFolder, icon: _busy ? const SizedBox( width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.folder_open), label: Text(l.vaultChooseFolder), ), ], ), ), ), ), ); } }