Remove in-list spinner + add monitored-repos setting; bump to 2.3
Build iOS App / Build .ipa and publish source (push) Successful in 22s
Build iOS App / Build .ipa and publish source (push) Successful in 22s
- Remove the in-list loading wheel overlay; the landing page just shows the cached list or the 'No open PRs' screen. - Add a 'Monitored repos' setting (Settings > Inbox) that limits the inbox to specific repos, cutting the number of API calls dramatically. Empty = monitor all accessible repos. Bump to 2.3.
This commit is contained in:
1 parent
75f5f3bc64
commit
880cff9e68
6 files changed
+120
-17
No files matched your search
@@ -187,7 +187,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 13;
|
CURRENT_PROJECT_VERSION = 14;
|
||||||
DEVELOPMENT_TEAM = "";
|
DEVELOPMENT_TEAM = "";
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = "GiteaPR";
|
INFOPLIST_KEY_CFBundleDisplayName = "GiteaPR";
|
||||||
@@ -197,7 +197,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 2.2;
|
MARKETING_VERSION = 2.3;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.bueso.giteapr;
|
PRODUCT_BUNDLE_IDENTIFIER = com.bueso.giteapr;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
@@ -211,7 +211,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 13;
|
CURRENT_PROJECT_VERSION = 14;
|
||||||
DEVELOPMENT_TEAM = "";
|
DEVELOPMENT_TEAM = "";
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = "GiteaPR";
|
INFOPLIST_KEY_CFBundleDisplayName = "GiteaPR";
|
||||||
@@ -221,7 +221,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 2.2;
|
MARKETING_VERSION = 2.3;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.bueso.giteapr;
|
PRODUCT_BUNDLE_IDENTIFIER = com.bueso.giteapr;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
|||||||
@@ -47,14 +47,21 @@ final class InboxViewModel: ObservableObject {
|
|||||||
|
|
||||||
do {
|
do {
|
||||||
let repos = try await client.repos()
|
let repos = try await client.repos()
|
||||||
LogStore.shared.log(.info, "Inbox: \(repos.count) repos")
|
// If the user has configured monitored repos, only fetch PRs for
|
||||||
// Fetch open PRs across all repos concurrently. A single repo that
|
// those — this cuts the number of API calls dramatically (the
|
||||||
// fails (e.g. 404) must NOT wipe the whole inbox — catch per-repo
|
// inbox was fetching every accessible repo).
|
||||||
// errors, log them, and keep whatever succeeded.
|
let monitored = SettingsStore.shared.monitoredRepos
|
||||||
|
let targets = monitored.isEmpty
|
||||||
|
? repos
|
||||||
|
: repos.filter { monitored.contains($0.fullName) }
|
||||||
|
LogStore.shared.log(.info, "Inbox: \(targets.count)/\(repos.count) repos")
|
||||||
|
// Fetch open PRs across the target repos concurrently. A single
|
||||||
|
// repo that fails (e.g. 404) must NOT wipe the whole inbox — catch
|
||||||
|
// per-repo errors, log them, and keep whatever succeeded.
|
||||||
var rows: [PRRow] = []
|
var rows: [PRRow] = []
|
||||||
var failures: [String] = []
|
var failures: [String] = []
|
||||||
await withTaskGroup(of: Result<[PRRow], Error>.self) { group in
|
await withTaskGroup(of: Result<[PRRow], Error>.self) { group in
|
||||||
for repo in repos {
|
for repo in targets {
|
||||||
group.addTask {
|
group.addTask {
|
||||||
do {
|
do {
|
||||||
let prs = try await client.pullRequests(owner: repo.owner.login,
|
let prs = try await client.pullRequests(owner: repo.owner.login,
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
/// Lets the user pick which repos the inbox monitors. Fetches the accessible
|
||||||
|
/// repos from the API and shows a toggle per repo. Empty selection = monitor
|
||||||
|
/// all.
|
||||||
|
struct MonitoredReposView: View {
|
||||||
|
@Binding var selection: [String]
|
||||||
|
@State private var repos: [Repository] = []
|
||||||
|
@State private var isLoading = true
|
||||||
|
@State private var errorMessage: String?
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Group {
|
||||||
|
if isLoading {
|
||||||
|
ProgressView("Loading repos…")
|
||||||
|
} else if let errorMessage {
|
||||||
|
ContentUnavailableView("Couldn't load repos",
|
||||||
|
systemImage: "exclamationmark.triangle",
|
||||||
|
description: Text(errorMessage))
|
||||||
|
} else {
|
||||||
|
List {
|
||||||
|
Section {
|
||||||
|
Button {
|
||||||
|
selection = []
|
||||||
|
} label: {
|
||||||
|
HStack {
|
||||||
|
Text("Monitor all repos")
|
||||||
|
Spacer()
|
||||||
|
if selection.isEmpty {
|
||||||
|
Image(systemName: "checkmark")
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Section("Monitored") {
|
||||||
|
ForEach(repos) { repo in
|
||||||
|
Button {
|
||||||
|
toggle(repo.fullName)
|
||||||
|
} label: {
|
||||||
|
HStack {
|
||||||
|
Text(repo.fullName)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
Spacer()
|
||||||
|
if selection.contains(repo.fullName) {
|
||||||
|
Image(systemName: "checkmark")
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle("Monitored repos")
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.task { await load() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private func load() async {
|
||||||
|
isLoading = true
|
||||||
|
errorMessage = nil
|
||||||
|
defer { isLoading = false }
|
||||||
|
guard let client = SettingsStore.shared.client() else {
|
||||||
|
errorMessage = "Not configured."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
repos = try await client.repos()
|
||||||
|
.sorted { $0.fullName < $1.fullName }
|
||||||
|
} catch {
|
||||||
|
errorMessage = error.localizedDescription
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func toggle(_ fullName: String) {
|
||||||
|
if selection.contains(fullName) {
|
||||||
|
selection.removeAll { $0 == fullName }
|
||||||
|
} else {
|
||||||
|
selection.append(fullName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,6 +27,12 @@ final class SettingsStore: ObservableObject {
|
|||||||
didSet { UserDefaults.standard.set(deleteBranchAfterMerge,
|
didSet { UserDefaults.standard.set(deleteBranchAfterMerge,
|
||||||
forKey: "deleteBranchAfterMerge") }
|
forKey: "deleteBranchAfterMerge") }
|
||||||
}
|
}
|
||||||
|
/// Repos to monitor for the inbox. Empty = monitor all accessible repos.
|
||||||
|
/// Stored as full names (e.g. "luis/config-nixos").
|
||||||
|
@Published var monitoredRepos: [String] {
|
||||||
|
didSet { UserDefaults.standard.set(monitoredRepos,
|
||||||
|
forKey: "monitoredRepos") }
|
||||||
|
}
|
||||||
|
|
||||||
private init() {
|
private init() {
|
||||||
instanceURL = KeychainHelper.get("instanceURL") ?? "https://gitea.bueso.eu"
|
instanceURL = KeychainHelper.get("instanceURL") ?? "https://gitea.bueso.eu"
|
||||||
@@ -34,6 +40,7 @@ final class SettingsStore: ObservableObject {
|
|||||||
let raw = UserDefaults.standard.string(forKey: "mergeGateMode")
|
let raw = UserDefaults.standard.string(forKey: "mergeGateMode")
|
||||||
mergeGateMode = MergeGateMode(rawValue: raw ?? "") ?? .warn
|
mergeGateMode = MergeGateMode(rawValue: raw ?? "") ?? .warn
|
||||||
deleteBranchAfterMerge = UserDefaults.standard.bool(forKey: "deleteBranchAfterMerge")
|
deleteBranchAfterMerge = UserDefaults.standard.bool(forKey: "deleteBranchAfterMerge")
|
||||||
|
monitoredRepos = UserDefaults.standard.stringArray(forKey: "monitoredRepos") ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
var isConfigured: Bool {
|
var isConfigured: Bool {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ struct SettingsView: View {
|
|||||||
@State private var token: String
|
@State private var token: String
|
||||||
@State private var mergeGateMode: MergeGateMode
|
@State private var mergeGateMode: MergeGateMode
|
||||||
@State private var deleteBranchAfterMerge: Bool
|
@State private var deleteBranchAfterMerge: Bool
|
||||||
|
@State private var monitoredRepos: [String]
|
||||||
@State private var testResult: TestResult?
|
@State private var testResult: TestResult?
|
||||||
@State private var testing = false
|
@State private var testing = false
|
||||||
@State private var showLogoutConfirm = false
|
@State private var showLogoutConfirm = false
|
||||||
@@ -20,6 +21,7 @@ struct SettingsView: View {
|
|||||||
_token = State(initialValue: SettingsStore.shared.token)
|
_token = State(initialValue: SettingsStore.shared.token)
|
||||||
_mergeGateMode = State(initialValue: SettingsStore.shared.mergeGateMode)
|
_mergeGateMode = State(initialValue: SettingsStore.shared.mergeGateMode)
|
||||||
_deleteBranchAfterMerge = State(initialValue: SettingsStore.shared.deleteBranchAfterMerge)
|
_deleteBranchAfterMerge = State(initialValue: SettingsStore.shared.deleteBranchAfterMerge)
|
||||||
|
_monitoredRepos = State(initialValue: SettingsStore.shared.monitoredRepos)
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -54,6 +56,16 @@ struct SettingsView: View {
|
|||||||
Toggle("Delete branch after merge", isOn: $deleteBranchAfterMerge)
|
Toggle("Delete branch after merge", isOn: $deleteBranchAfterMerge)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Section {
|
||||||
|
NavigationLink("Monitored repos") {
|
||||||
|
MonitoredReposView(selection: $monitoredRepos)
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
Text("Inbox")
|
||||||
|
} footer: {
|
||||||
|
Text("Limit the landing page to specific repos to speed it up. Leave empty to monitor all accessible repos.")
|
||||||
|
}
|
||||||
|
|
||||||
Section("Diagnostics") {
|
Section("Diagnostics") {
|
||||||
NavigationLink("View logs") {
|
NavigationLink("View logs") {
|
||||||
LogsView()
|
LogsView()
|
||||||
@@ -134,6 +146,7 @@ struct SettingsView: View {
|
|||||||
SettingsStore.shared.token = token
|
SettingsStore.shared.token = token
|
||||||
SettingsStore.shared.mergeGateMode = mergeGateMode
|
SettingsStore.shared.mergeGateMode = mergeGateMode
|
||||||
SettingsStore.shared.deleteBranchAfterMerge = deleteBranchAfterMerge
|
SettingsStore.shared.deleteBranchAfterMerge = deleteBranchAfterMerge
|
||||||
|
SettingsStore.shared.monitoredRepos = monitoredRepos
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -52,14 +52,6 @@ struct InboxView: View {
|
|||||||
didInitialLoad = true
|
didInitialLoad = true
|
||||||
await vm.load()
|
await vm.load()
|
||||||
}
|
}
|
||||||
.overlay {
|
|
||||||
// Only show a spinner when there's nothing cached to display
|
|
||||||
// yet; with a cache the list renders instantly and refreshes
|
|
||||||
// in the background.
|
|
||||||
if vm.isLoading && vm.displayedRows.isEmpty {
|
|
||||||
ProgressView()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.alert("Error", isPresented: .init(
|
.alert("Error", isPresented: .init(
|
||||||
get: { vm.errorMessage != nil },
|
get: { vm.errorMessage != nil },
|
||||||
set: { if !$0 { vm.errorMessage = nil } })) {
|
set: { if !$0 { vm.errorMessage = nil } })) {
|
||||||
|
|||||||
Reference in new issue
Block a user