261 lines
9.4 KiB
Dart
261 lines
9.4 KiB
Dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
|
import '../../../../core/constants/app_constants.dart';
|
|
import '../../../../core/errors/failure.dart';
|
|
import '../../../../core/network/api_handler.dart';
|
|
import '../../../../core/network/dio_client.dart';
|
|
import '../../../../core/theme/branding_config.dart';
|
|
import '../../../../core/theme/theme_provider.dart';
|
|
import '../../../../core/utils/favicon_store.dart';
|
|
import '../../../../core/utils/favicon_updater.dart';
|
|
import '../../../../core/utils/media_url.dart';
|
|
import '../../data/datasources/settings_local_data_source.dart';
|
|
import '../../data/datasources/settings_remote_data_source.dart';
|
|
import '../../data/repositories/settings_repository_impl.dart';
|
|
import '../../domain/entities/app_settings.dart';
|
|
import '../../domain/repositories/settings_repository.dart';
|
|
import '../../domain/usecases/get_settings_use_case.dart';
|
|
import '../../domain/usecases/save_settings_use_case.dart';
|
|
|
|
final settingsLocalDataSourceProvider = Provider<SettingsLocalDataSource>((ref) {
|
|
return SettingsLocalDataSource(ref.watch(sharedPreferencesProvider));
|
|
});
|
|
|
|
final settingsRemoteDataSourceProvider =
|
|
Provider<SettingsRemoteDataSource>((ref) {
|
|
return SettingsRemoteDataSource(ref.watch(dioProvider));
|
|
});
|
|
|
|
final settingsRepositoryProvider = Provider<SettingsRepository>((ref) {
|
|
return SettingsRepositoryImpl(
|
|
local: ref.watch(settingsLocalDataSourceProvider),
|
|
remote: ref.watch(settingsRemoteDataSourceProvider),
|
|
);
|
|
});
|
|
|
|
final getSettingsUseCaseProvider = Provider<GetSettingsUseCase>((ref) {
|
|
return GetSettingsUseCase(ref.watch(settingsRepositoryProvider));
|
|
});
|
|
|
|
final saveSettingsUseCaseProvider = Provider<SaveSettingsUseCase>((ref) {
|
|
return SaveSettingsUseCase(ref.watch(settingsRepositoryProvider));
|
|
});
|
|
|
|
final appSettingsProvider =
|
|
StateNotifierProvider<AppSettingsNotifier, AppSettings>((ref) {
|
|
return AppSettingsNotifier(
|
|
getSettings: ref.watch(getSettingsUseCaseProvider),
|
|
saveSettings: ref.watch(saveSettingsUseCaseProvider),
|
|
repository: ref.watch(settingsRepositoryProvider),
|
|
faviconStore: FaviconStore(ref.watch(sharedPreferencesProvider)),
|
|
syncAppLogo: (logoUrl, companyName) async {
|
|
final branding = ref.read(brandingProvider);
|
|
await ref.read(brandingProvider.notifier).updateBranding(
|
|
BrandingConfig(
|
|
logoUrl: logoUrl,
|
|
primaryColorValue: branding.primaryColorValue,
|
|
secondaryColorValue: branding.secondaryColorValue,
|
|
companyName: companyName ?? branding.companyName,
|
|
),
|
|
);
|
|
},
|
|
);
|
|
});
|
|
|
|
class AppSettingsNotifier extends StateNotifier<AppSettings> {
|
|
AppSettingsNotifier({
|
|
required GetSettingsUseCase getSettings,
|
|
required SaveSettingsUseCase saveSettings,
|
|
required SettingsRepository repository,
|
|
required FaviconStore faviconStore,
|
|
required Future<void> Function(String? logoUrl, String? companyName)
|
|
syncAppLogo,
|
|
}) : _getSettings = getSettings,
|
|
_saveSettings = saveSettings,
|
|
_repository = repository,
|
|
_faviconStore = faviconStore,
|
|
_syncAppLogo = syncAppLogo,
|
|
super(const AppSettings()) {
|
|
_load();
|
|
}
|
|
|
|
final GetSettingsUseCase _getSettings;
|
|
final SaveSettingsUseCase _saveSettings;
|
|
final SettingsRepository _repository;
|
|
final FaviconStore _faviconStore;
|
|
final Future<void> Function(String? logoUrl, String? companyName) _syncAppLogo;
|
|
Future<Failure?>? _companyProfileInFlight;
|
|
|
|
Future<void> _syncMainLogo(CompanyProfileSettings profile) async {
|
|
final logo = resolveMediaUrl(profile.logoUrl);
|
|
await _syncAppLogo(
|
|
(logo == null || logo.isEmpty) ? null : logo,
|
|
profile.companyName.isEmpty ? null : profile.companyName,
|
|
);
|
|
}
|
|
|
|
Future<void> _syncFavicon(String? faviconUrl) async {
|
|
final resolved = resolveMediaUrl(faviconUrl) ?? faviconUrl?.trim();
|
|
final value = (resolved == null || resolved.isEmpty) ? null : resolved;
|
|
await _faviconStore.write(value);
|
|
updateFavicon(
|
|
resolveFaviconHref(value ?? AppConstants.defaultFaviconAsset),
|
|
);
|
|
}
|
|
|
|
Future<void> _load() async {
|
|
final result = await _getSettings();
|
|
state = result.data ?? const AppSettings();
|
|
await _syncFavicon(state.companyProfile.faviconUrl);
|
|
await _syncMainLogo(state.companyProfile);
|
|
}
|
|
|
|
Future<Failure?> refreshCompanyProfile() {
|
|
final existing = _companyProfileInFlight;
|
|
if (existing != null) return existing;
|
|
|
|
final future = () async {
|
|
try {
|
|
final result = await _repository.fetchCompanyProfile();
|
|
if (result.failure == null && result.data != null) {
|
|
state = state.copyWith(companyProfile: result.data!);
|
|
await _syncMainLogo(result.data!);
|
|
await _syncFavicon(result.data!.faviconUrl);
|
|
}
|
|
return result.failure;
|
|
} finally {
|
|
_companyProfileInFlight = null;
|
|
}
|
|
}();
|
|
|
|
_companyProfileInFlight = future;
|
|
return future;
|
|
}
|
|
|
|
Future<Failure?> refreshEmailSettings() async {
|
|
final result = await _repository.fetchEmailSettings();
|
|
if (result.failure == null && result.data != null) {
|
|
state = state.copyWith(email: result.data!);
|
|
}
|
|
return result.failure;
|
|
}
|
|
|
|
/// Fetches Company Profile + Email Settings after login / session restore
|
|
/// and applies them app-wide (logo, favicon, company name, email config).
|
|
Future<void> syncCompanyAndEmailFromServer() async {
|
|
await Future.wait([
|
|
refreshCompanyProfile(),
|
|
refreshEmailSettings(),
|
|
]);
|
|
}
|
|
|
|
Future<void> _persist(AppSettings settings) async {
|
|
state = settings;
|
|
final result = await _saveSettings(settings);
|
|
state = result.data ?? settings;
|
|
}
|
|
|
|
Future<void> updateGeneral(GeneralSettings general) async {
|
|
await _persist(state.copyWith(general: general));
|
|
}
|
|
|
|
/// PUT `/settings/company` — returns failure when the API call fails.
|
|
Future<Failure?> updateCompanyProfile(CompanyProfileSettings profile) async {
|
|
final result = await _repository.saveCompanyProfile(profile);
|
|
if (result.failure != null) return result.failure;
|
|
if (result.data != null) {
|
|
state = state.copyWith(companyProfile: result.data!);
|
|
await _saveSettings(state);
|
|
await _syncMainLogo(result.data!);
|
|
await _syncFavicon(result.data!.faviconUrl);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// POST `/settings/company/logo` — also updates the app main logo.
|
|
Future<Result<String?>> uploadCompanyLogo(
|
|
List<int> bytes,
|
|
String filename,
|
|
) async {
|
|
final result = await _repository.uploadCompanyLogo(bytes, filename);
|
|
if (result.failure == null && result.data != null && result.data!.isNotEmpty) {
|
|
final logoUrl = resolveMediaUrl(result.data!) ?? result.data!;
|
|
final profile = state.companyProfile.copyWith(logoUrl: logoUrl);
|
|
state = state.copyWith(companyProfile: profile);
|
|
await _saveSettings(state);
|
|
await _syncMainLogo(profile);
|
|
return (data: logoUrl, failure: null);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/// POST `/settings/company/favicon` — also updates the browser tab icon.
|
|
Future<Result<String?>> uploadCompanyFavicon(
|
|
List<int> bytes,
|
|
String filename,
|
|
) async {
|
|
final result = await _repository.uploadCompanyFavicon(bytes, filename);
|
|
if (result.failure == null &&
|
|
result.data != null &&
|
|
result.data!.isNotEmpty) {
|
|
final faviconUrl = resolveMediaUrl(result.data!) ?? result.data!;
|
|
final profile = state.companyProfile.copyWith(faviconUrl: faviconUrl);
|
|
state = state.copyWith(companyProfile: profile);
|
|
await _saveSettings(state);
|
|
await _syncFavicon(faviconUrl);
|
|
return (data: faviconUrl, failure: null);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/// Applies a local/preview logo to company profile + main app branding.
|
|
Future<void> applyLocalCompanyLogo(String logoUrl) async {
|
|
final resolved = resolveMediaUrl(logoUrl) ?? logoUrl;
|
|
final profile = state.companyProfile.copyWith(logoUrl: resolved);
|
|
state = state.copyWith(companyProfile: profile);
|
|
await _saveSettings(state);
|
|
await _syncMainLogo(profile);
|
|
}
|
|
|
|
/// Applies a local/preview favicon when upload is unavailable.
|
|
Future<void> applyLocalCompanyFavicon(String faviconUrl) async {
|
|
final resolved = resolveMediaUrl(faviconUrl) ?? faviconUrl;
|
|
final profile = state.companyProfile.copyWith(faviconUrl: resolved);
|
|
state = state.copyWith(companyProfile: profile);
|
|
await _saveSettings(state);
|
|
await _syncFavicon(resolved);
|
|
}
|
|
|
|
Future<void> updateUiPreferences(UiPreferencesSettings prefs) async {
|
|
await _persist(state.copyWith(uiPreferences: prefs));
|
|
}
|
|
|
|
Future<void> updateAsset(AssetSettingsConfig asset) async {
|
|
await _persist(state.copyWith(asset: asset));
|
|
}
|
|
|
|
Future<void> updateNotifications(NotificationSettingsConfig notifications) async {
|
|
await _persist(state.copyWith(notifications: notifications));
|
|
}
|
|
|
|
/// PUT `/settings/email` — returns failure when the API call fails.
|
|
Future<Failure?> updateEmail(EmailConfigurationSettings email) async {
|
|
final result = await _repository.saveEmailSettings(email);
|
|
if (result.failure != null) return result.failure;
|
|
if (result.data != null) {
|
|
state = state.copyWith(email: result.data!);
|
|
await _saveSettings(state);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
Future<void> updateSecurity(SecuritySettingsConfig security) async {
|
|
await _persist(state.copyWith(security: security));
|
|
}
|
|
|
|
Future<void> resetToDefaults() async {
|
|
await _persist(const AppSettings());
|
|
await _syncAppLogo(null, null);
|
|
}
|
|
}
|