Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions ios/MarkupApp/MarkupApp/GitHubService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,57 @@ final class GitHubService {
return url
}

/// The base directory holding all downloaded GitHub vaults.
nonisolated static var vaultsBase: URL {
FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
.appendingPathComponent("GitHubVaults", isDirectory: true)
}

/// All materialized GitHub vault directories on disk (those carrying a
/// `.markup` manifest sidecar), with their byte sizes.
nonisolated static func downloadedVaults() -> [(url: URL, bytes: Int64)] {
let fm = FileManager.default
guard let en = fm.enumerator(at: vaultsBase, includingPropertiesForKeys: [.isDirectoryKey])
else { return [] }
var out: [(URL, Int64)] = []
for case let url as URL in en {
let isDir = (try? url.resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory == true
if isDir, readMeta(vaultRoot: url) != nil {
out.append((url, directorySize(url)))
en.skipDescendants() // don't recurse into a vault we already counted
}
}
return out
}

/// Delete every downloaded GitHub vault except `keeping` (the active one),
/// plus the transient asset cache. Returns the number of vaults removed.
/// Safe: never touches the open vault or anything outside our containers.
@discardableResult
nonisolated static func clearCaches(keeping active: URL?) -> Int {
let fm = FileManager.default
try? fm.removeItem(at: cacheRoot) // transient per-doc asset cache
let keepPath = active?.standardizedFileURL.path
var removed = 0
for vault in downloadedVaults() where vault.url.standardizedFileURL.path != keepPath {
if (try? fm.removeItem(at: vault.url)) != nil { removed += 1 }
}
return removed
}

/// Recursive byte size of a directory (best-effort; 0 on error).
nonisolated static func directorySize(_ url: URL) -> Int64 {
let fm = FileManager.default
guard let en = fm.enumerator(at: url, includingPropertiesForKeys: [.fileSizeKey]) else {
return 0
}
var total: Int64 = 0
for case let f as URL in en {
total += Int64((try? f.resourceValues(forKeys: [.fileSizeKey]))?.fileSize ?? 0)
}
return total
}

/// Extract a GitHub zipball (one `owner-repo-sha/` wrapper dir) into `root`,
/// stripping the wrapper so paths are repo-root-relative. Builds the tree in
/// a sibling temp dir and **atomically** swaps it into place, so a re-open of
Expand Down
5 changes: 5 additions & 0 deletions ios/MarkupApp/MarkupApp/Localization.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ enum L {
case refreshFromGitHub, refreshing, refreshUpToDate, refreshUpdatedSuffix, refreshedFull
case ghPreparing, ghDownloading, ghExtracting
case refreshOverwriteTitle, refreshOverwriteBody, refreshOverwrite
case githubDownloaded, clearGithubCache

var en: String {
switch self {
Expand Down Expand Up @@ -185,6 +186,8 @@ enum L {
case .refreshOverwriteBody:
return "%d file(s) have changes you made since the last sync. Refreshing from GitHub will overwrite them."
case .refreshOverwrite: return "Refresh & Overwrite"
case .githubDownloaded: return "Downloaded repos"
case .clearGithubCache: return "Clear GitHub cache"
case .refreshing: return "Refreshing…"
case .refreshUpToDate: return "Already up to date"
case .refreshUpdatedSuffix: return "updated"
Expand Down Expand Up @@ -303,6 +306,8 @@ enum L {
case .refreshOverwriteBody:
return "有 %d 个文件在上次同步后被你修改过。从 GitHub 刷新会覆盖这些改动。"
case .refreshOverwrite: return "刷新并覆盖"
case .githubDownloaded: return "已下载仓库"
case .clearGithubCache: return "清理 GitHub 缓存"
case .refreshing: return "刷新中…"
case .refreshUpToDate: return "已是最新"
case .refreshUpdatedSuffix: return "项已更新"
Expand Down
8 changes: 8 additions & 0 deletions ios/MarkupApp/MarkupApp/RootView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,14 @@ struct RootView: View {
guard openingVault == nil else { return }
showGitHub = false
githubBrowse = nil
// Already downloaded? Open the local copy instantly instead of
// re-downloading the whole repo — "Refresh from GitHub" pulls the latest.
// Mirrors the desktop, which also reuses a materialized vault.
let existing = GitHubService.vaultRoot(for: link)
if GitHubService.readMeta(vaultRoot: existing) != nil {
vault.openLocalVault(existing)
return
}
openingVault = "\(link.owner)/\(link.repo)"
openingStatus = nil
Task {
Expand Down
27 changes: 27 additions & 0 deletions ios/MarkupApp/MarkupApp/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ struct SettingsView: View {
@AppStorage("reader.fontScale") private var fontScale = 1.0
@AppStorage("reader.maxWidth") private var maxWidth = 720
@AppStorage("reader.lineHeight") private var lineHeight = 1.65
/// Total bytes used by downloaded GitHub vaults (computed off-main).
@State private var cacheBytes: Int64 = 0

private func refreshCacheSize() async {
cacheBytes = await Task.detached(priority: .utility) {
GitHubService.downloadedVaults().reduce(0) { $0 + $1.bytes }
}.value
}

private var appVersion: String {
let v = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "—"
Expand Down Expand Up @@ -60,6 +68,24 @@ struct SettingsView: View {
Button(t(.reindex)) { vault.scan() }
}

if cacheBytes > 0 {
Section("GitHub") {
LabeledContent(
t(.githubDownloaded),
value: ByteCountFormatter.string(fromByteCount: cacheBytes, countStyle: .file))
Button(t(.clearGithubCache), role: .destructive) {
Task {
let active = vault.rootURL
await Task.detached(priority: .utility) {
GitHubService.clearCaches(keeping: active)
}.value
vault.forgetMissingVaults()
await refreshCacheSize()
}
}
}
}

Section(t(.about)) {
LabeledContent(t(.version), value: appVersion)
Link(t(.onGitHub), destination: URL(string: "https://github.com/oratis/Markup")!)
Expand All @@ -70,6 +96,7 @@ struct SettingsView: View {
}
.navigationTitle(t(.settings))
.navigationBarTitleDisplayMode(.inline)
.task { await refreshCacheSize() }
.toolbar { ToolbarItem(placement: .cancellationAction) { Button(t(.done)) { dismiss() } } }
}
}
Expand Down
8 changes: 8 additions & 0 deletions ios/MarkupApp/MarkupApp/VaultStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,14 @@ final class VaultStore {
saveKnownVaults()
}

/// Drop remembered vaults whose folder no longer exists on disk (e.g. after
/// clearing the GitHub cache), so the switcher doesn't list dead entries.
func forgetMissingVaults() {
let fm = FileManager.default
knownVaults.removeAll { !fm.fileExists(atPath: $0.path) }
saveKnownVaults()
}

var rootName: String { rootURL?.lastPathComponent ?? "No folder" }

/// A readable, `/`-joined path for the open vault, cleaning the iCloud
Expand Down
Loading