1import SwiftUI23struct MainView: View {4 @StateObject private var appState: AppState56 init(appState: AppState? = nil) {7 let appState = appState ?? AppState()8 _appState = StateObject(wrappedValue: appState)9 }1011 var body: some View {12 ZStack {13 VStack(spacing: 0) {14 15 HStack {16 Text("Haven")17 .font(.custom("Avenir Next Medium", size: 18))1819 Spacer()2021 22 HStack(spacing: 16) {23 TabButton(icon: "safari", tab: .explore, activeTab: $appState.activeTab)24 TabButton(icon: "folder", tab: .local, activeTab: $appState.activeTab)25 TabButton(icon: "heart", tab: .favorites, activeTab: $appState.activeTab)26 TabButton(icon: "clock", tab: .history, activeTab: $appState.activeTab)27 TabButton(icon: "gearshape", tab: .settings, activeTab: $appState.activeTab)28 }29 }30 .padding()31 .background(VisualEffectView(material: .headerView, blendingMode: .behindWindow))3233 Divider()3435 36 ZStack {37 switch appState.activeTab {38 case .explore:39 ExploreView(appState: appState)40 case .local:41 LocalView(appState: appState)42 case .favorites:43 FavoritesView(appState: appState)44 case .history:45 HistoryView(appState: appState)46 case .settings:47 SettingsView(appState: appState)48 }49 }50 .frame(maxWidth: .infinity, maxHeight: .infinity)51 }52 }53 .frame(width: 450, height: 640)54 .background(Color(nsColor: .windowBackgroundColor))55 .onAppear {56 Task { await appState.fetchWallpapers() }57 }58 }59}60616263struct ExploreView: View {64 @ObservedObject var appState: AppState6566 var body: some View {67 VStack(spacing: 0) {68 69 HStack {70 HStack {71 Image(systemName: "magnifyingglass")72 .foregroundColor(.secondary)73 TextField("Search...", text: $appState.searchText)74 }75 .padding(8)76 .background(Color(nsColor: .controlBackgroundColor))77 .cornerRadius(8)7879 Menu {80 Section("Categories") {81 Toggle("General", isOn: $appState.categories.general)82 Toggle("Anime", isOn: $appState.categories.anime)83 Toggle("People", isOn: $appState.categories.people)84 }8586 Section("Purity") {87 Toggle("SFW", isOn: $appState.purity.sfw)88 Toggle("Sketchy", isOn: $appState.purity.sketchy)89 }9091 Divider()9293 Picker("Resolution", selection: $appState.selectedResolution) {94 ForEach(appState.availableResolutions, id: \.self) { resolutionOption($0) }95 }96 Picker("Ratio", selection: $appState.selectedRatio) {97 ForEach(appState.availableRatios, id: \.self) { ratioOption($0) }98 }99 } label: {100 Image(systemName: "slider.horizontal.3")101 }102103 .menuStyle(.borderlessButton)104 .frame(width: 30)105106 Button(action: refreshAction) {107 Image(systemName: "arrow.clockwise")108 }109 .buttonStyle(.plain)110 .frame(width: 30)111 }112 .padding()113 .onChange(of: appState.selectedResolution) { _ in Task { await appState.fetchWallpapers() } }114 .onChange(of: appState.selectedRatio) { _ in Task { await appState.fetchWallpapers() } }115116 Divider()117118 119 120 121 HStack(spacing: 10) {122 ForEach(FeedMode.allCases) { mode in123 FeedModePill(mode: mode, isActive: appState.feedMode == mode) { select(mode) }124 }125 }126 .padding(.horizontal)127 .padding(.vertical, 10)128129 Divider()130131 WallpaperGrid(132 wallpapers: appState.wallpapers, appState: appState, onEndReached: loadMoreAction)133 }134 }135136 func resolutionOption(_ resolution: String) -> some View { Text(resolution).tag(resolution) }137 func ratioOption(_ ratio: String) -> some View { Text(ratio).tag(ratio) }138 func refresh() async { await appState.fetchWallpapers(reset: true) }139 func refreshAction() { Task { await refresh() } }140 func select(_ mode: FeedMode) {141 if appState.feedMode != mode { appState.feedMode = mode }142 }143 func loadMore() async { await appState.loadMore() }144 func loadMoreAction() { Task { await loadMore() } }145}146147struct FeedModePill: View {148 let mode: FeedMode149 let isActive: Bool150 let action: () -> Void151152 private var tint: Color {153 switch mode {154 case .latest: return Color(red: 0.45, green: 0.85, blue: 0.30) 155 case .toplist: return Color(red: 0.85, green: 0.40, blue: 0.85) 156 case .random: return Color(red: 0.98, green: 0.55, blue: 0.20) 157 }158 }159160 var body: some View {161 Button(action: action) {162 HStack(spacing: 6) {163 Image(systemName: mode.systemImage)164 Text(mode.displayName)165 .font(.system(size: 13, weight: .medium))166 }167 .padding(.horizontal, 14)168 .padding(.vertical, 8)169 .frame(maxWidth: .infinity)170 .foregroundColor(isActive ? .white : tint)171 .background(172 RoundedRectangle(cornerRadius: 10)173 .fill(isActive ? tint : Color.clear)174 )175 .overlay(176 RoundedRectangle(cornerRadius: 10)177 .stroke(tint.opacity(isActive ? 0 : 0.6), lineWidth: 1.5)178 )179 }180 .buttonStyle(.plain)181 }182}183184struct FavoritesView: View {185 @ObservedObject var appState: AppState186187 var body: some View {188 if appState.favoritesManager.favorites.isEmpty {189 EmptyState(icon: "heart.slash", text: "No favorites yet")190 } else {191 WallpaperGrid(192 wallpapers: appState.favoritesManager.favorites, appState: appState, onEndReached: nil)193 }194 }195}196197struct HistoryView: View {198 @ObservedObject var appState: AppState199200 var body: some View {201 VStack {202 if !appState.historyManager.history.isEmpty {203 HStack {204 Spacer()205 Button("Clear History") { clearHistory() }206 .font(.caption)207 .buttonStyle(.link)208 }209 .padding(.horizontal)210 .padding(.top, 8)211 }212213 if appState.historyManager.history.isEmpty {214 EmptyState(icon: "clock.arrow.circlepath", text: "No history yet")215 } else {216 WallpaperGrid(217 wallpapers: appState.historyManager.history, appState: appState, onEndReached: nil)218 }219 }220 }221222 func clearHistory() { appState.historyManager.clearHistory() }223}224225struct SettingsView: View {226 @ObservedObject var appState: AppState227228 var body: some View {229 VStack(alignment: .leading, spacing: 24) {230 VStack(alignment: .leading, spacing: 12) {231 Text("Storage")232 .font(.headline)233234 Toggle("Launch at Login", isOn: $appState.launchAtLogin)235 Toggle("Save wallpapers locally", isOn: $appState.saveToFolderEnabled)236237 if appState.saveToFolderEnabled {238 HStack {239 Text("Location")240 Spacer()241 Text(URL(fileURLWithPath: appState.saveFolderPath).lastPathComponent)242 .foregroundColor(.secondary)243 .lineLimit(1)244 .truncationMode(.middle)245 Button("Change…") { selectFolder() }246 }247 }248 }249250 Spacer()251252 Button(role: .destructive, action: quit) {253 Text("Quit Haven")254 .frame(maxWidth: .infinity)255 }256 .controlSize(.large)257 }258 .padding()259 .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)260 }261262 func selectFolder() { appState.selectSaveFolder() }263 func quit() { appState.quit() }264}265266struct EmptyState: View {267 let icon: String268 let text: String269270 var body: some View {271 VStack(spacing: 16) {272 Image(systemName: icon)273 .font(.system(size: 48))274 .foregroundColor(.gray.opacity(0.5))275 Text(text)276 .foregroundColor(.secondary)277 }278 .frame(maxWidth: .infinity, maxHeight: .infinity)279 }280}281282struct TabButton: View {283 let icon: String284 let tab: AppTab285 @Binding var activeTab: AppTab286287 var body: some View {288 Button(action: select) {289 Image(systemName: icon)290 .symbolVariant(activeTab == tab ? .fill : .none)291 .foregroundColor(activeTab == tab ? .accentColor : .secondary)292 .font(.system(size: 18))293 }294 .buttonStyle(.plain)295 }296297 func select() { activeTab = tab }298}299300301struct VisualEffectView: NSViewRepresentable {302 let material: NSVisualEffectView.Material303 let blendingMode: NSVisualEffectView.BlendingMode304305 func makeNSView(context: Context) -> NSVisualEffectView {306 let visualEffectView = NSVisualEffectView()307 visualEffectView.material = material308 visualEffectView.blendingMode = blendingMode309 visualEffectView.state = .active310 return visualEffectView311 }312313 func updateNSView(_ nsView: NSVisualEffectView, context: Context) {314 nsView.material = material315 nsView.blendingMode = blendingMode316 }317}