diff options
| author | Christian Cleberg <[email protected]> | 2026-07-17 17:34:22 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-07-17 17:34:57 -0500 |
| commit | 705c6029ab30adf094e6324006b9fb682d8189f2 (patch) | |
| tree | a7325e1d8900a4c7c5b05182cd95dbf6b7f374ce /octosentry/SecurityEventListView.swift | |
| parent | 29b01b0220968dd9ea70b6c84ca72603bc02042c (diff) | |
| download | octosentry-0.6.0.tar.gz octosentry-0.6.0.tar.bz2 octosentry-0.6.0.zip | |
Closes #11-#15 (milestones 0.6.0, 0.7.0, 1.0.0).
- Repo picker: on-demand broader OAuth scope (security_events repo),
requested only when the "Browse your repos" action is used, never by
default. Lists /user/repos via the existing pagination helper. Granted
scope persisted with backward-compatible decoding for existing state
files. Fixed a bug where a failed re-auth force-signed-out a user who
already had a valid narrower-scope token.
- Update checker: polls this repo's GitHub Releases API, surfaces a
banner linking to new releases. Skipped on the Mac App Store build via
a runtime receipt check rather than a separate build configuration.
- Fixed MARKETING_VERSION, stuck at Xcode's default "1.0" this whole
time unrelated to our git tags — now 1.0.0, matching this release.
- Added PrivacyInfo.xcprivacy (no tracking, no collected data).
- Added scripts/build-dmg.sh (archive, Developer ID export, notarize,
staple) and Casks/octosentry.rb (Homebrew Cask template), plus
DISTRIBUTION.md documenting both channels end to end.
Entitlements were already identical across all builds — no divergence
needed there. What remains for actual App Store submission and notarized
DMG builds is account-specific (Apple Developer Program membership,
certificates, App Store Connect submission) and can't be done from here;
documented clearly in DISTRIBUTION.md.
Diffstat (limited to 'octosentry/SecurityEventListView.swift')
| -rw-r--r-- | octosentry/SecurityEventListView.swift | 123 |
1 files changed, 116 insertions, 7 deletions
diff --git a/octosentry/SecurityEventListView.swift b/octosentry/SecurityEventListView.swift index d817370..84e6638 100644 --- a/octosentry/SecurityEventListView.swift +++ b/octosentry/SecurityEventListView.swift @@ -9,6 +9,7 @@ import SwiftUI struct SecurityEventListView: View { var store: SecurityEventStore var authStore: AuthStore + var updateStore: UpdateStore var isStandaloneWindow: Bool = false @State private var showingRepoManager = false @Environment(\.openWindow) private var openWindow @@ -16,6 +17,9 @@ struct SecurityEventListView: View { var body: some View { VStack(alignment: .leading, spacing: 0) { header + if let release = updateStore.availableRelease { + UpdateBanner(release: release) + } Divider() if !authStore.isSignedIn { SignInView(authStore: authStore) @@ -30,6 +34,9 @@ struct SecurityEventListView: View { await store.refresh() store.startPolling() } + .task { + await updateStore.checkForUpdate() + } } private var header: some View { @@ -142,6 +149,10 @@ private struct RepoManagerView: View { var store: SecurityEventStore var authStore: AuthStore @State private var newRepoText = "" + @State private var isBrowsingRepos = false + @State private var availableRepos: [String] = [] + @State private var isLoadingRepos = false + @State private var browseErrorMessage: String? var body: some View { VStack(alignment: .leading, spacing: 10) { @@ -171,13 +182,24 @@ private struct RepoManagerView: View { Divider() - HStack { - TextField("owner/repo", text: $newRepoText) - .textFieldStyle(.roundedBorder) - .onSubmit(addRepo) + if isBrowsingRepos { + browsingContent + } else { + HStack { + TextField("owner/repo", text: $newRepoText) + .textFieldStyle(.roundedBorder) + .onSubmit(addRepo) + + Button("Add", action: addRepo) + .disabled(newRepoText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) + } - Button("Add", action: addRepo) - .disabled(newRepoText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) + Button(action: startBrowsing) { + Label("Browse your repos", systemImage: "list.bullet") + .font(.caption) + } + .buttonStyle(.plain) + .foregroundStyle(Color.accentColor) } if let errorMessage = store.watchListErrorMessage { @@ -200,6 +222,75 @@ private struct RepoManagerView: View { .frame(maxWidth: .infinity, alignment: .leading) } + @ViewBuilder + private var browsingContent: some View { + VStack(alignment: .leading, spacing: 6) { + HStack { + Text("Your Repositories") + .font(.caption.weight(.semibold)) + Spacer() + Button { + isBrowsingRepos = false + } label: { + Image(systemName: "xmark.circle") + } + .buttonStyle(.plain) + } + + if isLoadingRepos { + ProgressView() + .controlSize(.small) + .frame(maxWidth: .infinity) + } else if let browseErrorMessage { + Text(browseErrorMessage) + .font(.caption2) + .foregroundStyle(.red) + } else { + let selectableRepos = availableRepos.filter { !store.watchedRepos.contains($0) } + if selectableRepos.isEmpty { + Text("All visible repos are already watched.") + .font(.caption2) + .foregroundStyle(.secondary) + } else { + ScrollView { + LazyVStack(alignment: .leading, spacing: 4) { + ForEach(selectableRepos, id: \.self) { repo in + Button { + Task { await store.addRepo(repo) } + isBrowsingRepos = false + } label: { + Text(repo) + .font(.callout) + .frame(maxWidth: .infinity, alignment: .leading) + } + .buttonStyle(.plain) + } + } + } + .frame(maxHeight: 160) + } + } + } + } + + private func startBrowsing() { + guard authStore.hasRepoAccess else { + authStore.requestRepoAccess() + return + } + isBrowsingRepos = true + isLoadingRepos = true + browseErrorMessage = nil + Task { + do { + availableRepos = try await store.fetchAccessibleRepos() + } catch { + browseErrorMessage = (error as? LocalizedError)?.errorDescription ?? error.localizedDescription + } + isLoadingRepos = false + } + } + private func addRepo() { let text = newRepoText newRepoText = "" @@ -207,6 +298,24 @@ private struct RepoManagerView: View { } } +private struct UpdateBanner: View { + let release: UpdateChecker.LatestRelease + + var body: some View { + Button { + NSWorkspace.shared.open(release.htmlURL) + } label: { + Label("Update available: \(release.version)", systemImage: "arrow.down.circle.fill") + .font(.caption) + .frame(maxWidth: .infinity, alignment: .leading) + } + .buttonStyle(.plain) + .foregroundStyle(.blue) + .padding(8) + .background(.blue.opacity(0.1)) + } +} + private struct ErrorBanner: View { let messages: [String] @@ -262,6 +371,6 @@ private struct StatusView: View { } #Preview { - SecurityEventListView(store: SecurityEventStore(), authStore: AuthStore()) + SecurityEventListView(store: SecurityEventStore(), authStore: AuthStore(), updateStore: UpdateStore()) .frame(width: 380, height: 420) } |
