Skip to content

Commit c8721f5

Browse files
mikebarkminCopilot
andcommitted
fix(settings): store settings in classi projects
Move library-specific settings out of global shared preferences and into a project-scoped sidecar inside each .classi library. This keeps theme, sorting, grade systems, lock settings, and WebDAV configuration portable with the project while keeping secrets device-local and keyed per project. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 476568c commit c8721f5

16 files changed

Lines changed: 948 additions & 173 deletions

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ and portable across devices.
3434
Classi stores your library in a `.classi` folder that you choose during the
3535
first-run setup. The setup wizard always requires an explicit folder selection
3636
so your data is never silently placed inside an app-private directory.
37+
Library-specific settings such as grade systems, sorting, theme, lock, and
38+
WebDAV backup configuration are stored inside that `.classi` folder too, so
39+
they move with the project instead of being kept as global app preferences.
3740

3841
**Recommended locations:**
3942

lib/core/providers/app_providers.dart

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import '../session/app_session_controller.dart';
2626
import '../storage/database_path_service.dart';
2727
import '../storage/library_backup_preferences_service.dart';
2828
import '../storage/library_backup_service.dart';
29+
import '../storage/project_settings_store.dart';
2930
import '../update/app_update_controller.dart';
3031

3132
final keyServiceProvider = Provider<KeyService>((ref) => KeyService());
@@ -34,13 +35,23 @@ final databasePathServiceProvider = Provider<DatabasePathService>(
3435
(ref) => DatabasePathService(),
3536
);
3637

38+
final projectSettingsStoreProvider = Provider<ProjectSettingsStore>(
39+
(ref) => ProjectSettingsStore(
40+
databasePathService: ref.watch(databasePathServiceProvider),
41+
),
42+
);
43+
3744
final securityPreferencesServiceProvider = Provider<SecurityPreferencesService>(
38-
(ref) => SecurityPreferencesService(),
45+
(ref) => SecurityPreferencesService(
46+
projectSettingsStore: ref.watch(projectSettingsStoreProvider),
47+
),
3948
);
4049

4150
final libraryBackupPreferencesServiceProvider =
4251
Provider<LibraryBackupPreferencesService>(
43-
(ref) => LibraryBackupPreferencesService(),
52+
(ref) => LibraryBackupPreferencesService(
53+
projectSettingsStore: ref.watch(projectSettingsStoreProvider),
54+
),
4455
);
4556

