feat: add marketing video mode and Remotion marketing video project
Add debug-only Marketing Video Mode toggle that enables hands-free screen recording across the app: auto-scrolling Featured Trips carousel, auto-filling trip wizard, smooth trip detail scrolling via CADisplayLink, and trip options auto-sort with scroll. Add Remotion marketing video project with 6 scene compositions using image sequences extracted from screen recordings, varied phone entrance animations, and deduped frames for smooth playback. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,7 @@ struct HomeContent_Classic: View {
|
||||
@Binding var showNewTrip: Bool
|
||||
@Binding var selectedTab: Int
|
||||
@Binding var selectedSuggestedTrip: SuggestedTrip?
|
||||
@Binding var marketingAutoScroll: Bool
|
||||
|
||||
let savedTrips: [SavedTrip]
|
||||
let suggestedTripsGenerator: SuggestedTripsGenerator
|
||||
@@ -124,36 +125,50 @@ struct HomeContent_Classic: View {
|
||||
.padding(.horizontal, Theme.Spacing.md)
|
||||
|
||||
// Horizontal carousel grouped by region
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: Theme.Spacing.lg) {
|
||||
ForEach(suggestedTripsGenerator.tripsByRegion, id: \.region) { regionGroup in
|
||||
VStack(alignment: .leading, spacing: Theme.Spacing.sm) {
|
||||
// Region header
|
||||
HStack(spacing: Theme.Spacing.xs) {
|
||||
Image(systemName: regionGroup.region.iconName)
|
||||
.font(.caption)
|
||||
.accessibilityHidden(true)
|
||||
Text(regionGroup.region.shortName)
|
||||
.font(.subheadline)
|
||||
}
|
||||
.foregroundStyle(Theme.textSecondary(colorScheme))
|
||||
ScrollViewReader { proxy in
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: Theme.Spacing.lg) {
|
||||
ForEach(suggestedTripsGenerator.tripsByRegion, id: \.region) { regionGroup in
|
||||
VStack(alignment: .leading, spacing: Theme.Spacing.sm) {
|
||||
// Region header
|
||||
HStack(spacing: Theme.Spacing.xs) {
|
||||
Image(systemName: regionGroup.region.iconName)
|
||||
.font(.caption)
|
||||
.accessibilityHidden(true)
|
||||
Text(regionGroup.region.shortName)
|
||||
.font(.subheadline)
|
||||
}
|
||||
.foregroundStyle(Theme.textSecondary(colorScheme))
|
||||
|
||||
// Trip cards for this region
|
||||
HStack(spacing: Theme.Spacing.md) {
|
||||
ForEach(regionGroup.trips) { suggestedTrip in
|
||||
Button {
|
||||
selectedSuggestedTrip = suggestedTrip
|
||||
} label: {
|
||||
SuggestedTripCard(suggestedTrip: suggestedTrip)
|
||||
// Trip cards for this region
|
||||
HStack(spacing: Theme.Spacing.md) {
|
||||
ForEach(regionGroup.trips) { suggestedTrip in
|
||||
Button {
|
||||
selectedSuggestedTrip = suggestedTrip
|
||||
} label: {
|
||||
SuggestedTripCard(suggestedTrip: suggestedTrip)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
.id(regionGroup.region)
|
||||
}
|
||||
}
|
||||
}
|
||||
.contentMargins(.horizontal, Theme.Spacing.md, for: .scrollContent)
|
||||
.onChange(of: marketingAutoScroll) { _, shouldScroll in
|
||||
if shouldScroll,
|
||||
let lastRegion = suggestedTripsGenerator.tripsByRegion.last?.region {
|
||||
withAnimation(.easeInOut(duration: 3.0)) {
|
||||
proxy.scrollTo(lastRegion, anchor: .trailing)
|
||||
}
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 3.5) {
|
||||
marketingAutoScroll = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.contentMargins(.horizontal, Theme.Spacing.md, for: .scrollContent)
|
||||
}
|
||||
} else if let error = suggestedTripsGenerator.error {
|
||||
// Error state
|
||||
|
||||
@@ -15,6 +15,7 @@ struct HomeContent_ClassicAnimated: View {
|
||||
@Binding var showNewTrip: Bool
|
||||
@Binding var selectedTab: Int
|
||||
@Binding var selectedSuggestedTrip: SuggestedTrip?
|
||||
@Binding var marketingAutoScroll: Bool
|
||||
|
||||
let savedTrips: [SavedTrip]
|
||||
let suggestedTripsGenerator: SuggestedTripsGenerator
|
||||
@@ -123,36 +124,50 @@ struct HomeContent_ClassicAnimated: View {
|
||||
.padding(.horizontal, Theme.Spacing.md)
|
||||
|
||||
// Horizontal carousel grouped by region
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: Theme.Spacing.lg) {
|
||||
ForEach(suggestedTripsGenerator.tripsByRegion, id: \.region) { regionGroup in
|
||||
VStack(alignment: .leading, spacing: Theme.Spacing.sm) {
|
||||
// Region header
|
||||
HStack(spacing: Theme.Spacing.xs) {
|
||||
Image(systemName: regionGroup.region.iconName)
|
||||
.font(.caption)
|
||||
.accessibilityHidden(true)
|
||||
Text(regionGroup.region.shortName)
|
||||
.font(.subheadline)
|
||||
}
|
||||
.foregroundStyle(Theme.textSecondary(colorScheme))
|
||||
ScrollViewReader { proxy in
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: Theme.Spacing.lg) {
|
||||
ForEach(suggestedTripsGenerator.tripsByRegion, id: \.region) { regionGroup in
|
||||
VStack(alignment: .leading, spacing: Theme.Spacing.sm) {
|
||||
// Region header
|
||||
HStack(spacing: Theme.Spacing.xs) {
|
||||
Image(systemName: regionGroup.region.iconName)
|
||||
.font(.caption)
|
||||
.accessibilityHidden(true)
|
||||
Text(regionGroup.region.shortName)
|
||||
.font(.subheadline)
|
||||
}
|
||||
.foregroundStyle(Theme.textSecondary(colorScheme))
|
||||
|
||||
// Trip cards for this region
|
||||
HStack(spacing: Theme.Spacing.md) {
|
||||
ForEach(regionGroup.trips) { suggestedTrip in
|
||||
Button {
|
||||
selectedSuggestedTrip = suggestedTrip
|
||||
} label: {
|
||||
SuggestedTripCard(suggestedTrip: suggestedTrip)
|
||||
// Trip cards for this region
|
||||
HStack(spacing: Theme.Spacing.md) {
|
||||
ForEach(regionGroup.trips) { suggestedTrip in
|
||||
Button {
|
||||
selectedSuggestedTrip = suggestedTrip
|
||||
} label: {
|
||||
SuggestedTripCard(suggestedTrip: suggestedTrip)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
.id(regionGroup.region)
|
||||
}
|
||||
}
|
||||
}
|
||||
.contentMargins(.horizontal, Theme.Spacing.md, for: .scrollContent)
|
||||
.onChange(of: marketingAutoScroll) { _, shouldScroll in
|
||||
if shouldScroll,
|
||||
let lastRegion = suggestedTripsGenerator.tripsByRegion.last?.region {
|
||||
withAnimation(.easeInOut(duration: 3.0)) {
|
||||
proxy.scrollTo(lastRegion, anchor: .trailing)
|
||||
}
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 3.5) {
|
||||
marketingAutoScroll = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.contentMargins(.horizontal, Theme.Spacing.md, for: .scrollContent)
|
||||
}
|
||||
} else if let error = suggestedTripsGenerator.error {
|
||||
// Error state
|
||||
|
||||
Reference in New Issue
Block a user