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()118 119 WallpaperGrid(wallpapers: appState.wallpapers, appState: appState) {120 Task { await appState.loadMore() }121 }122 }123 }124}125126struct FavoritesView: View {127 @ObservedObject var appState: AppState128 129 var body: some View {130 if appState.favoritesManager.favorites.isEmpty {131 EmptyState(icon: "heart.slash", text: "No favorites yet")132 } else {133 WallpaperGrid(wallpapers: appState.favoritesManager.favorites, appState: appState, onEndReached: nil)134 }135 }136}137138struct HistoryView: View {139 @ObservedObject var appState: AppState140 141 var body: some View {142 VStack {143 if !appState.historyManager.history.isEmpty {144 HStack {145 Spacer()146 Button("Clear History") {147 appState.historyManager.clearHistory()148 }149 .font(.caption)150 .buttonStyle(.link)151 }152 .padding(.horizontal)153 .padding(.top, 8)154 }155 156 if appState.historyManager.history.isEmpty {157 EmptyState(icon: "clock.arrow.circlepath", text: "No history yet")158 } else {159 WallpaperGrid(wallpapers: appState.historyManager.history, appState: appState, onEndReached: nil)160 }161 }162 }163}164165struct SettingsView: View {166 @ObservedObject var appState: AppState167168 var body: some View {169 VStack(alignment: .leading, spacing: 24) {170 VStack(alignment: .leading, spacing: 12) {171 Text("Storage")172 .font(.headline)173174 Toggle("Launch at Login", isOn: $appState.launchAtLogin)175 Toggle("Save wallpapers locally", isOn: $appState.saveToFolderEnabled)176177 if appState.saveToFolderEnabled {178 HStack {179 Text("Location")180 Spacer()181 Text(URL(fileURLWithPath: appState.saveFolderPath).lastPathComponent)182 .foregroundColor(.secondary)183 .lineLimit(1)184 .truncationMode(.middle)185 Button("Change…") {186 appState.selectSaveFolder()187 }188 }189 }190 }191192 Spacer()193194 Button(role: .destructive) {195 appState.quit()196 } label: {197 Text("Quit Haven")198 .frame(maxWidth: .infinity)199 }200 .controlSize(.large)201 }202 .padding()203 .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)204 }205}206207struct EmptyState: View {208 let icon: String209 let text: String210 211 var body: some View {212 VStack(spacing: 16) {213 Image(systemName: icon)214 .font(.system(size: 48))215 .foregroundColor(.gray.opacity(0.5))216 Text(text)217 .foregroundColor(.secondary)218 }219 .frame(maxWidth: .infinity, maxHeight: .infinity)220 }221}222223struct TabButton: View {224 let icon: String225 let tab: AppTab226 @Binding var activeTab: AppTab227 228 var body: some View {229 Button {230 activeTab = tab231 } label: {232 Image(systemName: icon)233 .symbolVariant(activeTab == tab ? .fill : .none)234 .foregroundColor(activeTab == tab ? .accentColor : .secondary)235 .font(.system(size: 18))236 }237 .buttonStyle(.plain)238 }239}240241242struct VisualEffectView: NSViewRepresentable {243 let material: NSVisualEffectView.Material244 let blendingMode: NSVisualEffectView.BlendingMode245 246 func makeNSView(context: Context) -> NSVisualEffectView {247 let visualEffectView = NSVisualEffectView()248 visualEffectView.material = material249 visualEffectView.blendingMode = blendingMode250 visualEffectView.state = .active251 return visualEffectView252 }253 254 func updateNSView(_ nsView: NSVisualEffectView, context: Context) {255 nsView.material = material256 nsView.blendingMode = blendingMode257 }258}