Skip to content
This repository was archived by the owner on Mar 7, 2026. It is now read-only.

Commit 8132535

Browse files
authored
Add Sources Manager
1 parent 6cca61c commit 8132535

9 files changed

Lines changed: 486 additions & 41 deletions

File tree

Sources/prostore/prostore.swift

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ struct ProStore: App {
1717
}
1818

1919
struct MainSidebarView: View {
20+
@StateObject private var sourcesViewModel = SourcesViewModel()
2021
@State private var selected: SidebarItem? = .store
2122

2223
var body: some View {
@@ -46,22 +47,15 @@ struct MainSidebarView: View {
4647
}
4748
case .store:
4849
NavigationStack {
49-
AppsView(repoURLs: [
50-
URL(string: "https://repository.apptesters.org/")!,
51-
URL(string: "https://wuxu1.github.io/wuxu-complete.json")!,
52-
URL(string: "https://wuxu1.github.io/wuxu-complete-plus.json")!,
53-
URL(string: "https://raw.githubusercontent.com/swaggyP36000/TrollStore-IPAs/main/apps_esign.json")!,
54-
URL(string: "https://ipa.cypwn.xyz/cypwn.json")!,
55-
URL(string: "https://quarksources.github.io/dist/quantumsource.min.json")!,
56-
URL(string: "https://bit.ly/quantumsource-plus-min")!,
57-
URL(string: "https://raw.githubusercontent.com/Neoncat-OG/TrollStore-IPAs/main/apps_esign.json")!
58-
])
50+
AppsView()
51+
.environmentObject(sourcesViewModel)
5952
.navigationTitle("Store")
6053
.navigationBarTitleDisplayMode(.large)
6154
}
6255
case .settings:
6356
NavigationStack {
6457
SettingsView()
58+
.environmentObject(sourcesViewModel)
6559
.navigationTitle("Settings")
6660
.navigationBarTitleDisplayMode(.large)
6761
}
@@ -84,7 +78,4 @@ enum SidebarItem: Hashable {
8478
case store
8579
case settings
8680

87-
}
88-
89-
90-
81+
}
File renamed without changes.

Sources/prostore/ModelsAndManagers.swift renamed to Sources/prostore/viewModelsAndHelpers/ModelsAndManagers.swift

File renamed without changes.
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
import SwiftUI
2+
import Combine
3+
4+
class SourcesViewModel: ObservableObject {
5+
@Published var sources: [Source] = []
6+
@Published var validationStates: [URL: ValidationState] = [:]
7+
@Published var isAddingNew = false
8+
@Published var newSourceURL = ""
9+
@Published var editingSource: Source? = nil
10+
11+
private let fileURL: URL
12+
13+
enum ValidationState {
14+
case pending
15+
case loading
16+
case valid
17+
case invalid(Error)
18+
19+
var description: String {
20+
switch self {
21+
case .pending: return "Not checked"
22+
case .loading: return "Checking..."
23+
case .valid: return "✓ Valid"
24+
case .invalid(let error): return "✗ Error: \(error.localizedDescription)"
25+
}
26+
}
27+
28+
var icon: String {
29+
switch self {
30+
case .pending: return "questionmark.circle"
31+
case .loading: return "arrow.triangle.2.circlepath"
32+
case .valid: return "checkmark.circle.fill"
33+
case .invalid: return "xmark.circle.fill"
34+
}
35+
}
36+
37+
var color: Color {
38+
switch self {
39+
case .pending: return .gray
40+
case .loading: return .blue
41+
case .valid: return .green
42+
case .invalid: return .red
43+
}
44+
}
45+
}
46+
47+
struct Source: Identifiable, Codable, Equatable {
48+
let id: UUID
49+
var urlString: String
50+
51+
var url: URL? {
52+
URL(string: urlString)
53+
}
54+
55+
init(id: UUID = UUID(), urlString: String) {
56+
self.id = id
57+
self.urlString = urlString
58+
}
59+
60+
static func == (lhs: Source, rhs: Source) -> Bool {
61+
lhs.id == rhs.id && lhs.urlString == rhs.urlString
62+
}
63+
}
64+
65+
init() {
66+
let appFolder = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
67+
self.fileURL = appFolder.appendingPathComponent("sources.json")
68+
loadSources()
69+
}
70+
71+
private func loadSources() {
72+
do {
73+
if FileManager.default.fileExists(atPath: fileURL.path) {
74+
let data = try Data(contentsOf: fileURL)
75+
let decoded = try JSONDecoder().decode([Source].self, from: data)
76+
self.sources = decoded
77+
} else {
78+
// Load default sources
79+
let defaultSources = [
80+
"https://repository.apptesters.org/",
81+
"https://wuxu1.github.io/wuxu-complete.json",
82+
"https://wuxu1.github.io/wuxu-complete-plus.json",
83+
"https://raw.githubusercontent.com/swaggyP36000/TrollStore-IPAs/main/apps_esign.json",
84+
"https://ipa.cypwn.xyz/cypwn.json",
85+
"https://quarksources.github.io/dist/quantumsource.min.json",
86+
"https://bit.ly/quantumsource-plus-min",
87+
"https://raw.githubusercontent.com/Neoncat-OG/TrollStore-IPAs/main/apps_esign.json"
88+
]
89+
self.sources = defaultSources.map { Source(urlString: $0) }
90+
saveSources()
91+
}
92+
} catch {
93+
print("Failed to load sources: \(error)")
94+
loadDefaultSources()
95+
}
96+
}
97+
98+
private func loadDefaultSources() {
99+
let defaultSources = [
100+
"https://repository.apptesters.org/",
101+
"https://wuxu1.github.io/wuxu-complete.json",
102+
"https://wuxu1.github.io/wuxu-complete-plus.json",
103+
"https://raw.githubusercontent.com/swaggyP36000/TrollStore-IPAs/main/apps_esign.json",
104+
"https://ipa.cypwn.xyz/cypwn.json",
105+
"https://quarksources.github.io/dist/quantumsource.min.json",
106+
"https://bit.ly/quantumsource-plus-min",
107+
"https://raw.githubusercontent.com/Neoncat-OG/TrollStore-IPAs/main/apps_esign.json"
108+
]
109+
self.sources = defaultSources.map { Source(urlString: $0) }
110+
saveSources()
111+
}
112+
113+
private func saveSources() {
114+
do {
115+
let data = try JSONEncoder().encode(sources)
116+
try data.write(to: fileURL)
117+
} catch {
118+
print("Failed to save sources: \(error)")
119+
}
120+
}
121+
122+
func addSource(urlString: String) {
123+
let trimmed = urlString.trimmingCharacters(in: .whitespacesAndNewlines)
124+
guard !trimmed.isEmpty else { return }
125+
126+
var formattedURL = trimmed
127+
128+
// Add https:// if no scheme is present
129+
if !formattedURL.hasPrefix("http://") && !formattedURL.hasPrefix("https://") {
130+
formattedURL = "https://" + formattedURL
131+
}
132+
133+
// Force HTTPS (convert http:// to https://)
134+
if formattedURL.hasPrefix("http://") {
135+
formattedURL = formattedURL.replacingOccurrences(of: "http://", with: "https://")
136+
}
137+
138+
let newSource = Source(urlString: formattedURL)
139+
sources.append(newSource)
140+
saveSources()
141+
validateSource(newSource)
142+
}
143+
144+
func deleteSource(at indexSet: IndexSet) {
145+
sources.remove(atOffsets: indexSet)
146+
saveSources()
147+
}
148+
149+
func moveSource(from source: IndexSet, to destination: Int) {
150+
sources.move(fromOffsets: source, toOffset: destination)
151+
saveSources()
152+
}
153+
154+
func startEditing(_ source: Source) {
155+
editingSource = source
156+
newSourceURL = source.urlString
157+
}
158+
159+
func updateSource(source: Source, newURLString: String) {
160+
guard let index = sources.firstIndex(where: { $0.id == source.id }) else { return }
161+
162+
var formattedURL = newURLString.trimmingCharacters(in: .whitespacesAndNewlines)
163+
164+
// Add https:// if no scheme is present
165+
if !formattedURL.hasPrefix("http://") && !formattedURL.hasPrefix("https://") {
166+
formattedURL = "https://" + formattedURL
167+
}
168+
169+
// Force HTTPS
170+
if formattedURL.hasPrefix("http://") {
171+
formattedURL = formattedURL.replacingOccurrences(of: "http://", with: "https://")
172+
}
173+
174+
sources[index].urlString = formattedURL
175+
saveSources()
176+
validateSource(sources[index])
177+
editingSource = nil
178+
newSourceURL = ""
179+
}
180+
181+
func validateSource(_ source: Source) {
182+
guard let url = source.url else {
183+
validationStates[source.urlString] = .invalid(NSError(domain: "Invalid URL", code: 0, userInfo: nil))
184+
return
185+
}
186+
187+
validationStates[url] = .loading
188+
189+
var request = URLRequest(url: url)
190+
request.setValue("AppTestersListView/1.0 (iOS)", forHTTPHeaderField: "User-Agent")
191+
request.timeoutInterval = 10
192+
193+
URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
194+
DispatchQueue.main.async {
195+
if let error = error {
196+
self?.validationStates[url] = .invalid(error)
197+
} else if let httpResponse = response as? HTTPURLResponse, !(200...299).contains(httpResponse.statusCode) {
198+
let error = NSError(domain: "HTTP Error", code: httpResponse.statusCode, userInfo: nil)
199+
self?.validationStates[url] = .invalid(error)
200+
} else {
201+
self?.validationStates[url] = .valid
202+
}
203+
}
204+
}.resume()
205+
}
206+
207+
func validateAllSources() {
208+
for source in sources {
209+
validateSource(source)
210+
}
211+
}
212+
213+
func getSourcesURLs() -> [URL] {
214+
sources.compactMap { $0.url }
215+
}
216+
}

Sources/prostore/UIComponents.swift renamed to Sources/prostore/viewModelsAndHelpers/UIComponents.swift

File renamed without changes.

0 commit comments

Comments
 (0)