chadtmiller.com/knotless
git that lives in your Atmosphere account
1
10
The iOS app's repository and profile screens follow GitHub's
ios/Knotless/API/Client.swift+16−5
| @@ -45,18 +45,28 @@ actor Client { | ||
/// A write, as the account signed in. What it answers is left, as a screen | ||
/// that writes reads what it shows again. | ||
func send(_ write: Write) async throws { | ||
try await call(write.method, write.input) | ||
#if DEBUG | ||
if DevAccount.recording != nil { return } | ||
#endif | ||
_ = try await call(write) | ||
} | ||
| ||
private func call(_ method: String, _ input: [String: String]) async throws { | ||
/// A write whose answer the screen needs, as a fork's says what it made. | ||
func answer<T: Decodable & Sendable>(to write: Write) async throws -> T { | ||
#if DEBUG | ||
if DevAccount.recording != nil { return } | ||
if let recorded = DevAccount.recording { | ||
return try decoder.decode(T.self, from: recorded.answer(write.method, write.input)) | ||
} | ||
#endif | ||
var request = URLRequest(url: Self.site.appendingPathComponent("xrpc/dev.knotless." + method)) | ||
return try decoder.decode(T.self, from: await call(write)) | ||
} | ||
| ||
private func call(_ write: Write) async throws -> Data { | ||
var request = URLRequest(url: Self.site.appendingPathComponent("xrpc/dev.knotless." + write.method)) | ||
request.httpMethod = "POST" | ||
request.timeoutInterval = 60 | ||
request.setValue("application/json", forHTTPHeaderField: "Content-Type") | ||
request.httpBody = try JSONSerialization.data(withJSONObject: input) | ||
request.httpBody = try JSONSerialization.data(withJSONObject: write.input) | ||
authorize(&request, nil) | ||
let (data, response) = try await session.data(for: request) | ||
let status = (response as? HTTPURLResponse)?.statusCode ?? 0 | ||
| @@ -65,6 +75,7 @@ actor Client { | ||
let text = failure == nil ? String(data: data, encoding: .utf8) : nil | ||
throw APIError(status: status, name: failure?.error, message: failure?.message ?? text) | ||
} | ||
return data | ||
} | ||
| ||
/// Trades the code a login came back with for the session's token. | ||
ios/Knotless/API/Models.swift+100−0
| @@ -24,10 +24,23 @@ struct Repo: Decodable, Identifiable, Hashable { | ||
var starCount: Int? | ||
var forkCount: Int? | ||
var `private`: Bool? | ||
var language: Language? | ||
var id: String { uri } | ||
var ref: RepoRef { RepoRef(did: owner.did, handle: owner.handle, name: name) } | ||
} | ||
| ||
struct Language: Decodable, Hashable { | ||
let name: String | ||
var color: String? | ||
} | ||
| ||
/// A link a repository or an account shows, by its title or else its host. | ||
struct WebLink: Decodable, Hashable { | ||
let url: String | ||
var title: String? | ||
var label: String { title ?? URL(string: url)?.host() ?? url } | ||
} | ||
| ||
struct RepoList: Decodable { | ||
var repos: [Repo] = [] | ||
} | ||
| @@ -42,14 +55,93 @@ struct RepoDetail: Decodable { | ||
let openIssues: Int | ||
var starCount: Int? | ||
var `private`: Bool? | ||
var links: [WebLink]? | ||
var forkOf: Basic? | ||
var releases: Releases? | ||
var viewer: Viewing? | ||
| ||
struct Viewing: Decodable { | ||
/// The asker's star of it, if they starred it. | ||
var star: String? | ||
/// What they chose to hear of it: `all`, `releases` or `ignore`. | ||
var subscription: String? | ||
} | ||
| ||
struct Basic: Decodable, Hashable { | ||
let did: String | ||
let handle: String | ||
let name: String | ||
var ref: RepoRef { RepoRef(did: did, handle: handle, name: name) } | ||
} | ||
| ||
struct Releases: Decodable { | ||
let count: Int | ||
var release: Latest? | ||
struct Latest: Decodable { | ||
let package: String | ||
let version: String | ||
var latest: Bool? | ||
var createdAt: String? | ||
} | ||
} | ||
} | ||
| ||
struct RepoMade: Decodable, Sendable { | ||
let uri: String | ||
let name: String | ||
} | ||
| ||
struct Branch: Decodable, Identifiable, Hashable { | ||
let name: String | ||
var id: String { name } | ||
} | ||
| ||
struct BranchList: Decodable { | ||
var branches: [Branch] = [] | ||
} | ||
| ||
struct CommitList: Decodable { | ||
var commits: [Commit] = [] | ||
var cursor: String? | ||
} | ||
| ||
struct Release: Decodable, Identifiable, Hashable { | ||
let uri: String | ||
let package: String | ||
let version: String | ||
var latest: Bool? | ||
var notes: String? | ||
var files: [File]? | ||
var createdAt: String? | ||
var id: String { uri } | ||
| ||
struct File: Decodable, Hashable { | ||
let name: String | ||
var size: Int? | ||
} | ||
} | ||
| ||
struct ReleaseList: Decodable { | ||
var releases: [Release] = [] | ||
var cursor: String? | ||
} | ||
| ||
struct Run: Decodable, Identifiable, Hashable { | ||
let check: Check | ||
let ref: String | ||
var pull: Pull? | ||
var batch: Int? | ||
var subject: String? | ||
var id: String { check.id } | ||
| ||
struct Pull: Decodable, Hashable { var title: String? } | ||
} | ||
| ||
struct RunList: Decodable { | ||
var runs: [Run] = [] | ||
var cursor: String? | ||
} | ||
| ||
struct ItemLabel: Decodable, Hashable { | ||
let name: String | ||
var color: String? | ||
| @@ -145,6 +237,7 @@ struct Check: Decodable, Identifiable, Hashable { | ||
let status: String | ||
/// Why it was skipped, when it was. | ||
var skipped: String? | ||
var startedAt: String? | ||
var id: String { uri } | ||
} | ||
| ||
| @@ -254,7 +347,14 @@ struct Profile: Decodable { | ||
var avatar: String? | ||
var followersCount: Int? | ||
var followsCount: Int? | ||
var links: [WebLink]? | ||
var viewer: Viewing? | ||
var orgs: [Org]? | ||
| ||
struct Viewing: Decodable { | ||
/// The asker's follow of them, if they follow them. | ||
var following: String? | ||
} | ||
} | ||
| ||
struct InboxItem: Decodable, Identifiable, Hashable { | ||
ios/Knotless/API/Writes.swift+13−0
| @@ -40,6 +40,19 @@ enum Writes { | ||
Write(method: undo ? "unstarRepo" : "starRepo", input: ["repo": repo.uri]) | ||
} | ||
| ||
static func fork(repo: RepoRef) -> Write { | ||
Write(method: "forkRepo", input: ["repo": repo.uri]) | ||
} | ||
| ||
/// `all`, `releases` or `ignore`, or `participating` to forget the choice. | ||
static func hear(repo: RepoRef, level: String) -> Write { | ||
Write(method: "putSubscription", input: ["subject": repo.uri, "level": level]) | ||
} | ||
| ||
static func follow(did: String, undo: Bool) -> Write { | ||
Write(method: undo ? "unfollowActor" : "followActor", input: ["subject": did]) | ||
} | ||
| ||
static func seen(at: String) -> Write { | ||
Write(method: "updateSeen", input: ["seenAt": at]) | ||
} | ||
ios/Knotless/Assets.xcassets/oct-bell-fill.imageset/bell-fill.svgnew+1−0
| @@ -0,0 +1 @@ | ||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M8 16c.9 0 1.7-.6 1.9-1.5.1-.3-.1-.5-.4-.5h-3c-.3 0-.5.2-.4.5.2.9 1 1.5 1.9 1.5ZM3 5c0-2.8 2.2-5 5-5s5 2.2 5 5v3l1.7 2.6c.2.2.3.5.3.8 0 .8-.7 1.5-1.5 1.5h-11c-.8.1-1.5-.6-1.5-1.4 0-.3.1-.6.3-.8L3 8.1V5Z"/></svg> | ||
| \ No newline at end of file | ||
ios/Knotless/Assets.xcassets/oct-bell-fill.imageset/Contents.jsonnew+1−0
| @@ -0,0 +1 @@ | ||
{"images":[{"filename":"bell-fill.svg","idiom":"universal"}],"info":{"author":"xcode","version":1},"properties":{"preserves-vector-representation":true,"template-rendering-intent":"template"}} | ||
| \ No newline at end of file | ||
ios/Knotless/Assets.xcassets/oct-bell-slash.imageset/bell-slash.svgnew+1−0
| @@ -0,0 +1 @@ | ||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="m4.182 4.31.016.011 10.104 7.316.013.01 1.375.996a.75.75 0 1 1-.88 1.214L13.626 13H2.518a1.516 1.516 0 0 1-1.263-2.36l1.703-2.554A.255.255 0 0 0 3 7.947V5.305L.31 3.357a.75.75 0 1 1 .88-1.214Zm7.373 7.19L4.5 6.391v1.556c0 .346-.102.683-.294.97l-1.703 2.556a.017.017 0 0 0-.003.01c0 .005.002.009.005.012l.006.004.007.001ZM8 1.5c-.997 0-1.895.416-2.534 1.086A.75.75 0 1 1 4.38 1.55 5 5 0 0 1 13 5v2.373a.75.75 0 0 1-1.5 0V5A3.5 3.5 0 0 0 8 1.5ZM8 16a2 2 0 0 1-1.985-1.75c-.017-.137.097-.25.235-.25h3.5c.138 0 .252.113.235.25A2 2 0 0 1 8 16Z"/></svg> | ||
| \ No newline at end of file | ||
ios/Knotless/Assets.xcassets/oct-bell-slash.imageset/Contents.jsonnew+1−0
| @@ -0,0 +1 @@ | ||
{"images":[{"filename":"bell-slash.svg","idiom":"universal"}],"info":{"author":"xcode","version":1},"properties":{"preserves-vector-representation":true,"template-rendering-intent":"template"}} | ||
| \ No newline at end of file | ||
ios/Knotless/Assets.xcassets/oct-bell.imageset/bell.svgnew+1−0
| @@ -0,0 +1 @@ | ||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M8 16a2 2 0 0 0 1.985-1.75c.017-.137-.097-.25-.235-.25h-3.5c-.138 0-.252.113-.235.25A2 2 0 0 0 8 16ZM3 5a5 5 0 0 1 10 0v2.947c0 .05.015.098.042.139l1.703 2.555A1.519 1.519 0 0 1 13.482 13H2.518a1.516 1.516 0 0 1-1.263-2.36l1.703-2.554A.255.255 0 0 0 3 7.947Zm5-3.5A3.5 3.5 0 0 0 4.5 5v2.947c0 .346-.102.683-.294.97l-1.703 2.556a.017.017 0 0 0-.003.01l.001.006c0 .002.002.004.004.006l.006.004.007.001h10.964l.007-.001.006-.004.004-.006.001-.007a.017.017 0 0 0-.003-.01l-1.703-2.554a1.745 1.745 0 0 1-.294-.97V5A3.5 3.5 0 0 0 8 1.5Z"/></svg> | ||
| \ No newline at end of file | ||
ios/Knotless/Assets.xcassets/oct-bell.imageset/Contents.jsonnew+1−0
| @@ -0,0 +1 @@ | ||
{"images":[{"filename":"bell.svg","idiom":"universal"}],"info":{"author":"xcode","version":1},"properties":{"preserves-vector-representation":true,"template-rendering-intent":"template"}} | ||
| \ No newline at end of file | ||
ios/Knotless/Assets.xcassets/oct-git-branch.imageset/Contents.jsonnew+1−0
| @@ -0,0 +1 @@ | ||
{"images":[{"filename":"git-branch.svg","idiom":"universal"}],"info":{"author":"xcode","version":1},"properties":{"preserves-vector-representation":true,"template-rendering-intent":"template"}} | ||
| \ No newline at end of file | ||
ios/Knotless/Assets.xcassets/oct-git-branch.imageset/git-branch.svgnew+1−0
ios/Knotless/Assets.xcassets/oct-info.imageset/Contents.jsonnew+1−0
ios/Knotless/Assets.xcassets/oct-info.imageset/info.svgnew+1−0
ios/Knotless/Assets.xcassets/oct-link.imageset/Contents.jsonnew+1−0
ios/Knotless/Assets.xcassets/oct-link.imageset/link.svgnew+1−0
ios/Knotless/Assets.xcassets/oct-organization.imageset/Contents.jsonnew+1−0
ios/Knotless/Assets.xcassets/oct-organization.imageset/organization.svgnew+1−0
ios/Knotless/Assets.xcassets/oct-play.imageset/Contents.jsonnew+1−0
ios/Knotless/Assets.xcassets/oct-play.imageset/play.svgnew+1−0
ios/Knotless/Assets.xcassets/oct-repo-forked.imageset/Contents.jsonnew+1−0
ios/Knotless/Assets.xcassets/oct-repo-forked.imageset/repo-forked.svgnew+1−0
ios/Knotless/Assets.xcassets/oct-repo.imageset/Contents.jsonnew+1−0
ios/Knotless/Assets.xcassets/oct-repo.imageset/repo.svgnew+1−0
ios/Knotless/Assets.xcassets/oct-star-fill.imageset/Contents.jsonnew+1−0
ios/Knotless/Assets.xcassets/oct-star-fill.imageset/star-fill.svgnew+1−0
ios/Knotless/Assets.xcassets/oct-star.imageset/Contents.jsonnew+1−0
ios/Knotless/Assets.xcassets/oct-star.imageset/star.svgnew+1−0
ios/Knotless/Assets.xcassets/oct-tag.imageset/Contents.jsonnew+1−0
ios/Knotless/Assets.xcassets/oct-tag.imageset/tag.svgnew+1−0
ios/Knotless/Views/FilesView.swift+17−0
ios/Knotless/Views/ProfileView.swift+174−22
ios/Knotless/Views/RepoView.swift+621−88
709 lines changed: not loaded until asked for.