summaryrefslogtreecommitdiff
path: root/Hutch/Views/Projects/ProjectPinStore.swift
blob: ed7e3864ac8aaf631383ba70b98f3cee37e1d2c3 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
import Foundation

enum ProjectPinStore {
    static func loadPinnedProjectIDs(
        for userKey: String,
        defaults: UserDefaults = .standard
    ) -> [String] {
        let generalizedPins = HomePinStore.pinnedProjectIDs(for: userKey, defaults: defaults)
        if !generalizedPins.isEmpty {
            return generalizedPins
        }

        let pinnedProjects = loadAll(defaults: defaults)
        return normalizedProjectIDs(pinnedProjects[userKey] ?? [])
    }

    static func isPinned(
        projectID: String,
        for userKey: String,
        defaults: UserDefaults = .standard
    ) -> Bool {
        loadPinnedProjectIDs(for: userKey, defaults: defaults).contains(projectID)
    }

    static func togglePin(
        projectID: String,
        for userKey: String,
        defaults: UserDefaults = .standard
    ) {
        let trimmed = projectID.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !trimmed.isEmpty else { return }

        HomePinStore.togglePin(
            HomePinRecord(
                kind: .project,
                value: trimmed,
                title: "Pinned Project",
                subtitle: "Project",
                ownerUsername: nil,
                service: nil
            ),
            for: userKey,
            defaults: defaults
        )

        var pinnedProjects = loadAll(defaults: defaults)
        var projectIDs = normalizedProjectIDs(pinnedProjects[userKey] ?? [])

        if let index = projectIDs.firstIndex(of: projectID) {
            projectIDs.remove(at: index)
        } else {
            if let normalizedProjectID = normalizedProjectID(projectID) {
                projectIDs.append(normalizedProjectID)
            }
        }

        pinnedProjects[userKey] = projectIDs
        save(pinnedProjects, defaults: defaults)
    }

    static func loadLegacyPinnedProjectIDs(
        for userKey: String,
        defaults: UserDefaults = .standard
    ) -> [String] {
        let pinnedProjects = loadAll(defaults: defaults)
        return normalizedProjectIDs(pinnedProjects[userKey] ?? [])
    }

    private static func loadAll(defaults: UserDefaults) -> [String: [String]] {
        guard let data = defaults.data(forKey: AppStorageKeys.pinnedHomeProjects) else {
            return [:]
        }

        let decoded = (try? JSONDecoder().decode([String: [String]].self, from: data)) ?? [:]
        return decoded.reduce(into: [String: [String]]()) { result, entry in
            let normalizedUserKey = entry.key.trimmingCharacters(in: .whitespacesAndNewlines)
            guard !normalizedUserKey.isEmpty else { return }
            let normalizedProjectIDs = normalizedProjectIDs(entry.value)
            if !normalizedProjectIDs.isEmpty {
                result[normalizedUserKey] = normalizedProjectIDs
            }
        }
    }

    private static func save(_ pinnedProjects: [String: [String]], defaults: UserDefaults) {
        guard let data = try? JSONEncoder().encode(pinnedProjects) else { return }
        defaults.set(data, forKey: AppStorageKeys.pinnedHomeProjects)
    }

    private static func normalizedProjectIDs(_ projectIDs: [String]) -> [String] {
        var seen = Set<String>()
        return projectIDs.compactMap { projectID in
            guard let normalizedProjectID = normalizedProjectID(projectID) else { return nil }
            guard seen.insert(normalizedProjectID).inserted else { return nil }
            return normalizedProjectID
        }
    }

    private static func normalizedProjectID(_ projectID: String) -> String? {
        let trimmed = projectID.trimmingCharacters(in: .whitespacesAndNewlines)
        return trimmed.isEmpty ? nil : trimmed
    }
}