FrontPageView.swift 8.1 KB

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