FrontPageView.swift 8.6 KB

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