1import SwiftUI23struct MainView: View {4 @StateObject private var appState = AppState()5 6 var body: some View {7 ZStack {8 VStack(spacing: 0) {9 10 HStack {11 Text("Haven")12 .font(.custom("Avenir Next Medium", size: 18))13 14 Spacer()15 16 17 HStack(spacing: 16) {18 TabButton(icon: "safari", tab: .explore, activeTab: $appState.activeTab)19 TabButton(icon: "folder", tab: .local, activeTab: $appState.activeTab)20 TabButton(icon: "heart", tab: .favorites, activeTab: $appState.activeTab)21 TabButton(icon: "clock", tab: .history, activeTab: $appState.activeTab)22 TabButton(icon: "gearshape", tab: .settings, activeTab: $appState.activeTab)23 }24 }25 .padding()26 .background(VisualEffectView(material: .headerView, blendingMode: .behindWindow))27 28 Divider()29 30 31 ZStack {32 switch appState.activeTab {33 case .explore:34 ExploreView(appState: appState)35 case .local:36 LocalView(appState: appState)37 case .favorites:38 FavoritesView(appState: appState)39 case .history:40 HistoryView(appState: appState)41 case .settings:42 SettingsView(appState: appState)43 }44 }45 .frame(maxWidth: .infinity, maxHeight: .infinity)46 }47 }48 .frame(width: 450, height: 640)49 .background(Color(nsColor: .windowBackgroundColor))50 .onAppear {51 Task { await appState.fetchWallpapers() }52 }53 }54}55565758struct ExploreView: View {59 @ObservedObject var appState: AppState60 61 var body: some View {62 VStack(spacing: 0) {63 64 HStack {65 HStack {66 Image(systemName: "magnifyingglass")67 .foregroundColor(.secondary)68 TextField("Search...", text: $appState.searchText)69 }70 .padding(8)71 .background(Color(nsColor: .controlBackgroundColor))72 .cornerRadius(8)73 74 Menu {75 Section("Categories") {76 Toggle("General", isOn: $appState.categories.general)77 Toggle("Anime", isOn: $appState.categories.anime)78 Toggle("People", isOn: $appState.categories.people)79 }80 81 Section("Purity") {82 Toggle("SFW", isOn: $appState.purity.sfw)83 Toggle("Sketchy", isOn: $appState.purity.sketchy)84 }85 86 Divider()87 88 Picker("Resolution", selection: $appState.selectedResolution) {89 ForEach(appState.availableResolutions, id: \.self) { res in90 Text(res).tag(res)91 }92 }93 Picker("Ratio", selection: $appState.selectedRatio) {94 ForEach(appState.availableRatios, id: \.self) { ratio in95 Text(ratio).tag(ratio)96 }97 }98 } label: {99 Image(systemName: "slider.horizontal.3")100 }101102 .menuStyle(.borderlessButton)103 .frame(width: 30)104 105 Button {106 Task { await appState.fetchWallpapers(reset: true) }107 } label: {108 Image(systemName: "arrow.clockwise")109 }110 .buttonStyle(.plain)111 .frame(width: 30)112 }113 .padding()114 .onChange(of: appState.selectedResolution) { _ in Task { await appState.fetchWallpapers() } }115 .onChange(of: appState.selectedRatio) { _ in Task { await appState.fetchWallpapers() } }116 117 Divider()118119 120 121 122 HStack(spacing: 10) {123 ForEach(FeedMode.allCases) { mode in124 FeedModePill(mode: mode, isActive: appState.feedMode == mode) {125 if appState.feedMode != mode {126 appState.feedMode = mode127 }128 }129 }130 }131 .padding(.horizontal)132 .padding(.vertical, 10)133134 Divider()135136 WallpaperGrid(wallpapers: appState.wallpapers, appState: appState) {137 Task { await appState.loadMore() }138 }139 }140 }141}142143struct FeedModePill: View {144 let mode: FeedMode145 let isActive: Bool146 let action: () -> Void147148 private var tint: Color {149 switch mode {150 case .latest: return Color(red: 0.45, green: 0.85, blue: 0.30) 151 case .toplist: return Color(red: 0.85, green: 0.40, blue: 0.85) 152 case .random: return Color(red: 0.98, green: 0.55, blue: 0.20) 153 }154 }155156 var body: some View {157 Button(action: action) {158 HStack(spacing: 6) {159 Image(systemName: mode.systemImage)160 Text(mode.displayName)161 .font(.system(size: 13, weight: .medium))162 }163 .padding(.horizontal, 14)164 .padding(.vertical, 8)165 .frame(maxWidth: .infinity)166 .foregroundColor(isActive ? .white : tint)167 .background(168 RoundedRectangle(cornerRadius: 10)169 .fill(isActive ? tint : Color.clear)170 )171 .overlay(172 RoundedRectangle(cornerRadius: 10)173 .stroke(tint.opacity(isActive ? 0 : 0.6), lineWidth: 1.5)174 )175 }176 .buttonStyle(.plain)177 }178}179180struct FavoritesView: View {181 @ObservedObject var appState: AppState182 183 var body: some View {184 if appState.favoritesManager.favorites.isEmpty {185 EmptyState(icon: "heart.slash", text: "No favorites yet")186 } else {187 WallpaperGrid(wallpapers: appState.favoritesManager.favorites, appState: appState, onEndReached: nil)188 }189 }190}191192struct HistoryView: View {193 @ObservedObject var appState: AppState194 195 var body: some View {196 VStack {197 if !appState.historyManager.history.isEmpty {198 HStack {199 Spacer()200 Button("Clear History") {201 appState.historyManager.clearHistory()202 }203 .font(.caption)204 .buttonStyle(.link)205 }206 .padding(.horizontal)207 .padding(.top, 8)208 }209 210 if appState.historyManager.history.isEmpty {211 EmptyState(icon: "clock.arrow.circlepath", text: "No history yet")212 } else {213 WallpaperGrid(wallpapers: appState.historyManager.history, appState: appState, onEndReached: nil)214 }215 }216 }217}218219struct SettingsView: View {220 @ObservedObject var appState: AppState221222 var body: some View {223 VStack(alignment: .leading, spacing: 24) {224 VStack(alignment: .leading, spacing: 12) {225 Text("Storage")226 .font(.headline)227228 Toggle("Launch at Login", isOn: $appState.launchAtLogin)229 Toggle("Save wallpapers locally", isOn: $appState.saveToFolderEnabled)230231 if appState.saveToFolderEnabled {232 HStack {233 Text("Location")234 Spacer()235 Text(URL(fileURLWithPath: appState.saveFolderPath).lastPathComponent)236 .foregroundColor(.secondary)237 .lineLimit(1)238 .truncationMode(.middle)239 Button("Change…") {240 appState.selectSaveFolder()241 }242 }243 }244 }245246 Spacer()247248 Button(role: .destructive) {249 appState.quit()250 } label: {251 Text("Quit Haven")252 .frame(maxWidth: .infinity)253 }254 .controlSize(.large)255 }256 .padding()257 .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)258 }259}260261struct EmptyState: View {262 let icon: String263 let text: String264 265 var body: some View {266 VStack(spacing: 16) {267 Image(systemName: icon)268 .font(.system(size: 48))269 .foregroundColor(.gray.opacity(0.5))270 Text(text)271 .foregroundColor(.secondary)272 }273 .frame(maxWidth: .infinity, maxHeight: .infinity)274 }275}276277struct TabButton: View {278 let icon: String279 let tab: AppTab280 @Binding var activeTab: AppTab281 282 var body: some View {283 Button {284 activeTab = tab285 } label: {286 Image(systemName: icon)287 .symbolVariant(activeTab == tab ? .fill : .none)288 .foregroundColor(activeTab == tab ? .accentColor : .secondary)289 .font(.system(size: 18))290 }291 .buttonStyle(.plain)292 }293}294295296struct VisualEffectView: NSViewRepresentable {297 let material: NSVisualEffectView.Material298 let blendingMode: NSVisualEffectView.BlendingMode299 300 func makeNSView(context: Context) -> NSVisualEffectView {301 let visualEffectView = NSVisualEffectView()302 visualEffectView.material = material303 visualEffectView.blendingMode = blendingMode304 visualEffectView.state = .active305 return visualEffectView306 }307 308 func updateNSView(_ nsView: NSVisualEffectView, context: Context) {309 nsView.material = material310 nsView.blendingMode = blendingMode311 }312}