diff --git a/lib/features/onboarding_v2/src/biz/onboarding_v2_bloc.j.dart b/lib/features/onboarding_v2/src/biz/onboarding_v2_bloc.j.dart index 39f5244f..d3e71d82 100644 --- a/lib/features/onboarding_v2/src/biz/onboarding_v2_bloc.j.dart +++ b/lib/features/onboarding_v2/src/biz/onboarding_v2_bloc.j.dart @@ -1,9 +1,15 @@ +import 'dart:math' as math; + import 'package:Prism/core/di/injection.dart'; import 'package:Prism/core/error/failure.dart'; import 'package:Prism/core/persistence/data_sources/settings_local_data_source.dart'; import 'package:Prism/core/personalization/personalized_interests_catalog.dart'; import 'package:Prism/core/state/app_state.dart' as app_state; import 'package:Prism/core/utils/status.dart'; +import 'package:Prism/features/ai_wallpaper/data/repositories/ai_generation_repository_impl.dart'; +import 'package:Prism/features/ai_wallpaper/domain/entities/ai_charge_mode.dart'; +import 'package:Prism/features/ai_wallpaper/domain/entities/ai_quality_tier.dart'; +import 'package:Prism/features/ai_wallpaper/domain/entities/ai_style_preset.dart'; import 'package:Prism/features/category_feed/domain/repositories/category_feed_repository.dart'; import 'package:Prism/features/onboarding_v2/src/data/repo/onboarding_v2_repo.dart'; import 'package:Prism/features/onboarding_v2/src/domain/usecases/complete_onboarding_v2_usecase.dart'; @@ -47,6 +53,9 @@ class OnboardingV2Bloc extends Bloc { on<_FirstWallpaperStepContinued>(_onFirstWallpaperStepContinued); on<_PaywallResultReceived>(_onPaywallResultReceived); on<_StepBack>(_onStepBack); + on<_AiGenerationRequested>(_onAiGenerationRequested); + on<_AiGenerationCompleted>(_onAiGenerationCompleted); + on<_AiGenerationStepContinued>(_onAiGenerationStepContinued); } final FetchStarterPackUseCase _fetchStarterPackUseCase; @@ -57,6 +66,11 @@ class OnboardingV2Bloc extends Bloc { final CategoryFeedRepository _categoryFeedRepository; final OnboardingV2Repository _onboardingRepository; + // Instantiated directly — same pattern as AiWallpaperTabPage. + final AiGenerationRepositoryImpl _aiRepository = AiGenerationRepositoryImpl(); + + final math.Random _random = math.Random(); + DateTime? _f3EnteredAt; Future _onStarted(_Started event, Emitter emit) async { @@ -84,6 +98,7 @@ class OnboardingV2Bloc extends Bloc { final List creatorVms = starterPackResult.fold( onSuccess: (entities) { final sorted = [...entities]..sort((a, b) => a.rank.compareTo(b.rank)); + final autoSelectedEmails = sorted.take(OnboardingV2Config.minFollows).map((e) => e.email).toSet(); return sorted .map( (e) => OnboardingCreatorVm( @@ -93,7 +108,7 @@ class OnboardingV2Bloc extends Bloc { photoUrl: e.photoUrl, previewUrls: e.previewUrls, rank: e.rank, - isSelected: false, + isSelected: autoSelectedEmails.contains(e.email), bio: e.bio, followerCount: e.followerCount, ), @@ -103,13 +118,15 @@ class OnboardingV2Bloc extends Bloc { onFailure: (_) => [], ); + final autoSelectedEmails = creatorVms.where((c) => c.isSelected).map((c) => c.email).toList(growable: false); + final wallpaperVm = await _firstWallpaperService.recommendForOnboarding([]); emit( state.copyWith( loadStatus: LoadStatus.success, interestsData: state.interestsData.copyWith(available: availableCategories, categoryImages: categoryImages), - starterPackData: OnboardingStarterPackData(creators: creatorVms, selectedEmails: []), + starterPackData: OnboardingStarterPackData(creators: creatorVms, selectedEmails: autoSelectedEmails), wallpaperData: OnboardingWallpaperData(wallpaper: wallpaperVm, status: FirstWallpaperStatus.idle), ), ); @@ -122,7 +139,7 @@ class OnboardingV2Bloc extends Bloc { bool skipInterests = false; bool skipStarterPack = false; - if (userId.isNotEmpty) { + if (!OnboardingV2Config.debugForceOnboarding && userId.isNotEmpty) { final statusResult = await _onboardingRepository.fetchUserCompletionStatus(userId: userId); statusResult.fold( onSuccess: (status) { @@ -196,7 +213,7 @@ class OnboardingV2Bloc extends Bloc { final refreshedWallpaper = await _firstWallpaperService.recommendForOnboarding(selectedInterests); - final nextStep = state.skipStarterPack ? OnboardingV2Step.firstWallpaper : OnboardingV2Step.starterPack; + final nextStep = state.skipStarterPack ? OnboardingV2Step.aiGenerate : OnboardingV2Step.starterPack; emit( state.copyWith( actionStatus: ActionStatus.success, @@ -267,10 +284,10 @@ class OnboardingV2Bloc extends Bloc { emit(state.copyWith(navRequest: OnboardingV2NavRequest.openPaywall)); } } else { - logger.d('starterPackConfirmed — emitting step=firstWallpaper', tag: 'OnboardingV2Bloc'); - _f3EnteredAt = DateTime.now(); - emit(state.copyWith(actionStatus: ActionStatus.success, step: OnboardingV2Step.firstWallpaper)); - logger.d('emitted firstWallpaper step, current state.step=${state.step}', tag: 'OnboardingV2Bloc'); + logger.d('starterPackConfirmed — emitting step=aiGenerate', tag: 'OnboardingV2Bloc'); + final aiData = _pickRandomAiPrompt(state.interestsData.selected); + emit(state.copyWith(actionStatus: ActionStatus.success, step: OnboardingV2Step.aiGenerate, aiData: aiData)); + logger.d('emitted aiGenerate step, current state.step=${state.step}', tag: 'OnboardingV2Bloc'); } } @@ -349,8 +366,8 @@ class OnboardingV2Bloc extends Bloc { final OnboardingV2Step? prevStep = switch (state.step) { OnboardingV2Step.interests => OnboardingV2Step.auth, OnboardingV2Step.starterPack => state.skipInterests ? OnboardingV2Step.auth : OnboardingV2Step.interests, - OnboardingV2Step.firstWallpaper => - state.skipStarterPack ? OnboardingV2Step.interests : OnboardingV2Step.starterPack, + OnboardingV2Step.aiGenerate => state.skipStarterPack ? OnboardingV2Step.interests : OnboardingV2Step.starterPack, + OnboardingV2Step.firstWallpaper => OnboardingV2Step.aiGenerate, OnboardingV2Step.auth => null, }; @@ -358,4 +375,91 @@ class OnboardingV2Bloc extends Bloc { emit(state.copyWith(step: prevStep, navRequest: null)); } } + + // --------------------------------------------------------------------------- + // AI generation step handlers + // --------------------------------------------------------------------------- + + Future _onAiGenerationRequested(_AiGenerationRequested event, Emitter emit) async { + emit(state.copyWith(aiData: state.aiData.copyWith(status: AiGenerateStatus.loading), navRequest: null)); + + try { + final record = await _aiRepository.generate( + prompt: state.aiData.prompt, + stylePreset: state.aiData.stylePreset, + qualityTier: AiQualityTier.fast, + targetSize: event.targetSize, + chargeMode: AiChargeMode.freeTrial, + coinsSpent: 0, + ); + if (!isClosed) { + add( + OnboardingV2Event.aiGenerationCompleted(imageUrl: record.imageUrl, thumbnailUrl: record.watermarkedImageUrl), + ); + } + } catch (e) { + logger.e('AI onboarding generation failed: $e', tag: 'OnboardingV2Bloc'); + if (!isClosed) { + add(const OnboardingV2Event.aiGenerationCompleted(imageUrl: null, thumbnailUrl: null)); + } + } + } + + void _onAiGenerationCompleted(_AiGenerationCompleted event, Emitter emit) { + final succeeded = event.imageUrl != null && event.imageUrl!.isNotEmpty; + final updatedAiData = state.aiData.copyWith( + status: succeeded ? AiGenerateStatus.success : AiGenerateStatus.failure, + imageUrl: event.imageUrl, + thumbnailUrl: event.thumbnailUrl, + ); + + if (succeeded) { + // Pre-populate wallpaperData with the generated image so F4 displays it. + final generatedVm = OnboardingWallpaperVm( + fullUrl: event.imageUrl!, + thumbnailUrl: event.thumbnailUrl ?? event.imageUrl!, + title: 'Your AI wallpaper', + authorName: 'AI', + sourceCategory: state.aiData.stylePreset.label, + ); + emit( + state.copyWith( + aiData: updatedAiData, + wallpaperData: OnboardingWallpaperData(wallpaper: generatedVm, status: FirstWallpaperStatus.idle), + ), + ); + } else { + emit(state.copyWith(aiData: updatedAiData)); + } + } + + Future _onAiGenerationStepContinued(_AiGenerationStepContinued event, Emitter emit) async { + _f3EnteredAt = DateTime.now(); + emit(state.copyWith(step: OnboardingV2Step.firstWallpaper, navRequest: null)); + } + + /// Picks a style and prompt based on the user's selected interest categories. + /// Falls back to a random style from the curated pool if no keyword matches. + OnboardingAiData _pickRandomAiPrompt(List selectedInterests) { + AiStylePreset? matchedStyle; + + outer: + for (final interest in selectedInterests) { + final lower = interest.toLowerCase(); + for (final entry in OnboardingV2Config.aiInterestStyleMap.entries) { + if (lower.contains(entry.key) || entry.key.contains(lower)) { + // Only match single-character keys if they are an exact full match. + if (entry.key.length == 1 && lower != entry.key) continue; + matchedStyle = entry.value; + break outer; + } + } + } + + const styles = OnboardingV2Config.aiOnboardingStyles; + final style = matchedStyle ?? styles[_random.nextInt(styles.length)]; + final prompts = OnboardingV2Config.aiOnboardingPromptPool[style] ?? []; + final prompt = prompts.isNotEmpty ? prompts[_random.nextInt(prompts.length)] : 'a beautiful wallpaper'; + return OnboardingAiData(prompt: prompt, stylePreset: style, status: AiGenerateStatus.idle); + } } diff --git a/lib/features/onboarding_v2/src/biz/onboarding_v2_bloc.j.freezed.dart b/lib/features/onboarding_v2/src/biz/onboarding_v2_bloc.j.freezed.dart index 9a4101d6..f3653b66 100644 --- a/lib/features/onboarding_v2/src/biz/onboarding_v2_bloc.j.freezed.dart +++ b/lib/features/onboarding_v2/src/biz/onboarding_v2_bloc.j.freezed.dart @@ -55,7 +55,7 @@ extension OnboardingV2EventPatterns on OnboardingV2Event { /// } /// ``` -@optionalTypeArgs TResult maybeMap({TResult Function( _Started value)? started,TResult Function( _AuthCompleted value)? authCompleted,TResult Function( _AuthLoadingChanged value)? authLoadingChanged,TResult Function( _InterestToggled value)? interestToggled,TResult Function( _InterestsConfirmed value)? interestsConfirmed,TResult Function( _CreatorFollowToggled value)? creatorFollowToggled,TResult Function( _StarterPackConfirmed value)? starterPackConfirmed,TResult Function( _FirstWallpaperActionRequested value)? firstWallpaperActionRequested,TResult Function( _FirstWallpaperActionCompleted value)? firstWallpaperActionCompleted,TResult Function( _FirstWallpaperStepContinued value)? firstWallpaperStepContinued,TResult Function( _PaywallResultReceived value)? paywallResultReceived,TResult Function( _StepBack value)? stepBack,required TResult orElse(),}){ +@optionalTypeArgs TResult maybeMap({TResult Function( _Started value)? started,TResult Function( _AuthCompleted value)? authCompleted,TResult Function( _AuthLoadingChanged value)? authLoadingChanged,TResult Function( _InterestToggled value)? interestToggled,TResult Function( _InterestsConfirmed value)? interestsConfirmed,TResult Function( _CreatorFollowToggled value)? creatorFollowToggled,TResult Function( _StarterPackConfirmed value)? starterPackConfirmed,TResult Function( _FirstWallpaperActionRequested value)? firstWallpaperActionRequested,TResult Function( _FirstWallpaperActionCompleted value)? firstWallpaperActionCompleted,TResult Function( _FirstWallpaperStepContinued value)? firstWallpaperStepContinued,TResult Function( _PaywallResultReceived value)? paywallResultReceived,TResult Function( _StepBack value)? stepBack,TResult Function( _AiGenerationRequested value)? aiGenerationRequested,TResult Function( _AiGenerationCompleted value)? aiGenerationCompleted,TResult Function( _AiGenerationStepContinued value)? aiGenerationStepContinued,required TResult orElse(),}){ final _that = this; switch (_that) { case _Started() when started != null: @@ -70,7 +70,10 @@ return firstWallpaperActionRequested(_that);case _FirstWallpaperActionCompleted( return firstWallpaperActionCompleted(_that);case _FirstWallpaperStepContinued() when firstWallpaperStepContinued != null: return firstWallpaperStepContinued(_that);case _PaywallResultReceived() when paywallResultReceived != null: return paywallResultReceived(_that);case _StepBack() when stepBack != null: -return stepBack(_that);case _: +return stepBack(_that);case _AiGenerationRequested() when aiGenerationRequested != null: +return aiGenerationRequested(_that);case _AiGenerationCompleted() when aiGenerationCompleted != null: +return aiGenerationCompleted(_that);case _AiGenerationStepContinued() when aiGenerationStepContinued != null: +return aiGenerationStepContinued(_that);case _: return orElse(); } @@ -88,7 +91,7 @@ return stepBack(_that);case _: /// } /// ``` -@optionalTypeArgs TResult map({required TResult Function( _Started value) started,required TResult Function( _AuthCompleted value) authCompleted,required TResult Function( _AuthLoadingChanged value) authLoadingChanged,required TResult Function( _InterestToggled value) interestToggled,required TResult Function( _InterestsConfirmed value) interestsConfirmed,required TResult Function( _CreatorFollowToggled value) creatorFollowToggled,required TResult Function( _StarterPackConfirmed value) starterPackConfirmed,required TResult Function( _FirstWallpaperActionRequested value) firstWallpaperActionRequested,required TResult Function( _FirstWallpaperActionCompleted value) firstWallpaperActionCompleted,required TResult Function( _FirstWallpaperStepContinued value) firstWallpaperStepContinued,required TResult Function( _PaywallResultReceived value) paywallResultReceived,required TResult Function( _StepBack value) stepBack,}){ +@optionalTypeArgs TResult map({required TResult Function( _Started value) started,required TResult Function( _AuthCompleted value) authCompleted,required TResult Function( _AuthLoadingChanged value) authLoadingChanged,required TResult Function( _InterestToggled value) interestToggled,required TResult Function( _InterestsConfirmed value) interestsConfirmed,required TResult Function( _CreatorFollowToggled value) creatorFollowToggled,required TResult Function( _StarterPackConfirmed value) starterPackConfirmed,required TResult Function( _FirstWallpaperActionRequested value) firstWallpaperActionRequested,required TResult Function( _FirstWallpaperActionCompleted value) firstWallpaperActionCompleted,required TResult Function( _FirstWallpaperStepContinued value) firstWallpaperStepContinued,required TResult Function( _PaywallResultReceived value) paywallResultReceived,required TResult Function( _StepBack value) stepBack,required TResult Function( _AiGenerationRequested value) aiGenerationRequested,required TResult Function( _AiGenerationCompleted value) aiGenerationCompleted,required TResult Function( _AiGenerationStepContinued value) aiGenerationStepContinued,}){ final _that = this; switch (_that) { case _Started(): @@ -103,7 +106,10 @@ return firstWallpaperActionRequested(_that);case _FirstWallpaperActionCompleted( return firstWallpaperActionCompleted(_that);case _FirstWallpaperStepContinued(): return firstWallpaperStepContinued(_that);case _PaywallResultReceived(): return paywallResultReceived(_that);case _StepBack(): -return stepBack(_that);case _: +return stepBack(_that);case _AiGenerationRequested(): +return aiGenerationRequested(_that);case _AiGenerationCompleted(): +return aiGenerationCompleted(_that);case _AiGenerationStepContinued(): +return aiGenerationStepContinued(_that);case _: throw StateError('Unexpected subclass'); } @@ -120,7 +126,7 @@ return stepBack(_that);case _: /// } /// ``` -@optionalTypeArgs TResult? mapOrNull({TResult? Function( _Started value)? started,TResult? Function( _AuthCompleted value)? authCompleted,TResult? Function( _AuthLoadingChanged value)? authLoadingChanged,TResult? Function( _InterestToggled value)? interestToggled,TResult? Function( _InterestsConfirmed value)? interestsConfirmed,TResult? Function( _CreatorFollowToggled value)? creatorFollowToggled,TResult? Function( _StarterPackConfirmed value)? starterPackConfirmed,TResult? Function( _FirstWallpaperActionRequested value)? firstWallpaperActionRequested,TResult? Function( _FirstWallpaperActionCompleted value)? firstWallpaperActionCompleted,TResult? Function( _FirstWallpaperStepContinued value)? firstWallpaperStepContinued,TResult? Function( _PaywallResultReceived value)? paywallResultReceived,TResult? Function( _StepBack value)? stepBack,}){ +@optionalTypeArgs TResult? mapOrNull({TResult? Function( _Started value)? started,TResult? Function( _AuthCompleted value)? authCompleted,TResult? Function( _AuthLoadingChanged value)? authLoadingChanged,TResult? Function( _InterestToggled value)? interestToggled,TResult? Function( _InterestsConfirmed value)? interestsConfirmed,TResult? Function( _CreatorFollowToggled value)? creatorFollowToggled,TResult? Function( _StarterPackConfirmed value)? starterPackConfirmed,TResult? Function( _FirstWallpaperActionRequested value)? firstWallpaperActionRequested,TResult? Function( _FirstWallpaperActionCompleted value)? firstWallpaperActionCompleted,TResult? Function( _FirstWallpaperStepContinued value)? firstWallpaperStepContinued,TResult? Function( _PaywallResultReceived value)? paywallResultReceived,TResult? Function( _StepBack value)? stepBack,TResult? Function( _AiGenerationRequested value)? aiGenerationRequested,TResult? Function( _AiGenerationCompleted value)? aiGenerationCompleted,TResult? Function( _AiGenerationStepContinued value)? aiGenerationStepContinued,}){ final _that = this; switch (_that) { case _Started() when started != null: @@ -135,7 +141,10 @@ return firstWallpaperActionRequested(_that);case _FirstWallpaperActionCompleted( return firstWallpaperActionCompleted(_that);case _FirstWallpaperStepContinued() when firstWallpaperStepContinued != null: return firstWallpaperStepContinued(_that);case _PaywallResultReceived() when paywallResultReceived != null: return paywallResultReceived(_that);case _StepBack() when stepBack != null: -return stepBack(_that);case _: +return stepBack(_that);case _AiGenerationRequested() when aiGenerationRequested != null: +return aiGenerationRequested(_that);case _AiGenerationCompleted() when aiGenerationCompleted != null: +return aiGenerationCompleted(_that);case _AiGenerationStepContinued() when aiGenerationStepContinued != null: +return aiGenerationStepContinued(_that);case _: return null; } @@ -152,7 +161,7 @@ return stepBack(_that);case _: /// } /// ``` -@optionalTypeArgs TResult maybeWhen({TResult Function()? started,TResult Function()? authCompleted,TResult Function( bool isLoading)? authLoadingChanged,TResult Function( String categoryName)? interestToggled,TResult Function()? interestsConfirmed,TResult Function( String creatorEmail)? creatorFollowToggled,TResult Function()? starterPackConfirmed,TResult Function()? firstWallpaperActionRequested,TResult Function( bool success, int elapsedMs)? firstWallpaperActionCompleted,TResult Function()? firstWallpaperStepContinued,TResult Function( bool didPurchase)? paywallResultReceived,TResult Function()? stepBack,required TResult orElse(),}) {final _that = this; +@optionalTypeArgs TResult maybeWhen({TResult Function()? started,TResult Function()? authCompleted,TResult Function( bool isLoading)? authLoadingChanged,TResult Function( String categoryName)? interestToggled,TResult Function()? interestsConfirmed,TResult Function( String creatorEmail)? creatorFollowToggled,TResult Function()? starterPackConfirmed,TResult Function()? firstWallpaperActionRequested,TResult Function( bool success, int elapsedMs)? firstWallpaperActionCompleted,TResult Function()? firstWallpaperStepContinued,TResult Function( bool didPurchase)? paywallResultReceived,TResult Function()? stepBack,TResult Function( String targetSize)? aiGenerationRequested,TResult Function( String? imageUrl, String? thumbnailUrl)? aiGenerationCompleted,TResult Function()? aiGenerationStepContinued,required TResult orElse(),}) {final _that = this; switch (_that) { case _Started() when started != null: return started();case _AuthCompleted() when authCompleted != null: @@ -166,7 +175,10 @@ return firstWallpaperActionRequested();case _FirstWallpaperActionCompleted() whe return firstWallpaperActionCompleted(_that.success,_that.elapsedMs);case _FirstWallpaperStepContinued() when firstWallpaperStepContinued != null: return firstWallpaperStepContinued();case _PaywallResultReceived() when paywallResultReceived != null: return paywallResultReceived(_that.didPurchase);case _StepBack() when stepBack != null: -return stepBack();case _: +return stepBack();case _AiGenerationRequested() when aiGenerationRequested != null: +return aiGenerationRequested(_that.targetSize);case _AiGenerationCompleted() when aiGenerationCompleted != null: +return aiGenerationCompleted(_that.imageUrl,_that.thumbnailUrl);case _AiGenerationStepContinued() when aiGenerationStepContinued != null: +return aiGenerationStepContinued();case _: return orElse(); } @@ -184,7 +196,7 @@ return stepBack();case _: /// } /// ``` -@optionalTypeArgs TResult when({required TResult Function() started,required TResult Function() authCompleted,required TResult Function( bool isLoading) authLoadingChanged,required TResult Function( String categoryName) interestToggled,required TResult Function() interestsConfirmed,required TResult Function( String creatorEmail) creatorFollowToggled,required TResult Function() starterPackConfirmed,required TResult Function() firstWallpaperActionRequested,required TResult Function( bool success, int elapsedMs) firstWallpaperActionCompleted,required TResult Function() firstWallpaperStepContinued,required TResult Function( bool didPurchase) paywallResultReceived,required TResult Function() stepBack,}) {final _that = this; +@optionalTypeArgs TResult when({required TResult Function() started,required TResult Function() authCompleted,required TResult Function( bool isLoading) authLoadingChanged,required TResult Function( String categoryName) interestToggled,required TResult Function() interestsConfirmed,required TResult Function( String creatorEmail) creatorFollowToggled,required TResult Function() starterPackConfirmed,required TResult Function() firstWallpaperActionRequested,required TResult Function( bool success, int elapsedMs) firstWallpaperActionCompleted,required TResult Function() firstWallpaperStepContinued,required TResult Function( bool didPurchase) paywallResultReceived,required TResult Function() stepBack,required TResult Function( String targetSize) aiGenerationRequested,required TResult Function( String? imageUrl, String? thumbnailUrl) aiGenerationCompleted,required TResult Function() aiGenerationStepContinued,}) {final _that = this; switch (_that) { case _Started(): return started();case _AuthCompleted(): @@ -198,7 +210,10 @@ return firstWallpaperActionRequested();case _FirstWallpaperActionCompleted(): return firstWallpaperActionCompleted(_that.success,_that.elapsedMs);case _FirstWallpaperStepContinued(): return firstWallpaperStepContinued();case _PaywallResultReceived(): return paywallResultReceived(_that.didPurchase);case _StepBack(): -return stepBack();case _: +return stepBack();case _AiGenerationRequested(): +return aiGenerationRequested(_that.targetSize);case _AiGenerationCompleted(): +return aiGenerationCompleted(_that.imageUrl,_that.thumbnailUrl);case _AiGenerationStepContinued(): +return aiGenerationStepContinued();case _: throw StateError('Unexpected subclass'); } @@ -215,7 +230,7 @@ return stepBack();case _: /// } /// ``` -@optionalTypeArgs TResult? whenOrNull({TResult? Function()? started,TResult? Function()? authCompleted,TResult? Function( bool isLoading)? authLoadingChanged,TResult? Function( String categoryName)? interestToggled,TResult? Function()? interestsConfirmed,TResult? Function( String creatorEmail)? creatorFollowToggled,TResult? Function()? starterPackConfirmed,TResult? Function()? firstWallpaperActionRequested,TResult? Function( bool success, int elapsedMs)? firstWallpaperActionCompleted,TResult? Function()? firstWallpaperStepContinued,TResult? Function( bool didPurchase)? paywallResultReceived,TResult? Function()? stepBack,}) {final _that = this; +@optionalTypeArgs TResult? whenOrNull({TResult? Function()? started,TResult? Function()? authCompleted,TResult? Function( bool isLoading)? authLoadingChanged,TResult? Function( String categoryName)? interestToggled,TResult? Function()? interestsConfirmed,TResult? Function( String creatorEmail)? creatorFollowToggled,TResult? Function()? starterPackConfirmed,TResult? Function()? firstWallpaperActionRequested,TResult? Function( bool success, int elapsedMs)? firstWallpaperActionCompleted,TResult? Function()? firstWallpaperStepContinued,TResult? Function( bool didPurchase)? paywallResultReceived,TResult? Function()? stepBack,TResult? Function( String targetSize)? aiGenerationRequested,TResult? Function( String? imageUrl, String? thumbnailUrl)? aiGenerationCompleted,TResult? Function()? aiGenerationStepContinued,}) {final _that = this; switch (_that) { case _Started() when started != null: return started();case _AuthCompleted() when authCompleted != null: @@ -229,7 +244,10 @@ return firstWallpaperActionRequested();case _FirstWallpaperActionCompleted() whe return firstWallpaperActionCompleted(_that.success,_that.elapsedMs);case _FirstWallpaperStepContinued() when firstWallpaperStepContinued != null: return firstWallpaperStepContinued();case _PaywallResultReceived() when paywallResultReceived != null: return paywallResultReceived(_that.didPurchase);case _StepBack() when stepBack != null: -return stepBack();case _: +return stepBack();case _AiGenerationRequested() when aiGenerationRequested != null: +return aiGenerationRequested(_that.targetSize);case _AiGenerationCompleted() when aiGenerationCompleted != null: +return aiGenerationCompleted(_that.imageUrl,_that.thumbnailUrl);case _AiGenerationStepContinued() when aiGenerationStepContinued != null: +return aiGenerationStepContinued();case _: return null; } @@ -793,6 +811,172 @@ String toString() { +/// @nodoc + + +class _AiGenerationRequested implements OnboardingV2Event { + const _AiGenerationRequested({required this.targetSize}); + + + final String targetSize; + +/// Create a copy of OnboardingV2Event +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$AiGenerationRequestedCopyWith<_AiGenerationRequested> get copyWith => __$AiGenerationRequestedCopyWithImpl<_AiGenerationRequested>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _AiGenerationRequested&&(identical(other.targetSize, targetSize) || other.targetSize == targetSize)); +} + + +@override +int get hashCode => Object.hash(runtimeType,targetSize); + +@override +String toString() { + return 'OnboardingV2Event.aiGenerationRequested(targetSize: $targetSize)'; +} + + +} + +/// @nodoc +abstract mixin class _$AiGenerationRequestedCopyWith<$Res> implements $OnboardingV2EventCopyWith<$Res> { + factory _$AiGenerationRequestedCopyWith(_AiGenerationRequested value, $Res Function(_AiGenerationRequested) _then) = __$AiGenerationRequestedCopyWithImpl; +@useResult +$Res call({ + String targetSize +}); + + + + +} +/// @nodoc +class __$AiGenerationRequestedCopyWithImpl<$Res> + implements _$AiGenerationRequestedCopyWith<$Res> { + __$AiGenerationRequestedCopyWithImpl(this._self, this._then); + + final _AiGenerationRequested _self; + final $Res Function(_AiGenerationRequested) _then; + +/// Create a copy of OnboardingV2Event +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') $Res call({Object? targetSize = null,}) { + return _then(_AiGenerationRequested( +targetSize: null == targetSize ? _self.targetSize : targetSize // ignore: cast_nullable_to_non_nullable +as String, + )); +} + + +} + +/// @nodoc + + +class _AiGenerationCompleted implements OnboardingV2Event { + const _AiGenerationCompleted({required this.imageUrl, required this.thumbnailUrl}); + + + final String? imageUrl; + final String? thumbnailUrl; + +/// Create a copy of OnboardingV2Event +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$AiGenerationCompletedCopyWith<_AiGenerationCompleted> get copyWith => __$AiGenerationCompletedCopyWithImpl<_AiGenerationCompleted>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _AiGenerationCompleted&&(identical(other.imageUrl, imageUrl) || other.imageUrl == imageUrl)&&(identical(other.thumbnailUrl, thumbnailUrl) || other.thumbnailUrl == thumbnailUrl)); +} + + +@override +int get hashCode => Object.hash(runtimeType,imageUrl,thumbnailUrl); + +@override +String toString() { + return 'OnboardingV2Event.aiGenerationCompleted(imageUrl: $imageUrl, thumbnailUrl: $thumbnailUrl)'; +} + + +} + +/// @nodoc +abstract mixin class _$AiGenerationCompletedCopyWith<$Res> implements $OnboardingV2EventCopyWith<$Res> { + factory _$AiGenerationCompletedCopyWith(_AiGenerationCompleted value, $Res Function(_AiGenerationCompleted) _then) = __$AiGenerationCompletedCopyWithImpl; +@useResult +$Res call({ + String? imageUrl, String? thumbnailUrl +}); + + + + +} +/// @nodoc +class __$AiGenerationCompletedCopyWithImpl<$Res> + implements _$AiGenerationCompletedCopyWith<$Res> { + __$AiGenerationCompletedCopyWithImpl(this._self, this._then); + + final _AiGenerationCompleted _self; + final $Res Function(_AiGenerationCompleted) _then; + +/// Create a copy of OnboardingV2Event +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') $Res call({Object? imageUrl = freezed,Object? thumbnailUrl = freezed,}) { + return _then(_AiGenerationCompleted( +imageUrl: freezed == imageUrl ? _self.imageUrl : imageUrl // ignore: cast_nullable_to_non_nullable +as String?,thumbnailUrl: freezed == thumbnailUrl ? _self.thumbnailUrl : thumbnailUrl // ignore: cast_nullable_to_non_nullable +as String?, + )); +} + + +} + +/// @nodoc + + +class _AiGenerationStepContinued implements OnboardingV2Event { + const _AiGenerationStepContinued(); + + + + + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _AiGenerationStepContinued); +} + + +@override +int get hashCode => runtimeType.hashCode; + +@override +String toString() { + return 'OnboardingV2Event.aiGenerationStepContinued()'; +} + + +} + + + + /// @nodoc mixin _$OnboardingInterestsData { @@ -1344,6 +1528,275 @@ as List, } +} + +/// @nodoc +mixin _$OnboardingAiData { + + String get prompt; AiStylePreset get stylePreset; AiGenerateStatus get status; String? get imageUrl; String? get thumbnailUrl; +/// Create a copy of OnboardingAiData +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$OnboardingAiDataCopyWith get copyWith => _$OnboardingAiDataCopyWithImpl(this as OnboardingAiData, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is OnboardingAiData&&(identical(other.prompt, prompt) || other.prompt == prompt)&&(identical(other.stylePreset, stylePreset) || other.stylePreset == stylePreset)&&(identical(other.status, status) || other.status == status)&&(identical(other.imageUrl, imageUrl) || other.imageUrl == imageUrl)&&(identical(other.thumbnailUrl, thumbnailUrl) || other.thumbnailUrl == thumbnailUrl)); +} + + +@override +int get hashCode => Object.hash(runtimeType,prompt,stylePreset,status,imageUrl,thumbnailUrl); + +@override +String toString() { + return 'OnboardingAiData(prompt: $prompt, stylePreset: $stylePreset, status: $status, imageUrl: $imageUrl, thumbnailUrl: $thumbnailUrl)'; +} + + +} + +/// @nodoc +abstract mixin class $OnboardingAiDataCopyWith<$Res> { + factory $OnboardingAiDataCopyWith(OnboardingAiData value, $Res Function(OnboardingAiData) _then) = _$OnboardingAiDataCopyWithImpl; +@useResult +$Res call({ + String prompt, AiStylePreset stylePreset, AiGenerateStatus status, String? imageUrl, String? thumbnailUrl +}); + + + + +} +/// @nodoc +class _$OnboardingAiDataCopyWithImpl<$Res> + implements $OnboardingAiDataCopyWith<$Res> { + _$OnboardingAiDataCopyWithImpl(this._self, this._then); + + final OnboardingAiData _self; + final $Res Function(OnboardingAiData) _then; + +/// Create a copy of OnboardingAiData +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? prompt = null,Object? stylePreset = null,Object? status = null,Object? imageUrl = freezed,Object? thumbnailUrl = freezed,}) { + return _then(_self.copyWith( +prompt: null == prompt ? _self.prompt : prompt // ignore: cast_nullable_to_non_nullable +as String,stylePreset: null == stylePreset ? _self.stylePreset : stylePreset // ignore: cast_nullable_to_non_nullable +as AiStylePreset,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable +as AiGenerateStatus,imageUrl: freezed == imageUrl ? _self.imageUrl : imageUrl // ignore: cast_nullable_to_non_nullable +as String?,thumbnailUrl: freezed == thumbnailUrl ? _self.thumbnailUrl : thumbnailUrl // ignore: cast_nullable_to_non_nullable +as String?, + )); +} + +} + + +/// Adds pattern-matching-related methods to [OnboardingAiData]. +extension OnboardingAiDataPatterns on OnboardingAiData { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _OnboardingAiData value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _OnboardingAiData() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _OnboardingAiData value) $default,){ +final _that = this; +switch (_that) { +case _OnboardingAiData(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _OnboardingAiData value)? $default,){ +final _that = this; +switch (_that) { +case _OnboardingAiData() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String prompt, AiStylePreset stylePreset, AiGenerateStatus status, String? imageUrl, String? thumbnailUrl)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _OnboardingAiData() when $default != null: +return $default(_that.prompt,_that.stylePreset,_that.status,_that.imageUrl,_that.thumbnailUrl);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String prompt, AiStylePreset stylePreset, AiGenerateStatus status, String? imageUrl, String? thumbnailUrl) $default,) {final _that = this; +switch (_that) { +case _OnboardingAiData(): +return $default(_that.prompt,_that.stylePreset,_that.status,_that.imageUrl,_that.thumbnailUrl);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String prompt, AiStylePreset stylePreset, AiGenerateStatus status, String? imageUrl, String? thumbnailUrl)? $default,) {final _that = this; +switch (_that) { +case _OnboardingAiData() when $default != null: +return $default(_that.prompt,_that.stylePreset,_that.status,_that.imageUrl,_that.thumbnailUrl);case _: + return null; + +} +} + +} + +/// @nodoc + + +class _OnboardingAiData implements OnboardingAiData { + const _OnboardingAiData({required this.prompt, required this.stylePreset, required this.status, this.imageUrl, this.thumbnailUrl}); + + +@override final String prompt; +@override final AiStylePreset stylePreset; +@override final AiGenerateStatus status; +@override final String? imageUrl; +@override final String? thumbnailUrl; + +/// Create a copy of OnboardingAiData +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$OnboardingAiDataCopyWith<_OnboardingAiData> get copyWith => __$OnboardingAiDataCopyWithImpl<_OnboardingAiData>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _OnboardingAiData&&(identical(other.prompt, prompt) || other.prompt == prompt)&&(identical(other.stylePreset, stylePreset) || other.stylePreset == stylePreset)&&(identical(other.status, status) || other.status == status)&&(identical(other.imageUrl, imageUrl) || other.imageUrl == imageUrl)&&(identical(other.thumbnailUrl, thumbnailUrl) || other.thumbnailUrl == thumbnailUrl)); +} + + +@override +int get hashCode => Object.hash(runtimeType,prompt,stylePreset,status,imageUrl,thumbnailUrl); + +@override +String toString() { + return 'OnboardingAiData(prompt: $prompt, stylePreset: $stylePreset, status: $status, imageUrl: $imageUrl, thumbnailUrl: $thumbnailUrl)'; +} + + +} + +/// @nodoc +abstract mixin class _$OnboardingAiDataCopyWith<$Res> implements $OnboardingAiDataCopyWith<$Res> { + factory _$OnboardingAiDataCopyWith(_OnboardingAiData value, $Res Function(_OnboardingAiData) _then) = __$OnboardingAiDataCopyWithImpl; +@override @useResult +$Res call({ + String prompt, AiStylePreset stylePreset, AiGenerateStatus status, String? imageUrl, String? thumbnailUrl +}); + + + + +} +/// @nodoc +class __$OnboardingAiDataCopyWithImpl<$Res> + implements _$OnboardingAiDataCopyWith<$Res> { + __$OnboardingAiDataCopyWithImpl(this._self, this._then); + + final _OnboardingAiData _self; + final $Res Function(_OnboardingAiData) _then; + +/// Create a copy of OnboardingAiData +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? prompt = null,Object? stylePreset = null,Object? status = null,Object? imageUrl = freezed,Object? thumbnailUrl = freezed,}) { + return _then(_OnboardingAiData( +prompt: null == prompt ? _self.prompt : prompt // ignore: cast_nullable_to_non_nullable +as String,stylePreset: null == stylePreset ? _self.stylePreset : stylePreset // ignore: cast_nullable_to_non_nullable +as AiStylePreset,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable +as AiGenerateStatus,imageUrl: freezed == imageUrl ? _self.imageUrl : imageUrl // ignore: cast_nullable_to_non_nullable +as String?,thumbnailUrl: freezed == thumbnailUrl ? _self.thumbnailUrl : thumbnailUrl // ignore: cast_nullable_to_non_nullable +as String?, + )); +} + + } /// @nodoc @@ -1636,7 +2089,7 @@ $OnboardingWallpaperVmCopyWith<$Res>? get wallpaper { /// @nodoc mixin _$OnboardingV2State { - OnboardingV2Step get step; LoadStatus get loadStatus; ActionStatus get actionStatus; bool get isAuthLoading; OnboardingInterestsData get interestsData; OnboardingStarterPackData get starterPackData; OnboardingWallpaperData get wallpaperData; bool get skipInterests; bool get skipStarterPack; OnboardingV2NavRequest? get navRequest; Failure? get failure; + OnboardingV2Step get step; LoadStatus get loadStatus; ActionStatus get actionStatus; bool get isAuthLoading; OnboardingInterestsData get interestsData; OnboardingStarterPackData get starterPackData; OnboardingWallpaperData get wallpaperData; OnboardingAiData get aiData; bool get skipInterests; bool get skipStarterPack; OnboardingV2NavRequest? get navRequest; Failure? get failure; /// Create a copy of OnboardingV2State /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -1647,16 +2100,16 @@ $OnboardingV2StateCopyWith get copyWith => _$OnboardingV2Stat @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is OnboardingV2State&&(identical(other.step, step) || other.step == step)&&(identical(other.loadStatus, loadStatus) || other.loadStatus == loadStatus)&&(identical(other.actionStatus, actionStatus) || other.actionStatus == actionStatus)&&(identical(other.isAuthLoading, isAuthLoading) || other.isAuthLoading == isAuthLoading)&&(identical(other.interestsData, interestsData) || other.interestsData == interestsData)&&(identical(other.starterPackData, starterPackData) || other.starterPackData == starterPackData)&&(identical(other.wallpaperData, wallpaperData) || other.wallpaperData == wallpaperData)&&(identical(other.skipInterests, skipInterests) || other.skipInterests == skipInterests)&&(identical(other.skipStarterPack, skipStarterPack) || other.skipStarterPack == skipStarterPack)&&(identical(other.navRequest, navRequest) || other.navRequest == navRequest)&&(identical(other.failure, failure) || other.failure == failure)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is OnboardingV2State&&(identical(other.step, step) || other.step == step)&&(identical(other.loadStatus, loadStatus) || other.loadStatus == loadStatus)&&(identical(other.actionStatus, actionStatus) || other.actionStatus == actionStatus)&&(identical(other.isAuthLoading, isAuthLoading) || other.isAuthLoading == isAuthLoading)&&(identical(other.interestsData, interestsData) || other.interestsData == interestsData)&&(identical(other.starterPackData, starterPackData) || other.starterPackData == starterPackData)&&(identical(other.wallpaperData, wallpaperData) || other.wallpaperData == wallpaperData)&&(identical(other.aiData, aiData) || other.aiData == aiData)&&(identical(other.skipInterests, skipInterests) || other.skipInterests == skipInterests)&&(identical(other.skipStarterPack, skipStarterPack) || other.skipStarterPack == skipStarterPack)&&(identical(other.navRequest, navRequest) || other.navRequest == navRequest)&&(identical(other.failure, failure) || other.failure == failure)); } @override -int get hashCode => Object.hash(runtimeType,step,loadStatus,actionStatus,isAuthLoading,interestsData,starterPackData,wallpaperData,skipInterests,skipStarterPack,navRequest,failure); +int get hashCode => Object.hash(runtimeType,step,loadStatus,actionStatus,isAuthLoading,interestsData,starterPackData,wallpaperData,aiData,skipInterests,skipStarterPack,navRequest,failure); @override String toString() { - return 'OnboardingV2State(step: $step, loadStatus: $loadStatus, actionStatus: $actionStatus, isAuthLoading: $isAuthLoading, interestsData: $interestsData, starterPackData: $starterPackData, wallpaperData: $wallpaperData, skipInterests: $skipInterests, skipStarterPack: $skipStarterPack, navRequest: $navRequest, failure: $failure)'; + return 'OnboardingV2State(step: $step, loadStatus: $loadStatus, actionStatus: $actionStatus, isAuthLoading: $isAuthLoading, interestsData: $interestsData, starterPackData: $starterPackData, wallpaperData: $wallpaperData, aiData: $aiData, skipInterests: $skipInterests, skipStarterPack: $skipStarterPack, navRequest: $navRequest, failure: $failure)'; } @@ -1667,11 +2120,11 @@ abstract mixin class $OnboardingV2StateCopyWith<$Res> { factory $OnboardingV2StateCopyWith(OnboardingV2State value, $Res Function(OnboardingV2State) _then) = _$OnboardingV2StateCopyWithImpl; @useResult $Res call({ - OnboardingV2Step step, LoadStatus loadStatus, ActionStatus actionStatus, bool isAuthLoading, OnboardingInterestsData interestsData, OnboardingStarterPackData starterPackData, OnboardingWallpaperData wallpaperData, bool skipInterests, bool skipStarterPack, OnboardingV2NavRequest? navRequest, Failure? failure + OnboardingV2Step step, LoadStatus loadStatus, ActionStatus actionStatus, bool isAuthLoading, OnboardingInterestsData interestsData, OnboardingStarterPackData starterPackData, OnboardingWallpaperData wallpaperData, OnboardingAiData aiData, bool skipInterests, bool skipStarterPack, OnboardingV2NavRequest? navRequest, Failure? failure }); -$OnboardingInterestsDataCopyWith<$Res> get interestsData;$OnboardingStarterPackDataCopyWith<$Res> get starterPackData;$OnboardingWallpaperDataCopyWith<$Res> get wallpaperData; +$OnboardingInterestsDataCopyWith<$Res> get interestsData;$OnboardingStarterPackDataCopyWith<$Res> get starterPackData;$OnboardingWallpaperDataCopyWith<$Res> get wallpaperData;$OnboardingAiDataCopyWith<$Res> get aiData; } /// @nodoc @@ -1684,7 +2137,7 @@ class _$OnboardingV2StateCopyWithImpl<$Res> /// Create a copy of OnboardingV2State /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? step = null,Object? loadStatus = null,Object? actionStatus = null,Object? isAuthLoading = null,Object? interestsData = null,Object? starterPackData = null,Object? wallpaperData = null,Object? skipInterests = null,Object? skipStarterPack = null,Object? navRequest = freezed,Object? failure = freezed,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? step = null,Object? loadStatus = null,Object? actionStatus = null,Object? isAuthLoading = null,Object? interestsData = null,Object? starterPackData = null,Object? wallpaperData = null,Object? aiData = null,Object? skipInterests = null,Object? skipStarterPack = null,Object? navRequest = freezed,Object? failure = freezed,}) { return _then(_self.copyWith( step: null == step ? _self.step : step // ignore: cast_nullable_to_non_nullable as OnboardingV2Step,loadStatus: null == loadStatus ? _self.loadStatus : loadStatus // ignore: cast_nullable_to_non_nullable @@ -1693,7 +2146,8 @@ as ActionStatus,isAuthLoading: null == isAuthLoading ? _self.isAuthLoading : isA as bool,interestsData: null == interestsData ? _self.interestsData : interestsData // ignore: cast_nullable_to_non_nullable as OnboardingInterestsData,starterPackData: null == starterPackData ? _self.starterPackData : starterPackData // ignore: cast_nullable_to_non_nullable as OnboardingStarterPackData,wallpaperData: null == wallpaperData ? _self.wallpaperData : wallpaperData // ignore: cast_nullable_to_non_nullable -as OnboardingWallpaperData,skipInterests: null == skipInterests ? _self.skipInterests : skipInterests // ignore: cast_nullable_to_non_nullable +as OnboardingWallpaperData,aiData: null == aiData ? _self.aiData : aiData // ignore: cast_nullable_to_non_nullable +as OnboardingAiData,skipInterests: null == skipInterests ? _self.skipInterests : skipInterests // ignore: cast_nullable_to_non_nullable as bool,skipStarterPack: null == skipStarterPack ? _self.skipStarterPack : skipStarterPack // ignore: cast_nullable_to_non_nullable as bool,navRequest: freezed == navRequest ? _self.navRequest : navRequest // ignore: cast_nullable_to_non_nullable as OnboardingV2NavRequest?,failure: freezed == failure ? _self.failure : failure // ignore: cast_nullable_to_non_nullable @@ -1727,6 +2181,15 @@ $OnboardingWallpaperDataCopyWith<$Res> get wallpaperData { return $OnboardingWallpaperDataCopyWith<$Res>(_self.wallpaperData, (value) { return _then(_self.copyWith(wallpaperData: value)); }); +}/// Create a copy of OnboardingV2State +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$OnboardingAiDataCopyWith<$Res> get aiData { + + return $OnboardingAiDataCopyWith<$Res>(_self.aiData, (value) { + return _then(_self.copyWith(aiData: value)); + }); } } @@ -1809,10 +2272,10 @@ return $default(_that);case _: /// } /// ``` -@optionalTypeArgs TResult maybeWhen(TResult Function( OnboardingV2Step step, LoadStatus loadStatus, ActionStatus actionStatus, bool isAuthLoading, OnboardingInterestsData interestsData, OnboardingStarterPackData starterPackData, OnboardingWallpaperData wallpaperData, bool skipInterests, bool skipStarterPack, OnboardingV2NavRequest? navRequest, Failure? failure)? $default,{required TResult orElse(),}) {final _that = this; +@optionalTypeArgs TResult maybeWhen(TResult Function( OnboardingV2Step step, LoadStatus loadStatus, ActionStatus actionStatus, bool isAuthLoading, OnboardingInterestsData interestsData, OnboardingStarterPackData starterPackData, OnboardingWallpaperData wallpaperData, OnboardingAiData aiData, bool skipInterests, bool skipStarterPack, OnboardingV2NavRequest? navRequest, Failure? failure)? $default,{required TResult orElse(),}) {final _that = this; switch (_that) { case _OnboardingV2State() when $default != null: -return $default(_that.step,_that.loadStatus,_that.actionStatus,_that.isAuthLoading,_that.interestsData,_that.starterPackData,_that.wallpaperData,_that.skipInterests,_that.skipStarterPack,_that.navRequest,_that.failure);case _: +return $default(_that.step,_that.loadStatus,_that.actionStatus,_that.isAuthLoading,_that.interestsData,_that.starterPackData,_that.wallpaperData,_that.aiData,_that.skipInterests,_that.skipStarterPack,_that.navRequest,_that.failure);case _: return orElse(); } @@ -1830,10 +2293,10 @@ return $default(_that.step,_that.loadStatus,_that.actionStatus,_that.isAuthLoadi /// } /// ``` -@optionalTypeArgs TResult when(TResult Function( OnboardingV2Step step, LoadStatus loadStatus, ActionStatus actionStatus, bool isAuthLoading, OnboardingInterestsData interestsData, OnboardingStarterPackData starterPackData, OnboardingWallpaperData wallpaperData, bool skipInterests, bool skipStarterPack, OnboardingV2NavRequest? navRequest, Failure? failure) $default,) {final _that = this; +@optionalTypeArgs TResult when(TResult Function( OnboardingV2Step step, LoadStatus loadStatus, ActionStatus actionStatus, bool isAuthLoading, OnboardingInterestsData interestsData, OnboardingStarterPackData starterPackData, OnboardingWallpaperData wallpaperData, OnboardingAiData aiData, bool skipInterests, bool skipStarterPack, OnboardingV2NavRequest? navRequest, Failure? failure) $default,) {final _that = this; switch (_that) { case _OnboardingV2State(): -return $default(_that.step,_that.loadStatus,_that.actionStatus,_that.isAuthLoading,_that.interestsData,_that.starterPackData,_that.wallpaperData,_that.skipInterests,_that.skipStarterPack,_that.navRequest,_that.failure);case _: +return $default(_that.step,_that.loadStatus,_that.actionStatus,_that.isAuthLoading,_that.interestsData,_that.starterPackData,_that.wallpaperData,_that.aiData,_that.skipInterests,_that.skipStarterPack,_that.navRequest,_that.failure);case _: throw StateError('Unexpected subclass'); } @@ -1850,10 +2313,10 @@ return $default(_that.step,_that.loadStatus,_that.actionStatus,_that.isAuthLoadi /// } /// ``` -@optionalTypeArgs TResult? whenOrNull(TResult? Function( OnboardingV2Step step, LoadStatus loadStatus, ActionStatus actionStatus, bool isAuthLoading, OnboardingInterestsData interestsData, OnboardingStarterPackData starterPackData, OnboardingWallpaperData wallpaperData, bool skipInterests, bool skipStarterPack, OnboardingV2NavRequest? navRequest, Failure? failure)? $default,) {final _that = this; +@optionalTypeArgs TResult? whenOrNull(TResult? Function( OnboardingV2Step step, LoadStatus loadStatus, ActionStatus actionStatus, bool isAuthLoading, OnboardingInterestsData interestsData, OnboardingStarterPackData starterPackData, OnboardingWallpaperData wallpaperData, OnboardingAiData aiData, bool skipInterests, bool skipStarterPack, OnboardingV2NavRequest? navRequest, Failure? failure)? $default,) {final _that = this; switch (_that) { case _OnboardingV2State() when $default != null: -return $default(_that.step,_that.loadStatus,_that.actionStatus,_that.isAuthLoading,_that.interestsData,_that.starterPackData,_that.wallpaperData,_that.skipInterests,_that.skipStarterPack,_that.navRequest,_that.failure);case _: +return $default(_that.step,_that.loadStatus,_that.actionStatus,_that.isAuthLoading,_that.interestsData,_that.starterPackData,_that.wallpaperData,_that.aiData,_that.skipInterests,_that.skipStarterPack,_that.navRequest,_that.failure);case _: return null; } @@ -1865,7 +2328,7 @@ return $default(_that.step,_that.loadStatus,_that.actionStatus,_that.isAuthLoadi class _OnboardingV2State implements OnboardingV2State { - const _OnboardingV2State({required this.step, required this.loadStatus, required this.actionStatus, required this.isAuthLoading, required this.interestsData, required this.starterPackData, required this.wallpaperData, required this.skipInterests, required this.skipStarterPack, this.navRequest, this.failure}); + const _OnboardingV2State({required this.step, required this.loadStatus, required this.actionStatus, required this.isAuthLoading, required this.interestsData, required this.starterPackData, required this.wallpaperData, required this.aiData, required this.skipInterests, required this.skipStarterPack, this.navRequest, this.failure}); @override final OnboardingV2Step step; @@ -1875,6 +2338,7 @@ class _OnboardingV2State implements OnboardingV2State { @override final OnboardingInterestsData interestsData; @override final OnboardingStarterPackData starterPackData; @override final OnboardingWallpaperData wallpaperData; +@override final OnboardingAiData aiData; @override final bool skipInterests; @override final bool skipStarterPack; @override final OnboardingV2NavRequest? navRequest; @@ -1890,16 +2354,16 @@ _$OnboardingV2StateCopyWith<_OnboardingV2State> get copyWith => __$OnboardingV2S @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _OnboardingV2State&&(identical(other.step, step) || other.step == step)&&(identical(other.loadStatus, loadStatus) || other.loadStatus == loadStatus)&&(identical(other.actionStatus, actionStatus) || other.actionStatus == actionStatus)&&(identical(other.isAuthLoading, isAuthLoading) || other.isAuthLoading == isAuthLoading)&&(identical(other.interestsData, interestsData) || other.interestsData == interestsData)&&(identical(other.starterPackData, starterPackData) || other.starterPackData == starterPackData)&&(identical(other.wallpaperData, wallpaperData) || other.wallpaperData == wallpaperData)&&(identical(other.skipInterests, skipInterests) || other.skipInterests == skipInterests)&&(identical(other.skipStarterPack, skipStarterPack) || other.skipStarterPack == skipStarterPack)&&(identical(other.navRequest, navRequest) || other.navRequest == navRequest)&&(identical(other.failure, failure) || other.failure == failure)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is _OnboardingV2State&&(identical(other.step, step) || other.step == step)&&(identical(other.loadStatus, loadStatus) || other.loadStatus == loadStatus)&&(identical(other.actionStatus, actionStatus) || other.actionStatus == actionStatus)&&(identical(other.isAuthLoading, isAuthLoading) || other.isAuthLoading == isAuthLoading)&&(identical(other.interestsData, interestsData) || other.interestsData == interestsData)&&(identical(other.starterPackData, starterPackData) || other.starterPackData == starterPackData)&&(identical(other.wallpaperData, wallpaperData) || other.wallpaperData == wallpaperData)&&(identical(other.aiData, aiData) || other.aiData == aiData)&&(identical(other.skipInterests, skipInterests) || other.skipInterests == skipInterests)&&(identical(other.skipStarterPack, skipStarterPack) || other.skipStarterPack == skipStarterPack)&&(identical(other.navRequest, navRequest) || other.navRequest == navRequest)&&(identical(other.failure, failure) || other.failure == failure)); } @override -int get hashCode => Object.hash(runtimeType,step,loadStatus,actionStatus,isAuthLoading,interestsData,starterPackData,wallpaperData,skipInterests,skipStarterPack,navRequest,failure); +int get hashCode => Object.hash(runtimeType,step,loadStatus,actionStatus,isAuthLoading,interestsData,starterPackData,wallpaperData,aiData,skipInterests,skipStarterPack,navRequest,failure); @override String toString() { - return 'OnboardingV2State(step: $step, loadStatus: $loadStatus, actionStatus: $actionStatus, isAuthLoading: $isAuthLoading, interestsData: $interestsData, starterPackData: $starterPackData, wallpaperData: $wallpaperData, skipInterests: $skipInterests, skipStarterPack: $skipStarterPack, navRequest: $navRequest, failure: $failure)'; + return 'OnboardingV2State(step: $step, loadStatus: $loadStatus, actionStatus: $actionStatus, isAuthLoading: $isAuthLoading, interestsData: $interestsData, starterPackData: $starterPackData, wallpaperData: $wallpaperData, aiData: $aiData, skipInterests: $skipInterests, skipStarterPack: $skipStarterPack, navRequest: $navRequest, failure: $failure)'; } @@ -1910,11 +2374,11 @@ abstract mixin class _$OnboardingV2StateCopyWith<$Res> implements $OnboardingV2S factory _$OnboardingV2StateCopyWith(_OnboardingV2State value, $Res Function(_OnboardingV2State) _then) = __$OnboardingV2StateCopyWithImpl; @override @useResult $Res call({ - OnboardingV2Step step, LoadStatus loadStatus, ActionStatus actionStatus, bool isAuthLoading, OnboardingInterestsData interestsData, OnboardingStarterPackData starterPackData, OnboardingWallpaperData wallpaperData, bool skipInterests, bool skipStarterPack, OnboardingV2NavRequest? navRequest, Failure? failure + OnboardingV2Step step, LoadStatus loadStatus, ActionStatus actionStatus, bool isAuthLoading, OnboardingInterestsData interestsData, OnboardingStarterPackData starterPackData, OnboardingWallpaperData wallpaperData, OnboardingAiData aiData, bool skipInterests, bool skipStarterPack, OnboardingV2NavRequest? navRequest, Failure? failure }); -@override $OnboardingInterestsDataCopyWith<$Res> get interestsData;@override $OnboardingStarterPackDataCopyWith<$Res> get starterPackData;@override $OnboardingWallpaperDataCopyWith<$Res> get wallpaperData; +@override $OnboardingInterestsDataCopyWith<$Res> get interestsData;@override $OnboardingStarterPackDataCopyWith<$Res> get starterPackData;@override $OnboardingWallpaperDataCopyWith<$Res> get wallpaperData;@override $OnboardingAiDataCopyWith<$Res> get aiData; } /// @nodoc @@ -1927,7 +2391,7 @@ class __$OnboardingV2StateCopyWithImpl<$Res> /// Create a copy of OnboardingV2State /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? step = null,Object? loadStatus = null,Object? actionStatus = null,Object? isAuthLoading = null,Object? interestsData = null,Object? starterPackData = null,Object? wallpaperData = null,Object? skipInterests = null,Object? skipStarterPack = null,Object? navRequest = freezed,Object? failure = freezed,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? step = null,Object? loadStatus = null,Object? actionStatus = null,Object? isAuthLoading = null,Object? interestsData = null,Object? starterPackData = null,Object? wallpaperData = null,Object? aiData = null,Object? skipInterests = null,Object? skipStarterPack = null,Object? navRequest = freezed,Object? failure = freezed,}) { return _then(_OnboardingV2State( step: null == step ? _self.step : step // ignore: cast_nullable_to_non_nullable as OnboardingV2Step,loadStatus: null == loadStatus ? _self.loadStatus : loadStatus // ignore: cast_nullable_to_non_nullable @@ -1936,7 +2400,8 @@ as ActionStatus,isAuthLoading: null == isAuthLoading ? _self.isAuthLoading : isA as bool,interestsData: null == interestsData ? _self.interestsData : interestsData // ignore: cast_nullable_to_non_nullable as OnboardingInterestsData,starterPackData: null == starterPackData ? _self.starterPackData : starterPackData // ignore: cast_nullable_to_non_nullable as OnboardingStarterPackData,wallpaperData: null == wallpaperData ? _self.wallpaperData : wallpaperData // ignore: cast_nullable_to_non_nullable -as OnboardingWallpaperData,skipInterests: null == skipInterests ? _self.skipInterests : skipInterests // ignore: cast_nullable_to_non_nullable +as OnboardingWallpaperData,aiData: null == aiData ? _self.aiData : aiData // ignore: cast_nullable_to_non_nullable +as OnboardingAiData,skipInterests: null == skipInterests ? _self.skipInterests : skipInterests // ignore: cast_nullable_to_non_nullable as bool,skipStarterPack: null == skipStarterPack ? _self.skipStarterPack : skipStarterPack // ignore: cast_nullable_to_non_nullable as bool,navRequest: freezed == navRequest ? _self.navRequest : navRequest // ignore: cast_nullable_to_non_nullable as OnboardingV2NavRequest?,failure: freezed == failure ? _self.failure : failure // ignore: cast_nullable_to_non_nullable @@ -1971,6 +2436,15 @@ $OnboardingWallpaperDataCopyWith<$Res> get wallpaperData { return $OnboardingWallpaperDataCopyWith<$Res>(_self.wallpaperData, (value) { return _then(_self.copyWith(wallpaperData: value)); }); +}/// Create a copy of OnboardingV2State +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$OnboardingAiDataCopyWith<$Res> get aiData { + + return $OnboardingAiDataCopyWith<$Res>(_self.aiData, (value) { + return _then(_self.copyWith(aiData: value)); + }); } } diff --git a/lib/features/onboarding_v2/src/biz/onboarding_v2_event.dart b/lib/features/onboarding_v2/src/biz/onboarding_v2_event.dart index 7c636c34..a30e081d 100644 --- a/lib/features/onboarding_v2/src/biz/onboarding_v2_event.dart +++ b/lib/features/onboarding_v2/src/biz/onboarding_v2_event.dart @@ -15,4 +15,8 @@ abstract class OnboardingV2Event with _$OnboardingV2Event { const factory OnboardingV2Event.firstWallpaperStepContinued() = _FirstWallpaperStepContinued; const factory OnboardingV2Event.paywallResultReceived({required bool didPurchase}) = _PaywallResultReceived; const factory OnboardingV2Event.stepBack() = _StepBack; + const factory OnboardingV2Event.aiGenerationRequested({required String targetSize}) = _AiGenerationRequested; + const factory OnboardingV2Event.aiGenerationCompleted({required String? imageUrl, required String? thumbnailUrl}) = + _AiGenerationCompleted; + const factory OnboardingV2Event.aiGenerationStepContinued() = _AiGenerationStepContinued; } diff --git a/lib/features/onboarding_v2/src/biz/onboarding_v2_state.dart b/lib/features/onboarding_v2/src/biz/onboarding_v2_state.dart index c6ac55aa..d2a63384 100644 --- a/lib/features/onboarding_v2/src/biz/onboarding_v2_state.dart +++ b/lib/features/onboarding_v2/src/biz/onboarding_v2_state.dart @@ -1,11 +1,13 @@ part of 'onboarding_v2_bloc.j.dart'; -enum OnboardingV2Step { auth, interests, starterPack, firstWallpaper } +enum OnboardingV2Step { auth, interests, starterPack, aiGenerate, firstWallpaper } enum OnboardingV2NavRequest { openPaywall, completeOnboarding } enum FirstWallpaperStatus { idle, loading, success, failure } +enum AiGenerateStatus { idle, loading, success, failure } + @freezed abstract class OnboardingInterestsData with _$OnboardingInterestsData { const OnboardingInterestsData._(); @@ -38,6 +40,20 @@ abstract class OnboardingStarterPackData with _$OnboardingStarterPackData { bool get canContinue => selectedEmails.length >= OnboardingV2Config.minFollows; } +@freezed +abstract class OnboardingAiData with _$OnboardingAiData { + const factory OnboardingAiData({ + required String prompt, + required AiStylePreset stylePreset, + required AiGenerateStatus status, + String? imageUrl, + String? thumbnailUrl, + }) = _OnboardingAiData; + + factory OnboardingAiData.initial() => + const OnboardingAiData(prompt: '', stylePreset: AiStylePreset.abstract, status: AiGenerateStatus.idle); +} + @freezed abstract class OnboardingWallpaperData with _$OnboardingWallpaperData { const factory OnboardingWallpaperData({ @@ -59,6 +75,7 @@ abstract class OnboardingV2State with _$OnboardingV2State { required OnboardingInterestsData interestsData, required OnboardingStarterPackData starterPackData, required OnboardingWallpaperData wallpaperData, + required OnboardingAiData aiData, required bool skipInterests, required bool skipStarterPack, OnboardingV2NavRequest? navRequest, @@ -73,6 +90,7 @@ abstract class OnboardingV2State with _$OnboardingV2State { interestsData: OnboardingInterestsData.initial(), starterPackData: OnboardingStarterPackData.initial(), wallpaperData: OnboardingWallpaperData.initial(), + aiData: OnboardingAiData.initial(), skipInterests: false, skipStarterPack: false, ); diff --git a/lib/features/onboarding_v2/src/theme/onboarding_layout.dart b/lib/features/onboarding_v2/src/theme/onboarding_layout.dart index 7ab98203..e6c014a4 100644 --- a/lib/features/onboarding_v2/src/theme/onboarding_layout.dart +++ b/lib/features/onboarding_v2/src/theme/onboarding_layout.dart @@ -72,6 +72,12 @@ class OnboardingLayout { static const double loadingIndicatorSize = 18; static const double loadingIndicatorStroke = 2.2; + // AI generation step content (below the two-line headline at stepTitleY=100) + static const double aiChipX = 27; + static const double aiChipY = 216; + static const double aiPreviewX = 27; + static const double aiPreviewY = 298; + static const double softenedBlurSigma = 100; static const double bottomOverlayY = 646; static const double bottomOverlayHeight = 78; diff --git a/lib/features/onboarding_v2/src/utils/onboarding_v2_config.dart b/lib/features/onboarding_v2/src/utils/onboarding_v2_config.dart index 040d0174..7302791d 100644 --- a/lib/features/onboarding_v2/src/utils/onboarding_v2_config.dart +++ b/lib/features/onboarding_v2/src/utils/onboarding_v2_config.dart @@ -1,7 +1,13 @@ +import 'package:Prism/features/ai_wallpaper/domain/entities/ai_style_preset.dart'; + class OnboardingV2Config { const OnboardingV2Config._(); - static const int minInterests = 5; + /// Set to true locally to force onboarding for already-onboarded accounts. + /// NEVER commit as true. + static const bool debugForceOnboarding = false; + + static const int minInterests = 3; static const int minFollows = 3; static const int paywallSoftGateSeconds = 4; static const int targetCreatorsCount = 10; @@ -12,4 +18,94 @@ class OnboardingV2Config { static const String remoteConfigStarterPackKey = 'onboarding_starter_pack_v1'; static const String remoteConfigV2EnabledKey = 'onboarding_v2_enabled'; static const String excludedCategory = 'Community'; + + // --------------------------------------------------------------------------- + // AI generation step — onboarding free generation + // --------------------------------------------------------------------------- + + static const List aiOnboardingStyles = [ + AiStylePreset.nature, + AiStylePreset.abstract, + AiStylePreset.watercolor, + AiStylePreset.minimal, + AiStylePreset.cyberpunk, + ]; + + /// Maps lowercase interest-category keywords to the best-matching AI style. + /// Keyword matching is done with `contains`, so partial names work too. + static const Map aiInterestStyleMap = { + 'nature': AiStylePreset.nature, + 'landscape': AiStylePreset.nature, + 'forest': AiStylePreset.nature, + 'mountain': AiStylePreset.nature, + 'floral': AiStylePreset.nature, + 'botanical': AiStylePreset.nature, + 'ocean': AiStylePreset.nature, + 'beach': AiStylePreset.nature, + 'anime': AiStylePreset.anime, + 'manga': AiStylePreset.anime, + 'illustration': AiStylePreset.anime, + 'minimal': AiStylePreset.minimal, + 'clean': AiStylePreset.minimal, + 'simple': AiStylePreset.minimal, + 'monochrome': AiStylePreset.minimal, + 'black': AiStylePreset.minimal, + 'white': AiStylePreset.minimal, + 'cyberpunk': AiStylePreset.cyberpunk, + 'neon': AiStylePreset.cyberpunk, + 'tech': AiStylePreset.cyberpunk, + 'city': AiStylePreset.cyberpunk, + 'urban': AiStylePreset.cyberpunk, + 'futuristic': AiStylePreset.cyberpunk, + 'watercolor': AiStylePreset.watercolor, + 'paint': AiStylePreset.watercolor, + 'pastel': AiStylePreset.watercolor, + 'art': AiStylePreset.watercolor, + 'gradient': AiStylePreset.meshGradient, + 'colorful': AiStylePreset.meshGradient, + 'vibrant': AiStylePreset.meshGradient, + 'abstract': AiStylePreset.abstract, + 'digital': AiStylePreset.abstract, + 'geometric': AiStylePreset.abstract, + 'space': AiStylePreset.abstract, + 'dark': AiStylePreset.abstract, + }; + + static const Map> aiOnboardingPromptPool = { + AiStylePreset.nature: [ + 'a misty mountain range at sunrise with warm golden light', + 'a dense evergreen forest with sun rays breaking through the canopy', + 'a dramatic coastal cliff with crashing waves and moody sky', + 'a calm alpine lake under a twilight sky with star reflections', + 'a desert canyon with warm golden light and dramatic rock formations', + ], + AiStylePreset.abstract: [ + 'an abstract fractal composition with dynamic curves and vivid color', + 'layered liquid forms with modern depth and metallic sheen', + 'bold abstract shapes with soft edges and cinematic contrast', + 'a surreal abstract field of floating geometry bathed in neon light', + 'organic abstract waves with rich texture and depth of field', + ], + AiStylePreset.watercolor: [ + 'a watercolor forest valley at dawn with soft pastel mist', + 'a hand-painted mountain lake scene in dreamy watercolor tones', + 'soft watercolor clouds above rolling hills at golden hour', + 'a tranquil riverbank with pastel watercolor reflections', + 'an artistic watercolor coastline at sunset with warm hues', + ], + AiStylePreset.minimal: [ + 'minimal sand dunes and a single sun disk on the horizon', + 'clean geometric mountain layers in soft muted tones', + 'a serene monochrome ocean horizon at dusk', + 'simple abstract lines with perfectly balanced negative space', + 'a soft gradient sky over flat minimalist hills', + ], + AiStylePreset.cyberpunk: [ + 'a neon megacity street with wet reflections and flying vehicles', + 'a futuristic alley with holographic billboards and teal-magenta light', + 'a high-tech skyline at night with glowing traffic lanes', + 'a cyberpunk rooftop overlooking a city of glowing towers', + 'a midnight city crossing drenched in teal and magenta neon', + ], + }; } diff --git a/lib/features/onboarding_v2/src/views/onboarding_v2_shell.dart b/lib/features/onboarding_v2/src/views/onboarding_v2_shell.dart index 4b447c18..d139c9bd 100644 --- a/lib/features/onboarding_v2/src/views/onboarding_v2_shell.dart +++ b/lib/features/onboarding_v2/src/views/onboarding_v2_shell.dart @@ -14,6 +14,7 @@ import 'package:Prism/features/onboarding_v2/src/utils/onboarding_v2_config.dart import 'package:Prism/features/onboarding_v2/src/views/pages/f0_auth_page.dart'; import 'package:Prism/features/onboarding_v2/src/views/pages/f1_interests_page.dart'; import 'package:Prism/features/onboarding_v2/src/views/pages/f2_starter_pack_page.dart'; +import 'package:Prism/features/onboarding_v2/src/views/pages/f3_ai_generate_page.dart'; import 'package:Prism/features/onboarding_v2/src/views/pages/f3_first_wallpaper_page.dart'; import 'package:Prism/features/onboarding_v2/src/views/widgets/onboarding_background.dart'; import 'package:Prism/features/onboarding_v2/src/views/widgets/onboarding_copy.dart'; @@ -45,7 +46,13 @@ class _OnboardingV2ShellState extends State { late final TapGestureRecognizer _legalTap; bool _imagesPrecached = false; - static const List _pages = [F0AuthPage(), F1InterestsPage(), F2StarterPackPage(), F3FirstWallpaperPage()]; + static const List _pages = [ + F0AuthPage(), + F1InterestsPage(), + F2StarterPackPage(), + F3AiGeneratePage(), + F3FirstWallpaperPage(), + ]; static final _systemUiStyle = edgeToEdgeOverlayStyle( statusBarIconBrightness: Brightness.dark, @@ -135,6 +142,9 @@ class _OnboardingV2ShellState extends State { _bloc.add(const OnboardingV2Event.interestsConfirmed()); case OnboardingV2Step.starterPack: _bloc.add(const OnboardingV2Event.starterPackConfirmed()); + case OnboardingV2Step.aiGenerate: + // Success auto-advances via the shell listener; CTA always triggers or re-triggers generation. + _bloc.add(OnboardingV2Event.aiGenerationRequested(targetSize: _targetSizeForDevice())); case OnboardingV2Step.firstWallpaper: final wallpaper = _bloc.state.wallpaperData.wallpaper; if (wallpaper == null) { @@ -145,6 +155,27 @@ class _OnboardingV2ShellState extends State { } } + String _targetSizeForDevice() { + if (!mounted) return '1080x1920'; + const int minShortEdge = 720; + const int maxLongEdge = 2048; + final media = MediaQuery.of(context); + final double dpr = media.devicePixelRatio.clamp(1.0, 3.0); + final int rawW = (media.size.width * dpr).round().clamp(360, 4096); + final int rawH = (media.size.height * dpr).round().clamp(640, 4096); + final bool portrait = rawH >= rawW; + final int longRaw = portrait ? rawH : rawW; + final int shortRaw = portrait ? rawW : rawH; + final double aspect = longRaw / shortRaw; + int shortTarget = shortRaw.clamp(minShortEdge, maxLongEdge); + int longTarget = (shortTarget * aspect).round(); + if (longTarget > maxLongEdge) { + longTarget = maxLongEdge; + shortTarget = (longTarget / aspect).round().clamp(minShortEdge, maxLongEdge); + } + return portrait ? '${shortTarget}x$longTarget' : '${longTarget}x$shortTarget'; + } + Widget _pageFor(OnboardingV2Step step) { final idx = OnboardingV2Step.values.indexOf(step).clamp(0, _pages.length - 1); return KeyedSubtree(key: ValueKey(step), child: _pages[idx]); @@ -161,7 +192,11 @@ class _OnboardingV2ShellState extends State { curr.step == OnboardingV2Step.firstWallpaper && prev.wallpaperData.status != curr.wallpaperData.status && curr.wallpaperData.status == FirstWallpaperStatus.success; - return navChanged || wallpaperSucceeded; + final aiGenerationSucceeded = + curr.step == OnboardingV2Step.aiGenerate && + prev.aiData.status != AiGenerateStatus.success && + curr.aiData.status == AiGenerateStatus.success; + return navChanged || wallpaperSucceeded || aiGenerationSucceeded; }, listener: (context, state) { logger.d('listener fired step=${state.step} navRequest=${state.navRequest}', tag: 'OnboardingV2Shell'); @@ -170,6 +205,9 @@ class _OnboardingV2ShellState extends State { toasts.success(defaultTargetPlatform == TargetPlatform.android ? 'Wallpaper set!' : 'Saved to Photos!'); _bloc.add(const OnboardingV2Event.firstWallpaperStepContinued()); } + if (state.step == OnboardingV2Step.aiGenerate && state.aiData.status == AiGenerateStatus.success) { + _bloc.add(const OnboardingV2Event.aiGenerationStepContinued()); + } }, buildWhen: (prev, curr) => prev.step != curr.step || @@ -178,7 +216,8 @@ class _OnboardingV2ShellState extends State { prev.interestsData.selected.length != curr.interestsData.selected.length || prev.starterPackData.selectedEmails.length != curr.starterPackData.selectedEmails.length || prev.wallpaperData.status != curr.wallpaperData.status || - prev.wallpaperData.wallpaper?.thumbnailUrl != curr.wallpaperData.wallpaper?.thumbnailUrl, + prev.wallpaperData.wallpaper?.thumbnailUrl != curr.wallpaperData.wallpaper?.thumbnailUrl || + prev.aiData != curr.aiData, builder: (context, state) { return AnnotatedRegion( value: _systemUiStyle, @@ -340,6 +379,7 @@ class _SharedOverlayState extends State<_SharedOverlay> { visible: _bottomTextVisible, legalTap: widget.legalTap, wallpaperCategory: widget.state.wallpaperData.wallpaper?.sourceCategory, + aiGenerateStatus: widget.state.aiData.status, ), ], ); @@ -367,12 +407,14 @@ class _Progress extends StatelessWidget { final visible = step == OnboardingV2Step.interests || step == OnboardingV2Step.starterPack || + step == OnboardingV2Step.aiGenerate || step == OnboardingV2Step.firstWallpaper; final progressStep = switch (step) { OnboardingV2Step.interests => 1, OnboardingV2Step.starterPack => 2, - _ => 3, + OnboardingV2Step.aiGenerate => 3, + _ => 4, }; final color = step == OnboardingV2Step.firstWallpaper ? wallpaperColor : OnboardingColors.progressActive; @@ -389,7 +431,7 @@ class _Progress extends StatelessWidget { scaleX: sx, scaleY: sy, alignment: Alignment.topCenter, - child: OnboardingProgressIndicator(step: progressStep, color: color), + child: OnboardingProgressIndicator(step: progressStep, totalSteps: 4, color: color), ), ), ), @@ -423,14 +465,16 @@ class _Headline extends StatelessWidget { OnboardingV2Step.auth => 0, OnboardingV2Step.interests => OnboardingLayout.step2TitleX, OnboardingV2Step.starterPack => OnboardingLayout.step3TitleX, - _ => OnboardingLayout.step4TitleX, + OnboardingV2Step.aiGenerate => OnboardingLayout.step4TitleX, + OnboardingV2Step.firstWallpaper => OnboardingLayout.step4TitleX, }; static String _headlineText(OnboardingV2Step step) => switch (step) { OnboardingV2Step.auth => 'Your screen,\nreimagined.', OnboardingV2Step.interests => 'Pick your vibe', OnboardingV2Step.starterPack => 'Find your people', - _ => 'Make it yours', + OnboardingV2Step.aiGenerate => 'Create your\nfirst wallpaper', + OnboardingV2Step.firstWallpaper => 'Make it yours', }; @override @@ -479,6 +523,7 @@ class _CtaButton extends StatelessWidget { final isLoading = switch (step) { OnboardingV2Step.auth => state.isAuthLoading, OnboardingV2Step.interests || OnboardingV2Step.starterPack => state.actionStatus == ActionStatus.inProgress, + OnboardingV2Step.aiGenerate => state.aiData.status == AiGenerateStatus.loading, OnboardingV2Step.firstWallpaper => state.wallpaperData.status == FirstWallpaperStatus.loading, }; @@ -486,6 +531,7 @@ class _CtaButton extends StatelessWidget { OnboardingV2Step.auth => true, OnboardingV2Step.interests => state.interestsData.canContinue, OnboardingV2Step.starterPack => state.starterPackData.canContinue, + OnboardingV2Step.aiGenerate => true, OnboardingV2Step.firstWallpaper => true, }; @@ -495,10 +541,8 @@ class _CtaButton extends StatelessWidget { final selected = state.interestsData.selected.length; return selected < OnboardingV2Config.minInterests ? 'continue ($selected selected)' : 'continue'; }(), - OnboardingV2Step.starterPack => () { - final selected = state.starterPackData.selectedEmails.length; - return selected < OnboardingV2Config.minFollows ? 'continue ($selected selected)' : 'continue'; - }(), + OnboardingV2Step.starterPack => 'continue', + OnboardingV2Step.aiGenerate => 'generate my wallpaper', OnboardingV2Step.firstWallpaper => 'set as wallpaper', }; @@ -524,6 +568,7 @@ class _BottomText extends StatelessWidget { required this.visible, required this.legalTap, this.wallpaperCategory, + this.aiGenerateStatus, }); final OnboardingV2Step step; @@ -532,10 +577,16 @@ class _BottomText extends StatelessWidget { final bool visible; final TapGestureRecognizer legalTap; final String? wallpaperCategory; + final AiGenerateStatus? aiGenerateStatus; String _helperText() => switch (step) { - OnboardingV2Step.interests => 'select at least 5 categories to personalize your feed', - OnboardingV2Step.starterPack => 'follow at least 3 creators to personalize your feed', + OnboardingV2Step.interests => 'select at least 3 categories to personalize your feed', + OnboardingV2Step.starterPack => 'we are suggesting you follow these 3 creators', + OnboardingV2Step.aiGenerate => switch (aiGenerateStatus) { + AiGenerateStatus.success => 'looking good! tap "use this wallpaper" to continue', + AiGenerateStatus.failure => 'something went wrong — tap generate to try again', + _ => 'your first wallpaper, generated by AI', + }, _ => (wallpaperCategory != null && wallpaperCategory!.isNotEmpty) ? 'we picked this wallpaper based on your interest in $wallpaperCategory' diff --git a/lib/features/onboarding_v2/src/views/pages/f3_ai_generate_page.dart b/lib/features/onboarding_v2/src/views/pages/f3_ai_generate_page.dart new file mode 100644 index 00000000..ac67304a --- /dev/null +++ b/lib/features/onboarding_v2/src/views/pages/f3_ai_generate_page.dart @@ -0,0 +1,265 @@ +import 'package:Prism/features/onboarding_v2/src/biz/onboarding_v2_bloc.j.dart'; +import 'package:Prism/features/onboarding_v2/src/theme/onboarding_theme.dart'; +import 'package:Prism/features/onboarding_v2/src/views/widgets/onboarding_frame.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +/// F3 unique content: AI wallpaper generation step. +/// Background, headline, progress, CTA button, and helper text are owned by +/// the shell overlay. This page owns the prompt chip, preview area, and skip. +class F3AiGeneratePage extends StatelessWidget { + const F3AiGeneratePage({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + buildWhen: (prev, curr) => prev.aiData != curr.aiData, + builder: (context, state) { + final aiData = state.aiData; + + return OnboardingFrame( + builder: (context, sx, sy) { + // Preview fills from below the chip to just above the CTA. + const previewBottom = OnboardingLayout.ctaY - 12; + const previewHeight = previewBottom - OnboardingLayout.aiPreviewY; + + return Stack( + fit: StackFit.expand, + children: [ + // ── Prompt chip — sits below the two-line headline ── + Positioned( + top: OnboardingLayout.aiChipY * sy, + left: OnboardingLayout.aiChipX * sx, + right: OnboardingLayout.aiChipX * sx, + child: _PromptChip(prompt: aiData.prompt, style: aiData.stylePreset.label), + ), + + // ── Preview area (loading / result / failure) ── + Positioned( + top: OnboardingLayout.aiPreviewY * sy, + left: OnboardingLayout.aiPreviewX * sx, + right: OnboardingLayout.aiPreviewX * sx, + height: previewHeight * sy, + child: _PreviewArea(aiData: aiData), + ), + + // ── Skip link (top-right, same position as F4 skip) ── + Align( + alignment: Alignment.topRight, + child: Padding( + padding: EdgeInsets.only( + top: OnboardingLayout.skipY * sy, + right: (OnboardingLayout.designWidth - OnboardingLayout.skipX - 32) * sx, + ), + child: GestureDetector( + onTap: () => + context.read().add(const OnboardingV2Event.aiGenerationStepContinued()), + child: Text( + 'skip', + style: OnboardingTypography.skip.copyWith(color: OnboardingColors.textPrimary), + ), + ), + ), + ), + ], + ); + }, + ); + }, + ); + } +} + +class _PromptChip extends StatelessWidget { + const _PromptChip({required this.prompt, required this.style}); + + final String prompt; + final String style; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.35), + borderRadius: BorderRadius.circular(14), + border: Border.all(color: Colors.white.withValues(alpha: 0.18)), + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(6), + ), + child: Text( + style, + style: const TextStyle( + fontFamily: OnboardingTypography.sans, + fontSize: 11, + fontWeight: FontWeight.w600, + color: Colors.black, + ), + ), + ), + const SizedBox(width: 10), + Expanded( + child: Text( + prompt, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontFamily: OnboardingTypography.sans, + fontSize: 13, + color: Colors.black.withValues(alpha: 0.75), + ), + ), + ), + ], + ), + ); + } +} + +class _PreviewArea extends StatelessWidget { + const _PreviewArea({required this.aiData}); + + final OnboardingAiData aiData; + + @override + Widget build(BuildContext context) { + return AnimatedSwitcher( + duration: const Duration(milliseconds: 400), + child: switch (aiData.status) { + AiGenerateStatus.idle => const _IdlePlaceholder(key: ValueKey('idle')), + AiGenerateStatus.loading => const _LoadingView(key: ValueKey('loading')), + AiGenerateStatus.success => _ResultImage( + key: const ValueKey('success'), + imageUrl: aiData.thumbnailUrl ?? aiData.imageUrl ?? '', + ), + AiGenerateStatus.failure => const _FailureView(key: ValueKey('failure')), + }, + ); + } +} + +class _IdlePlaceholder extends StatelessWidget { + const _IdlePlaceholder({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.25), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: Colors.white.withValues(alpha: 0.12)), + ), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.auto_awesome_rounded, color: Colors.black.withValues(alpha: 0.35), size: 40), + const SizedBox(height: 12), + Text( + 'tap generate to create\nyour unique wallpaper', + textAlign: TextAlign.center, + style: TextStyle( + fontFamily: OnboardingTypography.sans, + fontSize: 14, + color: Colors.black.withValues(alpha: 0.45), + ), + ), + ], + ), + ), + ); + } +} + +class _LoadingView extends StatelessWidget { + const _LoadingView({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.25), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: Colors.white.withValues(alpha: 0.12)), + ), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + CircularProgressIndicator(color: Colors.black.withValues(alpha: 0.6), strokeWidth: 2.5), + const SizedBox(height: 16), + Text( + 'crafting your wallpaper…', + style: TextStyle( + fontFamily: OnboardingTypography.sans, + fontSize: 14, + color: Colors.black.withValues(alpha: 0.6), + ), + ), + ], + ), + ), + ); + } +} + +class _ResultImage extends StatelessWidget { + const _ResultImage({super.key, required this.imageUrl}); + + final String imageUrl; + + @override + Widget build(BuildContext context) { + return ClipRRect( + borderRadius: BorderRadius.circular(20), + child: CachedNetworkImage( + imageUrl: imageUrl, + fit: BoxFit.cover, + width: double.infinity, + height: double.infinity, + fadeInDuration: const Duration(milliseconds: 300), + errorWidget: (_, _, _) => const _FailureView(), + ), + ); + } +} + +class _FailureView extends StatelessWidget { + const _FailureView({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.25), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: Colors.white.withValues(alpha: 0.12)), + ), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.refresh_rounded, color: Colors.black.withValues(alpha: 0.45), size: 36), + const SizedBox(height: 12), + Text( + 'something went wrong.\ntap generate to try again.', + textAlign: TextAlign.center, + style: TextStyle( + fontFamily: OnboardingTypography.sans, + fontSize: 14, + color: Colors.black.withValues(alpha: 0.45), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/onboarding_v2/src/views/widgets/onboarding_background.dart b/lib/features/onboarding_v2/src/views/widgets/onboarding_background.dart index 7ba6d85b..c51b2f35 100644 --- a/lib/features/onboarding_v2/src/views/widgets/onboarding_background.dart +++ b/lib/features/onboarding_v2/src/views/widgets/onboarding_background.dart @@ -176,6 +176,7 @@ class _OnboardingStepBackgroundState extends State wit OnboardingV2Step.auth => 0, OnboardingV2Step.interests => 40, OnboardingV2Step.starterPack => 70, + OnboardingV2Step.aiGenerate => 40, OnboardingV2Step.firstWallpaper => 0, }; diff --git a/lib/features/onboarding_v2/src/views/widgets/onboarding_progress_indicator.dart b/lib/features/onboarding_v2/src/views/widgets/onboarding_progress_indicator.dart index 45c6ed9a..15e326ee 100644 --- a/lib/features/onboarding_v2/src/views/widgets/onboarding_progress_indicator.dart +++ b/lib/features/onboarding_v2/src/views/widgets/onboarding_progress_indicator.dart @@ -2,39 +2,39 @@ import 'package:Prism/features/onboarding_v2/src/theme/onboarding_theme.dart'; import 'package:flutter/material.dart'; class OnboardingProgressIndicator extends StatelessWidget { - const OnboardingProgressIndicator({super.key, required this.step, this.color = OnboardingColors.progressActive}); + const OnboardingProgressIndicator({ + super.key, + required this.step, + this.totalSteps = 3, + this.color = OnboardingColors.progressActive, + }); final int step; + + /// Total number of steps shown as dots. Defaults to 3 for backward compat. + final int totalSteps; final Color color; @override Widget build(BuildContext context) { - final activeLeft = switch (step) { - 1 => 0.0, - 2 => OnboardingLayout.progressStepSpacing, - _ => OnboardingLayout.progressTrailingSpacing, - }; - final inactive = switch (step) { - 1 => const [OnboardingLayout.progressTrailingSpacing, OnboardingLayout.progressLastDotX], - 2 => const [0.0, OnboardingLayout.progressLastDotX], - _ => const [0.0, OnboardingLayout.progressStepSpacing], - }; + // Each dot is spaced progressStepSpacing apart: 0, 16, 32, 48 for 4 steps. + final activeLeft = (step - 1) * OnboardingLayout.progressStepSpacing; + final inactivePositions = [ + for (int i = 0; i < totalSteps; i++) + if (i != step - 1) i * OnboardingLayout.progressStepSpacing, + ]; return SizedBox( width: OnboardingLayout.progressWidth, height: OnboardingLayout.progressHeight, child: Stack( children: [ - Positioned( - left: inactive[0], - top: 0, - child: _Dot(active: false, wide: false, color: color), - ), - Positioned( - left: inactive[1], - top: 0, - child: _Dot(active: false, wide: false, color: color), - ), + for (final left in inactivePositions) + Positioned( + left: left, + top: 0, + child: _Dot(active: false, wide: false, color: color), + ), Positioned( left: activeLeft, top: 0, diff --git a/lib/features/startup/views/pages/splash_widget.dart b/lib/features/startup/views/pages/splash_widget.dart index 0b192b0b..0cb83bb0 100644 --- a/lib/features/startup/views/pages/splash_widget.dart +++ b/lib/features/startup/views/pages/splash_widget.dart @@ -3,6 +3,7 @@ import 'package:Prism/core/persistence/data_sources/settings_local_data_source.d import 'package:Prism/core/router/app_router.dart'; import 'package:Prism/core/state/app_state.dart' as app_state; import 'package:Prism/core/utils/status.dart'; +import 'package:Prism/features/onboarding_v2/src/utils/onboarding_v2_config.dart'; import 'package:Prism/features/startup/biz/bloc/startup_bloc.j.dart'; import 'package:Prism/features/startup/views/pages/old_version_screen.dart'; import 'package:Prism/logger/logger.dart'; @@ -24,6 +25,10 @@ class _SplashWidgetState extends State { bool _navigated = false; bool _notchMeasured = false; + // Tracks whether the debug-forced onboarding redirect has already fired this + // app session. Resets on process restart (static lives for the process lifetime). + static bool _debugOnboardingShownThisSession = false; + @override void initState() { super.initState(); @@ -56,14 +61,16 @@ class _SplashWidgetState extends State { return; } _navigated = true; - final isOnboarded = _settingsLocal.get('onboarded_v2_new', defaultValue: false); - final v2Enabled = context.read().state.config?.onboardingV2Enabled ?? false; + final effectiveDebugForce = OnboardingV2Config.debugForceOnboarding && !_debugOnboardingShownThisSession; + final isOnboarded = !effectiveDebugForce && _settingsLocal.get('onboarded_v2_new', defaultValue: false); + final v2Enabled = effectiveDebugForce || (context.read().state.config?.onboardingV2Enabled ?? false); WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) { return; } final isLoggedIn = app_state.prismUser.loggedIn; if (!isLoggedIn || (!isOnboarded && v2Enabled)) { + _debugOnboardingShownThisSession = true; context.router.replaceAll([const OnboardingV2ShellRoute()]); } else { context.router.replaceAll([const DashboardRoute()]);