pdf_signature/lib/data/services/preferences_providers.dart

245 lines
7.7 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:pdf_signature/l10n/app_localizations.dart';
import 'package:flutter_localized_locales/flutter_localized_locales.dart';
// Helpers to work with BCP-47 language tags
String toLanguageTag(Locale loc) {
final lang = loc.languageCode.toLowerCase();
final region = loc.countryCode;
if (region == null || region.isEmpty) return lang;
return '$lang-${region.toUpperCase()}';
}
Locale _parseLanguageTag(String tag) {
final cleaned = tag.replaceAll('_', '-');
final parts = cleaned.split('-');
if (parts.length >= 2 && parts[1].isNotEmpty) {
return Locale(parts[0].toLowerCase(), parts[1].toUpperCase());
}
return Locale(parts[0].toLowerCase());
}
Set<String> _supportedTags() {
return AppLocalizations.supportedLocales.map((l) => toLanguageTag(l)).toSet();
}
// Keys
const _kTheme = 'theme'; // 'light'|'dark'|'system'
const _kLanguage = 'language'; // BCP-47 tag like 'en', 'zh-TW', 'es'
const _kPageView = 'page_view'; // now only 'continuous'
const _kExportDpi = 'export_dpi'; // double, allowed: 96,144,200,300
String _normalizeLanguageTag(String tag) {
final tags = _supportedTags();
if (tag.isEmpty) return tags.contains('en') ? 'en' : tags.first;
// Replace underscore with hyphen and canonicalize case
final normalized = () {
final t = tag.replaceAll('_', '-');
final parts = t.split('-');
final lang = parts[0].toLowerCase();
if (parts.length >= 2 && parts[1].isNotEmpty) {
return '$lang-${parts[1].toUpperCase()}';
}
return lang;
}();
// Exact match
if (tags.contains(normalized)) return normalized;
// Try fallback to language-only if available
final langOnly = normalized.split('-')[0];
if (tags.contains(langOnly)) return langOnly;
// Try to pick first tag with same language
final candidate = tags.firstWhere(
(t) => t.split('-')[0] == langOnly,
orElse: () => '',
);
if (candidate.isNotEmpty) return candidate;
// Final fallback to English or first supported
return tags.contains('en') ? 'en' : tags.first;
}
class PreferencesState {
final String theme; // 'light' | 'dark' | 'system'
final String language; // 'en' | 'zh-TW' | 'es'
final String pageView; // only 'continuous'
final double exportDpi; // 96.0 | 144.0 | 200.0 | 300.0
const PreferencesState({
required this.theme,
required this.language,
required this.pageView,
required this.exportDpi,
});
PreferencesState copyWith({
String? theme,
String? language,
String? pageView,
double? exportDpi,
}) => PreferencesState(
theme: theme ?? this.theme,
language: language ?? this.language,
pageView: pageView ?? this.pageView,
exportDpi: exportDpi ?? this.exportDpi,
);
}
class PreferencesNotifier extends StateNotifier<PreferencesState> {
final SharedPreferences prefs;
PreferencesNotifier(this.prefs)
: super(
PreferencesState(
theme: prefs.getString(_kTheme) ?? 'system',
language: _normalizeLanguageTag(
prefs.getString(_kLanguage) ??
WidgetsBinding.instance.platformDispatcher.locale
.toLanguageTag(),
),
pageView: prefs.getString(_kPageView) ?? 'continuous',
exportDpi: _readDpi(prefs),
),
) {
// normalize language to supported/fallback
_ensureValid();
}
static double _readDpi(SharedPreferences prefs) {
final d = prefs.getDouble(_kExportDpi);
if (d == null) return 144.0;
const allowed = [96.0, 144.0, 200.0, 300.0];
return allowed.contains(d) ? d : 144.0;
}
void _ensureValid() {
final themeValid = {'light', 'dark', 'system'};
if (!themeValid.contains(state.theme)) {
state = state.copyWith(theme: 'system');
prefs.setString(_kTheme, 'system');
}
final normalized = _normalizeLanguageTag(state.language);
if (normalized != state.language) {
state = state.copyWith(language: normalized);
prefs.setString(_kLanguage, normalized);
}
final pageViewValid = {'continuous'};
if (!pageViewValid.contains(state.pageView)) {
state = state.copyWith(pageView: 'continuous');
prefs.setString(_kPageView, 'continuous');
}
// Ensure DPI is one of allowed values
const allowed = [96.0, 144.0, 200.0, 300.0];
if (!allowed.contains(state.exportDpi)) {
state = state.copyWith(exportDpi: 144.0);
prefs.setDouble(_kExportDpi, 144.0);
}
}
Future<void> setTheme(String theme) async {
final valid = {'light', 'dark', 'system'};
if (!valid.contains(theme)) return;
state = state.copyWith(theme: theme);
await prefs.setString(_kTheme, theme);
}
Future<void> setLanguage(String language) async {
final normalized = _normalizeLanguageTag(language);
state = state.copyWith(language: normalized);
await prefs.setString(_kLanguage, normalized);
}
Future<void> resetToDefaults() async {
final device =
WidgetsBinding.instance.platformDispatcher.locale.toLanguageTag();
final normalized = _normalizeLanguageTag(device);
state = PreferencesState(
theme: 'system',
language: normalized,
pageView: 'continuous',
exportDpi: 144.0,
);
await prefs.setString(_kTheme, 'system');
await prefs.setString(_kLanguage, normalized);
await prefs.setString(_kPageView, 'continuous');
await prefs.setDouble(_kExportDpi, 144.0);
}
Future<void> setPageView(String pageView) async {
final valid = {'continuous'};
if (!valid.contains(pageView)) return;
state = state.copyWith(pageView: pageView);
await prefs.setString(_kPageView, pageView);
}
Future<void> setExportDpi(double dpi) async {
const allowed = [96.0, 144.0, 200.0, 300.0];
if (!allowed.contains(dpi)) return;
state = state.copyWith(exportDpi: dpi);
await prefs.setDouble(_kExportDpi, dpi);
}
}
final sharedPreferencesProvider = FutureProvider<SharedPreferences>((
ref,
) async {
final p = await SharedPreferences.getInstance();
return p;
});
final preferencesProvider =
StateNotifierProvider<PreferencesNotifier, PreferencesState>((ref) {
// In tests, you can override sharedPreferencesProvider
final prefs = ref
.watch(sharedPreferencesProvider)
.maybeWhen(
data: (p) => p,
orElse: () => throw StateError('SharedPreferences not ready'),
);
return PreferencesNotifier(prefs);
});
// pageViewModeProvider removed; the app always runs in continuous mode.
/// Derive the active ThemeMode based on preference and platform brightness
final themeModeProvider = Provider<ThemeMode>((ref) {
final prefs = ref.watch(preferencesProvider);
switch (prefs.theme) {
case 'light':
return ThemeMode.light;
case 'dark':
return ThemeMode.dark;
case 'system':
default:
return ThemeMode.system;
}
});
final localeProvider = Provider<Locale?>((ref) {
final prefs = ref.watch(preferencesProvider);
final supported = _supportedTags();
// Return explicit Locale for supported ones; if not supported, null to follow device
if (supported.contains(prefs.language)) {
return _parseLanguageTag(prefs.language);
}
return null;
});
/// Provides a map of BCP-47 tag -> autonym (self name), independent of UI locale.
final languageAutonymsProvider = FutureProvider<Map<String, String>>((
ref,
) async {
final tags = _supportedTags().toList()..sort();
final delegate = LocaleNamesLocalizationsDelegate();
final Map<String, String> result = {};
for (final tag in tags) {
final locale = _parseLanguageTag(tag);
final names = await delegate.load(locale);
final name = names.nameOf(tag) ?? tag;
result[tag] = name;
}
return result;
});