FrontPageView.swift 8.9 KB

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