MainView.swift

10.5 KB · raw

1import SwiftUI23struct MainView: View {4    @StateObject private var appState = AppState()5    6    var body: some View {7        ZStack {8            VStack(spacing: 0) {9                // Header (Universal)10                HStack {11                    Text("Haven")12                        .font(.custom("Avenir Next Medium", size: 18))13                    14                    Spacer()15                    16                    // Tab Bar17                    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                // Content31                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}5556// MARK: - Subviews5758struct ExploreView: View {59    @ObservedObject var appState: AppState60    61    var body: some View {62        VStack(spacing: 0) {63            // Controls64            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            // Feed-mode selector: Latest, Toplist, Random. The active mode is120            // visually emphasised; taps update appState.feedMode and trigger121            // a reset fetch via the Combine observer in AppState.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)   // green151        case .toplist: return Color(red: 0.85, green: 0.40, blue: 0.85)  // magenta152        case .random: return Color(red: 0.98, green: 0.55, blue: 0.20)   // orange153        }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}294295// Redeclaring this as it was in the original file, or we can move to a separate file296struct 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}