FrontPageView.swift 8.4 KB

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