feat(vault): pick a notebook vault folder on first run
Some checks failed
CI / Windows build (push) Has been cancelled
Some checks failed
CI / Windows build (push) Has been cancelled
Phase 0 of the file-based storage plan (docs/plans/ 2026-06-24-file-based-storage.md). Foundation only — no editor or DB change yet. - VaultService (SharedPreferences): stores the vault root path, vaultRootValid() = path set AND directory exists. - VaultSetupScreen: first-run folder picker (file_picker, Windows). - main.dart gates HomeScreen behind a valid vault, re-prompting if the saved folder is gone. - Settings: a Vault section to change the folder. Editors still use SQLite; later phases move annotations into per-file sidecars under the vault. analyze clean, tests green.
This commit is contained in:
70
lib/services/vault_service.dart
Normal file
70
lib/services/vault_service.dart
Normal file
@@ -0,0 +1,70 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart' show visibleForTesting;
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
/// Records the user-picked vault root folder (an Obsidian-style vault) and
|
||||
/// gates app startup behind a valid choice.
|
||||
///
|
||||
/// The vault root is the single folder under which all notebooks will live.
|
||||
/// Phase 0 only persists the path and validates it exists; no data is moved
|
||||
/// into the vault yet (later phases do that).
|
||||
///
|
||||
/// Persistence is SharedPreferences-backed under [vaultRootKey]. The service is
|
||||
/// easily mockable: inject a [SharedPreferences] (e.g. from
|
||||
/// `SharedPreferences.setMockInitialValues`) via the constructor for tests.
|
||||
class VaultService {
|
||||
/// SharedPreferences key under which the vault root path is stored.
|
||||
static const String vaultRootKey = 'vaultRoot';
|
||||
|
||||
final SharedPreferences _prefs;
|
||||
|
||||
VaultService._(this._prefs);
|
||||
|
||||
static VaultService? _instance;
|
||||
|
||||
/// Singleton accessor, mirroring [DatabaseService.getInstance]. Lazily reads
|
||||
/// the shared [SharedPreferences] instance.
|
||||
static Future<VaultService> getInstance() async {
|
||||
if (_instance != null) return _instance!;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final service = VaultService._(prefs);
|
||||
_instance = service;
|
||||
return service;
|
||||
}
|
||||
|
||||
/// Test-only constructor: inject a (typically mock) [SharedPreferences] so
|
||||
/// the vault root can be exercised without platform channels.
|
||||
@visibleForTesting
|
||||
VaultService.forTest(SharedPreferences prefs) : _prefs = prefs;
|
||||
|
||||
/// Test-only: drop the cached singleton so the next [getInstance] rebuilds.
|
||||
@visibleForTesting
|
||||
static void resetForTest() {
|
||||
_instance = null;
|
||||
}
|
||||
|
||||
/// The currently stored vault root path, or null if none has been chosen.
|
||||
String? get vaultRoot => _prefs.getString(vaultRootKey);
|
||||
|
||||
/// Persist [path] as the vault root.
|
||||
Future<void> setVaultRoot(String path) async {
|
||||
await _prefs.setString(vaultRootKey, path);
|
||||
}
|
||||
|
||||
/// Forget the stored vault root (e.g. to re-prompt the user).
|
||||
Future<void> clearVaultRoot() async {
|
||||
await _prefs.remove(vaultRootKey);
|
||||
}
|
||||
|
||||
/// True iff a vault root is set AND that directory currently exists.
|
||||
///
|
||||
/// Returns false when no path is stored or when the stored path no longer
|
||||
/// resolves to a directory (external drive unplugged, folder deleted) — the
|
||||
/// caller then re-prompts rather than silently scattering data elsewhere.
|
||||
Future<bool> vaultRootValid() async {
|
||||
final path = vaultRoot;
|
||||
if (path == null || path.isEmpty) return false;
|
||||
return Directory(path).exists();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user