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()113114 Divider()115116 117 118 119 HStack(spacing: 10) {120 ForEach(FeedMode.allCases) { mode in121 FeedModePill(mode: mode, isActive: appState.feedMode == mode) { select(mode) }122 }123 }124 .padding(.horizontal)125 .padding(.vertical, 10)126127 Divider()128129 WallpaperGrid(130 wallpapers: appState.wallpapers, appState: appState, onEndReached: loadMoreAction)131 }132 }133134 func resolutionOption(_ resolution: String) -> some View { Text(resolution).tag(resolution) }135 func ratioOption(_ ratio: String) -> some View { Text(ratio).tag(ratio) }136 func refresh() async { await appState.fetchWallpapers(reset: true) }137 func refreshAction() { Task { await refresh() } }138 func select(_ mode: FeedMode) {139 if appState.feedMode != mode { appState.feedMode = mode }140 }141 func loadMore() async { await appState.loadMore() }142 func loadMoreAction() { Task { await loadMore() } }143}144145struct FeedModePill: View {146 let mode: FeedMode147 let isActive: Bool148 let action: () -> Void149150 private var tint: Color {151 switch mode {152 case .latest: return Color(red: 0.45, green: 0.85, blue: 0.30) 153 case .toplist: return Color(red: 0.85, green: 0.40, blue: 0.85) 154 case .random: return Color(red: 0.98, green: 0.55, blue: 0.20) 155 }156 }157158 var body: some View {159 Button(action: action) {160 HStack(spacing: 6) {161 Image(systemName: mode.systemImage)162 Text(mode.displayName)163 .font(.system(size: 13, weight: .medium))164 }165 .padding(.horizontal, 14)166 .padding(.vertical, 8)167 .frame(maxWidth: .infinity)168 .foregroundColor(isActive ? .white : tint)169 .background(170 RoundedRectangle(cornerRadius: 10)171 .fill(isActive ? tint : Color.clear)172 )173 .overlay(174 RoundedRectangle(cornerRadius: 10)175 .stroke(tint.opacity(isActive ? 0 : 0.6), lineWidth: 1.5)176 )177 }178 .buttonStyle(.plain)179 }180}181182struct FavoritesView: View {183 @ObservedObject var appState: AppState184185 var body: some View {186 if appState.favoritesManager.favorites.isEmpty {187 EmptyState(icon: "heart.slash", text: "No favorites yet")188 } else {189 WallpaperGrid(190 wallpapers: appState.favoritesManager.favorites, appState: appState, onEndReached: nil)191 }192 }193}194195struct HistoryView: View {196 @ObservedObject var appState: AppState197198 var body: some View {199 VStack {200 if !appState.historyManager.history.isEmpty {201 HStack {202 Spacer()203 Button("Clear History") { clearHistory() }204 .font(.caption)205 .buttonStyle(.link)206 }207 .padding(.horizontal)208 .padding(.top, 8)209 }210211 if appState.historyManager.history.isEmpty {212 EmptyState(icon: "clock.arrow.circlepath", text: "No history yet")213 } else {214 WallpaperGrid(215 wallpapers: appState.historyManager.history, appState: appState, onEndReached: nil)216 }217 }218 }219220 func clearHistory() { appState.historyManager.clearHistory() }221}222223struct SettingsView: View {224 @ObservedObject var appState: AppState225226 var body: some View {227 VStack(alignment: .leading, spacing: 24) {228 VStack(alignment: .leading, spacing: 12) {229 Text("Storage")230 .font(.headline)231232 Toggle("Launch at Login", isOn: $appState.launchAtLogin)233 Toggle("Save wallpapers locally", isOn: $appState.saveToFolderEnabled)234235 if appState.saveToFolderEnabled {236 HStack {237 Text("Location")238 Spacer()239 Text(URL(fileURLWithPath: appState.saveFolderPath).lastPathComponent)240 .foregroundColor(.secondary)241 .lineLimit(1)242 .truncationMode(.middle)243 Button("Change…") { selectFolder() }244 }245 }246 }247248 Spacer()249250 Button(role: .destructive, action: quit) {251 Text("Quit Haven")252 .frame(maxWidth: .infinity)253 }254 .controlSize(.large)255 }256 .padding()257 .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)258 }259260 func selectFolder() { appState.selectSaveFolder() }261 func quit() { appState.quit() }262}263264struct EmptyState: View {265 let icon: String266 let text: String267268 var body: some View {269 VStack(spacing: 16) {270 Image(systemName: icon)271 .font(.system(size: 48))272 .foregroundColor(.gray.opacity(0.5))273 Text(text)274 .foregroundColor(.secondary)275 }276 .frame(maxWidth: .infinity, maxHeight: .infinity)277 }278}279280struct TabButton: View {281 let icon: String282 let tab: AppTab283 @Binding var activeTab: AppTab284285 var body: some View {286 Button(action: select) {287 Image(systemName: icon)288 .symbolVariant(activeTab == tab ? .fill : .none)289 .foregroundColor(activeTab == tab ? .accentColor : .secondary)290 .font(.system(size: 18))291 }292 .buttonStyle(.plain)293 }294295 func select() { activeTab = tab }296}297298299struct VisualEffectView: NSViewRepresentable {300 let material: NSVisualEffectView.Material301 let blendingMode: NSVisualEffectView.BlendingMode302303 func makeNSView(context: Context) -> NSVisualEffectView {304 let visualEffectView = NSVisualEffectView()305 visualEffectView.material = material306 visualEffectView.blendingMode = blendingMode307 visualEffectView.state = .active308 return visualEffectView309 }310311 func updateNSView(_ nsView: NSVisualEffectView, context: Context) {312 nsView.material = material313 nsView.blendingMode = blendingMode314 }315}