krz/hutch

an ios client for sourcehut

clone: git clone https://gitbay.org/krz/hutch.git

v3.10.0: Hutch/Views/Projects/ProjectPinStore.swift · raw

  1import Foundation
  2
  3enum ProjectPinStore {
  4    static func loadPinnedProjectIDs(
  5        for userKey: String,
  6        defaults: UserDefaults = .standard
  7    ) -> [String] {
  8        let generalizedPins = HomePinStore.pinnedProjectIDs(for: userKey, defaults: defaults)
  9        if !generalizedPins.isEmpty {
 10            return generalizedPins
 11        }
 12
 13        let pinnedProjects = loadAll(defaults: defaults)
 14        return normalizedProjectIDs(pinnedProjects[userKey] ?? [])
 15    }
 16
 17    static func isPinned(
 18        projectID: String,
 19        for userKey: String,
 20        defaults: UserDefaults = .standard
 21    ) -> Bool {
 22        loadPinnedProjectIDs(for: userKey, defaults: defaults).contains(projectID)
 23    }
 24
 25    static func togglePin(
 26        projectID: String,
 27        for userKey: String,
 28        defaults: UserDefaults = .standard
 29    ) {
 30        let trimmed = projectID.trimmingCharacters(in: .whitespacesAndNewlines)
 31        guard !trimmed.isEmpty else { return }
 32
 33        HomePinStore.togglePin(
 34            HomePinRecord(
 35                kind: .project,
 36                value: trimmed,
 37                title: "Pinned Project",
 38                subtitle: "Project",
 39                ownerUsername: nil,
 40                service: nil
 41            ),
 42            for: userKey,
 43            defaults: defaults
 44        )
 45
 46        var pinnedProjects = loadAll(defaults: defaults)
 47        var projectIDs = normalizedProjectIDs(pinnedProjects[userKey] ?? [])
 48
 49        if let index = projectIDs.firstIndex(of: projectID) {
 50            projectIDs.remove(at: index)
 51        } else {
 52            if let normalizedProjectID = normalizedProjectID(projectID) {
 53                projectIDs.append(normalizedProjectID)
 54            }
 55        }
 56
 57        pinnedProjects[userKey] = projectIDs
 58        save(pinnedProjects, defaults: defaults)
 59    }
 60
 61    static func loadLegacyPinnedProjectIDs(
 62        for userKey: String,
 63        defaults: UserDefaults = .standard
 64    ) -> [String] {
 65        let pinnedProjects = loadAll(defaults: defaults)
 66        return normalizedProjectIDs(pinnedProjects[userKey] ?? [])
 67    }
 68
 69    private static func loadAll(defaults: UserDefaults) -> [String: [String]] {
 70        guard let data = defaults.data(forKey: AppStorageKeys.pinnedHomeProjects) else {
 71            return [:]
 72        }
 73
 74        let decoded = (try? JSONDecoder().decode([String: [String]].self, from: data)) ?? [:]
 75        return decoded.reduce(into: [String: [String]]()) { result, entry in
 76            let normalizedUserKey = entry.key.trimmingCharacters(in: .whitespacesAndNewlines)
 77            guard !normalizedUserKey.isEmpty else { return }
 78            let normalizedProjectIDs = normalizedProjectIDs(entry.value)
 79            if !normalizedProjectIDs.isEmpty {
 80                result[normalizedUserKey] = normalizedProjectIDs
 81            }
 82        }
 83    }
 84
 85    private static func save(_ pinnedProjects: [String: [String]], defaults: UserDefaults) {
 86        guard let data = try? JSONEncoder().encode(pinnedProjects) else { return }
 87        defaults.set(data, forKey: AppStorageKeys.pinnedHomeProjects)
 88    }
 89
 90    private static func normalizedProjectIDs(_ projectIDs: [String]) -> [String] {
 91        var seen = Set<String>()
 92        return projectIDs.compactMap { projectID in
 93            guard let normalizedProjectID = normalizedProjectID(projectID) else { return nil }
 94            guard seen.insert(normalizedProjectID).inserted else { return nil }
 95            return normalizedProjectID
 96        }
 97    }
 98
 99    private static func normalizedProjectID(_ projectID: String) -> String? {
100        let trimmed = projectID.trimmingCharacters(in: .whitespacesAndNewlines)
101        return trimmed.isEmpty ? nil : trimmed
102    }
103}