Files
de.doen1el.calibrewebcompanion/lib/features/settings/bloc/settings_bloc.dart
T

916 lines
25 KiB
Dart

import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:calibre_web_companion/core/services/widget_service.dart';
import 'package:calibre_web_companion/features/settings/bloc/settings_event.dart';
import 'package:calibre_web_companion/features/settings/bloc/settings_state.dart';
import 'package:calibre_web_companion/features/settings/data/models/book_details_action.dart';
import 'package:calibre_web_companion/features/settings/data/models/book_details_section.dart';
import 'package:calibre_web_companion/features/settings/data/models/discover_layout_config.dart';
import 'package:calibre_web_companion/features/settings/data/repositories/settings_repository.dart';
class SettingsBloc extends Bloc<SettingsEvent, SettingsState> {
final SettingsRepository repository;
final WidgetService widgetService;
SettingsBloc({required this.repository, required this.widgetService})
: super(const SettingsState()) {
on<LoadSettings>(_onLoadSettings);
on<SetThemeMode>(_onSetThemeMode);
on<SetThemeSource>(_onSetThemeSource);
on<SetSelectedColor>(_onSetSelectedColor);
on<SetDownloadFolder>(_onSetDownloadFolder);
on<SetDownloadSchema>(_onSetDownloadSchema);
on<SetCostumSend2EreaderEnabled>(_onSetSend2EreaderEnabled);
on<SetCostumSend2EreaderUrl>(_onSetSend2EreaderUrl);
on<SetDownloaderEnabled>(_onSetDownloaderEnabled);
on<SetDownloaderUrl>(_onSetDownloaderUrl);
on<SetDownloaderCredentials>(_onSetDownloaderCredentials);
on<SubmitFeedback>(_onSubmitFeedback);
on<SetLanguage>(_onSetLanguage);
on<SetShowReadNowButton>(_onSetShowReadNowButton);
on<SetStoreReadNowAndSendToEReaderOnDevice>(
_onSetStoreReadNowAndSendToEReaderOnDevice,
);
on<BuyMeACoffee>(_onBuyMeACoffee);
on<SetWebDavSyncEnabled>(_onSetWebDavSyncEnabled);
on<SetWebDavUrl>(_onSetWebDavUrl);
on<SetWebDavCredentials>(_onSetWebDavCredentials);
on<TestDownloaderConnection>(_onTestDownloaderConnection);
on<TestWebDavConnection>(_onTestWebDavConnection);
on<ResetConnectionTestStatus>(_onResetConnectionTestStatus);
on<SetShowSendToEReaderButton>(_onSetShowSendToEReaderButton);
on<SetEInkMode>(_onSetEInkMode);
on<SetTextScale>(_onSetTextScale);
on<SetBookActionsOrder>(_onSetBookActionsOrder);
on<SetBookActionEnabled>(_onSetBookActionEnabled);
on<ResetBookActionsCustomization>(_onResetBookActionsCustomization);
on<SetBookDetailsSectionsOrder>(_onSetBookDetailsSectionsOrder);
on<SetBookDetailsSectionEnabled>(_onSetBookDetailsSectionEnabled);
on<ResetBookDetailsSectionsCustomization>(
_onResetBookDetailsSectionsCustomization,
);
on<SetDiscoverMainSectionsOrder>(_onSetDiscoverMainSectionsOrder);
on<SetDiscoverMainSectionEnabled>(_onSetDiscoverMainSectionEnabled);
on<SetDiscoverItemsOrder>(_onSetDiscoverItemsOrder);
on<SetDiscoverItemEnabled>(_onSetDiscoverItemEnabled);
on<SetCategoryItemsOrder>(_onSetCategoryItemsOrder);
on<SetCategoryItemEnabled>(_onSetCategoryItemEnabled);
on<ResetDiscoverCustomization>(_onResetDiscoverCustomization);
}
Future<void> _onLoadSettings(
LoadSettings event,
Emitter<SettingsState> emit,
) async {
emit(state.copyWith(status: SettingsStatus.loading));
try {
final settings = await repository.getSettings();
final packageInfo = await PackageInfo.fromPlatform();
emit(
state.copyWith(
status: SettingsStatus.loaded,
themeMode: settings.themeMode,
themeSource: settings.themeSource,
selectedColorKey: settings.selectedColorKey,
isDownloaderEnabled: settings.isDownloaderEnabled,
downloaderUrl: settings.downloaderUrl,
downloaderUsername: settings.downloaderUsername,
downloaderPassword: settings.downloaderPassword,
isSend2ereaderEnabled: settings.isSend2ereaderEnabled,
send2ereaderUrl: settings.send2ereaderUrl,
defaultDownloadPath: settings.defaultDownloadPath,
downloadSchema: settings.downloadSchema,
showReadNowButton: settings.showReadNowButton,
showSendToEReaderButton: settings.showSendToEReaderButton,
appVersion: packageInfo.version,
buildNumber: packageInfo.buildNumber,
languageCode: settings.languageCode,
storeReadNowAndSendToEReaderOnDevice:
settings.storeReadNowAndSendToEReaderOnDevice,
webDavUrl: settings.webDavUrl,
webDavUsername: settings.webDavUsername,
webDavPassword: settings.webDavPassword,
isWebDavSyncEnabled: settings.isWebDavSyncEnabled,
isEInkMode: settings.isEInkMode,
textScale: settings.textScale,
bookActionsOrder: settings.bookActionsOrder,
enabledBookActions: settings.enabledBookActions,
bookDetailsSectionsOrder: settings.bookDetailsSectionsOrder,
enabledBookDetailsSections: settings.enabledBookDetailsSections,
discoverMainSectionsOrder: settings.discoverMainSectionsOrder,
enabledDiscoverMainSections: settings.enabledDiscoverMainSections,
discoverItemsOrder: settings.discoverItemsOrder,
enabledDiscoverItems: settings.enabledDiscoverItems,
categoryItemsOrder: settings.categoryItemsOrder,
enabledCategoryItems: settings.enabledCategoryItems,
),
);
// Keep the home-screen widgets themed to match the app.
await widgetService.pushThemeColors();
} catch (e) {
emit(
state.copyWith(
status: SettingsStatus.error,
errorMessage: e.toString(),
),
);
}
}
Future<void> _onSetThemeMode(
SetThemeMode event,
Emitter<SettingsState> emit,
) async {
try {
await repository.setThemeMode(event.themeMode);
emit(state.copyWith(themeMode: event.themeMode));
} catch (e) {
emit(
state.copyWith(
status: SettingsStatus.error,
errorMessage: e.toString(),
),
);
}
}
Future<void> _onSetThemeSource(
SetThemeSource event,
Emitter<SettingsState> emit,
) async {
try {
await repository.setThemeSource(event.themeSource);
emit(state.copyWith(themeSource: event.themeSource));
await widgetService.pushThemeColors();
} catch (e) {
emit(
state.copyWith(
status: SettingsStatus.error,
errorMessage: e.toString(),
),
);
}
}
Future<void> _onSetSelectedColor(
SetSelectedColor event,
Emitter<SettingsState> emit,
) async {
try {
await repository.setSelectedColor(event.colorKey);
emit(state.copyWith(selectedColorKey: event.colorKey));
await widgetService.pushThemeColors();
} catch (e) {
emit(
state.copyWith(
status: SettingsStatus.error,
errorMessage: e.toString(),
),
);
}
}
Future<void> _onSetDownloadFolder(
SetDownloadFolder event,
Emitter<SettingsState> emit,
) async {
try {
await repository.setDefaultDownloadPath(event.downloadFolder);
emit(state.copyWith(defaultDownloadPath: event.downloadFolder));
} catch (e) {
emit(
state.copyWith(
status: SettingsStatus.error,
errorMessage: e.toString(),
),
);
}
}
Future<void> _onSetDownloadSchema(
SetDownloadSchema event,
Emitter<SettingsState> emit,
) async {
try {
await repository.setDownloadSchema(event.downloadSchema);
emit(state.copyWith(downloadSchema: event.downloadSchema));
} catch (e) {
emit(
state.copyWith(
status: SettingsStatus.error,
errorMessage: e.toString(),
),
);
}
}
Future<void> _onSetSend2EreaderEnabled(
SetCostumSend2EreaderEnabled event,
Emitter<SettingsState> emit,
) async {
try {
await repository.setSend2ereaderEnabled(event.enabled);
emit(state.copyWith(isSend2ereaderEnabled: event.enabled));
if (!event.enabled) {
await repository.setSend2ereaderUrl('https://send.djazz.se');
emit(state.copyWith(send2ereaderUrl: 'https://send.djazz.se'));
}
} catch (e) {
emit(
state.copyWith(
status: SettingsStatus.error,
errorMessage: e.toString(),
),
);
}
}
Future<void> _onSetSend2EreaderUrl(
SetCostumSend2EreaderUrl event,
Emitter<SettingsState> emit,
) async {
try {
await repository.setSend2ereaderUrl(event.url);
emit(state.copyWith(send2ereaderUrl: event.url));
} catch (e) {
emit(
state.copyWith(
status: SettingsStatus.error,
errorMessage: e.toString(),
),
);
}
}
Future<void> _onSetDownloaderEnabled(
SetDownloaderEnabled event,
Emitter<SettingsState> emit,
) async {
try {
await repository.setDownloaderEnabled(event.enabled);
emit(state.copyWith(isDownloaderEnabled: event.enabled));
await widgetService.pushQuickActions();
} catch (e) {
emit(
state.copyWith(
status: SettingsStatus.error,
errorMessage: e.toString(),
),
);
}
}
Future<void> _onSetDownloaderUrl(
SetDownloaderUrl event,
Emitter<SettingsState> emit,
) async {
try {
await repository.setDownloaderUrl(event.url);
emit(state.copyWith(downloaderUrl: event.url));
} catch (e) {
emit(
state.copyWith(
status: SettingsStatus.error,
errorMessage: e.toString(),
),
);
}
}
Future<void> _onSetDownloaderCredentials(
SetDownloaderCredentials event,
Emitter<SettingsState> emit,
) async {
try {
await repository.setDownloaderCredentials(event.username, event.password);
emit(
state.copyWith(
downloaderUsername: event.username,
downloaderPassword: event.password,
),
);
} catch (e) {
emit(
state.copyWith(
status: SettingsStatus.error,
errorMessage: e.toString(),
),
);
}
}
Future<void> _onSubmitFeedback(
SubmitFeedback event,
Emitter<SettingsState> emit,
) async {
emit(state.copyWith(feedbackStatus: SettingsFeedbackStatus.loading));
try {
await repository.submitFeedback(
event.title ?? '',
event.description ?? '',
);
emit(state.copyWith(status: SettingsStatus.loaded));
} catch (e) {
emit(
state.copyWith(
feedbackStatus: SettingsFeedbackStatus.error,
errorMessage: e.toString(),
),
);
}
}
Future<void> _onSetLanguage(
SetLanguage event,
Emitter<SettingsState> emit,
) async {
try {
await repository.setLanguage(event.languageCode);
emit(state.copyWith(languageCode: event.languageCode));
} catch (e) {
emit(
state.copyWith(
status: SettingsStatus.error,
errorMessage: e.toString(),
),
);
}
}
Future<void> _onSetShowReadNowButton(
SetShowReadNowButton event,
Emitter<SettingsState> emit,
) async {
try {
await repository.setShowReadNowButton(event.enabled);
emit(state.copyWith(showReadNowButton: event.enabled));
} catch (e) {
emit(
state.copyWith(
status: SettingsStatus.error,
errorMessage: e.toString(),
),
);
}
}
Future<void> _onSetStoreReadNowAndSendToEReaderOnDevice(
SetStoreReadNowAndSendToEReaderOnDevice event,
Emitter<SettingsState> emit,
) async {
try {
await repository.setStoreReadNowAndSendToEReaderOnDevice(event.enabled);
emit(state.copyWith(storeReadNowAndSendToEReaderOnDevice: event.enabled));
} catch (e) {
emit(
state.copyWith(
status: SettingsStatus.error,
errorMessage: e.toString(),
),
);
}
}
Future<void> _onBuyMeACoffee(
BuyMeACoffee event,
Emitter<SettingsState> emit,
) async {
try {
await repository.buyMeACoffee();
} catch (e) {
emit(
state.copyWith(
status: SettingsStatus.error,
errorMessage: e.toString(),
),
);
}
}
Future<void> _onSetWebDavSyncEnabled(
SetWebDavSyncEnabled event,
Emitter<SettingsState> emit,
) async {
try {
await repository.setWebDavSyncEnabled(event.enabled);
emit(state.copyWith(isWebDavSyncEnabled: event.enabled));
} catch (e) {
emit(
state.copyWith(
status: SettingsStatus.error,
errorMessage: e.toString(),
),
);
}
}
Future<void> _onSetWebDavUrl(
SetWebDavUrl event,
Emitter<SettingsState> emit,
) async {
try {
await repository.setWebDavUrl(event.url);
emit(state.copyWith(webDavUrl: event.url));
} catch (e) {
emit(
state.copyWith(
status: SettingsStatus.error,
errorMessage: e.toString(),
),
);
}
}
Future<void> _onSetWebDavCredentials(
SetWebDavCredentials event,
Emitter<SettingsState> emit,
) async {
try {
await repository.setWebDavCredentials(event.username, event.password);
emit(
state.copyWith(
webDavUsername: event.username,
webDavPassword: event.password,
),
);
} catch (e) {
emit(
state.copyWith(
status: SettingsStatus.error,
errorMessage: e.toString(),
),
);
}
}
Future<void> _onTestDownloaderConnection(
TestDownloaderConnection event,
Emitter<SettingsState> emit,
) async {
emit(state.copyWith(downloaderTestStatus: ConnectionTestStatus.loading));
try {
final success = await repository.testDownloaderConnection(
event.url,
event.username,
event.password,
);
if (success) {
emit(
state.copyWith(downloaderTestStatus: ConnectionTestStatus.success),
);
} else {
emit(
state.copyWith(
downloaderTestStatus: ConnectionTestStatus.error,
testErrorMessage: "Login failed (Invalid credentials or URL)",
),
);
}
} catch (e) {
emit(
state.copyWith(
downloaderTestStatus: ConnectionTestStatus.error,
testErrorMessage: e.toString(),
),
);
}
}
Future<void> _onTestWebDavConnection(
TestWebDavConnection event,
Emitter<SettingsState> emit,
) async {
emit(state.copyWith(webDavTestStatus: ConnectionTestStatus.loading));
try {
await repository.testWebDavConnection(
event.url,
event.username,
event.password,
);
emit(state.copyWith(webDavTestStatus: ConnectionTestStatus.success));
} catch (e) {
emit(
state.copyWith(
webDavTestStatus: ConnectionTestStatus.error,
testErrorMessage: e.toString(),
),
);
}
}
void _onResetConnectionTestStatus(
ResetConnectionTestStatus event,
Emitter<SettingsState> emit,
) {
emit(
state.copyWith(
downloaderTestStatus: ConnectionTestStatus.initial,
webDavTestStatus: ConnectionTestStatus.initial,
testErrorMessage: null,
),
);
}
Future<void> _onSetShowSendToEReaderButton(
SetShowSendToEReaderButton event,
Emitter<SettingsState> emit,
) async {
try {
await repository.setShowSendToEReaderButton(event.enabled);
emit(state.copyWith(showSendToEReaderButton: event.enabled));
} catch (e) {
emit(
state.copyWith(
status: SettingsStatus.error,
errorMessage: e.toString(),
),
);
}
}
Future<void> _onSetTextScale(
SetTextScale event,
Emitter<SettingsState> emit,
) async {
try {
await repository.setTextScale(event.scale);
emit(state.copyWith(textScale: event.scale));
} catch (e) {
emit(
state.copyWith(
status: SettingsStatus.error,
errorMessage: e.toString(),
),
);
}
}
Future<void> _onSetEInkMode(
SetEInkMode event,
Emitter<SettingsState> emit,
) async {
try {
await repository.setEInkMode(event.enabled);
emit(state.copyWith(isEInkMode: event.enabled));
} catch (e) {
emit(
state.copyWith(
status: SettingsStatus.error,
errorMessage: e.toString(),
),
);
}
}
Future<void> _onSetBookActionsOrder(
SetBookActionsOrder event,
Emitter<SettingsState> emit,
) async {
final normalizedOrder = BookDetailsActionConfig.normalizeOrder(
event.actionKeys,
);
emit(state.copyWith(bookActionsOrder: normalizedOrder));
try {
await repository.setBookActionsOrder(normalizedOrder);
} catch (e) {
emit(
state.copyWith(
status: SettingsStatus.error,
errorMessage: e.toString(),
),
);
}
}
Future<void> _onSetBookActionEnabled(
SetBookActionEnabled event,
Emitter<SettingsState> emit,
) async {
try {
final updated = List<String>.from(state.enabledBookActions);
if (event.enabled) {
if (!updated.contains(event.actionKey)) {
updated.add(event.actionKey);
}
} else {
updated.remove(event.actionKey);
}
final normalized = BookDetailsActionConfig.normalizeEnabled(updated);
await repository.setEnabledBookActions(normalized);
emit(state.copyWith(enabledBookActions: normalized));
} catch (e) {
emit(
state.copyWith(
status: SettingsStatus.error,
errorMessage: e.toString(),
),
);
}
}
Future<void> _onResetBookActionsCustomization(
ResetBookActionsCustomization event,
Emitter<SettingsState> emit,
) async {
final defaults = List<String>.from(BookDetailsActionConfig.defaultOrder);
emit(
state.copyWith(bookActionsOrder: defaults, enabledBookActions: defaults),
);
try {
await Future.wait([
repository.setBookActionsOrder(defaults),
repository.setEnabledBookActions(defaults),
]);
} catch (e) {
emit(
state.copyWith(
status: SettingsStatus.error,
errorMessage: e.toString(),
),
);
}
}
Future<void> _onSetBookDetailsSectionsOrder(
SetBookDetailsSectionsOrder event,
Emitter<SettingsState> emit,
) async {
final normalizedOrder = BookDetailsSectionConfig.normalizeOrder(
event.sectionKeys,
);
emit(state.copyWith(bookDetailsSectionsOrder: normalizedOrder));
try {
await repository.setBookDetailsSectionsOrder(normalizedOrder);
} catch (e) {
emit(
state.copyWith(
status: SettingsStatus.error,
errorMessage: e.toString(),
),
);
}
}
Future<void> _onSetBookDetailsSectionEnabled(
SetBookDetailsSectionEnabled event,
Emitter<SettingsState> emit,
) async {
try {
final updated = List<String>.from(state.enabledBookDetailsSections);
if (event.enabled) {
if (!updated.contains(event.sectionKey)) {
updated.add(event.sectionKey);
}
} else {
updated.remove(event.sectionKey);
}
final normalized = BookDetailsSectionConfig.normalizeEnabled(updated);
await repository.setEnabledBookDetailsSections(normalized);
emit(state.copyWith(enabledBookDetailsSections: normalized));
} catch (e) {
emit(
state.copyWith(
status: SettingsStatus.error,
errorMessage: e.toString(),
),
);
}
}
Future<void> _onResetBookDetailsSectionsCustomization(
ResetBookDetailsSectionsCustomization event,
Emitter<SettingsState> emit,
) async {
final defaults = List<String>.from(BookDetailsSectionConfig.defaultOrder);
emit(
state.copyWith(
bookDetailsSectionsOrder: defaults,
enabledBookDetailsSections: defaults,
),
);
try {
await Future.wait([
repository.setBookDetailsSectionsOrder(defaults),
repository.setEnabledBookDetailsSections(defaults),
]);
} catch (e) {
emit(
state.copyWith(
status: SettingsStatus.error,
errorMessage: e.toString(),
),
);
}
}
Future<void> _onSetDiscoverMainSectionsOrder(
SetDiscoverMainSectionsOrder event,
Emitter<SettingsState> emit,
) async {
final normalizedOrder = DiscoverLayoutConfig.normalizeMainSectionsOrder(
event.sectionKeys,
);
emit(state.copyWith(discoverMainSectionsOrder: normalizedOrder));
try {
await repository.setDiscoverMainSectionsOrder(normalizedOrder);
} catch (e) {
emit(
state.copyWith(
status: SettingsStatus.error,
errorMessage: e.toString(),
),
);
}
}
Future<void> _onSetDiscoverMainSectionEnabled(
SetDiscoverMainSectionEnabled event,
Emitter<SettingsState> emit,
) async {
try {
final updated = List<String>.from(state.enabledDiscoverMainSections);
if (event.enabled) {
if (!updated.contains(event.sectionKey)) {
updated.add(event.sectionKey);
}
} else {
updated.remove(event.sectionKey);
}
final normalized = DiscoverLayoutConfig.normalizeEnabledMainSections(
updated,
);
await repository.setEnabledDiscoverMainSections(normalized);
emit(state.copyWith(enabledDiscoverMainSections: normalized));
} catch (e) {
emit(
state.copyWith(
status: SettingsStatus.error,
errorMessage: e.toString(),
),
);
}
}
Future<void> _onSetDiscoverItemsOrder(
SetDiscoverItemsOrder event,
Emitter<SettingsState> emit,
) async {
final normalizedOrder = DiscoverLayoutConfig.normalizeDiscoverItemsOrder(
event.itemKeys,
);
emit(state.copyWith(discoverItemsOrder: normalizedOrder));
try {
await repository.setDiscoverItemsOrder(normalizedOrder);
} catch (e) {
emit(
state.copyWith(
status: SettingsStatus.error,
errorMessage: e.toString(),
),
);
}
}
Future<void> _onSetDiscoverItemEnabled(
SetDiscoverItemEnabled event,
Emitter<SettingsState> emit,
) async {
try {
final updated = List<String>.from(state.enabledDiscoverItems);
if (event.enabled) {
if (!updated.contains(event.itemKey)) {
updated.add(event.itemKey);
}
} else {
updated.remove(event.itemKey);
}
final normalized = DiscoverLayoutConfig.normalizeEnabledDiscoverItems(
updated,
);
await repository.setEnabledDiscoverItems(normalized);
emit(state.copyWith(enabledDiscoverItems: normalized));
} catch (e) {
emit(
state.copyWith(
status: SettingsStatus.error,
errorMessage: e.toString(),
),
);
}
}
Future<void> _onSetCategoryItemsOrder(
SetCategoryItemsOrder event,
Emitter<SettingsState> emit,
) async {
final normalizedOrder = DiscoverLayoutConfig.normalizeCategoryItemsOrder(
event.itemKeys,
);
emit(state.copyWith(categoryItemsOrder: normalizedOrder));
try {
await repository.setCategoryItemsOrder(normalizedOrder);
} catch (e) {
emit(
state.copyWith(
status: SettingsStatus.error,
errorMessage: e.toString(),
),
);
}
}
Future<void> _onSetCategoryItemEnabled(
SetCategoryItemEnabled event,
Emitter<SettingsState> emit,
) async {
try {
final updated = List<String>.from(state.enabledCategoryItems);
if (event.enabled) {
if (!updated.contains(event.itemKey)) {
updated.add(event.itemKey);
}
} else {
updated.remove(event.itemKey);
}
final normalized = DiscoverLayoutConfig.normalizeEnabledCategoryItems(
updated,
);
await repository.setEnabledCategoryItems(normalized);
emit(state.copyWith(enabledCategoryItems: normalized));
} catch (e) {
emit(
state.copyWith(
status: SettingsStatus.error,
errorMessage: e.toString(),
),
);
}
}
Future<void> _onResetDiscoverCustomization(
ResetDiscoverCustomization event,
Emitter<SettingsState> emit,
) async {
final mainDefaults = List<String>.from(
DiscoverLayoutConfig.defaultMainSectionsOrder,
);
final discoverDefaults = List<String>.from(
DiscoverLayoutConfig.defaultDiscoverItemsOrder,
);
final categoryDefaults = List<String>.from(
DiscoverLayoutConfig.defaultCategoryItemsOrder,
);
emit(
state.copyWith(
discoverMainSectionsOrder: mainDefaults,
enabledDiscoverMainSections: mainDefaults,
discoverItemsOrder: discoverDefaults,
enabledDiscoverItems: discoverDefaults,
categoryItemsOrder: categoryDefaults,
enabledCategoryItems: categoryDefaults,
),
);
try {
await Future.wait([
repository.setDiscoverMainSectionsOrder(mainDefaults),
repository.setEnabledDiscoverMainSections(mainDefaults),
repository.setDiscoverItemsOrder(discoverDefaults),
repository.setEnabledDiscoverItems(discoverDefaults),
repository.setCategoryItemsOrder(categoryDefaults),
repository.setEnabledCategoryItems(categoryDefaults),
]);
} catch (e) {
emit(
state.copyWith(
status: SettingsStatus.error,
errorMessage: e.toString(),
),
);
}
}
}