4657
final libraryBackupServiceProvider = Provider<LibraryBackupService>(
@@ -70,10 +81,21 @@ final appSessionProvider = ChangeNotifierProvider<AppSessionController>((ref) {
7081
return controller;
7182
});
7283

84+
final selectedDatabasePathProvider = Provider<String?>(
85+
(ref) => ref.watch(appSessionProvider).databasePath,
86+
);
87+
7388
final studentSortControllerProvider =
7489
ChangeNotifierProvider<StudentSortController>((ref) {
75-
final controller = StudentSortController();
90+
final controller = StudentSortController(
91+
projectSettingsStore: ref.watch(projectSettingsStoreProvider),
92+
);
7693
unawaited(controller.initialize());
94+
ref.listen<String?>(selectedDatabasePathProvider, (previous, next) {
95+
if (previous != next) {
96+
unawaited(controller.initialize());
97+
}
98+
});
7799
return controller;
78100
});
79101

@@ -82,8 +104,15 @@ final studentSortFieldProvider = Provider<StudentSortField>(
82104
);
83105

84106
final themeControllerProvider = ChangeNotifierProvider<ThemeController>((ref) {
85-
final controller = ThemeController();
107+
final controller = ThemeController(
108+
projectSettingsStore: ref.watch(projectSettingsStoreProvider),
109+
);
86110
unawaited(controller.initialize());
111+
ref.listen<String?>(selectedDatabasePathProvider, (previous, next) {
112+
if (previous != next) {
113+
unawaited(controller.initialize());
114+
}
115+
});
87116
return controller;
88117
});
89118

@@ -93,8 +122,15 @@ final themeModeProvider = Provider<ThemeMode>(
93122

94123
final gradeSystemControllerProvider =
95124
ChangeNotifierProvider<GradeSystemController>((ref) {
96-
final controller = GradeSystemController();
125+
final controller = GradeSystemController(
126+
projectSettingsStore: ref.watch(projectSettingsStoreProvider),
127+
);
97128
unawaited(controller.initialize());
129+
ref.listen<String?>(selectedDatabasePathProvider, (previous, next) {
130+
if (previous != next) {
131+
unawaited(controller.initialize());
132+
}
133+
});
98134
return controller;
99135
});
100136

lib/core/security/key_service.dart

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,7 @@ class KeyService {
4242
/// Stores [passphrase] in secure storage so that a successful biometric
4343
/// authentication can retrieve it and unlock the database without re-typing.
4444
Future<void> saveBiometricPassphrase(File dbFile, String passphrase) =>
45-
_storage.write(
46-
key: _biometricPassphraseKey(dbFile),
47-
value: passphrase,
48-
);
45+
_storage.write(key: _biometricPassphraseKey(dbFile), value: passphrase);
4946

5047
/// Returns the passphrase previously saved for biometric unlock, or `null`
5148
/// when none has been stored.
@@ -56,19 +53,38 @@ class KeyService {
5653
Future<void> clearBiometricPassphrase(File dbFile) =>
5754
_storage.delete(key: _biometricPassphraseKey(dbFile));
5855

59-
static const String _webDavPasswordKey = 'backup.webdav_password';
56+
static const String _legacyWebDavPasswordKey = 'backup.webdav_password';
57+
58+
String _webDavPasswordKey(File dbFile) =>
59+
'backup.webdav_password.${dbFile.path}';
60+
61+
/// Returns the stored WebDAV password for [dbFile], or `null` if none exists.
62+
Future<String?> getWebDavPassword(File dbFile) async {
63+
final scopedKey = _webDavPasswordKey(dbFile);
64+
final scoped = await _storage.read(key: scopedKey);
65+
if (scoped != null) {
66+
return scoped;
67+
}
6068

61-
/// Returns the stored WebDAV password, or `null` if none has been set.
62-
Future<String?> getWebDavPassword() =>
63-
_storage.read(key: _webDavPasswordKey);
69+
final legacy = await _storage.read(key: _legacyWebDavPasswordKey);
70+
if (legacy != null) {
71+
await _storage.write(key: scopedKey, value: legacy);
72+
await _storage.delete(key: _legacyWebDavPasswordKey);
73+
}
74+
return legacy;
75+
}
6476

65-
/// Stores the WebDAV password in secure storage.
66-
Future<void> setWebDavPassword(String password) =>
67-
_storage.write(key: _webDavPasswordKey, value: password);
77+
/// Stores the WebDAV password in secure storage for [dbFile].
78+
Future<void> setWebDavPassword(File dbFile, String password) async {
79+
await _storage.write(key: _webDavPasswordKey(dbFile), value: password);
80+
await _storage.delete(key: _legacyWebDavPasswordKey);
81+
}
6882

69-
/// Removes the stored WebDAV password.
70-
Future<void> clearWebDavPassword() =>
71-
_storage.delete(key: _webDavPasswordKey);
83+
/// Removes the stored WebDAV password for [dbFile].
84+
Future<void> clearWebDavPassword(File dbFile) async {
85+
await _storage.delete(key: _webDavPasswordKey(dbFile));
86+
await _storage.delete(key: _legacyWebDavPasswordKey);
87+
}
7288

7389
Future<SecurityBootstrapResult> bootstrapSecurity({
7490
required File dbFile,
Lines changed: 110 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,91 @@
11
import 'package:shared_preferences/shared_preferences.dart';
22

3+
import '../storage/project_settings_store.dart';
4+
35
class SecurityPreferencesService {
6+
SecurityPreferencesService({ProjectSettingsStore? projectSettingsStore})
7+
: _projectSettingsStore = projectSettingsStore ?? ProjectSettingsStore();
8+
49
static const String _lockOnBackgroundKey = 'security.lock_on_background';
510
static const String _inactivityTimeoutSecondsKey =
611
'security.inactivity_timeout_seconds';
712
static const String _sessionDirtyKey = 'security.session_dirty';
813
static const String _biometricEnabledKey = 'security.biometric_enabled';
14+
static const List<String> _lockOnBackgroundPath = [
15+
'security',
16+
'lockOnBackground',
17+
];
18+
static const List<String> _inactivityTimeoutPath = [
19+
'security',
20+
'inactivityTimeoutSeconds',
21+
];
22+
static const List<String> _biometricEnabledPath = [
23+
'security',
24+
'biometricEnabled',
25+
];
926

1027
static const Duration defaultInactivityTimeout = Duration(minutes: 5);
1128

29+
final ProjectSettingsStore _projectSettingsStore;
30+
1231
Future<bool> lockOnBackground() async {
32+
final settings = await _projectSettingsStore.read();
33+
final stored = ProjectSettingsStore.boolAt(settings, _lockOnBackgroundPath);
34+
if (stored != null) {
35+
return stored;
36+
}
37+
1338
final prefs = await SharedPreferences.getInstance();
14-
return prefs.getBool(_lockOnBackgroundKey) ?? true;
39+
final legacy = prefs.getBool(_lockOnBackgroundKey);
40+
if (legacy != null) {
41+
if (await _projectSettingsStore.hasProjectContainer()) {
42+
await _writeBool(
43+
_lockOnBackgroundPath,
44+
legacy,
45+
removeLegacyKey: _lockOnBackgroundKey,
46+
);
47+
}
48+
return legacy;
49+
}
50+
return true;
1551
}
1652

1753
Future<void> setLockOnBackground(bool value) async {
18-
final prefs = await SharedPreferences.getInstance();
19-
await prefs.setBool(_lockOnBackgroundKey, value);
54+
await _writeBool(
55+
_lockOnBackgroundPath,
56+
value,
57+
removeLegacyKey: _lockOnBackgroundKey,
58+
);
2059
}
2160

2261
Future<Duration> inactivityTimeout() async {
62+
final settings = await _projectSettingsStore.read();
63+
final storedSeconds = ProjectSettingsStore.intAt(
64+
settings,
65+
_inactivityTimeoutPath,
66+
);
67+
if (storedSeconds != null && storedSeconds > 0) {
68+
return Duration(seconds: storedSeconds);
69+
}
70+
2371
final prefs = await SharedPreferences.getInstance();
24-
final seconds = prefs.getInt(_inactivityTimeoutSecondsKey);
25-
if (seconds == null || seconds <= 0) {
72+
final legacySeconds = prefs.getInt(_inactivityTimeoutSecondsKey);
73+
if (legacySeconds == null || legacySeconds <= 0) {
2674
return defaultInactivityTimeout;
2775
}
28-
return Duration(seconds: seconds);
76+
await _writeInt(
77+
_inactivityTimeoutPath,
78+
legacySeconds,
79+
removeLegacyKey: _inactivityTimeoutSecondsKey,
80+
);
81+
return Duration(seconds: legacySeconds);
2982
}
3083

3184
Future<void> setInactivityTimeout(Duration duration) async {
32-
final prefs = await SharedPreferences.getInstance();
33-
await prefs.setInt(
34-
_inactivityTimeoutSecondsKey,
85+
await _writeInt(
86+
_inactivityTimeoutPath,
3587
duration.inSeconds,
88+
removeLegacyKey: _inactivityTimeoutSecondsKey,
3689
);
3790
}
3891

@@ -47,12 +100,58 @@ class SecurityPreferencesService {
47100
}
48101

49102
Future<bool> biometricEnabled() async {
103+
final settings = await _projectSettingsStore.read();
104+
final stored = ProjectSettingsStore.boolAt(settings, _biometricEnabledPath);
105+
if (stored != null) {
106+
return stored;
107+
}
108+
50109
final prefs = await SharedPreferences.getInstance();
51-
return prefs.getBool(_biometricEnabledKey) ?? false;
110+
final legacy = prefs.getBool(_biometricEnabledKey);
111+
if (legacy != null) {
112+
if (await _projectSettingsStore.hasProjectContainer()) {
113+
await _writeBool(
114+
_biometricEnabledPath,
115+
legacy,
116+
removeLegacyKey: _biometricEnabledKey,
117+
);
118+
}
119+
return legacy;
120+
}
121+
return false;
52122
}
53123

54124
Future<void> setBiometricEnabled(bool value) async {
125+
await _writeBool(
126+
_biometricEnabledPath,
127+
value,
128+
removeLegacyKey: _biometricEnabledKey,
129+
);
130+
}
131+
132+
Future<void> _writeBool(
133+
List<String> path,
134+
bool value, {
135+
required String removeLegacyKey,
136+
}) async {
137+
await _projectSettingsStore.update((settings) {
138+
ProjectSettingsStore.setPath(settings, path, value);
139+
return settings;
140+
});
141+
final prefs = await SharedPreferences.getInstance();
142+
await prefs.remove(removeLegacyKey);
143+
}
144+
145+
Future<void> _writeInt(
146+
List<String> path,
147+
int value, {
148+
required String removeLegacyKey,
149+
}) async {
150+
await _projectSettingsStore.update((settings) {
151+
ProjectSettingsStore.setPath(settings, path, value);
152+
return settings;
153+
});
55154
final prefs = await SharedPreferences.getInstance();
56-
await prefs.setBool(_biometricEnabledKey, value);
155+
await prefs.remove(removeLegacyKey);
57156
}
58157
}

0 commit comments

Comments
 (0)