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

Commit 8ec8a15

Browse files
committed
feat: add credits to aboutview
1 parent a923b23 commit 8ec8a15

File tree

7 files changed

+240
-59
lines changed

7 files changed

+240
-59
lines changed

Protokolle/Extensions/Image+appIconStyle.swift renamed to Protokolle/Extensions/Image++.swift

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,18 @@ import SwiftUI
99

1010
extension Image {
1111
/// Applies a certain style to an image
12-
func appIconStyle(size: CGFloat = 56, lineWidth: CGFloat = 1) -> some View {
12+
func appIconStyle(
13+
size: CGFloat = 56,
14+
lineWidth: CGFloat = 1,
15+
isCircle: Bool = false
16+
) -> some View {
1317
self.resizable()
1418
.scaledToFit()
1519
.frame(width: size, height: size)
1620
.overlay {
17-
RoundedRectangle(cornerRadius: size * 0.2337, style: .continuous)
21+
RoundedRectangle(cornerRadius: isCircle ? (size * 2) : (size * 0.2337), style: .continuous)
1822
.strokeBorder(.gray.opacity(0.3), lineWidth: lineWidth)
1923
}
20-
.clipShape(RoundedRectangle(cornerRadius: size * 0.2337, style: .continuous))
24+
.clipShape(RoundedRectangle(cornerRadius: isCircle ? (size * 2) : (size * 0.2337), style: .continuous))
2125
}
2226
}

Protokolle/Resources/Localizable.xcstrings

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,17 @@
2424
}
2525
}
2626
},
27+
"💚 This couldn't of been done without my sponsors!" : {
28+
"extractionState" : "manual",
29+
"localizations" : {
30+
"en" : {
31+
"stringUnit" : {
32+
"state" : "translated",
33+
"value" : "💚 This couldn't of been done without my sponsors!"
34+
}
35+
}
36+
}
37+
},
2738
"About %@" : {
2839
"extractionState" : "manual",
2940
"localizations" : {
@@ -133,6 +144,9 @@
133144
}
134145
}
135146
}
147+
},
148+
"Credits" : {
149+
136150
},
137151
"Date" : {
138152
"extractionState" : "manual",
@@ -559,6 +573,9 @@
559573
}
560574
}
561575
}
576+
},
577+
"Sponsors" : {
578+
562579
},
563580
"Start Streaming" : {
564581
"extractionState" : "manual",
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
//
2+
// FetchService.swift
3+
// Loader
4+
//
5+
// Created by samara on 14.03.2025.
6+
//
7+
8+
import Foundation
9+
10+
// MARK: - Class
11+
public class NBFetchService {
12+
13+
public enum NBFetchServiceError: Error, LocalizedError {
14+
case invalidURL
15+
case networkError(Error)
16+
case noData
17+
case parsingError(Error)
18+
19+
public var errorDescription: String? {
20+
switch self {
21+
case .invalidURL:
22+
return "The URL is invalid."
23+
case .networkError(let error):
24+
return "Network error: \(error.localizedDescription)"
25+
case .noData:
26+
return "No data received."
27+
case .parsingError(let error):
28+
return "Failed to parse data: \(error.localizedDescription)"
29+
}
30+
}
31+
}
32+
33+
public init() {}
34+
}
35+
36+
// MARK: - Class extension: fetch
37+
extension NBFetchService {
38+
public func fetch<T: Decodable>(
39+
from urlString: String,
40+
completion: @escaping (Result<T, Error>) -> Void
41+
) {
42+
guard let url = URL(string: urlString) else {
43+
completion(.failure(NBFetchServiceError.invalidURL))
44+
return
45+
}
46+
47+
fetch(from: url, completion: completion)
48+
}
49+
50+
public func fetch<T: Decodable>(
51+
from url: URL,
52+
completion: @escaping (Result<T, Error>) -> Void
53+
) {
54+
DispatchQueue.global(qos: .userInitiated).async {
55+
let task = URLSession.shared.dataTask(with: url) { data, response, error in
56+
if let error = error {
57+
completion(.failure(NBFetchServiceError.networkError(error)))
58+
return
59+
}
60+
61+
guard let data = data else {
62+
completion(.failure(NBFetchServiceError.noData))
63+
return
64+
}
65+
66+
do {
67+
let decoder = JSONDecoder()
68+
let decodedData = try decoder.decode(T.self, from: data)
69+
completion(.success(decodedData))
70+
} catch {
71+
completion(.failure(NBFetchServiceError.parsingError(error)))
72+
}
73+
}
74+
75+
task.resume()
76+
}
77+
}
78+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
//
2+
// CreditsPerson.swift
3+
// Feather
4+
//
5+
// Created by samara on 3.05.2025.
6+
//
7+
8+
struct CreditsModel: Codable, Hashable {
9+
let name: String?
10+
let desc: String?
11+
let github: String
12+
}

Protokolle/Views/Settings/About/SYAboutView.swift

Lines changed: 126 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,27 +8,136 @@
88
import SwiftUI
99

1010
struct SYAboutView: View {
11+
typealias CreditsDataHandler = Result<[CreditsModel], Error>
12+
private let _dataService = NBFetchService()
13+
14+
@State private var _credits: [CreditsModel] = []
15+
@State private var _donators: [CreditsModel] = []
16+
@State var isLoading = true
17+
18+
private let _creditsUrl = "https://raw.githubusercontent.com/khcrysalis/project-credits/refs/heads/main/protokolle/credits.json"
19+
private let _donatorsUrl = "https://raw.githubusercontent.com/khcrysalis/project-credits/refs/heads/main/sponsors/credits.json"
20+
1121
var body: some View {
12-
ZStack {
13-
VStack {
14-
Image(uiImage: (UIImage(named: Bundle.main.iconFileName ?? ""))! )
15-
.appIconStyle(size: 72)
16-
17-
Text(Bundle.main.name)
18-
.font(.largeTitle)
19-
.bold()
20-
.foregroundStyle(.tint)
21-
22-
HStack(spacing: 4) {
23-
Text(.localized("Version"))
24-
Text(Bundle.main.version)
22+
Form {
23+
Section {
24+
VStack {
25+
Image(uiImage: (UIImage(named: Bundle.main.iconFileName ?? ""))! )
26+
.appIconStyle(size: 72)
27+
28+
Text(Bundle.main.name)
29+
.font(.largeTitle)
30+
.bold()
31+
.foregroundStyle(.tint)
32+
33+
HStack(spacing: 4) {
34+
Text(.localized("Version"))
35+
Text(Bundle.main.version)
36+
}
37+
.font(.footnote)
38+
.foregroundStyle(.secondary)
39+
40+
}
41+
}
42+
.frame(maxWidth: .infinity)
43+
.listRowBackground(EmptyView())
44+
45+
Section("Credits") {
46+
if !_credits.isEmpty {
47+
ForEach(_credits, id: \.github) { credit in
48+
_credit(name: credit.name, desc: credit.desc, github: credit.github)
49+
}
50+
.transition(.slide)
51+
}
52+
}
53+
54+
Section("Sponsors") {
55+
if !_donators.isEmpty {
56+
Group {
57+
Text(try! AttributedString(markdown: _donators.map {
58+
"[\($0.name ?? $0.github)](https://github.com/\($0.github))"
59+
}.joined(separator: ", ")))
60+
61+
Text(.localized("💚 This couldn't of been done without my sponsors!"))
62+
.foregroundStyle(.secondary)
63+
.padding(.vertical, 2)
64+
}
65+
.transition(.slide)
2566
}
26-
.font(.footnote)
27-
.foregroundStyle(.secondary)
2867
}
29-
.ignoresSafeArea(.all)
3068
}
31-
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
3269
.navigationTitle(.localized("About"))
70+
.animation(.default, value: isLoading)
71+
.task {
72+
await _fetchAllData()
73+
}
3374
}
75+
76+
private func _fetchAllData() async {
77+
await withTaskGroup(of: (String, CreditsDataHandler).self) { group in
78+
group.addTask { return await _fetchCredits(self._creditsUrl, using: _dataService) }
79+
group.addTask { return await _fetchCredits(self._donatorsUrl, using: _dataService) }
80+
81+
for await (type, result) in group {
82+
await MainActor.run {
83+
switch result {
84+
case .success(let data):
85+
if type == "credits" {
86+
self._credits = data
87+
} else {
88+
self._donators = data
89+
}
90+
case .failure(_): break
91+
}
92+
}
93+
}
94+
}
95+
96+
await MainActor.run {
97+
isLoading = false
98+
}
99+
}
100+
101+
private func _fetchCredits(_ urlString: String, using service: NBFetchService) async -> (String, CreditsDataHandler) {
102+
let type = urlString == _creditsUrl
103+
? "credits"
104+
: "donators"
105+
106+
return await withCheckedContinuation { continuation in
107+
service.fetch(from: urlString) { (result: CreditsDataHandler) in
108+
continuation.resume(returning: (type, result))
109+
}
110+
}
111+
}
112+
}
113+
114+
// MARK: - Extension: view
115+
extension SYAboutView {
116+
@ViewBuilder
117+
private func _credit(
118+
name: String?,
119+
desc: String?,
120+
github: String
121+
) -> some View {
122+
Button {
123+
UIApplication.open("https://github.com/\(github)")
124+
} label: {
125+
HStack(spacing: 12) {
126+
AsyncImage(url: URL(string: "https://github.com/\(github).png")) { image in
127+
image.appIconStyle(isCircle: true)
128+
} placeholder: {
129+
EmptyView()
130+
}
131+
132+
VStack(alignment: .leading, spacing: 2) {
133+
Text(name ?? github)
134+
.font(.headline)
135+
Text(desc ?? "")
136+
.font(.subheadline)
137+
.foregroundColor(.secondary)
138+
}
139+
}
140+
}
141+
}
34142
}
143+

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,6 @@ Using the makefile will automatically create an adhoc ipa inside the packages di
7777
- [Samara](https://github.com/khcrysalis) - The maker
7878
- [Antoine](https://github.com/NSAntoine/Antoine) - Code for filtering, refresh, and the sole reason why I even made this.
7979
- [idevice](https://github.com/jkcoxson/idevice) - Backend functionality, uses `os_trace_relay` to retrieve messages.
80-
- [Stossy11](https://github.com/stossy11/) - [StosVPN](https://github.com/SideStore/StosVPN) tunnel code, very appreciated.
8180

8281
## License
8382

license_plist.yml

Lines changed: 0 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -19,44 +19,6 @@ manual:
1919
- name: "Antoine"
2020
file: "LICENSE_ANTOINE"
2121

22-
- source: https://github.com/SideStore/StosVPN
23-
name: StosVPN
24-
body: |-
25-
StosVPN License
26-
27-
Copyright (c) 2025 SideStore Team
28-
29-
Permission is hereby granted, free of charge, to any person obtaining a copy
30-
of this software and associated documentation files (the "Software"), to deal
31-
in the Software without restriction, including without limitation the rights
32-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
33-
copies of the Software, and to permit persons to whom the Software is
34-
furnished to do so, subject to the following conditions:
35-
36-
The above copyright notice and this permission notice shall be included in all
37-
copies or substantial portions of the Software.
38-
39-
**Attribution is clearly given** to the original project and author(s) in a
40-
prominent place (e.g., README, About page, or documentation).
41-
42-
Any derived project **must clearly state** that it is **based on** or **uses
43-
code from** the original project.
44-
45-
Redistribution, rebranding, or publishing of the **entire project** (or a
46-
substantially similar copy) under the same or similar name or branding,
47-
**without explicit written permission**, is **prohibited**.
48-
49-
This clause is intended to prevent unauthorized re-uploads, clones, or
50-
misrepresentation of the original work.
51-
52-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
53-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
54-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
55-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
56-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
57-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
58-
SOFTWARE.
59-
6022
exclude:
6123
- swift-html-entities
6224
- LicensePlist

0 commit comments

Comments
 (0)