FrontPageView.swift 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. import SwiftUI
  2. struct FrontPageView: View {
  3. @EnvironmentObject private var subscriptions: SubscriptionManager
  4. @EnvironmentObject private var appRating: AppRatingManager
  5. @Environment(AppearanceManager.self) private var appearanceManager
  6. @Bindable var session: AppSessionState
  7. @Bindable private var freeUsageManager = AIFreeUsageManager.shared
  8. private var viewModel: FrontPageViewModel { session.frontPage }
  9. private var paywallViewModel: PaywallViewModel { session.paywall }
  10. private var postGeneratorViewModel: PostGeneratorViewModel { session.postGenerator }
  11. private var titleOptimizerViewModel: TitleOptimizerViewModel { session.titleOptimizer }
  12. private var commentWriterViewModel: CommentWriterViewModel { session.commentWriter }
  13. private var settingsViewModel: SettingsViewModel { session.settings }
  14. private var historyViewModel: AIHistoryViewModel { session.history }
  15. var body: some View {
  16. let _ = appearanceManager.isDarkMode
  17. ZStack {
  18. HStack(spacing: 0) {
  19. SidebarView(
  20. viewModel: viewModel,
  21. isPremium: subscriptions.hasPremiumAccess
  22. )
  23. Rectangle()
  24. .fill(AppTheme.border)
  25. .frame(width: 1)
  26. mainContent
  27. .frame(maxWidth: .infinity, maxHeight: .infinity)
  28. }
  29. .fullScreenLayoutWhenNeeded()
  30. .environment(\.requirePremiumAccess, {
  31. if subscriptions.isResolvingEntitlements {
  32. return subscriptions.hasPremiumAccess
  33. }
  34. if subscriptions.hasPremiumAccess { return true }
  35. viewModel.showPaywall()
  36. return false
  37. })
  38. if viewModel.isPaywallPresented {
  39. PaywallView(viewModel: paywallViewModel) {
  40. viewModel.dismissPaywall()
  41. }
  42. .frame(maxWidth: .infinity, maxHeight: .infinity)
  43. .ignoresSafeArea()
  44. .zIndex(1)
  45. .transition(.opacity)
  46. }
  47. }
  48. .animation(.easeInOut(duration: 0.2), value: viewModel.isPaywallPresented)
  49. .frame(maxWidth: .infinity, maxHeight: .infinity)
  50. .frame(minWidth: AppWindow.minWidth, minHeight: AppWindow.minHeight)
  51. .onChange(of: subscriptions.hasPremiumAccess) { _, hasPremium in
  52. viewModel.dismissPaywall()
  53. syncToolSubscriptionState()
  54. appRating.updatePremiumStatus(hasPremium)
  55. if subscriptions.hasResolvedPremiumStatus {
  56. configureRedditAccessForPremiumStatus()
  57. }
  58. }
  59. .onChange(of: subscriptions.hasEverPurchasedPremium) { _, _ in
  60. syncToolSubscriptionState()
  61. }
  62. .onChange(of: subscriptions.hasResolvedPremiumStatus) { _, resolved in
  63. if resolved {
  64. syncToolSubscriptionState()
  65. configureRedditAccessForPremiumStatus()
  66. }
  67. }
  68. .onChange(of: freeUsageManager.usedCount) { _, _ in
  69. syncToolSubscriptionState()
  70. }
  71. .onAppear {
  72. syncToolSubscriptionState()
  73. configureToolPaywallHandlers()
  74. appRating.updatePremiumStatus(subscriptions.hasPremiumAccess)
  75. if subscriptions.hasResolvedPremiumStatus {
  76. configureRedditAccessForPremiumStatus()
  77. }
  78. }
  79. .background(AppTheme.background)
  80. .alert("Sign-in issue", isPresented: authErrorBinding) {
  81. Button("OK") {
  82. viewModel.clearAuthError()
  83. }
  84. } message: {
  85. Text(viewModel.authErrorMessage ?? "")
  86. }
  87. }
  88. private var authErrorBinding: Binding<Bool> {
  89. Binding(
  90. get: { viewModel.authErrorMessage != nil },
  91. set: { isPresented in
  92. if !isPresented {
  93. viewModel.clearAuthError()
  94. }
  95. }
  96. )
  97. }
  98. @ViewBuilder
  99. private var mainContent: some View {
  100. VStack(spacing: 0) {
  101. if viewModel.isRedditActive {
  102. AppTheme.sidebarBackground
  103. .frame(height: AppWindow.redditTopInset)
  104. .frame(maxWidth: .infinity)
  105. .allowsHitTesting(false)
  106. }
  107. if viewModel.isRedditActive, viewModel.redditCanGoBack {
  108. RedditWebBackBar(
  109. canGoBack: true,
  110. onBack: { viewModel.goBackInReddit() }
  111. )
  112. }
  113. ZStack {
  114. if viewModel.isRedditActive || viewModel.hasOpenedReddit {
  115. RedditWebView(
  116. url: viewModel.redditLoadURL,
  117. reloadTrigger: viewModel.redditReloadTrigger,
  118. isDarkMode: appearanceManager.isDarkMode,
  119. goBackTrigger: viewModel.redditGoBackTrigger,
  120. onCanGoBackChange: { viewModel.updateRedditCanGoBack($0) },
  121. onAuthError: { viewModel.showAuthError($0) }
  122. )
  123. .id("reddit-webview")
  124. .opacity(viewModel.isRedditActive ? 1 : 0)
  125. .allowsHitTesting(viewModel.isRedditActive)
  126. .accessibilityHidden(!viewModel.isRedditActive)
  127. .frame(maxWidth: .infinity, maxHeight: .infinity)
  128. }
  129. if viewModel.isSettingsActive {
  130. SettingsView(viewModel: settingsViewModel)
  131. } else if viewModel.isHistoryActive {
  132. AIHistoryView(viewModel: historyViewModel) { entry in
  133. openHistoryEntry(entry)
  134. }
  135. } else if !viewModel.isRedditActive, let item = viewModel.selectedNavItem {
  136. toolContent(for: item)
  137. }
  138. }
  139. }
  140. .background(viewModel.isRedditActive ? AppTheme.sidebarBackground : AppTheme.background)
  141. .frame(maxWidth: .infinity, maxHeight: .infinity)
  142. .ignoresSafeArea(.container, edges: viewModel.isRedditActive ? .top : [])
  143. }
  144. @ViewBuilder
  145. private func toolContent(for item: SidebarNavItem) -> some View {
  146. switch item.iconKind {
  147. case .postGenerator:
  148. PostGeneratorView(viewModel: postGeneratorViewModel)
  149. case .titleOptimizer:
  150. TitleOptimizerView(viewModel: titleOptimizerViewModel)
  151. case .commentWriter:
  152. CommentWriterView(viewModel: commentWriterViewModel)
  153. default:
  154. ToolPlaceholderView(item: item)
  155. }
  156. }
  157. private func syncToolSubscriptionState() {
  158. let hasPremium = subscriptions.hasPremiumAccess
  159. let hasEverPurchased = subscriptions.hasEverPurchasedPremium
  160. postGeneratorViewModel.setSubscriptionState(
  161. hasPremiumAccess: hasPremium,
  162. hasEverPurchasedPremium: hasEverPurchased
  163. )
  164. titleOptimizerViewModel.setSubscriptionState(
  165. hasPremiumAccess: hasPremium,
  166. hasEverPurchasedPremium: hasEverPurchased
  167. )
  168. commentWriterViewModel.setSubscriptionState(
  169. hasPremiumAccess: hasPremium,
  170. hasEverPurchasedPremium: hasEverPurchased
  171. )
  172. }
  173. private func configureToolPaywallHandlers() {
  174. let showPaywall = { [viewModel] in viewModel.showPaywall() }
  175. postGeneratorViewModel.onPaywallRequired = showPaywall
  176. titleOptimizerViewModel.onPaywallRequired = showPaywall
  177. commentWriterViewModel.onPaywallRequired = showPaywall
  178. }
  179. private func configureRedditAccessForPremiumStatus() {
  180. guard subscriptions.hasResolvedPremiumStatus else { return }
  181. if !subscriptions.hasPremiumAccess, viewModel.isRedditActive {
  182. viewModel.deactivateRedditForPremiumGate()
  183. viewModel.selectNavItem(FrontPageViewModel.postGeneratorItem)
  184. }
  185. }
  186. private func openHistoryEntry(_ entry: AIHistoryEntry) {
  187. switch entry.toolKind {
  188. case .postGenerator:
  189. if let item = viewModel.createItems.first(where: { $0.iconKind == .postGenerator }) {
  190. postGeneratorViewModel.restore(from: entry)
  191. viewModel.selectNavItem(item)
  192. }
  193. case .titleOptimizer:
  194. if let item = viewModel.createItems.first(where: { $0.iconKind == .titleOptimizer }) {
  195. titleOptimizerViewModel.restore(from: entry)
  196. viewModel.selectNavItem(item)
  197. }
  198. case .commentWriter:
  199. if let item = viewModel.createItems.first(where: { $0.iconKind == .commentWriter }) {
  200. commentWriterViewModel.restore(from: entry)
  201. viewModel.selectNavItem(item)
  202. }
  203. }
  204. }
  205. }
  206. private struct ToolPlaceholderView: View {
  207. let item: SidebarNavItem
  208. var body: some View {
  209. VStack(spacing: 12) {
  210. ModernSidebarIconView(kind: item.iconKind, size: 56)
  211. Text(item.title)
  212. .font(.system(size: 20, weight: .semibold))
  213. .foregroundStyle(AppTheme.textPrimary)
  214. Text(item.subtitle)
  215. .font(.system(size: 13))
  216. .foregroundStyle(AppTheme.textSecondary)
  217. }
  218. .frame(maxWidth: .infinity, maxHeight: .infinity)
  219. .background(AppTheme.background)
  220. }
  221. }
  222. #Preview {
  223. FrontPageView(session: AppSessionState())
  224. .environmentObject(SubscriptionManager())
  225. .environmentObject(AppRatingManager())
  226. .environment(AppearanceManager.shared)
  227. .frame(width: AppWindow.width, height: AppWindow.height)
  228. }