01aa362d3c
Previously, if the backend was unreachable at launch, the splash screen routed to the login screen. The server URL remained persisted in SharedPreferences but visually appeared lost, frustrating any user who isn't the service operator. - New AuthStatus.offline distinguishes network failures (NetworkException) from HTTP 401 in AuthNotifier.verify(). - Persist has_ever_logged_in flag on first successful verify/login. - Offline + ever-logged-in lands on the briefing with a sticky offline banner (retry button) instead of being punted to login. - OfflineBanner widget is Tier-2-ready so we can surface "last sync X min ago" once real caching lands (Fable task #147).
142 lines
4.2 KiB
Dart
142 lines
4.2 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
import 'api_client_provider.dart';
|
|
|
|
const _kServerUrl = 'server_url';
|
|
const _kThemeMode = 'theme_mode';
|
|
const _kForgejoRepoUrl = 'forgejo_repo_url';
|
|
const _kHasEverLoggedIn = 'has_ever_logged_in';
|
|
|
|
final sharedPreferencesProvider = Provider<SharedPreferences>((ref) {
|
|
throw UnimplementedError('Override in ProviderScope');
|
|
});
|
|
|
|
final cookiesPathProvider = Provider<String>((ref) {
|
|
throw UnimplementedError('Override in ProviderScope');
|
|
});
|
|
|
|
final themeModeProvider =
|
|
NotifierProvider<ThemeModeNotifier, ThemeMode>(ThemeModeNotifier.new);
|
|
|
|
class ThemeModeNotifier extends Notifier<ThemeMode> {
|
|
@override
|
|
ThemeMode build() {
|
|
final prefs = ref.watch(sharedPreferencesProvider);
|
|
return _fromString(prefs.getString(_kThemeMode));
|
|
}
|
|
|
|
static ThemeMode _fromString(String? value) => switch (value) {
|
|
'light' => ThemeMode.light,
|
|
'dark' => ThemeMode.dark,
|
|
_ => ThemeMode.system,
|
|
};
|
|
|
|
Future<void> setMode(ThemeMode mode) async {
|
|
final value = switch (mode) {
|
|
ThemeMode.light => 'light',
|
|
ThemeMode.dark => 'dark',
|
|
_ => 'system',
|
|
};
|
|
await ref.read(sharedPreferencesProvider).setString(_kThemeMode, value);
|
|
state = mode;
|
|
}
|
|
}
|
|
|
|
final forgejoRepoUrlProvider =
|
|
NotifierProvider<ForgejoRepoUrlNotifier, String?>(
|
|
ForgejoRepoUrlNotifier.new);
|
|
|
|
class ForgejoRepoUrlNotifier extends Notifier<String?> {
|
|
@override
|
|
String? build() {
|
|
final prefs = ref.watch(sharedPreferencesProvider);
|
|
return prefs.getString(_kForgejoRepoUrl) ??
|
|
'https://git.fabledsword.com/bvandeusen/FabledApp';
|
|
}
|
|
|
|
Future<void> setUrl(String url) async {
|
|
final clean = url.endsWith('/') ? url.substring(0, url.length - 1) : url;
|
|
await ref.read(sharedPreferencesProvider).setString(_kForgejoRepoUrl, clean);
|
|
state = clean;
|
|
}
|
|
}
|
|
|
|
final serverUrlProvider =
|
|
NotifierProvider<ServerUrlNotifier, String?>(ServerUrlNotifier.new);
|
|
|
|
class ServerUrlNotifier extends Notifier<String?> {
|
|
@override
|
|
String? build() {
|
|
final prefs = ref.watch(sharedPreferencesProvider);
|
|
return prefs.getString(_kServerUrl);
|
|
}
|
|
|
|
Future<void> setUrl(String url) async {
|
|
// Strip trailing slash
|
|
final clean = url.endsWith('/') ? url.substring(0, url.length - 1) : url;
|
|
await ref.read(sharedPreferencesProvider).setString(_kServerUrl, clean);
|
|
state = clean;
|
|
}
|
|
|
|
Future<void> clear() async {
|
|
await ref.read(sharedPreferencesProvider).remove(_kServerUrl);
|
|
state = null;
|
|
}
|
|
}
|
|
|
|
/// Tracks whether this install has ever completed a successful login.
|
|
/// Used so offline users who've logged in before land on the briefing (with a
|
|
/// banner) instead of being dumped onto the login screen as if freshly installed.
|
|
final hasEverLoggedInProvider =
|
|
NotifierProvider<HasEverLoggedInNotifier, bool>(HasEverLoggedInNotifier.new);
|
|
|
|
class HasEverLoggedInNotifier extends Notifier<bool> {
|
|
@override
|
|
bool build() {
|
|
final prefs = ref.watch(sharedPreferencesProvider);
|
|
return prefs.getBool(_kHasEverLoggedIn) ?? false;
|
|
}
|
|
|
|
Future<void> markLoggedIn() async {
|
|
if (state) return;
|
|
await ref.read(sharedPreferencesProvider).setBool(_kHasEverLoggedIn, true);
|
|
state = true;
|
|
}
|
|
|
|
Future<void> clear() async {
|
|
await ref.read(sharedPreferencesProvider).remove(_kHasEverLoggedIn);
|
|
state = false;
|
|
}
|
|
}
|
|
|
|
final serverSettingsProvider =
|
|
AsyncNotifierProvider<ServerSettingsNotifier, Map<String, dynamic>>(
|
|
ServerSettingsNotifier.new);
|
|
|
|
class ServerSettingsNotifier extends AsyncNotifier<Map<String, dynamic>> {
|
|
@override
|
|
Future<Map<String, dynamic>> build() async {
|
|
try {
|
|
return await ref.read(settingsApiProvider).getAll();
|
|
} catch (_) {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
bool get rssEnabled {
|
|
final data = state.value ?? {};
|
|
return data['rss_enabled']?.toString().toLowerCase() == 'true';
|
|
}
|
|
|
|
Future<void> refresh() async {
|
|
state = AsyncData(await ref.read(settingsApiProvider).getAll());
|
|
}
|
|
}
|
|
|
|
final rssEnabledProvider = Provider<bool>((ref) {
|
|
final settings = ref.watch(serverSettingsProvider).value ?? {};
|
|
return settings['rss_enabled']?.toString().toLowerCase() == 'true';
|
|
});
|