diff --git a/lib/core/di/injection.config.dart b/lib/core/di/injection.config.dart index b599ae828..1fd53ed63 100644 --- a/lib/core/di/injection.config.dart +++ b/lib/core/di/injection.config.dart @@ -20,6 +20,15 @@ import 'package:internet_connection_checker/internet_connection_checker.dart' as _i973; import 'package:quick_actions/quick_actions.dart' as _i578; +import '../../features/auto_rotate/biz/bloc/auto_rotate_bloc.j.dart' as _i1100; +import '../../features/auto_rotate/data/data_sources/auto_rotate_local_data_source.dart' + as _i1101; +import '../../features/auto_rotate/data/repositories/auto_rotate_repository_impl.dart' + as _i1102; +import '../../features/auto_rotate/domain/repositories/auto_rotate_repository.dart' + as _i1103; +import '../../features/auto_rotate/domain/usecases/auto_rotate_usecases.dart' + as _i1104; import '../../features/admin_review/biz/bloc/review_batch_bloc.dart' as _i711; import '../../features/admin_review/data/review_batch_repository.dart' as _i122; import '../../features/ads/biz/bloc/ads_bloc.j.dart' as _i567; @@ -712,6 +721,41 @@ _i174.GetIt initGetIt( gh.factory<_i183.WotdBloc>( () => _i183.WotdBloc(gh<_i398.FetchWallOfTheDayUseCase>()), ); + // Auto Rotate + gh.lazySingleton<_i1101.AutoRotateLocalDataSource>( + () => _i1101.AutoRotateLocalDataSource(gh<_i496.LocalStore>()), + ); + gh.lazySingleton<_i1103.AutoRotateRepository>( + () => _i1102.AutoRotateRepositoryImpl( + gh<_i1101.AutoRotateLocalDataSource>(), + gh<_i406.FetchFavouriteWallsUseCase>(), + gh<_i301.FetchCategoryFeedUseCase>(), + ), + ); + gh.lazySingleton<_i1104.LoadAutoRotateConfigUseCase>( + () => _i1104.LoadAutoRotateConfigUseCase(gh<_i1103.AutoRotateRepository>()), + ); + gh.lazySingleton<_i1104.SaveAutoRotateConfigUseCase>( + () => _i1104.SaveAutoRotateConfigUseCase(gh<_i1103.AutoRotateRepository>()), + ); + gh.lazySingleton<_i1104.StartAutoRotateUseCase>( + () => _i1104.StartAutoRotateUseCase(gh<_i1103.AutoRotateRepository>()), + ); + gh.lazySingleton<_i1104.StopAutoRotateUseCase>( + () => _i1104.StopAutoRotateUseCase(gh<_i1103.AutoRotateRepository>()), + ); + gh.lazySingleton<_i1104.GetAutoRotateStatusUseCase>( + () => _i1104.GetAutoRotateStatusUseCase(gh<_i1103.AutoRotateRepository>()), + ); + gh.factory<_i1100.AutoRotateBloc>( + () => _i1100.AutoRotateBloc( + gh<_i1104.LoadAutoRotateConfigUseCase>(), + gh<_i1104.SaveAutoRotateConfigUseCase>(), + gh<_i1104.StartAutoRotateUseCase>(), + gh<_i1104.StopAutoRotateUseCase>(), + gh<_i1104.GetAutoRotateStatusUseCase>(), + ), + ); return getIt; } diff --git a/lib/core/persistence/persistence_keys.dart b/lib/core/persistence/persistence_keys.dart index a7a32cdc6..0013669bd 100644 --- a/lib/core/persistence/persistence_keys.dart +++ b/lib/core/persistence/persistence_keys.dart @@ -58,4 +58,14 @@ class PersistenceKeys { static const String quickTileFavsTarget = 'quick_tile.favs.target'; // JSON-encoded list of full-resolution URLs from the user's favourites. static const String quickTileFavWallUrls = 'quick_tile.favs.wall_urls'; + + // Auto-rotate configuration + static const String autoRotateEnabled = 'auto_rotate.enabled'; + static const String autoRotateSourceType = 'auto_rotate.source_type'; + static const String autoRotateCollectionName = 'auto_rotate.collection_name'; + static const String autoRotateCategoryName = 'auto_rotate.category_name'; + static const String autoRotateTarget = 'auto_rotate.target'; + static const String autoRotateIntervalMinutes = 'auto_rotate.interval_minutes'; + static const String autoRotateChargingTrigger = 'auto_rotate.charging_trigger'; + static const String autoRotateOrder = 'auto_rotate.order'; } diff --git a/lib/core/router/app_router.dart b/lib/core/router/app_router.dart index d3f8096ae..d3cae9211 100644 --- a/lib/core/router/app_router.dart +++ b/lib/core/router/app_router.dart @@ -34,6 +34,7 @@ import 'package:Prism/features/public_profile/views/pages/followers_screen.dart' import 'package:Prism/features/public_profile/views/pages/following_list_screen.dart'; import 'package:Prism/features/public_profile/views/pages/profile_screen.dart'; import 'package:Prism/features/public_profile/views/pages/user_profile_setup_view_screen.dart'; +import 'package:Prism/features/auto_rotate/views/pages/auto_rotate_screen.dart'; import 'package:Prism/features/quick_tiles/views/quick_tile_settings_screen.dart'; import 'package:Prism/features/session/views/pages/about_screen.dart'; import 'package:Prism/features/session/views/pages/coin_transactions_screen.dart'; @@ -147,6 +148,7 @@ class AppRouter extends RootStackRouter { AutoRoute(path: '/admin-review/swipe', page: SwipeReviewRoute.page, guards: [_adminGuard]), AutoRoute(path: '/admin-firestore-telemetry', page: FirestoreTelemetryRoute.page, guards: [_adminGuard]), AutoRoute(path: '/debug-panel', page: DebugPanelRoute.page, guards: [_adminGuard]), + AutoRoute(path: '/auto-rotate', page: AutoRotateRoute.page), AutoRoute(path: '/quick-tile-settings', page: QuickTileSettingsRoute.page), AutoRoute(path: '/not-found', page: NotFoundRoute.page), RedirectRoute(path: '*', redirectTo: '/not-found'), diff --git a/lib/core/router/app_router.gr.dart b/lib/core/router/app_router.gr.dart index b061b008f..aa34534ea 100644 --- a/lib/core/router/app_router.gr.dart +++ b/lib/core/router/app_router.gr.dart @@ -10,6 +10,22 @@ part of 'app_router.dart'; +/// generated route for +/// [AutoRotateScreen] +class AutoRotateRoute extends PageRouteInfo { + const AutoRotateRoute({List? children}) + : super(AutoRotateRoute.name, initialChildren: children); + + static const String name = 'AutoRotateRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + return const AutoRotateScreen(); + }, + ); +} + /// generated route for /// [AboutScreen] class AboutRoute extends PageRouteInfo { diff --git a/lib/features/auto_rotate/biz/bloc/auto_rotate_bloc.j.dart b/lib/features/auto_rotate/biz/bloc/auto_rotate_bloc.j.dart new file mode 100644 index 000000000..d931ad25a --- /dev/null +++ b/lib/features/auto_rotate/biz/bloc/auto_rotate_bloc.j.dart @@ -0,0 +1,141 @@ +import 'package:Prism/core/error/failure.dart'; +import 'package:Prism/core/usecase/usecase.dart'; +import 'package:Prism/core/utils/status.dart'; +import 'package:Prism/data/categories/categories.dart'; +import 'package:Prism/data/categories/category_definition.dart'; +import 'package:Prism/data/collections/provider/collectionsWithoutProvider.dart'; +import 'package:Prism/features/auto_rotate/domain/entities/auto_rotate_config_entity.dart'; +import 'package:Prism/features/auto_rotate/domain/usecases/auto_rotate_usecases.dart'; +import 'package:async_wallpaper/async_wallpaper.dart' as aw; +import 'package:bloc/bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:injectable/injectable.dart'; + +part 'auto_rotate_event.j.dart'; +part 'auto_rotate_state.j.dart'; +part 'auto_rotate_bloc.j.freezed.dart'; + +@injectable +class AutoRotateBloc extends Bloc { + AutoRotateBloc( + this._loadConfigUseCase, + this._saveConfigUseCase, + this._startAutoRotateUseCase, + this._stopAutoRotateUseCase, + this._getStatusUseCase, + ) : super(AutoRotateState.initial()) { + on<_Started>(_onStarted); + on<_SourceTypeChanged>(_onSourceTypeChanged); + on<_TargetChanged>(_onTargetChanged); + on<_IntervalChanged>(_onIntervalChanged); + on<_ChargingTriggerToggled>(_onChargingTriggerToggled); + on<_OrderChanged>(_onOrderChanged); + on<_StartRequested>(_onStartRequested); + on<_StopRequested>(_onStopRequested); + on<_StatusRefreshRequested>(_onStatusRefreshRequested); + on<_RotateNowRequested>(_onRotateNowRequested); + } + + final LoadAutoRotateConfigUseCase _loadConfigUseCase; + final SaveAutoRotateConfigUseCase _saveConfigUseCase; + final StartAutoRotateUseCase _startAutoRotateUseCase; + final StopAutoRotateUseCase _stopAutoRotateUseCase; + final GetAutoRotateStatusUseCase _getStatusUseCase; + + Future _onStarted(_Started event, Emitter emit) async { + emit(state.copyWith(status: LoadStatus.loading, failure: null)); + + // Load saved config + final configResult = await _loadConfigUseCase(const NoParams()); + final config = configResult.fold(onSuccess: (c) => c, onFailure: (_) => AutoRotateConfigEntity.defaults); + + // Load available collections + final collectionList = await getCollections(); + final availableCollections = (collectionList ?? []) + .whereType>() + .map((c) => c['name']?.toString() ?? '') + .where((name) => name.isNotEmpty) + .toList(); + + // Check current rotation status + final statusResult = await _getStatusUseCase(const NoParams()); + final isRunning = statusResult.fold(onSuccess: (v) => v, onFailure: (_) => false); + + emit( + state.copyWith( + status: LoadStatus.success, + config: config, + isRunning: isRunning, + availableCollections: availableCollections, + availableCategories: categoryDefinitions, + failure: null, + ), + ); + } + + Future _onSourceTypeChanged(_SourceTypeChanged event, Emitter emit) async { + final updated = state.config.copyWith( + sourceType: event.sourceType, + collectionName: event.sourceType == AutoRotateSourceType.collection ? event.name : null, + categoryName: event.sourceType == AutoRotateSourceType.category ? event.name : null, + ); + emit(state.copyWith(config: updated)); + await _saveConfigUseCase(updated); + } + + Future _onTargetChanged(_TargetChanged event, Emitter emit) async { + final updated = state.config.copyWith(target: event.target); + emit(state.copyWith(config: updated)); + await _saveConfigUseCase(updated); + } + + Future _onIntervalChanged(_IntervalChanged event, Emitter emit) async { + final updated = state.config.copyWith(intervalMinutes: event.minutes); + emit(state.copyWith(config: updated)); + await _saveConfigUseCase(updated); + } + + Future _onChargingTriggerToggled(_ChargingTriggerToggled event, Emitter emit) async { + final updated = state.config.copyWith(chargingTrigger: !state.config.chargingTrigger); + emit(state.copyWith(config: updated)); + await _saveConfigUseCase(updated); + } + + Future _onOrderChanged(_OrderChanged event, Emitter emit) async { + final updated = state.config.copyWith(order: event.order); + emit(state.copyWith(config: updated)); + await _saveConfigUseCase(updated); + } + + Future _onStartRequested(_StartRequested event, Emitter emit) async { + emit(state.copyWith(actionStatus: ActionStatus.inProgress, failure: null)); + final result = await _startAutoRotateUseCase(state.config); + result.fold( + onSuccess: (_) => emit(state.copyWith(actionStatus: ActionStatus.success, isRunning: true, failure: null)), + onFailure: (failure) => emit(state.copyWith(actionStatus: ActionStatus.failure, failure: failure)), + ); + } + + Future _onStopRequested(_StopRequested event, Emitter emit) async { + emit(state.copyWith(actionStatus: ActionStatus.inProgress, failure: null)); + final result = await _stopAutoRotateUseCase(const NoParams()); + result.fold( + onSuccess: (_) => emit(state.copyWith(actionStatus: ActionStatus.success, isRunning: false, failure: null)), + onFailure: (failure) => emit(state.copyWith(actionStatus: ActionStatus.failure, failure: failure)), + ); + } + + Future _onStatusRefreshRequested(_StatusRefreshRequested event, Emitter emit) async { + final result = await _getStatusUseCase(const NoParams()); + result.fold( + onSuccess: (isRunning) => emit(state.copyWith(isRunning: isRunning)), + onFailure: (_) {}, + ); + } + + Future _onRotateNowRequested(_RotateNowRequested event, Emitter emit) async { + try { + await aw.AsyncWallpaper.rotateWallpaperNow(); + } catch (_) {} + } +} diff --git a/lib/features/auto_rotate/biz/bloc/auto_rotate_bloc.j.freezed.dart b/lib/features/auto_rotate/biz/bloc/auto_rotate_bloc.j.freezed.dart new file mode 100644 index 000000000..6cb7b47bd --- /dev/null +++ b/lib/features/auto_rotate/biz/bloc/auto_rotate_bloc.j.freezed.dart @@ -0,0 +1,675 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'auto_rotate_bloc.j.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; +/// @nodoc +mixin _$AutoRotateEvent { + + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is AutoRotateEvent); +} + + +@override +int get hashCode => runtimeType.hashCode; + +@override +String toString() { + return 'AutoRotateEvent()'; +} + + +} + +/// @nodoc +class $AutoRotateEventCopyWith<$Res> { +$AutoRotateEventCopyWith(AutoRotateEvent _, $Res Function(AutoRotateEvent) __); +} + + +/// Adds pattern-matching-related methods to [AutoRotateEvent]. +extension AutoRotateEventPatterns on AutoRotateEvent { +@optionalTypeArgs TResult maybeMap({TResult Function( _Started value)? started,TResult Function( _SourceTypeChanged value)? sourceTypeChanged,TResult Function( _TargetChanged value)? targetChanged,TResult Function( _IntervalChanged value)? intervalChanged,TResult Function( _ChargingTriggerToggled value)? chargingTriggerToggled,TResult Function( _OrderChanged value)? orderChanged,TResult Function( _StartRequested value)? startRequested,TResult Function( _StopRequested value)? stopRequested,TResult Function( _StatusRefreshRequested value)? statusRefreshRequested,TResult Function( _RotateNowRequested value)? rotateNowRequested,required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _Started() when started != null: +return started(_that);case _SourceTypeChanged() when sourceTypeChanged != null: +return sourceTypeChanged(_that);case _TargetChanged() when targetChanged != null: +return targetChanged(_that);case _IntervalChanged() when intervalChanged != null: +return intervalChanged(_that);case _ChargingTriggerToggled() when chargingTriggerToggled != null: +return chargingTriggerToggled(_that);case _OrderChanged() when orderChanged != null: +return orderChanged(_that);case _StartRequested() when startRequested != null: +return startRequested(_that);case _StopRequested() when stopRequested != null: +return stopRequested(_that);case _StatusRefreshRequested() when statusRefreshRequested != null: +return statusRefreshRequested(_that);case _RotateNowRequested() when rotateNowRequested != null: +return rotateNowRequested(_that);case _: + return orElse(); + +} +} + +@optionalTypeArgs TResult map({required TResult Function( _Started value) started,required TResult Function( _SourceTypeChanged value) sourceTypeChanged,required TResult Function( _TargetChanged value) targetChanged,required TResult Function( _IntervalChanged value) intervalChanged,required TResult Function( _ChargingTriggerToggled value) chargingTriggerToggled,required TResult Function( _OrderChanged value) orderChanged,required TResult Function( _StartRequested value) startRequested,required TResult Function( _StopRequested value) stopRequested,required TResult Function( _StatusRefreshRequested value) statusRefreshRequested,required TResult Function( _RotateNowRequested value) rotateNowRequested,}){ +final _that = this; +switch (_that) { +case _Started(): +return started(_that);case _SourceTypeChanged(): +return sourceTypeChanged(_that);case _TargetChanged(): +return targetChanged(_that);case _IntervalChanged(): +return intervalChanged(_that);case _ChargingTriggerToggled(): +return chargingTriggerToggled(_that);case _OrderChanged(): +return orderChanged(_that);case _StartRequested(): +return startRequested(_that);case _StopRequested(): +return stopRequested(_that);case _StatusRefreshRequested(): +return statusRefreshRequested(_that);case _RotateNowRequested(): +return rotateNowRequested(_that);case _: + throw StateError('Unexpected subclass'); + +} +} + +@optionalTypeArgs TResult? mapOrNull({TResult? Function( _Started value)? started,TResult? Function( _SourceTypeChanged value)? sourceTypeChanged,TResult? Function( _TargetChanged value)? targetChanged,TResult? Function( _IntervalChanged value)? intervalChanged,TResult? Function( _ChargingTriggerToggled value)? chargingTriggerToggled,TResult? Function( _OrderChanged value)? orderChanged,TResult? Function( _StartRequested value)? startRequested,TResult? Function( _StopRequested value)? stopRequested,TResult? Function( _StatusRefreshRequested value)? statusRefreshRequested,TResult? Function( _RotateNowRequested value)? rotateNowRequested,}){ +final _that = this; +switch (_that) { +case _Started() when started != null: +return started(_that);case _SourceTypeChanged() when sourceTypeChanged != null: +return sourceTypeChanged(_that);case _TargetChanged() when targetChanged != null: +return targetChanged(_that);case _IntervalChanged() when intervalChanged != null: +return intervalChanged(_that);case _ChargingTriggerToggled() when chargingTriggerToggled != null: +return chargingTriggerToggled(_that);case _OrderChanged() when orderChanged != null: +return orderChanged(_that);case _StartRequested() when startRequested != null: +return startRequested(_that);case _StopRequested() when stopRequested != null: +return stopRequested(_that);case _StatusRefreshRequested() when statusRefreshRequested != null: +return statusRefreshRequested(_that);case _RotateNowRequested() when rotateNowRequested != null: +return rotateNowRequested(_that);case _: + return null; + +} +} + +@optionalTypeArgs TResult maybeWhen({TResult Function()? started,TResult Function( AutoRotateSourceType sourceType, String? name)? sourceTypeChanged,TResult Function( aw.WallpaperTarget target)? targetChanged,TResult Function( int minutes)? intervalChanged,TResult Function()? chargingTriggerToggled,TResult Function( AutoRotateOrder order)? orderChanged,TResult Function()? startRequested,TResult Function()? stopRequested,TResult Function()? statusRefreshRequested,TResult Function()? rotateNowRequested,required TResult orElse(),}) {final _that = this; +switch (_that) { +case _Started() when started != null: +return started();case _SourceTypeChanged() when sourceTypeChanged != null: +return sourceTypeChanged(_that.sourceType,_that.name);case _TargetChanged() when targetChanged != null: +return targetChanged(_that.target);case _IntervalChanged() when intervalChanged != null: +return intervalChanged(_that.minutes);case _ChargingTriggerToggled() when chargingTriggerToggled != null: +return chargingTriggerToggled();case _OrderChanged() when orderChanged != null: +return orderChanged(_that.order);case _StartRequested() when startRequested != null: +return startRequested();case _StopRequested() when stopRequested != null: +return stopRequested();case _StatusRefreshRequested() when statusRefreshRequested != null: +return statusRefreshRequested();case _RotateNowRequested() when rotateNowRequested != null: +return rotateNowRequested();case _: + return orElse(); + +} +} + +@optionalTypeArgs TResult when({required TResult Function() started,required TResult Function( AutoRotateSourceType sourceType, String? name) sourceTypeChanged,required TResult Function( aw.WallpaperTarget target) targetChanged,required TResult Function( int minutes) intervalChanged,required TResult Function() chargingTriggerToggled,required TResult Function( AutoRotateOrder order) orderChanged,required TResult Function() startRequested,required TResult Function() stopRequested,required TResult Function() statusRefreshRequested,required TResult Function() rotateNowRequested,}) {final _that = this; +switch (_that) { +case _Started(): +return started();case _SourceTypeChanged(): +return sourceTypeChanged(_that.sourceType,_that.name);case _TargetChanged(): +return targetChanged(_that.target);case _IntervalChanged(): +return intervalChanged(_that.minutes);case _ChargingTriggerToggled(): +return chargingTriggerToggled();case _OrderChanged(): +return orderChanged(_that.order);case _StartRequested(): +return startRequested();case _StopRequested(): +return stopRequested();case _StatusRefreshRequested(): +return statusRefreshRequested();case _RotateNowRequested(): +return rotateNowRequested();case _: + throw StateError('Unexpected subclass'); + +} +} + +@optionalTypeArgs TResult? whenOrNull({TResult? Function()? started,TResult? Function( AutoRotateSourceType sourceType, String? name)? sourceTypeChanged,TResult? Function( aw.WallpaperTarget target)? targetChanged,TResult? Function( int minutes)? intervalChanged,TResult? Function()? chargingTriggerToggled,TResult? Function( AutoRotateOrder order)? orderChanged,TResult? Function()? startRequested,TResult? Function()? stopRequested,TResult? Function()? statusRefreshRequested,TResult? Function()? rotateNowRequested,}) {final _that = this; +switch (_that) { +case _Started() when started != null: +return started();case _SourceTypeChanged() when sourceTypeChanged != null: +return sourceTypeChanged(_that.sourceType,_that.name);case _TargetChanged() when targetChanged != null: +return targetChanged(_that.target);case _IntervalChanged() when intervalChanged != null: +return intervalChanged(_that.minutes);case _ChargingTriggerToggled() when chargingTriggerToggled != null: +return chargingTriggerToggled();case _OrderChanged() when orderChanged != null: +return orderChanged(_that.order);case _StartRequested() when startRequested != null: +return startRequested();case _StopRequested() when stopRequested != null: +return stopRequested();case _StatusRefreshRequested() when statusRefreshRequested != null: +return statusRefreshRequested();case _RotateNowRequested() when rotateNowRequested != null: +return rotateNowRequested();case _: + return null; + +} +} + +} + +/// @nodoc + + +class _Started implements AutoRotateEvent { + const _Started(); + + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _Started); +} + + +@override +int get hashCode => runtimeType.hashCode; + +@override +String toString() { + return 'AutoRotateEvent.started()'; +} + + +} + + + + +/// @nodoc + + +class _SourceTypeChanged implements AutoRotateEvent { + const _SourceTypeChanged({required this.sourceType, this.name}); + + +@override final AutoRotateSourceType sourceType; +@override final String? name; + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _SourceTypeChanged&&(identical(other.sourceType, sourceType) || other.sourceType == sourceType)&&(identical(other.name, name) || other.name == name)); +} + + +@override +int get hashCode => Object.hash(runtimeType,sourceType,name); + +@override +String toString() { + return 'AutoRotateEvent.sourceTypeChanged(sourceType: $sourceType, name: $name)'; +} + + +} + + + + +/// @nodoc + + +class _TargetChanged implements AutoRotateEvent { + const _TargetChanged({required this.target}); + + +@override final aw.WallpaperTarget target; + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _TargetChanged&&(identical(other.target, target) || other.target == target)); +} + + +@override +int get hashCode => Object.hash(runtimeType,target); + +@override +String toString() { + return 'AutoRotateEvent.targetChanged(target: $target)'; +} + + +} + + + + +/// @nodoc + + +class _IntervalChanged implements AutoRotateEvent { + const _IntervalChanged({required this.minutes}); + + +@override final int minutes; + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _IntervalChanged&&(identical(other.minutes, minutes) || other.minutes == minutes)); +} + + +@override +int get hashCode => Object.hash(runtimeType,minutes); + +@override +String toString() { + return 'AutoRotateEvent.intervalChanged(minutes: $minutes)'; +} + + +} + + + + +/// @nodoc + + +class _ChargingTriggerToggled implements AutoRotateEvent { + const _ChargingTriggerToggled(); + + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _ChargingTriggerToggled); +} + + +@override +int get hashCode => runtimeType.hashCode; + +@override +String toString() { + return 'AutoRotateEvent.chargingTriggerToggled()'; +} + + +} + + + + +/// @nodoc + + +class _OrderChanged implements AutoRotateEvent { + const _OrderChanged({required this.order}); + + +@override final AutoRotateOrder order; + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _OrderChanged&&(identical(other.order, order) || other.order == order)); +} + + +@override +int get hashCode => Object.hash(runtimeType,order); + +@override +String toString() { + return 'AutoRotateEvent.orderChanged(order: $order)'; +} + + +} + + + + +/// @nodoc + + +class _StartRequested implements AutoRotateEvent { + const _StartRequested(); + + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _StartRequested); +} + + +@override +int get hashCode => runtimeType.hashCode; + +@override +String toString() { + return 'AutoRotateEvent.startRequested()'; +} + + +} + + + + +/// @nodoc + + +class _StopRequested implements AutoRotateEvent { + const _StopRequested(); + + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _StopRequested); +} + + +@override +int get hashCode => runtimeType.hashCode; + +@override +String toString() { + return 'AutoRotateEvent.stopRequested()'; +} + + +} + + + + +/// @nodoc + + +class _StatusRefreshRequested implements AutoRotateEvent { + const _StatusRefreshRequested(); + + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _StatusRefreshRequested); +} + + +@override +int get hashCode => runtimeType.hashCode; + +@override +String toString() { + return 'AutoRotateEvent.statusRefreshRequested()'; +} + + +} + + + + +/// @nodoc + + +class _RotateNowRequested implements AutoRotateEvent { + const _RotateNowRequested(); + + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _RotateNowRequested); +} + + +@override +int get hashCode => runtimeType.hashCode; + +@override +String toString() { + return 'AutoRotateEvent.rotateNowRequested()'; +} + + +} + + + + +/// @nodoc +mixin _$AutoRotateState { + + AutoRotateConfigEntity get config; bool get isRunning; LoadStatus get status; ActionStatus get actionStatus; List get availableCollections; List get availableCategories; Failure? get failure; +/// Create a copy of AutoRotateState +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$AutoRotateStateCopyWith get copyWith => _$AutoRotateStateCopyWithImpl(this as AutoRotateState, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is AutoRotateState&&(identical(other.config, config) || other.config == config)&&(identical(other.isRunning, isRunning) || other.isRunning == isRunning)&&(identical(other.status, status) || other.status == status)&&(identical(other.actionStatus, actionStatus) || other.actionStatus == actionStatus)&&const DeepCollectionEquality().equals(other.availableCollections, availableCollections)&&const DeepCollectionEquality().equals(other.availableCategories, availableCategories)&&(identical(other.failure, failure) || other.failure == failure)); +} + + +@override +int get hashCode => Object.hash(runtimeType,config,isRunning,status,actionStatus,const DeepCollectionEquality().hash(availableCollections),const DeepCollectionEquality().hash(availableCategories),failure); + +@override +String toString() { + return 'AutoRotateState(config: $config, isRunning: $isRunning, status: $status, actionStatus: $actionStatus, availableCollections: $availableCollections, availableCategories: $availableCategories, failure: $failure)'; +} + + +} + +/// @nodoc +abstract mixin class $AutoRotateStateCopyWith<$Res> { + factory $AutoRotateStateCopyWith(AutoRotateState value, $Res Function(AutoRotateState) _then) = _$AutoRotateStateCopyWithImpl; +@useResult +$Res call({ + AutoRotateConfigEntity config, bool isRunning, LoadStatus status, ActionStatus actionStatus, List availableCollections, List availableCategories, Failure? failure +}); + + + + +} +/// @nodoc +class _$AutoRotateStateCopyWithImpl<$Res> + implements $AutoRotateStateCopyWith<$Res> { + _$AutoRotateStateCopyWithImpl(this._self, this._then); + + final AutoRotateState _self; + final $Res Function(AutoRotateState) _then; + +/// Create a copy of AutoRotateState +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? config = null,Object? isRunning = null,Object? status = null,Object? actionStatus = null,Object? availableCollections = null,Object? availableCategories = null,Object? failure = freezed,}) { + return _then(_self.copyWith( +config: null == config ? _self.config : config // ignore: cast_nullable_to_non_nullable +as AutoRotateConfigEntity,isRunning: null == isRunning ? _self.isRunning : isRunning // ignore: cast_nullable_to_non_nullable +as bool,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable +as LoadStatus,actionStatus: null == actionStatus ? _self.actionStatus : actionStatus // ignore: cast_nullable_to_non_nullable +as ActionStatus,availableCollections: null == availableCollections ? _self.availableCollections : availableCollections // ignore: cast_nullable_to_non_nullable +as List,availableCategories: null == availableCategories ? _self.availableCategories : availableCategories // ignore: cast_nullable_to_non_nullable +as List,failure: freezed == failure ? _self.failure : failure // ignore: cast_nullable_to_non_nullable +as Failure?, + )); +} + +} + + +/// Adds pattern-matching-related methods to [AutoRotateState]. +extension AutoRotateStatePatterns on AutoRotateState { +@optionalTypeArgs TResult maybeMap(TResult Function( _AutoRotateState value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _AutoRotateState() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} + +@optionalTypeArgs TResult map(TResult Function( _AutoRotateState value) $default,){ +final _that = this; +switch (_that) { +case _AutoRotateState(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _AutoRotateState value)? $default,){ +final _that = this; +switch (_that) { +case _AutoRotateState() when $default != null: +return $default(_that);case _: + return null; + +} +} + +@optionalTypeArgs TResult maybeWhen(TResult Function( AutoRotateConfigEntity config, bool isRunning, LoadStatus status, ActionStatus actionStatus, List availableCollections, List availableCategories, Failure? failure)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _AutoRotateState() when $default != null: +return $default(_that.config,_that.isRunning,_that.status,_that.actionStatus,_that.availableCollections,_that.availableCategories,_that.failure);case _: + return orElse(); + +} +} + +@optionalTypeArgs TResult when(TResult Function( AutoRotateConfigEntity config, bool isRunning, LoadStatus status, ActionStatus actionStatus, List availableCollections, List availableCategories, Failure? failure) $default,) {final _that = this; +switch (_that) { +case _AutoRotateState(): +return $default(_that.config,_that.isRunning,_that.status,_that.actionStatus,_that.availableCollections,_that.availableCategories,_that.failure);case _: + throw StateError('Unexpected subclass'); + +} +} + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( AutoRotateConfigEntity config, bool isRunning, LoadStatus status, ActionStatus actionStatus, List availableCollections, List availableCategories, Failure? failure)? $default,) {final _that = this; +switch (_that) { +case _AutoRotateState() when $default != null: +return $default(_that.config,_that.isRunning,_that.status,_that.actionStatus,_that.availableCollections,_that.availableCategories,_that.failure);case _: + return null; + +} +} + +} + +/// @nodoc + + +class _AutoRotateState implements AutoRotateState { + const _AutoRotateState({required this.config, required this.isRunning, required this.status, required this.actionStatus, required final List availableCollections, required final List availableCategories, this.failure}): _availableCollections = availableCollections,_availableCategories = availableCategories; + + +@override final AutoRotateConfigEntity config; +@override final bool isRunning; +@override final LoadStatus status; +@override final ActionStatus actionStatus; + final List _availableCollections; +@override List get availableCollections { + if (_availableCollections is EqualUnmodifiableListView) return _availableCollections; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_availableCollections); +} + + final List _availableCategories; +@override List get availableCategories { + if (_availableCategories is EqualUnmodifiableListView) return _availableCategories; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_availableCategories); +} + +@override final Failure? failure; + +/// Create a copy of AutoRotateState +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$AutoRotateStateCopyWith<_AutoRotateState> get copyWith => __$AutoRotateStateCopyWithImpl<_AutoRotateState>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _AutoRotateState&&(identical(other.config, config) || other.config == config)&&(identical(other.isRunning, isRunning) || other.isRunning == isRunning)&&(identical(other.status, status) || other.status == status)&&(identical(other.actionStatus, actionStatus) || other.actionStatus == actionStatus)&&const DeepCollectionEquality().equals(other._availableCollections, _availableCollections)&&const DeepCollectionEquality().equals(other._availableCategories, _availableCategories)&&(identical(other.failure, failure) || other.failure == failure)); +} + + +@override +int get hashCode => Object.hash(runtimeType,config,isRunning,status,actionStatus,const DeepCollectionEquality().hash(_availableCollections),const DeepCollectionEquality().hash(_availableCategories),failure); + +@override +String toString() { + return 'AutoRotateState(config: $config, isRunning: $isRunning, status: $status, actionStatus: $actionStatus, availableCollections: $availableCollections, availableCategories: $availableCategories, failure: $failure)'; +} + + +} + +/// @nodoc +abstract mixin class _$AutoRotateStateCopyWith<$Res> implements $AutoRotateStateCopyWith<$Res> { + factory _$AutoRotateStateCopyWith(_AutoRotateState value, $Res Function(_AutoRotateState) _then) = __$AutoRotateStateCopyWithImpl; +@override @useResult +$Res call({ + AutoRotateConfigEntity config, bool isRunning, LoadStatus status, ActionStatus actionStatus, List availableCollections, List availableCategories, Failure? failure +}); + + + + +} +/// @nodoc +class __$AutoRotateStateCopyWithImpl<$Res> + implements _$AutoRotateStateCopyWith<$Res> { + __$AutoRotateStateCopyWithImpl(this._self, this._then); + + final _AutoRotateState _self; + final $Res Function(_AutoRotateState) _then; + +/// Create a copy of AutoRotateState +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? config = null,Object? isRunning = null,Object? status = null,Object? actionStatus = null,Object? availableCollections = null,Object? availableCategories = null,Object? failure = freezed,}) { + return _then(_AutoRotateState( +config: null == config ? _self.config : config // ignore: cast_nullable_to_non_nullable +as AutoRotateConfigEntity,isRunning: null == isRunning ? _self.isRunning : isRunning // ignore: cast_nullable_to_non_nullable +as bool,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable +as LoadStatus,actionStatus: null == actionStatus ? _self.actionStatus : actionStatus // ignore: cast_nullable_to_non_nullable +as ActionStatus,availableCollections: null == availableCollections ? _self._availableCollections : availableCollections // ignore: cast_nullable_to_non_nullable +as List,availableCategories: null == availableCategories ? _self._availableCategories : availableCategories // ignore: cast_nullable_to_non_nullable +as List,failure: freezed == failure ? _self.failure : failure // ignore: cast_nullable_to_non_nullable +as Failure?, + )); +} + + +} + +// dart format on diff --git a/lib/features/auto_rotate/biz/bloc/auto_rotate_event.j.dart b/lib/features/auto_rotate/biz/bloc/auto_rotate_event.j.dart new file mode 100644 index 000000000..d816dddda --- /dev/null +++ b/lib/features/auto_rotate/biz/bloc/auto_rotate_event.j.dart @@ -0,0 +1,16 @@ +part of 'auto_rotate_bloc.j.dart'; + +@freezed +abstract class AutoRotateEvent with _$AutoRotateEvent { + const factory AutoRotateEvent.started() = _Started; + const factory AutoRotateEvent.sourceTypeChanged({required AutoRotateSourceType sourceType, String? name}) = + _SourceTypeChanged; + const factory AutoRotateEvent.targetChanged({required aw.WallpaperTarget target}) = _TargetChanged; + const factory AutoRotateEvent.intervalChanged({required int minutes}) = _IntervalChanged; + const factory AutoRotateEvent.chargingTriggerToggled() = _ChargingTriggerToggled; + const factory AutoRotateEvent.orderChanged({required AutoRotateOrder order}) = _OrderChanged; + const factory AutoRotateEvent.startRequested() = _StartRequested; + const factory AutoRotateEvent.stopRequested() = _StopRequested; + const factory AutoRotateEvent.statusRefreshRequested() = _StatusRefreshRequested; + const factory AutoRotateEvent.rotateNowRequested() = _RotateNowRequested; +} diff --git a/lib/features/auto_rotate/biz/bloc/auto_rotate_state.j.dart b/lib/features/auto_rotate/biz/bloc/auto_rotate_state.j.dart new file mode 100644 index 000000000..b00330101 --- /dev/null +++ b/lib/features/auto_rotate/biz/bloc/auto_rotate_state.j.dart @@ -0,0 +1,23 @@ +part of 'auto_rotate_bloc.j.dart'; + +@freezed +abstract class AutoRotateState with _$AutoRotateState { + const factory AutoRotateState({ + required AutoRotateConfigEntity config, + required bool isRunning, + required LoadStatus status, + required ActionStatus actionStatus, + required List availableCollections, + required List availableCategories, + Failure? failure, + }) = _AutoRotateState; + + factory AutoRotateState.initial() => AutoRotateState( + config: AutoRotateConfigEntity.defaults, + isRunning: false, + status: LoadStatus.initial, + actionStatus: ActionStatus.idle, + availableCollections: const [], + availableCategories: categoryDefinitions, + ); +} diff --git a/lib/features/auto_rotate/data/data_sources/auto_rotate_local_data_source.dart b/lib/features/auto_rotate/data/data_sources/auto_rotate_local_data_source.dart new file mode 100644 index 000000000..fccc54b5b --- /dev/null +++ b/lib/features/auto_rotate/data/data_sources/auto_rotate_local_data_source.dart @@ -0,0 +1,69 @@ +import 'package:Prism/core/persistence/local_store.dart'; +import 'package:Prism/core/persistence/persistence_keys.dart'; +import 'package:Prism/features/auto_rotate/domain/entities/auto_rotate_config_entity.dart'; +import 'package:async_wallpaper/async_wallpaper.dart' as aw; +import 'package:injectable/injectable.dart'; + +@lazySingleton +class AutoRotateLocalDataSource { + AutoRotateLocalDataSource(this._store); + + final LocalStore _store; + + AutoRotateConfigEntity loadConfig() { + final defaults = AutoRotateConfigEntity.defaults; + + final isEnabled = _store.get(PersistenceKeys.autoRotateEnabled) as bool? ?? defaults.isEnabled; + + final sourceTypeRaw = _store.get(PersistenceKeys.autoRotateSourceType) as String?; + final sourceType = sourceTypeRaw != null + ? AutoRotateSourceType.values.firstWhere((e) => e.name == sourceTypeRaw, orElse: () => defaults.sourceType) + : defaults.sourceType; + + final collectionName = _store.get(PersistenceKeys.autoRotateCollectionName) as String?; + final categoryName = _store.get(PersistenceKeys.autoRotateCategoryName) as String?; + + final targetRaw = _store.get(PersistenceKeys.autoRotateTarget) as String?; + final target = targetRaw != null + ? aw.WallpaperTarget.values.firstWhere((e) => e.name == targetRaw, orElse: () => defaults.target) + : defaults.target; + + final intervalMinutes = _store.get(PersistenceKeys.autoRotateIntervalMinutes) as int? ?? defaults.intervalMinutes; + final chargingTrigger = _store.get(PersistenceKeys.autoRotateChargingTrigger) as bool? ?? defaults.chargingTrigger; + + final orderRaw = _store.get(PersistenceKeys.autoRotateOrder) as String?; + final order = orderRaw != null + ? AutoRotateOrder.values.firstWhere((e) => e.name == orderRaw, orElse: () => defaults.order) + : defaults.order; + + return AutoRotateConfigEntity( + isEnabled: isEnabled, + sourceType: sourceType, + collectionName: collectionName, + categoryName: categoryName, + target: target, + intervalMinutes: intervalMinutes, + chargingTrigger: chargingTrigger, + order: order, + ); + } + + Future saveConfig(AutoRotateConfigEntity config) async { + await Future.wait([ + _store.set(PersistenceKeys.autoRotateEnabled, config.isEnabled), + _store.set(PersistenceKeys.autoRotateSourceType, config.sourceType.name), + if (config.collectionName != null) + _store.set(PersistenceKeys.autoRotateCollectionName, config.collectionName) + else + _store.delete(PersistenceKeys.autoRotateCollectionName), + if (config.categoryName != null) + _store.set(PersistenceKeys.autoRotateCategoryName, config.categoryName) + else + _store.delete(PersistenceKeys.autoRotateCategoryName), + _store.set(PersistenceKeys.autoRotateTarget, config.target.name), + _store.set(PersistenceKeys.autoRotateIntervalMinutes, config.intervalMinutes), + _store.set(PersistenceKeys.autoRotateChargingTrigger, config.chargingTrigger), + _store.set(PersistenceKeys.autoRotateOrder, config.order.name), + ]); + } +} diff --git a/lib/features/auto_rotate/data/repositories/auto_rotate_repository_impl.dart b/lib/features/auto_rotate/data/repositories/auto_rotate_repository_impl.dart new file mode 100644 index 000000000..243c2b322 --- /dev/null +++ b/lib/features/auto_rotate/data/repositories/auto_rotate_repository_impl.dart @@ -0,0 +1,158 @@ +import 'package:Prism/core/error/failure.dart'; +import 'package:Prism/core/state/app_state.dart' as app_state; +import 'package:Prism/core/utils/result.dart'; +import 'package:Prism/data/categories/categories.dart'; +import 'package:Prism/data/collections/provider/collectionsWithoutProvider.dart'; +import 'package:Prism/features/auto_rotate/data/data_sources/auto_rotate_local_data_source.dart'; +import 'package:Prism/features/auto_rotate/domain/entities/auto_rotate_config_entity.dart'; +import 'package:Prism/features/auto_rotate/domain/repositories/auto_rotate_repository.dart'; +import 'package:Prism/features/category_feed/domain/entities/category_entity.dart'; +import 'package:Prism/features/category_feed/domain/entities/feed_item_entity.dart'; +import 'package:Prism/features/category_feed/domain/usecases/category_feed_usecases.dart'; +import 'package:Prism/features/favourite_walls/domain/usecases/favourite_walls_usecases.dart'; +import 'package:async_wallpaper/async_wallpaper.dart' as aw; +import 'package:injectable/injectable.dart'; + +@LazySingleton(as: AutoRotateRepository) +class AutoRotateRepositoryImpl implements AutoRotateRepository { + AutoRotateRepositoryImpl(this._localDataSource, this._fetchFavouriteWallsUseCase, this._fetchCategoryFeedUseCase); + + final AutoRotateLocalDataSource _localDataSource; + final FetchFavouriteWallsUseCase _fetchFavouriteWallsUseCase; + final FetchCategoryFeedUseCase _fetchCategoryFeedUseCase; + + @override + Future> loadConfig() async { + try { + return Result.success(_localDataSource.loadConfig()); + } catch (e) { + return Result.error(CacheFailure('Failed to load auto rotate config: $e')); + } + } + + @override + Future> saveConfig(AutoRotateConfigEntity config) async { + try { + await _localDataSource.saveConfig(config); + return Result.success(null); + } catch (e) { + return Result.error(CacheFailure('Failed to save auto rotate config: $e')); + } + } + + @override + Future> startRotation(AutoRotateConfigEntity config) async { + try { + final urls = await _fetchUrls(config); + if (urls.isEmpty) { + return Result.error(const ValidationFailure('No wallpapers found for the selected source.')); + } + + final sources = urls + .map((url) => aw.WallpaperRotationSource(sourceType: aw.WallpaperSourceType.url, source: url)) + .toList(); + + final triggers = {aw.WallpaperRotationTrigger.interval}; + if (config.chargingTrigger) { + triggers.add(aw.WallpaperRotationTrigger.charging); + } + + final request = aw.WallpaperRotationRequest( + sources: sources, + target: config.target, + intervalMinutes: config.intervalMinutes, + triggers: triggers, + order: config.order == AutoRotateOrder.shuffle + ? aw.WallpaperRotationOrder.shuffle + : aw.WallpaperRotationOrder.sequential, + ); + + final result = await aw.AsyncWallpaper.startWallpaperRotation(request); + if (!result.isSuccess) { + return Result.error(const UnknownFailure('Failed to start wallpaper rotation.')); + } + + await _localDataSource.saveConfig(config.copyWith(isEnabled: true)); + return Result.success(null); + } catch (e) { + return Result.error(UnknownFailure('Failed to start auto rotate: $e')); + } + } + + @override + Future> stopRotation() async { + try { + await aw.AsyncWallpaper.stopWallpaperRotation(); + final config = _localDataSource.loadConfig(); + await _localDataSource.saveConfig(config.copyWith(isEnabled: false)); + return Result.success(null); + } catch (e) { + return Result.error(UnknownFailure('Failed to stop auto rotate: $e')); + } + } + + @override + Future> isRotationActive() async { + try { + final status = await aw.AsyncWallpaper.getWallpaperRotationStatus(); + return Result.success(status.isRunning); + } catch (e) { + return Result.error(UnknownFailure('Failed to get rotation status: $e')); + } + } + + Future> _fetchUrls(AutoRotateConfigEntity config) async { + switch (config.sourceType) { + case AutoRotateSourceType.favourites: + return _fetchFavouriteUrls(); + case AutoRotateSourceType.category: + return _fetchCategoryUrls(config.categoryName ?? ''); + case AutoRotateSourceType.collection: + return _fetchCollectionUrls(config.collectionName ?? ''); + } + } + + Future> _fetchFavouriteUrls() async { + final userId = app_state.prismUser.id; + final result = await _fetchFavouriteWallsUseCase(FetchFavouriteWallsParams(userId: userId)); + return result.fold( + onSuccess: (walls) => walls.map((w) => w.fullUrl).where((url) => url.isNotEmpty).toList(), + onFailure: (_) => [], + ); + } + + Future> _fetchCategoryUrls(String categoryName) async { + final definition = categoryDefinitions.firstWhere( + (d) => d.name == categoryName, + orElse: () => categoryDefinitions.first, + ); + + final category = CategoryEntity( + name: definition.name, + source: definition.source, + searchType: definition.searchType, + image: definition.imageUrl, + image2: definition.secondaryImageUrl, + ); + + final result = await _fetchCategoryFeedUseCase(FetchCategoryFeedParams(category: category, refresh: true)); + return result.fold( + onSuccess: (page) => page.items.map(_extractUrl).where((url) => url.isNotEmpty).toList(), + onFailure: (_) => [], + ); + } + + Future> _fetchCollectionUrls(String collectionName) async { + await getCollectionWithName(collectionName); + final walls = anyCollectionWalls ?? []; + return walls.map((w) => w['wallpaper_url']?.toString() ?? '').where((url) => url.isNotEmpty).toList(); + } + + String _extractUrl(FeedItemEntity item) { + return item.when( + prism: (_, wallpaper) => wallpaper.fullUrl, + wallhaven: (_, wallpaper) => wallpaper.fullUrl, + pexels: (_, wallpaper) => wallpaper.fullUrl, + ); + } +} diff --git a/lib/features/auto_rotate/domain/entities/auto_rotate_config_entity.dart b/lib/features/auto_rotate/domain/entities/auto_rotate_config_entity.dart new file mode 100644 index 000000000..ff4b6302c --- /dev/null +++ b/lib/features/auto_rotate/domain/entities/auto_rotate_config_entity.dart @@ -0,0 +1,31 @@ +import 'package:async_wallpaper/async_wallpaper.dart' as aw; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'auto_rotate_config_entity.freezed.dart'; + +enum AutoRotateSourceType { collection, category, favourites } + +enum AutoRotateOrder { sequential, shuffle } + +@freezed +abstract class AutoRotateConfigEntity with _$AutoRotateConfigEntity { + const factory AutoRotateConfigEntity({ + required bool isEnabled, + required AutoRotateSourceType sourceType, + String? collectionName, + String? categoryName, + required aw.WallpaperTarget target, + required int intervalMinutes, + required bool chargingTrigger, + required AutoRotateOrder order, + }) = _AutoRotateConfigEntity; + + static AutoRotateConfigEntity get defaults => const AutoRotateConfigEntity( + isEnabled: false, + sourceType: AutoRotateSourceType.favourites, + target: aw.WallpaperTarget.both, + intervalMinutes: 60, + chargingTrigger: false, + order: AutoRotateOrder.shuffle, + ); +} diff --git a/lib/features/auto_rotate/domain/entities/auto_rotate_config_entity.freezed.dart b/lib/features/auto_rotate/domain/entities/auto_rotate_config_entity.freezed.dart new file mode 100644 index 000000000..c9d56cd5a --- /dev/null +++ b/lib/features/auto_rotate/domain/entities/auto_rotate_config_entity.freezed.dart @@ -0,0 +1,223 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'auto_rotate_config_entity.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; +/// @nodoc +mixin _$AutoRotateConfigEntity { + + bool get isEnabled; AutoRotateSourceType get sourceType; String? get collectionName; String? get categoryName; aw.WallpaperTarget get target; int get intervalMinutes; bool get chargingTrigger; AutoRotateOrder get order; +/// Create a copy of AutoRotateConfigEntity +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$AutoRotateConfigEntityCopyWith get copyWith => _$AutoRotateConfigEntityCopyWithImpl(this as AutoRotateConfigEntity, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is AutoRotateConfigEntity&&(identical(other.isEnabled, isEnabled) || other.isEnabled == isEnabled)&&(identical(other.sourceType, sourceType) || other.sourceType == sourceType)&&(identical(other.collectionName, collectionName) || other.collectionName == collectionName)&&(identical(other.categoryName, categoryName) || other.categoryName == categoryName)&&(identical(other.target, target) || other.target == target)&&(identical(other.intervalMinutes, intervalMinutes) || other.intervalMinutes == intervalMinutes)&&(identical(other.chargingTrigger, chargingTrigger) || other.chargingTrigger == chargingTrigger)&&(identical(other.order, order) || other.order == order)); +} + + +@override +int get hashCode => Object.hash(runtimeType,isEnabled,sourceType,collectionName,categoryName,target,intervalMinutes,chargingTrigger,order); + +@override +String toString() { + return 'AutoRotateConfigEntity(isEnabled: $isEnabled, sourceType: $sourceType, collectionName: $collectionName, categoryName: $categoryName, target: $target, intervalMinutes: $intervalMinutes, chargingTrigger: $chargingTrigger, order: $order)'; +} + + +} + +/// @nodoc +abstract mixin class $AutoRotateConfigEntityCopyWith<$Res> { + factory $AutoRotateConfigEntityCopyWith(AutoRotateConfigEntity value, $Res Function(AutoRotateConfigEntity) _then) = _$AutoRotateConfigEntityCopyWithImpl; +@useResult +$Res call({ + bool isEnabled, AutoRotateSourceType sourceType, String? collectionName, String? categoryName, aw.WallpaperTarget target, int intervalMinutes, bool chargingTrigger, AutoRotateOrder order +}); + + + + +} +/// @nodoc +class _$AutoRotateConfigEntityCopyWithImpl<$Res> + implements $AutoRotateConfigEntityCopyWith<$Res> { + _$AutoRotateConfigEntityCopyWithImpl(this._self, this._then); + + final AutoRotateConfigEntity _self; + final $Res Function(AutoRotateConfigEntity) _then; + +/// Create a copy of AutoRotateConfigEntity +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? isEnabled = null,Object? sourceType = null,Object? collectionName = freezed,Object? categoryName = freezed,Object? target = null,Object? intervalMinutes = null,Object? chargingTrigger = null,Object? order = null,}) { + return _then(_self.copyWith( +isEnabled: null == isEnabled ? _self.isEnabled : isEnabled // ignore: cast_nullable_to_non_nullable +as bool,sourceType: null == sourceType ? _self.sourceType : sourceType // ignore: cast_nullable_to_non_nullable +as AutoRotateSourceType,collectionName: freezed == collectionName ? _self.collectionName : collectionName // ignore: cast_nullable_to_non_nullable +as String?,categoryName: freezed == categoryName ? _self.categoryName : categoryName // ignore: cast_nullable_to_non_nullable +as String?,target: null == target ? _self.target : target // ignore: cast_nullable_to_non_nullable +as aw.WallpaperTarget,intervalMinutes: null == intervalMinutes ? _self.intervalMinutes : intervalMinutes // ignore: cast_nullable_to_non_nullable +as int,chargingTrigger: null == chargingTrigger ? _self.chargingTrigger : chargingTrigger // ignore: cast_nullable_to_non_nullable +as bool,order: null == order ? _self.order : order // ignore: cast_nullable_to_non_nullable +as AutoRotateOrder, + )); +} + +} + + +/// Adds pattern-matching-related methods to [AutoRotateConfigEntity]. +extension AutoRotateConfigEntityPatterns on AutoRotateConfigEntity { +@optionalTypeArgs TResult maybeMap(TResult Function( _AutoRotateConfigEntity value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _AutoRotateConfigEntity() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} + +@optionalTypeArgs TResult map(TResult Function( _AutoRotateConfigEntity value) $default,){ +final _that = this; +switch (_that) { +case _AutoRotateConfigEntity(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _AutoRotateConfigEntity value)? $default,){ +final _that = this; +switch (_that) { +case _AutoRotateConfigEntity() when $default != null: +return $default(_that);case _: + return null; + +} +} + +@optionalTypeArgs TResult maybeWhen(TResult Function( bool isEnabled, AutoRotateSourceType sourceType, String? collectionName, String? categoryName, aw.WallpaperTarget target, int intervalMinutes, bool chargingTrigger, AutoRotateOrder order)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _AutoRotateConfigEntity() when $default != null: +return $default(_that.isEnabled,_that.sourceType,_that.collectionName,_that.categoryName,_that.target,_that.intervalMinutes,_that.chargingTrigger,_that.order);case _: + return orElse(); + +} +} + +@optionalTypeArgs TResult when(TResult Function( bool isEnabled, AutoRotateSourceType sourceType, String? collectionName, String? categoryName, aw.WallpaperTarget target, int intervalMinutes, bool chargingTrigger, AutoRotateOrder order) $default,) {final _that = this; +switch (_that) { +case _AutoRotateConfigEntity(): +return $default(_that.isEnabled,_that.sourceType,_that.collectionName,_that.categoryName,_that.target,_that.intervalMinutes,_that.chargingTrigger,_that.order);case _: + throw StateError('Unexpected subclass'); + +} +} + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( bool isEnabled, AutoRotateSourceType sourceType, String? collectionName, String? categoryName, aw.WallpaperTarget target, int intervalMinutes, bool chargingTrigger, AutoRotateOrder order)? $default,) {final _that = this; +switch (_that) { +case _AutoRotateConfigEntity() when $default != null: +return $default(_that.isEnabled,_that.sourceType,_that.collectionName,_that.categoryName,_that.target,_that.intervalMinutes,_that.chargingTrigger,_that.order);case _: + return null; + +} +} + +} + +/// @nodoc + + +class _AutoRotateConfigEntity implements AutoRotateConfigEntity { + const _AutoRotateConfigEntity({required this.isEnabled, required this.sourceType, this.collectionName, this.categoryName, required this.target, required this.intervalMinutes, required this.chargingTrigger, required this.order}); + + +@override final bool isEnabled; +@override final AutoRotateSourceType sourceType; +@override final String? collectionName; +@override final String? categoryName; +@override final aw.WallpaperTarget target; +@override final int intervalMinutes; +@override final bool chargingTrigger; +@override final AutoRotateOrder order; + +/// Create a copy of AutoRotateConfigEntity +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$AutoRotateConfigEntityCopyWith<_AutoRotateConfigEntity> get copyWith => __$AutoRotateConfigEntityCopyWithImpl<_AutoRotateConfigEntity>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _AutoRotateConfigEntity&&(identical(other.isEnabled, isEnabled) || other.isEnabled == isEnabled)&&(identical(other.sourceType, sourceType) || other.sourceType == sourceType)&&(identical(other.collectionName, collectionName) || other.collectionName == collectionName)&&(identical(other.categoryName, categoryName) || other.categoryName == categoryName)&&(identical(other.target, target) || other.target == target)&&(identical(other.intervalMinutes, intervalMinutes) || other.intervalMinutes == intervalMinutes)&&(identical(other.chargingTrigger, chargingTrigger) || other.chargingTrigger == chargingTrigger)&&(identical(other.order, order) || other.order == order)); +} + + +@override +int get hashCode => Object.hash(runtimeType,isEnabled,sourceType,collectionName,categoryName,target,intervalMinutes,chargingTrigger,order); + +@override +String toString() { + return 'AutoRotateConfigEntity(isEnabled: $isEnabled, sourceType: $sourceType, collectionName: $collectionName, categoryName: $categoryName, target: $target, intervalMinutes: $intervalMinutes, chargingTrigger: $chargingTrigger, order: $order)'; +} + + +} + +/// @nodoc +abstract mixin class _$AutoRotateConfigEntityCopyWith<$Res> implements $AutoRotateConfigEntityCopyWith<$Res> { + factory _$AutoRotateConfigEntityCopyWith(_AutoRotateConfigEntity value, $Res Function(_AutoRotateConfigEntity) _then) = __$AutoRotateConfigEntityCopyWithImpl; +@override @useResult +$Res call({ + bool isEnabled, AutoRotateSourceType sourceType, String? collectionName, String? categoryName, aw.WallpaperTarget target, int intervalMinutes, bool chargingTrigger, AutoRotateOrder order +}); + + + + +} +/// @nodoc +class __$AutoRotateConfigEntityCopyWithImpl<$Res> + implements _$AutoRotateConfigEntityCopyWith<$Res> { + __$AutoRotateConfigEntityCopyWithImpl(this._self, this._then); + + final _AutoRotateConfigEntity _self; + final $Res Function(_AutoRotateConfigEntity) _then; + +/// Create a copy of AutoRotateConfigEntity +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? isEnabled = null,Object? sourceType = null,Object? collectionName = freezed,Object? categoryName = freezed,Object? target = null,Object? intervalMinutes = null,Object? chargingTrigger = null,Object? order = null,}) { + return _then(_AutoRotateConfigEntity( +isEnabled: null == isEnabled ? _self.isEnabled : isEnabled // ignore: cast_nullable_to_non_nullable +as bool,sourceType: null == sourceType ? _self.sourceType : sourceType // ignore: cast_nullable_to_non_nullable +as AutoRotateSourceType,collectionName: freezed == collectionName ? _self.collectionName : collectionName // ignore: cast_nullable_to_non_nullable +as String?,categoryName: freezed == categoryName ? _self.categoryName : categoryName // ignore: cast_nullable_to_non_nullable +as String?,target: null == target ? _self.target : target // ignore: cast_nullable_to_non_nullable +as aw.WallpaperTarget,intervalMinutes: null == intervalMinutes ? _self.intervalMinutes : intervalMinutes // ignore: cast_nullable_to_non_nullable +as int,chargingTrigger: null == chargingTrigger ? _self.chargingTrigger : chargingTrigger // ignore: cast_nullable_to_non_nullable +as bool,order: null == order ? _self.order : order // ignore: cast_nullable_to_non_nullable +as AutoRotateOrder, + )); +} + + +} + +// dart format on diff --git a/lib/features/auto_rotate/domain/repositories/auto_rotate_repository.dart b/lib/features/auto_rotate/domain/repositories/auto_rotate_repository.dart new file mode 100644 index 000000000..57e272e17 --- /dev/null +++ b/lib/features/auto_rotate/domain/repositories/auto_rotate_repository.dart @@ -0,0 +1,10 @@ +import 'package:Prism/core/utils/result.dart'; +import 'package:Prism/features/auto_rotate/domain/entities/auto_rotate_config_entity.dart'; + +abstract class AutoRotateRepository { + Future> loadConfig(); + Future> saveConfig(AutoRotateConfigEntity config); + Future> startRotation(AutoRotateConfigEntity config); + Future> stopRotation(); + Future> isRotationActive(); +} diff --git a/lib/features/auto_rotate/domain/usecases/auto_rotate_usecases.dart b/lib/features/auto_rotate/domain/usecases/auto_rotate_usecases.dart new file mode 100644 index 000000000..8039110dc --- /dev/null +++ b/lib/features/auto_rotate/domain/usecases/auto_rotate_usecases.dart @@ -0,0 +1,55 @@ +import 'package:Prism/core/usecase/usecase.dart'; +import 'package:Prism/core/utils/result.dart'; +import 'package:Prism/features/auto_rotate/domain/entities/auto_rotate_config_entity.dart'; +import 'package:Prism/features/auto_rotate/domain/repositories/auto_rotate_repository.dart'; +import 'package:injectable/injectable.dart'; + +@lazySingleton +class LoadAutoRotateConfigUseCase implements UseCase { + LoadAutoRotateConfigUseCase(this._repository); + + final AutoRotateRepository _repository; + + @override + Future> call(NoParams params) => _repository.loadConfig(); +} + +@lazySingleton +class SaveAutoRotateConfigUseCase implements UseCase { + SaveAutoRotateConfigUseCase(this._repository); + + final AutoRotateRepository _repository; + + @override + Future> call(AutoRotateConfigEntity params) => _repository.saveConfig(params); +} + +@lazySingleton +class StartAutoRotateUseCase implements UseCase { + StartAutoRotateUseCase(this._repository); + + final AutoRotateRepository _repository; + + @override + Future> call(AutoRotateConfigEntity params) => _repository.startRotation(params); +} + +@lazySingleton +class StopAutoRotateUseCase implements UseCase { + StopAutoRotateUseCase(this._repository); + + final AutoRotateRepository _repository; + + @override + Future> call(NoParams params) => _repository.stopRotation(); +} + +@lazySingleton +class GetAutoRotateStatusUseCase implements UseCase { + GetAutoRotateStatusUseCase(this._repository); + + final AutoRotateRepository _repository; + + @override + Future> call(NoParams params) => _repository.isRotationActive(); +} diff --git a/lib/features/auto_rotate/views/pages/auto_rotate_screen.dart b/lib/features/auto_rotate/views/pages/auto_rotate_screen.dart new file mode 100644 index 000000000..b32321924 --- /dev/null +++ b/lib/features/auto_rotate/views/pages/auto_rotate_screen.dart @@ -0,0 +1,458 @@ +import 'package:Prism/core/di/injection.dart'; +import 'package:Prism/core/utils/status.dart'; +import 'package:Prism/data/categories/category_definition.dart'; +import 'package:Prism/features/auto_rotate/biz/bloc/auto_rotate_bloc.j.dart'; +import 'package:Prism/features/auto_rotate/domain/entities/auto_rotate_config_entity.dart'; +import 'package:async_wallpaper/async_wallpaper.dart' as aw; +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +@RoutePage() +class AutoRotateScreen extends StatelessWidget { + const AutoRotateScreen({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => getIt()..add(const AutoRotateEvent.started()), + child: const _AutoRotateView(), + ); + } +} + +class _AutoRotateView extends StatelessWidget { + const _AutoRotateView(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Auto Rotate'), centerTitle: false), + body: BlocBuilder( + builder: (context, state) { + if (state.status == LoadStatus.loading) { + return const Center(child: CircularProgressIndicator()); + } + return _AutoRotateBody(state: state); + }, + ), + ); + } +} + +class _AutoRotateBody extends StatelessWidget { + const _AutoRotateBody({required this.state}); + + final AutoRotateState state; + + Color _accentColor(BuildContext context) { + final c = Theme.of(context).colorScheme.error; + return c == Colors.black ? Colors.grey : c; + } + + TextStyle _titleStyle(BuildContext context) => TextStyle( + color: Theme.of(context).colorScheme.secondary, + fontWeight: FontWeight.w500, + fontFamily: 'Proxima Nova', + ); + + Widget _sectionCard(BuildContext context, {required String title, required List children}) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + child: Card( + color: Theme.of(context).cardColor, + elevation: 0, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(16, 14, 16, 4), + child: Text( + title, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + letterSpacing: 0.8, + color: _accentColor(context), + fontFamily: 'Proxima Nova', + ), + ), + ), + ...children, + const SizedBox(height: 6), + ], + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + final bloc = context.read(); + final config = state.config; + final isRunning = state.isRunning; + final isLoading = state.actionStatus == ActionStatus.inProgress; + + return ListView( + padding: const EdgeInsets.only(bottom: 32), + children: [ + // ── Status Banner ───────────────────────────────────────────────────── + Padding( + padding: const EdgeInsets.fromLTRB(12, 12, 12, 6), + child: Card( + color: isRunning ? Colors.green.withValues(alpha: 0.15) : Theme.of(context).cardColor, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + side: BorderSide(color: isRunning ? Colors.green : Colors.transparent, width: 1), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.autorenew, color: isRunning ? Colors.green : Theme.of(context).hintColor), + const SizedBox(width: 10), + Text( + isRunning ? 'Auto Rotate is Running' : 'Auto Rotate is Stopped', + style: TextStyle( + fontWeight: FontWeight.bold, + color: isRunning ? Colors.green : Theme.of(context).colorScheme.secondary, + fontFamily: 'Proxima Nova', + ), + ), + ], + ), + if (isRunning) ...[ + const SizedBox(height: 8), + Text(_statusSubtitle(config), style: const TextStyle(fontSize: 12)), + const SizedBox(height: 10), + TextButton.icon( + onPressed: () => bloc.add(const AutoRotateEvent.rotateNowRequested()), + icon: const Icon(Icons.skip_next_rounded, size: 18), + label: const Text('Rotate Now'), + ), + ], + if (state.failure != null) + Padding( + padding: const EdgeInsets.only(top: 8), + child: Text( + state.failure!.message, + style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.error), + ), + ), + ], + ), + ), + ), + ), + + // ── Source Selection ────────────────────────────────────────────────── + _sectionCard( + context, + title: 'SOURCE', + children: [ + RadioListTile( + activeColor: _accentColor(context), + value: AutoRotateSourceType.favourites, + groupValue: config.sourceType, + title: Text('Favourites', style: _titleStyle(context)), + subtitle: const Text('Your saved wallpapers', style: TextStyle(fontSize: 12)), + onChanged: isRunning + ? null + : (_) => + bloc.add(const AutoRotateEvent.sourceTypeChanged(sourceType: AutoRotateSourceType.favourites)), + ), + RadioListTile( + activeColor: _accentColor(context), + value: AutoRotateSourceType.category, + groupValue: config.sourceType, + title: Text('Category', style: _titleStyle(context)), + subtitle: const Text('Wallpapers from a category', style: TextStyle(fontSize: 12)), + onChanged: isRunning + ? null + : (_) => bloc.add( + const AutoRotateEvent.sourceTypeChanged(sourceType: AutoRotateSourceType.category, name: null), + ), + ), + if (config.sourceType == AutoRotateSourceType.category) + _CategoryDropdown( + categories: state.availableCategories.toList(), + selectedName: config.categoryName, + accentColor: _accentColor(context), + enabled: !isRunning, + onChanged: (name) => + bloc.add(AutoRotateEvent.sourceTypeChanged(sourceType: AutoRotateSourceType.category, name: name)), + ), + RadioListTile( + activeColor: _accentColor(context), + value: AutoRotateSourceType.collection, + groupValue: config.sourceType, + title: Text('Collection', style: _titleStyle(context)), + subtitle: const Text('Wallpapers from a collection', style: TextStyle(fontSize: 12)), + onChanged: isRunning + ? null + : (_) => bloc.add( + const AutoRotateEvent.sourceTypeChanged(sourceType: AutoRotateSourceType.collection, name: null), + ), + ), + if (config.sourceType == AutoRotateSourceType.collection) + _CollectionDropdown( + collections: state.availableCollections, + selectedName: config.collectionName, + accentColor: _accentColor(context), + enabled: !isRunning, + onChanged: (name) => bloc.add( + AutoRotateEvent.sourceTypeChanged(sourceType: AutoRotateSourceType.collection, name: name), + ), + ), + ], + ), + + // ── Target Screen ───────────────────────────────────────────────────── + _sectionCard( + context, + title: 'SCREEN TARGET', + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), + child: SegmentedButton( + selected: {config.target}, + onSelectionChanged: isRunning + ? null + : (selection) => bloc.add(AutoRotateEvent.targetChanged(target: selection.first)), + segments: const [ + ButtonSegment(value: aw.WallpaperTarget.home, label: Text('Home'), icon: Icon(Icons.home_outlined)), + ButtonSegment(value: aw.WallpaperTarget.lock, label: Text('Lock'), icon: Icon(Icons.lock_outline)), + ButtonSegment( + value: aw.WallpaperTarget.both, + label: Text('Both'), + icon: Icon(Icons.phonelink_setup_outlined), + ), + ], + ), + ), + ], + ), + + // ── Interval ───────────────────────────────────────────────────────── + _sectionCard( + context, + title: 'INTERVAL', + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(12, 8, 12, 8), + child: Wrap( + spacing: 8, + runSpacing: 4, + children: _intervalOptions.map((opt) { + final isSelected = config.intervalMinutes == opt.minutes; + return ChoiceChip( + label: Text(opt.label), + selected: isSelected, + selectedColor: _accentColor(context).withValues(alpha: 0.2), + onSelected: isRunning + ? null + : (_) => bloc.add(AutoRotateEvent.intervalChanged(minutes: opt.minutes)), + ); + }).toList(), + ), + ), + ], + ), + + // ── Triggers ───────────────────────────────────────────────────────── + _sectionCard( + context, + title: 'TRIGGERS', + children: [ + SwitchListTile( + activeColor: _accentColor(context), + secondary: const Icon(Icons.bolt_outlined), + value: config.chargingTrigger, + title: Text('Change on Charging', style: _titleStyle(context)), + subtitle: const Text('Rotate when device is plugged in', style: TextStyle(fontSize: 12)), + onChanged: isRunning ? null : (_) => bloc.add(const AutoRotateEvent.chargingTriggerToggled()), + ), + ], + ), + + // ── Order ───────────────────────────────────────────────────────────── + _sectionCard( + context, + title: 'ORDER', + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), + child: SegmentedButton( + selected: {config.order}, + onSelectionChanged: isRunning + ? null + : (selection) => bloc.add(AutoRotateEvent.orderChanged(order: selection.first)), + segments: const [ + ButtonSegment( + value: AutoRotateOrder.sequential, + label: Text('Sequential'), + icon: Icon(Icons.format_list_numbered), + ), + ButtonSegment(value: AutoRotateOrder.shuffle, label: Text('Shuffle'), icon: Icon(Icons.shuffle)), + ], + ), + ), + ], + ), + + // ── Action Button ───────────────────────────────────────────────────── + Padding( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 8), + child: SizedBox( + width: double.infinity, + height: 52, + child: isLoading + ? const Center(child: CircularProgressIndicator()) + : isRunning + ? ElevatedButton.icon( + onPressed: () => bloc.add(const AutoRotateEvent.stopRequested()), + icon: const Icon(Icons.stop_circle_outlined), + label: const Text('Stop Auto Rotate'), + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.error, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), + ), + ) + : ElevatedButton.icon( + onPressed: _canStart(config) ? () => bloc.add(const AutoRotateEvent.startRequested()) : null, + icon: const Icon(Icons.play_circle_outline), + label: const Text('Start Auto Rotate'), + style: ElevatedButton.styleFrom( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), + ), + ), + ), + ), + ], + ); + } + + bool _canStart(AutoRotateConfigEntity config) { + if (config.sourceType == AutoRotateSourceType.category && (config.categoryName?.isEmpty ?? true)) return false; + if (config.sourceType == AutoRotateSourceType.collection && (config.collectionName?.isEmpty ?? true)) return false; + return true; + } + + String _statusSubtitle(AutoRotateConfigEntity config) { + final source = switch (config.sourceType) { + AutoRotateSourceType.favourites => 'Favourites', + AutoRotateSourceType.category => config.categoryName ?? 'Category', + AutoRotateSourceType.collection => config.collectionName ?? 'Collection', + }; + return 'Source: $source • Every ${_intervalLabel(config.intervalMinutes)}'; + } + + String _intervalLabel(int minutes) { + if (minutes < 60) return '${minutes}min'; + final hours = minutes ~/ 60; + return hours == 1 ? '1 hr' : '$hours hrs'; + } + + static const List<_IntervalOption> _intervalOptions = [ + _IntervalOption(minutes: 15, label: '15 min'), + _IntervalOption(minutes: 30, label: '30 min'), + _IntervalOption(minutes: 60, label: '1 hr'), + _IntervalOption(minutes: 180, label: '3 hr'), + _IntervalOption(minutes: 360, label: '6 hr'), + _IntervalOption(minutes: 720, label: '12 hr'), + _IntervalOption(minutes: 1440, label: '24 hr'), + ]; +} + +class _IntervalOption { + const _IntervalOption({required this.minutes, required this.label}); + + final int minutes; + final String label; +} + +class _CategoryDropdown extends StatelessWidget { + const _CategoryDropdown({ + required this.categories, + required this.selectedName, + required this.accentColor, + required this.enabled, + required this.onChanged, + }); + + final List categories; + final String? selectedName; + final Color accentColor; + final bool enabled; + final ValueChanged onChanged; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), + child: DropdownButtonFormField( + value: selectedName, + decoration: InputDecoration( + labelText: 'Select Category', + border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide(color: accentColor), + ), + contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + ), + items: categories.map((c) => DropdownMenuItem(value: c.name, child: Text(c.name))).toList(), + onChanged: enabled ? onChanged : null, + ), + ); + } +} + +class _CollectionDropdown extends StatelessWidget { + const _CollectionDropdown({ + required this.collections, + required this.selectedName, + required this.accentColor, + required this.enabled, + required this.onChanged, + }); + + final List collections; + final String? selectedName; + final Color accentColor; + final bool enabled; + final ValueChanged onChanged; + + @override + Widget build(BuildContext context) { + if (collections.isEmpty) { + return const Padding( + padding: EdgeInsets.fromLTRB(16, 0, 16, 8), + child: Text('No collections available.', style: TextStyle(fontSize: 12)), + ); + } + return Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), + child: DropdownButtonFormField( + value: selectedName, + decoration: InputDecoration( + labelText: 'Select Collection', + border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide(color: accentColor), + ), + contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + ), + items: collections.map((name) => DropdownMenuItem(value: name, child: Text(name))).toList(), + onChanged: enabled ? onChanged : null, + ), + ); + } +} diff --git a/lib/features/session/views/pages/settings_screen.dart b/lib/features/session/views/pages/settings_screen.dart index d36ce6507..566d7d9b5 100644 --- a/lib/features/session/views/pages/settings_screen.dart +++ b/lib/features/session/views/pages/settings_screen.dart @@ -281,6 +281,14 @@ class _SettingsScreenState extends State { return _sectionCard( title: 'ANDROID WIDGETS', children: [ + if (Platform.isAndroid) + ListTile( + leading: const Icon(Icons.autorenew), + title: Text('Auto Rotate', style: _titleStyle), + subtitle: const Text('Cycle wallpapers automatically', style: TextStyle(fontSize: 12)), + trailing: const Icon(Icons.chevron_right_rounded), + onTap: () => context.router.push(const AutoRotateRoute()), + ), ListTile( leading: const Icon(Icons.grid_view_rounded), title: Text('Quick Tile Settings', style: _titleStyle), diff --git a/pubspec.lock b/pubspec.lock index d27a3d3a1..13dfa92a4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -101,10 +101,10 @@ packages: dependency: "direct main" description: name: async_wallpaper - sha256: "4812b92a2d1ace8d44903f3964dc016b0881dd50f9a9c5cad33385092411e0f4" + sha256: "7daedf936bb5966a948b1f05cd30853eba10572f2ef34e996d38d56104f743cc" url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "3.1.0" auto_route: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 5953d2666..5b64792fe 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -18,7 +18,7 @@ environment: sdk: '>=3.8.0 <4.0.0' dependencies: - async_wallpaper: ^3.0.0 + async_wallpaper: ^3.1.0 bloc: ^8.1.4 animations: ^2.0.0 cached_network_image: ^3.0.0