diff --git a/android/app/src/main/kotlin/com/thomaskioko/tvmaniac/MainActivity.kt b/android/app/src/main/kotlin/com/thomaskioko/tvmaniac/MainActivity.kt index d19f66c85..22e02d9c8 100644 --- a/android/app/src/main/kotlin/com/thomaskioko/tvmaniac/MainActivity.kt +++ b/android/app/src/main/kotlin/com/thomaskioko/tvmaniac/MainActivity.kt @@ -34,7 +34,7 @@ class MainActivity : ComponentActivity() { enableEdgeToEdge() setContent { - val themeState by component.rootComponent.themeState.collectAsState() + val themeState by component.rootPresenter.themeState.collectAsState() val darkTheme = shouldUseDarkTheme(themeState) splashScreen.setKeepOnScreenCondition { themeState.isFetching } @@ -59,7 +59,7 @@ class MainActivity : ComponentActivity() { onDispose {} } - TvManiacTheme(darkTheme = darkTheme) { RootScreen(rootComponent = component.rootComponent) } + TvManiacTheme(darkTheme = darkTheme) { RootScreen(rootPresenter = component.rootPresenter) } } } } diff --git a/android/app/src/main/kotlin/com/thomaskioko/tvmaniac/RootScreen.kt b/android/app/src/main/kotlin/com/thomaskioko/tvmaniac/RootScreen.kt index e1515d42a..408a1a5a6 100644 --- a/android/app/src/main/kotlin/com/thomaskioko/tvmaniac/RootScreen.kt +++ b/android/app/src/main/kotlin/com/thomaskioko/tvmaniac/RootScreen.kt @@ -15,28 +15,28 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import com.arkivanov.decompose.extensions.compose.stack.Children import com.thomaskioko.tvmaniac.home.HomeScreen -import com.thomaskioko.tvmaniac.navigation.RootComponent +import com.thomaskioko.tvmaniac.navigation.RootPresenter import com.thomaskioko.tvmaniac.seasondetails.ui.SeasonDetailsScreen import com.thomaskioko.tvmaniac.ui.moreshows.MoreShowsScreen import com.thomaskioko.tvmaniac.ui.showdetails.ShowDetailsScreen import com.thomaskioko.tvmaniac.ui.trailers.videoplayer.TrailersScreen @Composable -fun RootScreen(rootComponent: RootComponent, modifier: Modifier = Modifier) { +fun RootScreen(rootPresenter: RootPresenter, modifier: Modifier = Modifier) { Surface(modifier = modifier, color = MaterialTheme.colorScheme.background) { Column( modifier = Modifier.fillMaxSize() .windowInsetsPadding(WindowInsets.systemBars.only(WindowInsetsSides.Horizontal)), ) { - ChildrenContent(rootComponent = rootComponent, modifier = Modifier.weight(1F)) + ChildrenContent(rootPresenter = rootPresenter, modifier = Modifier.weight(1F)) } } } @Composable -private fun ChildrenContent(rootComponent: RootComponent, modifier: Modifier = Modifier) { - val childStack by rootComponent.stack.collectAsState() +private fun ChildrenContent(rootPresenter: RootPresenter, modifier: Modifier = Modifier) { + val childStack by rootPresenter.stack.collectAsState() Children( modifier = modifier, @@ -44,26 +44,26 @@ private fun ChildrenContent(rootComponent: RootComponent, modifier: Modifier = M ) { child -> val fillMaxSizeModifier = Modifier.fillMaxSize() when (val screen = child.instance) { - is RootComponent.Child.Home -> + is RootPresenter.Child.Home -> HomeScreen(presenter = screen.presenter, modifier = fillMaxSizeModifier) - is RootComponent.Child.ShowDetails -> { + is RootPresenter.Child.ShowDetails -> { ShowDetailsScreen( presenter = screen.presenter, modifier = fillMaxSizeModifier, ) } - is RootComponent.Child.SeasonDetails -> { + is RootPresenter.Child.SeasonDetails -> { SeasonDetailsScreen( presenter = screen.presenter, modifier = fillMaxSizeModifier, ) } - is RootComponent.Child.Trailers -> + is RootPresenter.Child.Trailers -> TrailersScreen( presenter = screen.presenter, modifier = fillMaxSizeModifier, ) - is RootComponent.Child.MoreShows -> + is RootPresenter.Child.MoreShows -> MoreShowsScreen( presenter = screen.presenter, modifier = fillMaxSizeModifier, diff --git a/android/app/src/main/kotlin/com/thomaskioko/tvmaniac/inject/ActivityComponent.kt b/android/app/src/main/kotlin/com/thomaskioko/tvmaniac/inject/ActivityComponent.kt index 0e43fa2dd..0147fc510 100644 --- a/android/app/src/main/kotlin/com/thomaskioko/tvmaniac/inject/ActivityComponent.kt +++ b/android/app/src/main/kotlin/com/thomaskioko/tvmaniac/inject/ActivityComponent.kt @@ -4,7 +4,7 @@ import androidx.activity.ComponentActivity import com.arkivanov.decompose.ComponentContext import com.arkivanov.decompose.defaultComponentContext import com.thomaskioko.tvmaniac.core.base.annotations.ActivityScope -import com.thomaskioko.tvmaniac.navigation.RootComponent +import com.thomaskioko.tvmaniac.navigation.RootPresenter import com.thomaskioko.tvmaniac.navigation.di.NavigatorComponent import com.thomaskioko.tvmaniac.traktauth.api.TraktAuthManager import com.thomaskioko.tvmaniac.traktauth.implementation.TraktAuthManagerComponent @@ -21,7 +21,7 @@ abstract class ActivityComponent( ApplicationComponent.create(activity.application), ) : NavigatorComponent, TraktAuthManagerComponent { abstract val traktAuthManager: TraktAuthManager - abstract val rootComponent: RootComponent + abstract val rootPresenter: RootPresenter companion object } diff --git a/ios/ios/Discover/DiscoverView.swift b/ios/ios/Discover/DiscoverView.swift index ee7f34977..957c06263 100644 --- a/ios/ios/Discover/DiscoverView.swift +++ b/ios/ios/Discover/DiscoverView.swift @@ -4,217 +4,217 @@ import TvManiac import TvManiacUI struct DiscoverView: View { - @Environment(\.colorScheme) var scheme - @State private var currentIndex: Int = 2 - @StateFlow private var uiState: DiscoverState - - private let component: DiscoverShowsComponent - - init(component: DiscoverShowsComponent) { - self.component = component - _uiState = StateFlow(component.state) + @Environment(\.colorScheme) var scheme + @State private var currentIndex: Int = 2 + @StateFlow private var uiState: DiscoverState + + private let presenter: DiscoverShowsPresenter + + init(presenter: DiscoverShowsPresenter) { + self.presenter = presenter + _uiState = StateFlow(presenter.state) + } + + var body: some View { + VStack { + switch onEnum(of: uiState) { + case .loading: + LoadingIndicatorView() + .frame(maxWidth: UIScreen.main.bounds.width, maxHeight: UIScreen.main.bounds.height, alignment: .center) + case .dataLoaded(let state): + LoadedContent(state: state) + case .emptyState: + emptyView + case .errorState(let error): + FullScreenView( + systemName: "exclamationmark.arrow.triangle.2.circlepath", + message: error.errorMessage ?? "Something went wrong!!" + ) + } } + } - var body: some View { - VStack { - switch onEnum(of: uiState) { - case .loading: - LoadingIndicatorView() - .frame(maxWidth: UIScreen.main.bounds.width, maxHeight: UIScreen.main.bounds.height, alignment: .center) - case .dataLoaded(let state): - LoadedContent(state: state) - case .emptyState: - emptyView - case .errorState(let error): - FullScreenView( - systemName: "exclamationmark.arrow.triangle.2.circlepath", - message: error.errorMessage ?? "Something went wrong!!" - ) - } + @ViewBuilder + private func LoadedContent(state: DataLoaded) -> some View { + ZStack { + BackgroundView(state.featuredShows) + + ScrollableContent(state: state) + .refreshable { + presenter.dispatch(action: RefreshData()) } } + } + + @ViewBuilder + func ScrollableContent(state: DataLoaded) -> some View { + ScrollView(showsIndicators: false) { + if state.errorMessage != nil { + FullScreenView(systemName: "exclamationmark.triangle", message: state.errorMessage!) + } else { + FeaturedContentView(state.featuredShows) + + HorizontalItemListView( + title: "Upcoming", + chevronStyle: .chevronOnly, + items: state.upcomingShows.map { $0.toSwift() }, + onClick: { id in presenter.dispatch(action: ShowClicked(id: id)) }, + onMoreClicked: { presenter.dispatch(action: UpComingClicked()) } + ) - @ViewBuilder - private func LoadedContent(state: DataLoaded) -> some View { - ZStack { - BackgroundView(state.featuredShows) + HorizontalItemListView( + title: "Trending Today", + chevronStyle: .chevronOnly, + items: state.trendingToday.map { $0.toSwift() }, + onClick: { id in presenter.dispatch(action: ShowClicked(id: id)) }, + onMoreClicked: { presenter.dispatch(action: TrendingClicked()) } + ) - ScrollableContent(state: state) - .refreshable { - component.dispatch(action: RefreshData()) - } - } - } + HorizontalItemListView( + title: "Popular", + chevronStyle: .chevronOnly, + items: state.popularShows.map { $0.toSwift() }, + onClick: { id in presenter.dispatch(action: ShowClicked(id: id)) }, + onMoreClicked: { presenter.dispatch(action: PopularClicked()) } + ) - @ViewBuilder - func ScrollableContent(state: DataLoaded) -> some View { - ScrollView(showsIndicators: false) { - if state.errorMessage != nil { - FullScreenView(systemName: "exclamationmark.triangle", message: state.errorMessage!) - } else { - FeaturedContentView(state.featuredShows) - - HorizontalItemListView( - title: "Upcoming", - chevronStyle: .chevronOnly, - items: state.upcomingShows.map { $0.toSwift() }, - onClick: { id in component.dispatch(action: ShowClicked(id: id)) }, - onMoreClicked: { component.dispatch(action: UpComingClicked()) } - ) - - HorizontalItemListView( - title: "Trending Today", - chevronStyle: .chevronOnly, - items: state.trendingToday.map { $0.toSwift() }, - onClick: { id in component.dispatch(action: ShowClicked(id: id)) }, - onMoreClicked: { component.dispatch(action: TrendingClicked()) } - ) - - HorizontalItemListView( - title: "Popular", - chevronStyle: .chevronOnly, - items: state.popularShows.map { $0.toSwift() }, - onClick: { id in component.dispatch(action: ShowClicked(id: id)) }, - onMoreClicked: { component.dispatch(action: PopularClicked()) } - ) - - HorizontalItemListView( - title: "Top Rated", - chevronStyle: .chevronOnly, - items: state.topRatedShows.map { $0.toSwift() }, - onClick: { id in component.dispatch(action: ShowClicked(id: id)) }, - onMoreClicked: { component.dispatch(action: TopRatedClicked()) } - ) - } - } + HorizontalItemListView( + title: "Top Rated", + chevronStyle: .chevronOnly, + items: state.topRatedShows.map { $0.toSwift() }, + onClick: { id in presenter.dispatch(action: ShowClicked(id: id)) }, + onMoreClicked: { presenter.dispatch(action: TopRatedClicked()) } + ) + } } - - @ViewBuilder - func FeaturedContentView(_ shows: [DiscoverShow]?) -> some View { - if let shows = shows { - if !shows.isEmpty { - SnapCarousel( - spacing: 10, - trailingSpace: 120, - index: $currentIndex, - items: shows.map { $0.toSwift() } - ) { show in - GeometryReader { _ in - FeaturedContentPosterView( - showId: show.tmdbId, - title: show.title, - posterImageUrl: show.posterUrl, - isInLibrary: show.inLibrary, - onClick: { id in component.dispatch(action: ShowClicked(id: show.tmdbId)) } - ) - } - } - .edgesIgnoringSafeArea(.all) - .frame(height: 450) - .padding(.top, 70) - - CustomIndicator(shows) - .padding() - .padding(.top, 10) - } + } + + @ViewBuilder + func FeaturedContentView(_ shows: [DiscoverShow]?) -> some View { + if let shows = shows { + if !shows.isEmpty { + SnapCarousel( + spacing: 10, + trailingSpace: 120, + index: $currentIndex, + items: shows.map { $0.toSwift() } + ) { show in + GeometryReader { _ in + FeaturedContentPosterView( + showId: show.tmdbId, + title: show.title, + posterImageUrl: show.posterUrl, + isInLibrary: show.inLibrary, + onClick: { id in presenter.dispatch(action: ShowClicked(id: show.tmdbId)) } + ) + } } + .edgesIgnoringSafeArea(.all) + .frame(height: 450) + .padding(.top, 70) + + CustomIndicator(shows) + .padding() + .padding(.top, 10) + } } + } - @ViewBuilder - func BackgroundView(_ tvShows: [DiscoverShow]?) -> some View { - if let shows = tvShows { - if !shows.isEmpty { - GeometryReader { _ in - - TabView(selection: $currentIndex) { - ForEach(shows.indices, id: \.self) { index in - TransparentImageBackground(imageUrl: shows[index].posterImageUrl) - .tag(index) - } - } - .tabViewStyle(.page(indexDisplayMode: .never)) - .animation(.easeInOut, value: currentIndex) - - let color: Color = (scheme == .dark ? .black : .white) - // Custom Gradient - LinearGradient(colors: [ - .black, - .clear, - color.opacity(0.15), - color.opacity(0.5), - color.opacity(0.8), - color, - color - ], startPoint: .top, endPoint: .bottom) - - // Blurred Overlay - Rectangle() - .fill(.ultraThinMaterial) - } - .ignoresSafeArea() - } - } - } + @ViewBuilder + func BackgroundView(_ tvShows: [DiscoverShow]?) -> some View { + if let shows = tvShows { + if !shows.isEmpty { + GeometryReader { _ in - @ViewBuilder - func CustomIndicator(_ shows: [DiscoverShow]) -> some View { - HStack(spacing: 5) { + TabView(selection: $currentIndex) { ForEach(shows.indices, id: \.self) { index in - Circle() - .fill(currentIndex == index ? Color.accent : .gray.opacity(0.5)) - .frame(width: currentIndex == index ? 10 : 6, height: currentIndex == index ? 10 : 6) + TransparentImageBackground(imageUrl: shows[index].posterImageUrl) + .tag(index) } + } + .tabViewStyle(.page(indexDisplayMode: .never)) + .animation(.easeInOut, value: currentIndex) + + let color: Color = (scheme == .dark ? .black : .white) + // Custom Gradient + LinearGradient(colors: [ + .black, + .clear, + color.opacity(0.15), + color.opacity(0.5), + color.opacity(0.8), + color, + color + ], startPoint: .top, endPoint: .bottom) + + // Blurred Overlay + Rectangle() + .fill(.ultraThinMaterial) } - .animation(.easeInOut, value: currentIndex) + .ignoresSafeArea() + } } - - @ViewBuilder - private var emptyView: some View { - VStack { - Image(systemName: "list.bullet.below.rectangle") - .resizable() - .aspectRatio(contentMode: .fit) - .foregroundColor(Color.accent) - .font(Font.title.weight(.thin)) - .frame(width: 160, height: 180) - - Text("Looks like your stash is empty") - .titleSemiBoldFont(size: 18) - .padding(.top, 8) - - Text("Could be that you forgot to add your TMDB API Key. Once you set that up, you can get lost in the vast world of Tmdb's collection.") - .captionFont(size: 16) - .padding(.top, 1) - .padding(.bottom, 16) - - Button(action: { - component.dispatch(action: ReloadData()) - }, label: { - Text("Retry") - .bodyMediumFont(size: 16) - .foregroundColor(Color.accent) - }) - .buttonStyle(BorderlessButtonStyle()) - .padding(16) - .background( - RoundedRectangle(cornerRadius: 5) - .stroke(Color.accent, lineWidth: 2) - .background(.clear) - .cornerRadius(2)) - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - .padding([.trailing, .leading], 16) + } + + @ViewBuilder + func CustomIndicator(_ shows: [DiscoverShow]) -> some View { + HStack(spacing: 5) { + ForEach(shows.indices, id: \.self) { index in + Circle() + .fill(currentIndex == index ? Color.accent : .gray.opacity(0.5)) + .frame(width: currentIndex == index ? 10 : 6, height: currentIndex == index ? 10 : 6) + } + } + .animation(.easeInOut, value: currentIndex) + } + + @ViewBuilder + private var emptyView: some View { + VStack { + Image(systemName: "list.bullet.below.rectangle") + .resizable() + .aspectRatio(contentMode: .fit) + .foregroundColor(Color.accent) + .font(Font.title.weight(.thin)) + .frame(width: 160, height: 180) + + Text("Looks like your stash is empty") + .titleSemiBoldFont(size: 18) + .padding(.top, 8) + + Text("Could be that you forgot to add your TMDB API Key. Once you set that up, you can get lost in the vast world of Tmdb's collection.") + .captionFont(size: 16) + .padding(.top, 1) + .padding(.bottom, 16) + + Button(action: { + presenter.dispatch(action: ReloadData()) + }, label: { + Text("Retry") + .bodyMediumFont(size: 16) + .foregroundColor(Color.accent) + }) + .buttonStyle(BorderlessButtonStyle()) + .padding(16) + .background( + RoundedRectangle(cornerRadius: 5) + .stroke(Color.accent, lineWidth: 2) + .background(.clear) + .cornerRadius(2)) } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .padding([.trailing, .leading], 16) + } } extension DiscoverShow { - func toSwift() -> SwiftShow { - .init( - tmdbId: tmdbId, - title: title, - posterUrl: posterImageUrl, - backdropUrl: nil, - inLibrary: inLibrary - ) - } + func toSwift() -> SwiftShow { + .init( + tmdbId: tmdbId, + title: title, + posterUrl: posterImageUrl, + backdropUrl: nil, + inLibrary: inLibrary + ) + } } diff --git a/ios/ios/Feature/LibraryView.swift b/ios/ios/Feature/LibraryView.swift index cc449a62e..b6c693393 100644 --- a/ios/ios/Feature/LibraryView.swift +++ b/ios/ios/Feature/LibraryView.swift @@ -12,105 +12,105 @@ import TvManiac import TvManiacUI struct LibraryView: View { - private let component: LibraryComponent + private let presenter: LibraryPresenter - @StateFlow private var uiState: LibraryState + @StateFlow private var uiState: LibraryState - init(component: LibraryComponent) { - self.component = component - _uiState = StateFlow(component.state) - } + init(presenter: LibraryPresenter) { + self.presenter = presenter + _uiState = StateFlow(presenter.state) + } - var body: some View { - VStack { - switch onEnum(of: uiState) { - case .loadingShows: - // TODO: Show indicator on the toolbar - LoadingIndicatorView() - .frame(maxWidth: UIScreen.main.bounds.width, maxHeight: UIScreen.main.bounds.height, alignment: .center) - case .libraryContent(let content): GridViewContent(content) - case .errorLoadingShows: EmptyView() // TODO: Show Error - } - } - .navigationTitle("Library") - .navigationBarTitleDisplayMode(.large) - .background(Color.background) - .toolbar { - ToolbarItem(placement: .primaryAction) { - HStack { - filterButton - sortButton - } - } + var body: some View { + VStack { + switch onEnum(of: uiState) { + case .loadingShows: + // TODO: Show indicator on the toolbar + LoadingIndicatorView() + .frame(maxWidth: UIScreen.main.bounds.width, maxHeight: UIScreen.main.bounds.height, alignment: .center) + case .libraryContent(let content): GridViewContent(content) + case .errorLoadingShows: EmptyView() // TODO: Show Error + } + } + .navigationTitle("Library") + .navigationBarTitleDisplayMode(.large) + .background(Color.background) + .toolbar { + ToolbarItem(placement: .primaryAction) { + HStack { + filterButton + sortButton } + } } + } - @ViewBuilder - private func GridViewContent(_ content: LibraryContent) -> some View { - if !content.list.isEmpty { - GridView( - items: content.list.map { $0.toSwift() }, - onAction: { id in - component.dispatch(action: LibraryShowClicked(id: id)) - } - ) - } else { - empty + @ViewBuilder + private func GridViewContent(_ content: LibraryContent) -> some View { + if !content.list.isEmpty { + GridView( + items: content.list.map { $0.toSwift() }, + onAction: { id in + presenter.dispatch(action: LibraryShowClicked(id: id)) } + ) + } else { + empty } + } - private var filterButton: some View { - Button { - withAnimation { - // TODO: Show Filter menu - } - } label: { - Label("Sort List", systemImage: "line.3.horizontal.decrease") - .labelStyle(.iconOnly) - } - .buttonBorderShape(.roundedRectangle(radius: 16)) - .buttonStyle(.bordered) + private var filterButton: some View { + Button { + withAnimation { + // TODO: Show Filter menu + } + } label: { + Label("Sort List", systemImage: "line.3.horizontal.decrease") + .labelStyle(.iconOnly) } + .buttonBorderShape(.roundedRectangle(radius: 16)) + .buttonStyle(.bordered) + } - private var sortButton: some View { - Button { - // TODO: Add filer option - } label: { - Label("Sort Order", systemImage: "arrow.up.arrow.down.circle") - .labelStyle(.iconOnly) - } - .pickerStyle(.navigationLink) - .buttonBorderShape(.roundedRectangle(radius: 16)) - .buttonStyle(.bordered) + private var sortButton: some View { + Button { + // TODO: Add filer option + } label: { + Label("Sort Order", systemImage: "arrow.up.arrow.down.circle") + .labelStyle(.iconOnly) } + .pickerStyle(.navigationLink) + .buttonBorderShape(.roundedRectangle(radius: 16)) + .buttonStyle(.bordered) + } - @ViewBuilder - private var empty: some View { - if #available(iOS 17.0, *) { - ContentUnavailableView( - "Your stash is empty.", - systemImage: "rectangle.on.rectangle" - ) - .padding() - .multilineTextAlignment(.center) - .font(.callout) - .foregroundColor(.secondary) - } else { - FullScreenView( - systemName: "rectangle.on.rectangle", - message: "Your stash is empty." - ) - } + @ViewBuilder + private var empty: some View { + if #available(iOS 17.0, *) { + ContentUnavailableView( + "Your stash is empty.", + systemImage: "rectangle.on.rectangle" + ) + .padding() + .multilineTextAlignment(.center) + .font(.callout) + .foregroundColor(.secondary) + } else { + FullScreenView( + systemName: "rectangle.on.rectangle", + message: "Your stash is empty." + ) } + } } extension TvManiac.LibraryItem { - func toSwift() -> SwiftShow { - .init( - tmdbId: tmdbId, - title: title, - posterUrl: posterImageUrl, - inLibrary: true - ) - } + func toSwift() -> SwiftShow { + .init( + tmdbId: tmdbId, + title: title, + posterUrl: posterImageUrl, + inLibrary: true + ) + } } diff --git a/ios/ios/Feature/Profile/ProfileView.swift b/ios/ios/Feature/Profile/ProfileView.swift index 998bfe5c2..73843d983 100644 --- a/ios/ios/Feature/Profile/ProfileView.swift +++ b/ios/ios/Feature/Profile/ProfileView.swift @@ -1,56 +1,50 @@ - // - // ProfileView.swift - // tv-maniac - // - // Created by Kioko on 03/04/2023. - // Copyright © 2023 orgName. All rights reserved. - // +// +// ProfileView.swift +// tv-maniac +// +// Created by Kioko on 03/04/2023. +// Copyright © 2023 orgName. All rights reserved. +// import SwiftUI struct ProfileView: View { + @State var isPresented = false + @State var isAuthenticated = false - - @State var isPresented = false - @State var isAuthenticated = false - - - var body: some View { - NavigationView { - - if(!isAuthenticated){ - UnauthentivatedProfileView() - .toolbar{ - ToolbarItem(placement: .navigationBarTrailing) { - - Button( - action: { self.isPresented = true }, - label: { - Image(systemName: "gearshape") - .resizable() - .aspectRatio(contentMode: .fit) - .foregroundColor(.grey200) - .frame(width: 24, height: 24) - } - ) - .sheet( - isPresented: $isPresented, - content: { - //SettingsUIView() - } - ) - } - } - } else { - AuthenticatedProfileView() + var body: some View { + NavigationView { + if !isAuthenticated { + UnauthentivatedProfileView() + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button( + action: { self.isPresented = true }, + label: { + Image(systemName: "gearshape") + .resizable() + .aspectRatio(contentMode: .fit) + .foregroundColor(.grey200) + .frame(width: 24, height: 24) + } + ) + .sheet( + isPresented: $isPresented, + content: { + // SettingsUIView() + } + ) } - } - + } + } else { + AuthenticatedProfileView() + } } + } } struct ProfileView_Previews: PreviewProvider { - static var previews: some View { - ProfileView() - } + static var previews: some View { + ProfileView() + } } diff --git a/ios/ios/Home/HomeChildView.swift b/ios/ios/Home/HomeChildView.swift index 9e5000f0f..3e5e1c871 100644 --- a/ios/ios/Home/HomeChildView.swift +++ b/ios/ios/Home/HomeChildView.swift @@ -3,49 +3,47 @@ import SwiftUIComponents import TvManiac struct HomeChildView: View { - let screen: HomeComponentChild - let bottomTabActions: [BottomTabAction] - - var body: some View { - GeometryReader { geometry in - ZStack(alignment: .bottom) { - - VStack { - switch onEnum(of: screen) { - case .discover(let screen): - DiscoverView(component: screen.component) - case .search(let screen): - SearchView(component: screen.component) - case .library(let screen): - LibraryView(component: screen.component) - case .settings(let screen): - SettingsView(component: screen.component) - } - } - .bottomTabSafeArea() - .frame(width: geometry.size.width, height: geometry.size.height) + let screen: HomePresenterChild + let bottomTabActions: [BottomTabAction] - BottomNavigation(actions: bottomTabActions) - } + var body: some View { + GeometryReader { geometry in + ZStack(alignment: .bottom) { + VStack { + switch onEnum(of: screen) { + case .discover(let screen): + DiscoverView(presenter: screen.component) + case .search(let screen): + SearchView(presenter: screen.component) + case .library(let screen): + LibraryView(presenter: screen.component) + case .settings(let screen): + SettingsView(presenter: screen.component) + } } - .background(Color.background) - .edgesIgnoringSafeArea(.bottom) + .bottomTabSafeArea() + .frame(width: geometry.size.width, height: geometry.size.height) + + BottomNavigation(actions: bottomTabActions) + } } + .background(Color.background) + .edgesIgnoringSafeArea(.bottom) + } } struct BottomTabSafeArea: ViewModifier { - let height: CGFloat - - func body(content: Content) -> some View { - content.safeAreaInset(edge: .bottom) { - Color.clear.frame(height: height) - } + let height: CGFloat + + func body(content: Content) -> some View { + content.safeAreaInset(edge: .bottom) { + Color.clear.frame(height: height) } + } } extension View { - func bottomTabSafeArea(height: CGFloat = 74) -> some View { - self.modifier(BottomTabSafeArea(height: height)) - } + func bottomTabSafeArea(height: CGFloat = 74) -> some View { + self.modifier(BottomTabSafeArea(height: height)) + } } - diff --git a/ios/ios/Home/HomeTabView.swift b/ios/ios/Home/HomeTabView.swift index 605568fae..261b780ba 100644 --- a/ios/ios/Home/HomeTabView.swift +++ b/ios/ios/Home/HomeTabView.swift @@ -11,50 +11,50 @@ import SwiftUIComponents import TvManiac struct HomeTabView: View { - private let component: HomeComponent - @StateFlow private var stack: ChildStack - private var activeChild: HomeComponentChild { stack.active.instance } + private let presenter: HomePresenter + @StateFlow private var stack: ChildStack + private var activeChild: HomePresenterChild { stack.active.instance } - init(component: HomeComponent) { - self.component = component - _stack = StateFlow(component.stack) - } + init(presenter: HomePresenter) { + self.presenter = presenter + _stack = StateFlow(presenter.stack) + } - var body: some View { - VStack { - HomeChildView( - screen: activeChild, - bottomTabActions: bottomTabActions() - ) - } + var body: some View { + VStack { + HomeChildView( + screen: activeChild, + bottomTabActions: bottomTabActions() + ) } + } - private func bottomTabActions() -> [BottomTabAction] { - return [ - BottomTabAction( - title: "Discover", - systemImage: "tv", - isActive: activeChild is HomeComponentChildDiscover, - action: { component.onDiscoverClicked() } - ), - BottomTabAction( - title: "Search", - systemImage: "magnifyingglass", - isActive: activeChild is HomeComponentChildSearch, - action: { component.onSearchClicked() } - ), - BottomTabAction( - title: "Library", - systemImage: "list.bullet.below.rectangle", - isActive: activeChild is HomeComponentChildLibrary, - action: { component.onLibraryClicked() } - ), - BottomTabAction( - title: "Settings", - systemImage: "gearshape", - isActive: activeChild is HomeComponentChildSettings, - action: { component.onSettingsClicked() } - ) - ] - } + private func bottomTabActions() -> [BottomTabAction] { + return [ + BottomTabAction( + title: "Discover", + systemImage: "tv", + isActive: activeChild is HomePresenterChildDiscover, + action: { presenter.onDiscoverClicked() } + ), + BottomTabAction( + title: "Search", + systemImage: "magnifyingglass", + isActive: activeChild is HomePresenterChildSearch, + action: { presenter.onSearchClicked() } + ), + BottomTabAction( + title: "Library", + systemImage: "list.bullet.below.rectangle", + isActive: activeChild is HomePresenterChildLibrary, + action: { presenter.onLibraryClicked() } + ), + BottomTabAction( + title: "Settings", + systemImage: "gearshape", + isActive: activeChild is HomePresenterChildSettings, + action: { presenter.onSettingsClicked() } + ) + ] + } } diff --git a/ios/ios/MoreShows/MoreShowsView.swift b/ios/ios/MoreShows/MoreShowsView.swift index c25ee7f22..ccabba8d4 100644 --- a/ios/ios/MoreShows/MoreShowsView.swift +++ b/ios/ios/MoreShows/MoreShowsView.swift @@ -12,48 +12,48 @@ import TvManiac import TvManiacUI struct MoreShowsView: View { - private let component: MoreShowsComponent + private let presenter: MoreShowsPresenter + + @StateFlow private var uiState: MoreShowsState + @State private var query = String() + + init(presenter: MoreShowsPresenter) { + self.presenter = presenter + _uiState = StateFlow(presenter.state) + } + + var body: some View { + NavigationView { + VStack(spacing: 0) { + NavigationTopBar( + topBarTitle: uiState.categoryTitle, + onBackClicked: { presenter.dispatch(action: MoreBackClicked()) } + ) - @StateFlow private var uiState: MoreShowsState - @State private var query = String() + Spacer().frame(height: 10) - init(component: MoreShowsComponent) { - self.component = component - _uiState = StateFlow(component.state) + ShowsContent(uiState) + } + .edgesIgnoringSafeArea(.top) } + } - var body: some View { - NavigationView { - VStack(spacing: 0) { - NavigationTopBar( - topBarTitle: uiState.categoryTitle, - onBackClicked: { component.dispatch(action: MoreBackClicked()) } - ) - - Spacer().frame(height: 10) - - ShowsContent(uiState) - } - .edgesIgnoringSafeArea(.top) - } + @ViewBuilder + private func ShowsContent(_ state: MoreShowsState) -> some View { + let shows: [TvShow] = state.snapshotList.indices.compactMap { index in + presenter.getElement(index: Int32(index)) } - @ViewBuilder - private func ShowsContent(_ state: MoreShowsState) -> some View { - let shows: [TvShow] = state.snapshotList.indices.compactMap { index in - component.getElement(index: Int32(index)) - } - - GridView( - items: shows.map { $0.toSwift() }, - onAction: { id in - component.dispatch(action: MoreShowClicked(showId: id)) - } - ) - } + GridView( + items: shows.map { $0.toSwift() }, + onAction: { id in + presenter.dispatch(action: MoreShowClicked(showId: id)) + } + ) + } } private enum DimensionConstants { - static let posterColumns = [GridItem(.adaptive(minimum: 100), spacing: 4)] - static let spacing: CGFloat = 4 + static let posterColumns = [GridItem(.adaptive(minimum: 100), spacing: 4)] + static let spacing: CGFloat = 4 } diff --git a/ios/ios/NavigationModel.swift b/ios/ios/NavigationModel.swift index 5e173c4c5..43ec4ea18 100644 --- a/ios/ios/NavigationModel.swift +++ b/ios/ios/NavigationModel.swift @@ -10,18 +10,17 @@ import SwiftUI import TvManiac class NavigationModel: ObservableObject { - @Published private(set) var viewStack: [RootComponentChild] = [] - @Published private(set) var currentIndex: Int = 0 - @Published var dragOffset: CGFloat = 0 + @Published private(set) var viewStack: [RootPresenterChild] = [] + @Published private(set) var currentIndex: Int = 0 + @Published var dragOffset: CGFloat = 0 - func setStack(_ stack: ChildStack) { - viewStack = stack.items.compactMap { $0.instance } - currentIndex = viewStack.count - 1 - } + func setStack(_ stack: ChildStack) { + viewStack = stack.items.compactMap { $0.instance } + currentIndex = viewStack.count - 1 + } - func popView() { - guard currentIndex > 0 else { return } - currentIndex -= 1 - } + func popView() { + guard currentIndex > 0 else { return } + currentIndex -= 1 + } } - diff --git a/ios/ios/Root/RootView.swift b/ios/ios/Root/RootView.swift index 0ee581c86..e623d22f7 100644 --- a/ios/ios/Root/RootView.swift +++ b/ios/ios/Root/RootView.swift @@ -10,52 +10,52 @@ import SwiftUI import TvManiac struct RootView: View { - private let rootComponent: RootComponent - @StateFlow private var uiState: ThemeState - @State private var isShowingSplash = true + private let rootPresenter: RootPresenter + @StateFlow private var uiState: ThemeState + @State private var isShowingSplash = true - init(rootComponent: RootComponent) { - self.rootComponent = rootComponent - _uiState = StateFlow(rootComponent.themeState) - } + init(rootPresenter: RootPresenter) { + self.rootPresenter = rootPresenter + _uiState = StateFlow(rootPresenter.themeState) + } - var body: some View { - ZStack { - if isShowingSplash { - SplashScreenView() - } else { - StackView( - stack: StateFlow(rootComponent.stack), - onBack: rootComponent.onBackClicked, - content: { child in - childView(for: child) - } - ) - .environment(\.colorScheme, uiState.appTheme == .lightTheme ? .light : .dark) - } - } - .onAppear { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { // Adjust the delay as needed - withAnimation { - isShowingSplash = false - } - } + var body: some View { + ZStack { + if isShowingSplash { + SplashScreenView() + } else { + StackView( + stack: StateFlow(rootPresenter.stack), + onBack: rootPresenter.onBackClicked, + content: { child in + childView(for: child) + } + ) + .environment(\.colorScheme, uiState.appTheme == .lightTheme ? .light : .dark) + } + } + .onAppear { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { // Adjust the delay as needed + withAnimation { + isShowingSplash = false } + } } + } - @ViewBuilder - private func childView(for child: RootComponentChild) -> some View { - switch onEnum(of: child) { - case .home(let child): - HomeTabView(component: child.component) - case .showDetails(let child): - ShowDetailView(component: child.component) - case .seasonDetails(let child): - SeasonDetailsView(component: child.component) - case .moreShows(let child): - MoreShowsView(component: child.component) - case .trailers(_): - EmptyView() //TODO:: Add implementation - } + @ViewBuilder + private func childView(for child: RootPresenterChild) -> some View { + switch onEnum(of: child) { + case .home(let child): + HomeTabView(presenter: child.presenter) + case .showDetails(let child): + ShowDetailView(presenter: child.presenter) + case .seasonDetails(let child): + SeasonDetailsView(presenter: child.presenter) + case .moreShows(let child): + MoreShowsView(presenter: child.presenter) + case .trailers: + EmptyView() // TODO: Add implementation } + } } diff --git a/ios/ios/Search/SearchView.swift b/ios/ios/Search/SearchView.swift index 64aeb0d3c..9c5d94c38 100644 --- a/ios/ios/Search/SearchView.swift +++ b/ios/ios/Search/SearchView.swift @@ -12,14 +12,14 @@ import TvManiac import TvManiacUI struct SearchView: View { - private let component: SearchShowsComponent + private let presenter: SearchShowsPresenter @StateFlow private var uiState: SearchShowState @FocusState private var isSearchFocused: Bool @StateObject private var keyboard = KeyboardHeightManager() - init(component: SearchShowsComponent) { - self.component = component - _uiState = StateFlow(component.state) + init(presenter: SearchShowsPresenter) { + self.presenter = presenter + _uiState = StateFlow(presenter.state) } var body: some View { @@ -41,7 +41,7 @@ struct SearchView: View { systemName: "exclamationmark.arrow.triangle.2.circlepath", message: state.errorMessage ?? "No results found. Try a different keyword!", buttonText: "Retry", - action: { component.dispatch(action: ReloadShowContent()) } + action: { presenter.dispatch(action: ReloadShowContent()) } ) case .searchResultAvailable(let state): searchResultsContent(state: state) @@ -69,7 +69,7 @@ struct SearchView: View { set: { newValue in let trimmedValue = newValue.trimmingCharacters(in: .whitespaces) if !trimmedValue.isEmpty { - component.dispatch(action: QueryChanged(query: newValue)) + presenter.dispatch(action: QueryChanged(query: newValue)) } } ) @@ -143,7 +143,7 @@ struct SearchView: View { title: title, items: shows.map { $0.toSwift() }, onClick: { id in - component.dispatch(action: SearchShowClicked(id: id)) + presenter.dispatch(action: SearchShowClicked(id: id)) dismissKeyboard() } ) @@ -166,7 +166,7 @@ struct SearchView: View { SearchResultListView( items: shows.map { $0.toSwift() }, onClick: { id in - component.dispatch(action: SearchShowClicked(id: id)) + presenter.dispatch(action: SearchShowClicked(id: id)) dismissKeyboard() } ) @@ -175,7 +175,7 @@ struct SearchView: View { private func handleClearQuery() { if let query = uiState.query, !query.trimmingCharacters(in: .whitespaces).isEmpty { - component.dispatch(action: ClearQuery()) + presenter.dispatch(action: ClearQuery()) } dismissKeyboard() } diff --git a/ios/ios/SeasonDetails/SeasonDetailsView.swift b/ios/ios/SeasonDetails/SeasonDetailsView.swift index 3592df5df..d96304345 100644 --- a/ios/ios/SeasonDetails/SeasonDetailsView.swift +++ b/ios/ios/SeasonDetails/SeasonDetailsView.swift @@ -12,182 +12,182 @@ import TvManiac import TvManiacUI struct SeasonDetailsView: View { - private let component: SeasonDetailsComponent - - @Environment(\.presentationMode) var presentationMode + private let presenter: SeasonDetailsPresenter + + @Environment(\.presentationMode) var presentationMode - @StateFlow private var uiState: SeasonDetailState - @State private var isTruncated = false - @State private var showFullText = false - @State private var showModal = false - @State private var scrollOffset: CGFloat = 0 + @StateFlow private var uiState: SeasonDetailState + @State private var isTruncated = false + @State private var showFullText = false + @State private var showModal = false + @State private var scrollOffset: CGFloat = 0 - init(component: SeasonDetailsComponent) { - self.component = component - _uiState = StateFlow(component.state) - } + init(presenter: SeasonDetailsPresenter) { + self.presenter = presenter + _uiState = StateFlow(presenter.state) + } - var body: some View { - ZStack { - Color.background.edgesIgnoringSafeArea(.all) + var body: some View { + ZStack { + Color.background.edgesIgnoringSafeArea(.all) - switch onEnum(of: uiState) { - case .initialSeasonsState: LoadingIndicatorView(animate: true) - case .seasonDetailsLoaded(let state): SeasonDetailsContent(state) - case .seasonDetailsErrorState: - FullScreenView( - systemName: "exclamationmark.triangle.fill", - message: "Something went wrong", - buttonText: "Retry", - action: { component.dispatch(action: ReloadSeasonDetails()) } - ) - } - } - .ignoresSafeArea() - .sheet(isPresented: $showModal) { - if let uiState = uiState as? SeasonDetailsLoaded { - ImageGalleryContentView(items: uiState.seasonImages.map { $0.toSwift() }) - } - } + switch onEnum(of: uiState) { + case .initialSeasonsState: LoadingIndicatorView(animate: true) + case .seasonDetailsLoaded(let state): SeasonDetailsContent(state) + case .seasonDetailsErrorState: + FullScreenView( + systemName: "exclamationmark.triangle.fill", + message: "Something went wrong", + buttonText: "Retry", + action: { presenter.dispatch(action: ReloadSeasonDetails()) } + ) + } + } + .ignoresSafeArea() + .sheet(isPresented: $showModal) { + if let uiState = uiState as? SeasonDetailsLoaded { + ImageGalleryContentView(items: uiState.seasonImages.map { $0.toSwift() }) + } } + } - @ViewBuilder - private func SeasonDetailsContent(_ state: SeasonDetailsLoaded) -> some View { - ParallaxView( - title: state.seasonName, - isRefreshing: state.isUpdating, + @ViewBuilder + private func SeasonDetailsContent(_ state: SeasonDetailsLoaded) -> some View { + ParallaxView( + title: state.seasonName, + isRefreshing: state.isUpdating, + imageHeight: DimensionConstants.imageHeight, + collapsedImageHeight: DimensionConstants.collapsedImageHeight, + header: { proxy in + HeaderContent( + state: state, + progress: proxy.getTitleOpacity( + geometry: proxy, imageHeight: DimensionConstants.imageHeight, - collapsedImageHeight: DimensionConstants.collapsedImageHeight, - header: { proxy in - HeaderContent( - state: state, - progress: proxy.getTitleOpacity( - geometry: proxy, - imageHeight: DimensionConstants.imageHeight, - collapsedImageHeight: DimensionConstants.collapsedImageHeight - ), - headerHeight: proxy.getHeightForHeaderImage(proxy) - ) - }, - content: { titleRect in + collapsedImageHeight: DimensionConstants.collapsedImageHeight + ), + headerHeight: proxy.getHeightForHeaderImage(proxy) + ) + }, + content: { titleRect in - OverviewBoxView( - overview: state.seasonOverview, - titleRect: titleRect - ) - .padding() + OverviewBoxView( + overview: state.seasonOverview, + titleRect: titleRect + ) + .padding() - EpisodeListView( - episodeCount: state.episodeCount, - watchProgress: state.watchProgress, - expandEpisodeItems: state.expandEpisodeItems, - showSeasonWatchStateDialog: state.showSeasonWatchStateDialog, - isSeasonWatched: state.isSeasonWatched, - items: state.episodeDetailsList.map { $0.toSwift() }, - onEpisodeHeaderClicked: { component.dispatch(action: OnEpisodeHeaderClicked()) }, - onWatchedStateClicked: { component.dispatch(action: UpdateSeasonWatchedState()) } - ) - - CastListView(casts: toCastsList(state.seasonCast)) - }, - onBackClicked: { - component.dispatch(action: SeasonDetailsBackClicked()) - }, - onRefreshClicked: {} + EpisodeListView( + episodeCount: state.episodeCount, + watchProgress: state.watchProgress, + expandEpisodeItems: state.expandEpisodeItems, + showSeasonWatchStateDialog: state.showSeasonWatchStateDialog, + isSeasonWatched: state.isSeasonWatched, + items: state.episodeDetailsList.map { $0.toSwift() }, + onEpisodeHeaderClicked: { presenter.dispatch(action: OnEpisodeHeaderClicked()) }, + onWatchedStateClicked: { presenter.dispatch(action: UpdateSeasonWatchedState()) } ) - .onAppear { showModal = state.showSeasonWatchStateDialog } - } + + CastListView(casts: toCastsList(state.seasonCast)) + }, + onBackClicked: { + presenter.dispatch(action: SeasonDetailsBackClicked()) + }, + onRefreshClicked: {} + ) + .onAppear { showModal = state.showSeasonWatchStateDialog } + } - @ViewBuilder - private func HeaderContent(state: SeasonDetailsLoaded, progress: CGFloat, headerHeight: CGFloat) -> some View { - ZStack(alignment: .bottom) { - HeaderCoverArtWorkView( - imageUrl: state.imageUrl, - posterHeight: headerHeight - ) - .foregroundStyle(.ultraThinMaterial) - .overlay( - LinearGradient( - gradient: Gradient(colors: [ - .clear, - .clear, - .clear, - Color.background.opacity(0.6), - Color.background.opacity(0.8), - Color.background, - ]), - startPoint: .top, - endPoint: .bottom - ) - ) - .frame(height: headerHeight) + @ViewBuilder + private func HeaderContent(state: SeasonDetailsLoaded, progress: CGFloat, headerHeight: CGFloat) -> some View { + ZStack(alignment: .bottom) { + HeaderCoverArtWorkView( + imageUrl: state.imageUrl, + posterHeight: headerHeight + ) + .foregroundStyle(.ultraThinMaterial) + .overlay( + LinearGradient( + gradient: Gradient(colors: [ + .clear, + .clear, + .clear, + Color.background.opacity(0.6), + Color.background.opacity(0.8), + Color.background, + ]), + startPoint: .top, + endPoint: .bottom + ) + ) + .frame(height: headerHeight) - ZStack(alignment: .bottom) { - VStack { - Spacer() - HStack(spacing: 16) { - Image(systemName: "photo.fill.on.rectangle.fill") - .resizable() - .frame(width: 28.0, height: 28.0) - .fontDesign(.rounded) - .font(.callout) - .fontWeight(.regular) - .foregroundColor(.secondary) - .alignmentGuide(.view) { d in d[HorizontalAlignment.leading] } + ZStack(alignment: .bottom) { + VStack { + Spacer() + HStack(spacing: 16) { + Image(systemName: "photo.fill.on.rectangle.fill") + .resizable() + .frame(width: 28.0, height: 28.0) + .fontDesign(.rounded) + .font(.callout) + .fontWeight(.regular) + .foregroundColor(.secondary) + .alignmentGuide(.view) { d in d[HorizontalAlignment.leading] } - Text("^[\(state.seasonImages.count) Image](inflect: true)") - .bodyMediumFont(size: 16) - .foregroundColor(.textColor) - .lineLimit(1) - .alignmentGuide(.view) { d in d[HorizontalAlignment.center] } + Text("^[\(state.seasonImages.count) Image](inflect: true)") + .bodyMediumFont(size: 16) + .foregroundColor(.textColor) + .lineLimit(1) + .alignmentGuide(.view) { d in d[HorizontalAlignment.center] } - Spacer() - } - .padding(16) - .contentShape(Rectangle()) - .onTapGesture { - component.dispatch(action: SeasonGalleryClicked()) - showModal.toggle() - } - } - .frame(height: headerHeight) - .opacity(1 - progress) - } - - ProgressView(value: state.watchProgress, total: 1) - .progressViewStyle(RoundedRectProgressViewStyle()) + Spacer() + } + .padding(16) + .contentShape(Rectangle()) + .onTapGesture { + presenter.dispatch(action: SeasonGalleryClicked()) + showModal.toggle() + } } .frame(height: headerHeight) - .clipped() + .opacity(1 - progress) + } + + ProgressView(value: state.watchProgress, total: 1) + .progressViewStyle(RoundedRectProgressViewStyle()) } + .frame(height: headerHeight) + .clipped() + } - private func toCastsList(_ list: [Cast]) -> [SwiftCast] { - return list.map { cast -> SwiftCast in - .init(castId: cast.id, name: cast.name, characterName: cast.characterName, profileUrl: cast.profileUrl) - } + private func toCastsList(_ list: [Cast]) -> [SwiftCast] { + return list.map { cast -> SwiftCast in + .init(castId: cast.id, name: cast.name, characterName: cast.characterName, profileUrl: cast.profileUrl) } + } - @ViewBuilder - private var empty: some View { - if #available(iOS 17.0, *) { - ContentUnavailableView( - "Please wait while we get your content.", - systemImage: "rectangle.on.rectangle" - ) - .padding() - .multilineTextAlignment(.center) - .font(.callout) - .foregroundColor(.secondary) - } else { - FullScreenView( - systemName: "rectangle.on.rectangle", - message: "Please wait while we get your content." - ) - } + @ViewBuilder + private var empty: some View { + if #available(iOS 17.0, *) { + ContentUnavailableView( + "Please wait while we get your content.", + systemImage: "rectangle.on.rectangle" + ) + .padding() + .multilineTextAlignment(.center) + .font(.callout) + .foregroundColor(.secondary) + } else { + FullScreenView( + systemName: "rectangle.on.rectangle", + message: "Please wait while we get your content." + ) } + } } private enum DimensionConstants { - static let imageHeight: CGFloat = 320 - static let collapsedImageHeight: CGFloat = 120.0 + static let imageHeight: CGFloat = 320 + static let collapsedImageHeight: CGFloat = 120.0 } diff --git a/ios/ios/Settings/SettingsView.swift b/ios/ios/Settings/SettingsView.swift index 60011fad7..293a9d424 100644 --- a/ios/ios/Settings/SettingsView.swift +++ b/ios/ios/Settings/SettingsView.swift @@ -11,122 +11,114 @@ import SwiftUI import TvManiac struct SettingsView: View { + private let presenter: SettingsPresenter + @Environment(\.openURL) var openURL + @Environment(\.presentationMode) var presentationMode - private let component: SettingsComponent - @Environment(\.openURL) var openURL - @Environment(\.presentationMode) var presentationMode + @StateFlow private var uiState: SettingsState + @State private var theme: DeveiceAppTheme = .System + @State private var showingAlert: Bool = false + @State private var openInYouTube: Bool = false + @ObservedObject private var model = TraktAuthViewModel() - @StateFlow private var uiState: SettingsState - @State private var theme: DeveiceAppTheme = DeveiceAppTheme.System - @State private var showingAlert: Bool = false - @State private var openInYouTube: Bool = false - @ObservedObject private var model = TraktAuthViewModel() + init(presenter: SettingsPresenter) { + self.presenter = presenter + _uiState = StateFlow(presenter.state) + self.theme = toAppTheme(theme: uiState.appTheme) + } - - init(component: SettingsComponent){ - self.component = component - _uiState = StateFlow(component.state) - self.theme = toAppTheme(theme: uiState.appTheme) - } - - var body: some View { - Form { - - Section(header: Text("App Theme").bodyMediumFont(size: 16)) { - - Picker( - selection: $theme, - label: Text("Change Theme") - .bodyMediumFont(size: 16), - content: { - ForEach(DeveiceAppTheme.allCases, id: \.self) { theme in - - Text(theme.getName()) - .tag(theme.rawValue) + var body: some View { + Form { + Section(header: Text("App Theme").bodyMediumFont(size: 16)) { + Picker( + selection: $theme, + label: Text("Change Theme") + .bodyMediumFont(size: 16), + content: { + ForEach(DeveiceAppTheme.allCases, id: \.self) { theme in - } - }) - .pickerStyle(.segmented) - .padding(.vertical, 6) - .onChange(of: theme) { theme in - component.dispatch(action: ThemeSelected(appTheme: toTheme(appTheme: theme))) - } + Text(theme.getName()) + .tag(theme.rawValue) } + } + ) + .pickerStyle(.segmented) + .padding(.vertical, 6) + .onChange(of: theme) { theme in + presenter.dispatch(action: ThemeSelected(appTheme: toTheme(appTheme: theme))) + } + } - Section(header: Text("Trailer Settings").bodyMediumFont(size: 16)) { - Toggle(isOn: $openInYouTube) { - Text("Open Trailers in Youtube App") - } - } + Section(header: Text("Trailer Settings").bodyMediumFont(size: 16)) { + Toggle(isOn: $openInYouTube) { + Text("Open Trailers in Youtube App") + } + } - Section(header: Text("Trakt Account").bodyMediumFont(size: 16)) { - - SettingsItem( - image: "person.fill", - title: "Connect to Trakt", - description: "Trakt is a platform that does many things, but primarily keeps track of TV shows and movies you watch." - ) { - showingAlert = !(uiState.showTraktDialog) - } - .alert(isPresented: $showingAlert) { - Alert( - title: Text("Trakt Coming Soon"), - message: Text("Trakt is a platform that does many things, but primarily keeps track of TV shows and movies you watch."), - primaryButton: .default(Text("Login")) { - model.initiateAuthorization() + Section(header: Text("Trakt Account").bodyMediumFont(size: 16)) { + SettingsItem( + image: "person.fill", + title: "Connect to Trakt", + description: "Trakt is a platform that does many things, but primarily keeps track of TV shows and movies you watch." + ) { + showingAlert = !(uiState.showTraktDialog) + } + .alert(isPresented: $showingAlert) { + Alert( + title: Text("Trakt Coming Soon"), + message: Text("Trakt is a platform that does many things, but primarily keeps track of TV shows and movies you watch."), + primaryButton: .default(Text("Login")) { + model.initiateAuthorization() - }, - secondaryButton: .destructive(Text("Cancel")) - ) - } - } - - Section(header: Text("Info").bodyMediumFont(size: 16)) { - - SettingsItem( - image: "info.circle.fill", - title: "About TvManiac", - description: "Tv-Maniac is a Multiplatform app (Android & iOS) for viewing TV Shows from TMDB." - ) { - openURL(URL(string: "https://github.com/c0de-wizard/tv-maniac")!) - } - } + }, + secondaryButton: .destructive(Text("Cancel")) + ) } - .navigationTitle("Settings") - .navigationBarTitleDisplayMode(.large) - .onAppear { - self.theme = toAppTheme(theme: uiState.appTheme) + } + + Section(header: Text("Info").bodyMediumFont(size: 16)) { + SettingsItem( + image: "info.circle.fill", + title: "About TvManiac", + description: "Tv-Maniac is a Multiplatform app (Android & iOS) for viewing TV Shows from TMDB." + ) { + openURL(URL(string: "https://github.com/c0de-wizard/tv-maniac")!) } + } } - + .navigationTitle("Settings") + .navigationBarTitleDisplayMode(.large) + .onAppear { + self.theme = toAppTheme(theme: uiState.appTheme) + } + } } struct SettingsItem: View { - let image: String - let title: String - let description: String - let onClick: () -> Void - + let image: String + let title: String + let description: String + let onClick: () -> Void - var body: some View { - HStack(alignment: .center) { - Image(systemName: image) - .resizable() - .aspectRatio(contentMode: .fit) - .foregroundColor(Color.accent) - .frame(width: 20, height: 24, alignment: .leading) - .padding(.leading, 8) - .padding(.trailing, 8) + var body: some View { + HStack(alignment: .center) { + Image(systemName: image) + .resizable() + .aspectRatio(contentMode: .fit) + .foregroundColor(Color.accent) + .frame(width: 20, height: 24, alignment: .leading) + .padding(.leading, 8) + .padding(.trailing, 8) - VStack(alignment: .leading) { - Text(title) - .bodyMediumFont(size: 16) + VStack(alignment: .leading) { + Text(title) + .bodyMediumFont(size: 16) - Text(description) - .bodyFont(size: 16) - .padding(.top, 1.5) - } - } - .onTapGesture(perform: onClick) + Text(description) + .bodyFont(size: 16) + .padding(.top, 1.5) + } } + .onTapGesture(perform: onClick) + } } diff --git a/ios/ios/ShowDetails/ShowDetailView.swift b/ios/ios/ShowDetails/ShowDetailView.swift index 2b86a54ec..0f8fc44e0 100644 --- a/ios/ios/ShowDetails/ShowDetailView.swift +++ b/ios/ios/ShowDetails/ShowDetailView.swift @@ -12,105 +12,105 @@ import TvManiac import TvManiacUI struct ShowDetailView: View { - private let component: ShowDetailsComponent + private let presenter: ShowDetailsPresenter - @StateFlow private var uiState: ShowDetailsContent - @State private var scrollOffset: CGFloat = 0 + @StateFlow private var uiState: ShowDetailsContent + @State private var scrollOffset: CGFloat = 0 - init(component: ShowDetailsComponent) { - self.component = component - _uiState = StateFlow(component.state) - } + init(presenter: ShowDetailsPresenter) { + self.presenter = presenter + _uiState = StateFlow(presenter.state) + } - var body: some View { - if !uiState.isUpdating && uiState.showDetails == nil { - FullScreenView( - buttonText: "Retry", - action: { component.dispatch(action: ReloadShowDetails()) } - ) - } else if let showDetails = uiState.showDetails { - ParallaxView( - title: showDetails.title, - isRefreshing: uiState.isUpdating || uiState.showInfo is ShowInfoStateLoading, - imageHeight: DimensionConstants.imageHeight, - collapsedImageHeight: DimensionConstants.collapsedImageHeight, - header: { proxy in - HeaderView( - title: showDetails.title, - overview: showDetails.overview, - backdropImageUrl: showDetails.backdropImageUrl, - status: showDetails.status, - year: showDetails.year, - language: showDetails.language, - rating: showDetails.rating, - progress: proxy.getTitleOpacity( - geometry: proxy, - imageHeight: DimensionConstants.imageHeight, - collapsedImageHeight: DimensionConstants.collapsedImageHeight - ), - headerHeight: proxy.getHeightForHeaderImage(proxy) - ) - }, - content: { titleRect in - ShowInfoContent(show: showDetails, titleRect: titleRect) - }, - onBackClicked: { - component.dispatch(action: DetailBackClicked()) - }, - onRefreshClicked: { - component.dispatch(action: ReloadShowDetails()) - } - ) + var body: some View { + if !uiState.isUpdating && uiState.showDetails == nil { + FullScreenView( + buttonText: "Retry", + action: { presenter.dispatch(action: ReloadShowDetails()) } + ) + } else if let showDetails = uiState.showDetails { + ParallaxView( + title: showDetails.title, + isRefreshing: uiState.isUpdating || uiState.showInfo is ShowInfoStateLoading, + imageHeight: DimensionConstants.imageHeight, + collapsedImageHeight: DimensionConstants.collapsedImageHeight, + header: { proxy in + HeaderView( + title: showDetails.title, + overview: showDetails.overview, + backdropImageUrl: showDetails.backdropImageUrl, + status: showDetails.status, + year: showDetails.year, + language: showDetails.language, + rating: showDetails.rating, + progress: proxy.getTitleOpacity( + geometry: proxy, + imageHeight: DimensionConstants.imageHeight, + collapsedImageHeight: DimensionConstants.collapsedImageHeight + ), + headerHeight: proxy.getHeightForHeaderImage(proxy) + ) + }, + content: { titleRect in + ShowInfoContent(show: showDetails, titleRect: titleRect) + }, + onBackClicked: { + presenter.dispatch(action: DetailBackClicked()) + }, + onRefreshClicked: { + presenter.dispatch(action: ReloadShowDetails()) } + ) } + } - @ViewBuilder - func ShowInfoContent(show: ShowDetails, titleRect: Binding) -> some View { - switch onEnum(of: uiState.showInfo) { - case .loading, .empty: - LoadingIndicatorView(animate: true) - case .error: - FullScreenView( - buttonText: "Retry", - action: { component.dispatch(action: ReloadShowDetails()) } - ) - case .loaded(let state): - ShowInfoView( - isFollowed: show.isFollowed, - openTrailersInYoutube: state.openTrailersInYoutube, - genreList: show.genres.map { $0.toSwift() }, - seasonList: state.seasonsList.map { $0.toSwift() }, - providerList: state.providers.map { $0.toSwift() }, - trailerList: state.trailersList.map { $0.toSwift() }, - castsList: state.castsList.map { $0.toSwift() }, - recommendedShowList: state.recommendedShowList.map { $0.toSwift() }, - similarShows: state.similarShows.map { $0.toSwift() }, - onWatchTrailer: { - component.dispatch(action: WatchTrailerClicked(id: show.tmdbId)) - }, - onAddToLibrary: { - component.dispatch(action: FollowShowClicked(addToLibrary: show.isFollowed)) - }, - onSeasonClicked: { index, season in - let params = ShowSeasonDetailsParam( - showId: season.tvShowId, - seasonId: season.seasonId, - seasonNumber: season.seasonNumber, - selectedSeasonIndex: Int32(index) - ) + @ViewBuilder + func ShowInfoContent(show: ShowDetails, titleRect: Binding) -> some View { + switch onEnum(of: uiState.showInfo) { + case .loading, .empty: + LoadingIndicatorView(animate: true) + case .error: + FullScreenView( + buttonText: "Retry", + action: { presenter.dispatch(action: ReloadShowDetails()) } + ) + case .loaded(let state): + ShowInfoView( + isFollowed: show.isFollowed, + openTrailersInYoutube: state.openTrailersInYoutube, + genreList: show.genres.map { $0.toSwift() }, + seasonList: state.seasonsList.map { $0.toSwift() }, + providerList: state.providers.map { $0.toSwift() }, + trailerList: state.trailersList.map { $0.toSwift() }, + castsList: state.castsList.map { $0.toSwift() }, + recommendedShowList: state.recommendedShowList.map { $0.toSwift() }, + similarShows: state.similarShows.map { $0.toSwift() }, + onWatchTrailer: { + presenter.dispatch(action: WatchTrailerClicked(id: show.tmdbId)) + }, + onAddToLibrary: { + presenter.dispatch(action: FollowShowClicked(addToLibrary: show.isFollowed)) + }, + onSeasonClicked: { index, season in + let params = ShowSeasonDetailsParam( + showId: season.tvShowId, + seasonId: season.seasonId, + seasonNumber: season.seasonNumber, + selectedSeasonIndex: Int32(index) + ) - component.dispatch(action: SeasonClicked(params: params)) - }, - onShowClicked: { - component.dispatch(action: FollowShowClicked(addToLibrary: show.isFollowed)) - }, - titleRect: titleRect - ) - } + presenter.dispatch(action: SeasonClicked(params: params)) + }, + onShowClicked: { + presenter.dispatch(action: FollowShowClicked(addToLibrary: show.isFollowed)) + }, + titleRect: titleRect + ) } + } } private enum DimensionConstants { - static let imageHeight: CGFloat = 480 - static let collapsedImageHeight: CGFloat = 120.0 + static let imageHeight: CGFloat = 480 + static let collapsedImageHeight: CGFloat = 120.0 } diff --git a/ios/ios/iOSApp.swift b/ios/ios/iOSApp.swift index 2b2ab04b6..e7a07b3df 100644 --- a/ios/ios/iOSApp.swift +++ b/ios/ios/iOSApp.swift @@ -10,7 +10,7 @@ struct iOSApp: App { var body: some Scene { WindowGroup { - RootView(rootComponent: appDelegate.presenterComponent.rootComponent) + RootView(rootPresenter: appDelegate.presenterComponent.rootPresenter) .environmentObject(NavigationModel()) } } diff --git a/ios/tv-maniac.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ios/tv-maniac.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 8f1906084..a6a8dbc37 100644 --- a/ios/tv-maniac.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ios/tv-maniac.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "cffcd63d0b0475e4643c211bed2fc4b4285ec9a5bb2759ea0a8ec6ea45fb3559", + "originHash" : "19d4fae2c62b63ead914208f95cb965fbebfe48ad822de13ef624049daabc717", "pins" : [ { "identity" : "brightfutures", @@ -60,8 +60,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/SDWebImage/SDWebImageSwiftUI", "state" : { - "revision" : "5aa947356f4ea49a0c3b9968564267f6ea5abea7", - "version" : "3.1.2" + "revision" : "451c6dfd5ecec2cf626d1d9ca81c2d4a60355172", + "version" : "3.1.3" } }, { @@ -69,8 +69,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-snapshot-testing", "state" : { - "revision" : "7b0bbbae90c41f848f90ac7b4df6c4f50068256d", - "version" : "1.17.5" + "revision" : "42a086182681cf661f5c47c9b7dc3931de18c6d7", + "version" : "1.17.6" } }, { diff --git a/navigation/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/navigation/RootComponent.kt b/navigation/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/navigation/RootPresenter.kt similarity index 97% rename from navigation/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/navigation/RootComponent.kt rename to navigation/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/navigation/RootPresenter.kt index 17e6ba323..51eadf2b2 100644 --- a/navigation/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/navigation/RootComponent.kt +++ b/navigation/api/src/commonMain/kotlin/com/thomaskioko/tvmaniac/navigation/RootPresenter.kt @@ -8,7 +8,7 @@ import com.thomaskioko.tvmaniac.presentation.showdetails.ShowDetailsPresenter import com.thomaskioko.tvmaniac.presentation.trailers.TrailersPresenter import kotlinx.coroutines.flow.StateFlow -interface RootComponent { +interface RootPresenter { val stack: StateFlow> val themeState: StateFlow diff --git a/navigation/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/navigation/DefaultRootComponent.kt b/navigation/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/navigation/DefaultRootPresenter.kt similarity index 97% rename from navigation/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/navigation/DefaultRootComponent.kt rename to navigation/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/navigation/DefaultRootPresenter.kt index b66c57a76..2b9e56f96 100644 --- a/navigation/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/navigation/DefaultRootComponent.kt +++ b/navigation/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/navigation/DefaultRootPresenter.kt @@ -13,7 +13,7 @@ import com.arkivanov.decompose.value.Value import com.thomaskioko.tvmaniac.core.base.annotations.ActivityScope import com.thomaskioko.tvmaniac.core.base.extensions.coroutineScope import com.thomaskioko.tvmaniac.datastore.api.DatastoreRepository -import com.thomaskioko.tvmaniac.navigation.RootComponent.Child +import com.thomaskioko.tvmaniac.navigation.RootPresenter.Child import com.thomaskioko.tvmaniac.presentation.home.HomePresenterFactory import com.thomaskioko.tvmaniac.presentation.moreshows.MoreShowsPresenterFactory import com.thomaskioko.tvmaniac.presentation.seasondetails.SeasonDetailsPresenterFactory @@ -32,7 +32,7 @@ import me.tatarka.inject.annotations.Inject @Inject @ActivityScope -class DefaultRootComponent( +class DefaultRootPresenter( componentContext: ComponentContext, private val homePresenterFactory: HomePresenterFactory, private val moreShowsPresenterFactory: MoreShowsPresenterFactory, @@ -41,7 +41,7 @@ class DefaultRootComponent( private val trailersPresenterFactory: TrailersPresenterFactory, private val coroutineScope: CoroutineScope = componentContext.coroutineScope(), datastoreRepository: DatastoreRepository, -) : RootComponent, ComponentContext by componentContext { +) : RootPresenter, ComponentContext by componentContext { private val navigation = StackNavigation() private val childStack: Value> = diff --git a/navigation/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/navigation/di/NavigatorComponent.kt b/navigation/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/navigation/di/NavigatorComponent.kt index f81b6fc83..b45886f9c 100644 --- a/navigation/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/navigation/di/NavigatorComponent.kt +++ b/navigation/implementation/src/commonMain/kotlin/com/thomaskioko/tvmaniac/navigation/di/NavigatorComponent.kt @@ -1,13 +1,13 @@ package com.thomaskioko.tvmaniac.navigation.di import com.thomaskioko.tvmaniac.core.base.annotations.ActivityScope -import com.thomaskioko.tvmaniac.navigation.DefaultRootComponent -import com.thomaskioko.tvmaniac.navigation.RootComponent +import com.thomaskioko.tvmaniac.navigation.DefaultRootPresenter +import com.thomaskioko.tvmaniac.navigation.RootPresenter import me.tatarka.inject.annotations.Provides interface NavigatorComponent { @ActivityScope @Provides - fun provideRootComponent(bind: DefaultRootComponent): RootComponent = bind + fun provideRootComponent(bind: DefaultRootPresenter): RootPresenter = bind } diff --git a/navigation/implementation/src/commonTest/kotlin/com/thomaskioko/tvmaniac/navigation/DefaultRootComponentTest.kt b/navigation/implementation/src/commonTest/kotlin/com/thomaskioko/tvmaniac/navigation/DefaultRootComponentTest.kt index a4dbf282b..651a04200 100644 --- a/navigation/implementation/src/commonTest/kotlin/com/thomaskioko/tvmaniac/navigation/DefaultRootComponentTest.kt +++ b/navigation/implementation/src/commonTest/kotlin/com/thomaskioko/tvmaniac/navigation/DefaultRootComponentTest.kt @@ -17,9 +17,9 @@ import com.thomaskioko.tvmaniac.data.upcomingshows.testing.FakeUpcomingShowsRepo import com.thomaskioko.tvmaniac.data.watchproviders.testing.FakeWatchProviderRepository import com.thomaskioko.tvmaniac.datastore.api.AppTheme import com.thomaskioko.tvmaniac.datastore.testing.FakeDatastoreRepository -import com.thomaskioko.tvmaniac.navigation.RootComponent.Child.Home -import com.thomaskioko.tvmaniac.navigation.RootComponent.Child.MoreShows -import com.thomaskioko.tvmaniac.navigation.RootComponent.Child.ShowDetails +import com.thomaskioko.tvmaniac.navigation.RootPresenter.Child.Home +import com.thomaskioko.tvmaniac.navigation.RootPresenter.Child.MoreShows +import com.thomaskioko.tvmaniac.navigation.RootPresenter.Child.ShowDetails import com.thomaskioko.tvmaniac.presentation.discover.DiscoverShowsPresenter import com.thomaskioko.tvmaniac.presentation.discover.DiscoverShowsPresenterFactory import com.thomaskioko.tvmaniac.presentation.home.HomePresenter @@ -72,7 +72,7 @@ class DefaultRootComponentTest { private val popularShowsRepository = FakePopularShowsRepository() private val searchRepository = FakeSearchRepository() - private lateinit var presenter: DefaultRootComponent + private lateinit var presenter: DefaultRootPresenter @BeforeTest fun before() { @@ -81,7 +81,7 @@ class DefaultRootComponentTest { val componentContext = DefaultComponentContext(lifecycle = lifecycle) presenter = - DefaultRootComponent( + DefaultRootPresenter( componentContext = componentContext, moreShowsPresenterFactory = buildMoreShowsPresenterFactory(componentContext), showDetailsPresenterFactory = buildShowDetailsPresenterPresenterFactory(componentContext), diff --git a/shared/src/iosMain/kotlin/com.thomaskioko.tvmaniac.shared/IosViewPresenterComponent.kt b/shared/src/iosMain/kotlin/com.thomaskioko.tvmaniac.shared/IosViewPresenterComponent.kt index 4fd009c32..7f71e8577 100644 --- a/shared/src/iosMain/kotlin/com.thomaskioko.tvmaniac.shared/IosViewPresenterComponent.kt +++ b/shared/src/iosMain/kotlin/com.thomaskioko.tvmaniac.shared/IosViewPresenterComponent.kt @@ -2,7 +2,7 @@ package com.thomaskioko.tvmaniac.shared import com.arkivanov.decompose.ComponentContext import com.thomaskioko.tvmaniac.core.base.annotations.ActivityScope -import com.thomaskioko.tvmaniac.navigation.RootComponent +import com.thomaskioko.tvmaniac.navigation.RootPresenter import com.thomaskioko.tvmaniac.navigation.di.NavigatorComponent import com.thomaskioko.tvmaniac.traktauth.implementation.TraktAuthManagerComponent import me.tatarka.inject.annotations.Component @@ -14,7 +14,7 @@ abstract class IosViewPresenterComponent( @get:Provides val componentContext: ComponentContext, @Component val applicationComponent: ApplicationComponent, ) : NavigatorComponent, TraktAuthManagerComponent { - abstract val rootComponent: RootComponent + abstract val rootPresenter: RootPresenter companion object }