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+ }
0 commit comments