diff --git a/TESTING.md b/TESTING.md index 23991fd..aff5c92 100644 --- a/TESTING.md +++ b/TESTING.md @@ -159,6 +159,10 @@ xcrun simctl launch booted de.rfc1437.gotcha selections persist independently per repository and status persists. - [ ] The issue-filter icon uses a neutral color for Open + All Milestones + no labels, and the app accent color whenever any non-default filter is set. +- [ ] In the issue filter panel, the Status, Milestone, and Labels icons use the + app accent color independently when their filter is non-default. **Clear + Filters** restores Open + All Milestones + no labels, refreshes the rows, + returns every filter icon to neutral, and remains cleared after relaunch. - [ ] Tap the add button. **New Issue** appears as a native modal with Cancel and Save, title and Markdown body fields, multi-select labels, a single-select optional milestone, and an optional inline due-date picker. @@ -252,6 +256,10 @@ xcrun simctl launch booted de.rfc1437.gotcha server after relaunch. - [ ] The pull-request filter icon uses a neutral color for Open + All Milestones and the app accent color whenever either filter is non-default. +- [ ] In the pull-request filter panel, Status and Milestone icons use the app + accent color independently when non-default. **Clear Filters** restores + Open + All Milestones, refreshes the rows, returns both icons to neutral, + and remains cleared after relaunch. - [ ] Each row shows repository/number, title, author/update metadata, comment count, and draft/merged state where applicable. - [ ] Open a pull request and verify title, metadata, Markdown body, comments, diff --git a/crates/app/src/lib.rs b/crates/app/src/lib.rs index 3f88e12..adc2c35 100644 --- a/crates/app/src/lib.rs +++ b/crates/app/src/lib.rs @@ -336,6 +336,27 @@ impl GotchaCore { save_preferences(&state.preferences).map_err(Into::into) } + pub fn clear_issue_filters( + &self, + owner: String, + repository: String, + ) -> Result<(), GotchaError> { + let owner = owner.trim(); + let repository = repository.trim(); + if owner.is_empty() || repository.is_empty() { + return Err("Select a repository first.".into()); + } + let mut state = self.state.lock().unwrap(); + let server = state + .active_server + .and_then(|index| state.preferences.servers.get(index)) + .ok_or("Select a server first.")?; + let key = repository_key(&server.url, owner, repository); + state.preferences.issue_status = "open".into(); + state.preferences.issue_filters.remove(&key); + save_preferences(&state.preferences).map_err(Into::into) + } + pub async fn issue( &self, owner: String, @@ -556,6 +577,18 @@ impl GotchaCore { save_preferences(&state.preferences).map_err(Into::into) } + pub fn clear_pull_filters(&self) -> Result<(), GotchaError> { + let mut state = self.state.lock().unwrap(); + let server = state + .active_server + .and_then(|index| state.preferences.servers.get(index)) + .ok_or("Select a server first.")?; + let key = server.url.clone(); + state.preferences.pull_status = "open".into(); + state.preferences.pull_filters.remove(&key); + save_preferences(&state.preferences).map_err(Into::into) + } + pub async fn pulls(&self, page: u32) -> Result { let server = self.server()?; let (status, filter) = { diff --git a/ios/Generated/gotcha_core.swift b/ios/Generated/gotcha_core.swift index 95f4406..28b1f91 100644 --- a/ios/Generated/gotcha_core.swift +++ b/ios/Generated/gotcha_core.swift @@ -609,6 +609,10 @@ public protocol GotchaCoreProtocol: AnyObject, Sendable { func addServer(name: String, url: String, token: String) async throws -> UInt32 + func clearIssueFilters(owner: String, repository: String) throws + + func clearPullFilters() throws + func commitDiff(owner: String, repository: String, sha: String, path: String) async throws -> DiffPage func commitFiles(owner: String, repository: String, sha: String) async throws -> [FileRow] @@ -771,6 +775,24 @@ open func addServer(name: String, url: String, token: String)async throws -> UI ) } +open func clearIssueFilters(owner: String, repository: String)throws {try rustCallWithError(FfiConverterTypeGotchaError_lift) { + uniffiCallStatus in + uniffi_gotcha_core_fn_method_gotchacore_clear_issue_filters( + self.uniffiCloneHandle(), + FfiConverterString.lower(owner), + FfiConverterString.lower(repository),uniffiCallStatus + ) +} +} + +open func clearPullFilters()throws {try rustCallWithError(FfiConverterTypeGotchaError_lift) { + uniffiCallStatus in + uniffi_gotcha_core_fn_method_gotchacore_clear_pull_filters( + self.uniffiCloneHandle(),uniffiCallStatus + ) +} +} + open func commitDiff(owner: String, repository: String, sha: String, path: String)async throws -> DiffPage { return try await uniffiRustCallAsync( @@ -4015,6 +4037,12 @@ private let initializationResult: InitializationResult = { if (uniffi_gotcha_core_checksum_method_gotchacore_add_server() != 42705) { return InitializationResult.apiChecksumMismatch } + if (uniffi_gotcha_core_checksum_method_gotchacore_clear_issue_filters() != 11092) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_gotcha_core_checksum_method_gotchacore_clear_pull_filters() != 36676) { + return InitializationResult.apiChecksumMismatch + } if (uniffi_gotcha_core_checksum_method_gotchacore_commit_diff() != 56887) { return InitializationResult.apiChecksumMismatch } diff --git a/ios/Generated/gotcha_coreFFI.h b/ios/Generated/gotcha_coreFFI.h index 14a2159..37a40b3 100644 --- a/ios/Generated/gotcha_coreFFI.h +++ b/ios/Generated/gotcha_coreFFI.h @@ -274,6 +274,16 @@ RustBuffer uniffi_gotcha_core_fn_method_gotchacore_active_server_name(uint64_t p uint64_t uniffi_gotcha_core_fn_method_gotchacore_add_server(uint64_t ptr, RustBuffer name, RustBuffer url, RustBuffer token ); #endif +#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_CLEAR_ISSUE_FILTERS +#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_CLEAR_ISSUE_FILTERS +void uniffi_gotcha_core_fn_method_gotchacore_clear_issue_filters(uint64_t ptr, RustBuffer owner, RustBuffer repository, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_CLEAR_PULL_FILTERS +#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_CLEAR_PULL_FILTERS +void uniffi_gotcha_core_fn_method_gotchacore_clear_pull_filters(uint64_t ptr, RustCallStatus *_Nonnull out_status +); +#endif #ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_COMMIT_DIFF #define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_COMMIT_DIFF uint64_t uniffi_gotcha_core_fn_method_gotchacore_commit_diff(uint64_t ptr, RustBuffer owner, RustBuffer repository, RustBuffer sha, RustBuffer path @@ -715,6 +725,18 @@ uint16_t uniffi_gotcha_core_checksum_method_gotchacore_active_server_name(void #define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_ADD_SERVER uint16_t uniffi_gotcha_core_checksum_method_gotchacore_add_server(void +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_CLEAR_ISSUE_FILTERS +#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_CLEAR_ISSUE_FILTERS +uint16_t uniffi_gotcha_core_checksum_method_gotchacore_clear_issue_filters(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_CLEAR_PULL_FILTERS +#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_CLEAR_PULL_FILTERS +uint16_t uniffi_gotcha_core_checksum_method_gotchacore_clear_pull_filters(void + ); #endif #ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_COMMIT_DIFF diff --git a/ios/Sources/ContentScreens.swift b/ios/Sources/ContentScreens.swift index a31ad7d..28345c3 100644 --- a/ios/Sources/ContentScreens.swift +++ b/ios/Sources/ContentScreens.swift @@ -129,7 +129,7 @@ final class IssuesViewController: RefreshingTableViewController { let current = context.core.settings().issueStatus let status = UIMenu( title: "Status", - image: context.symbol("circle.lefthalf.filled"), + image: filterMenuImage("circle.lefthalf.filled", active: current != "open"), options: .singleSelection, children: ["open", "closed"].map { status in UIAction( @@ -154,6 +154,7 @@ final class IssuesViewController: RefreshingTableViewController { } else { children.append(UIAction(title: "Loading filters…", attributes: .disabled) { _ in }) } + children.append(clearFiltersMenu()) filterButton.menu = UIMenu(children: children) filterButton.accessibilityLabel = "Filter issues" updateFilterTint() @@ -172,7 +173,7 @@ final class IssuesViewController: RefreshingTableViewController { } return UIMenu( title: "Milestone", - image: context.symbol("flag"), + image: filterMenuImage("flag", active: !selected.isEmpty), options: .singleSelection, children: [all] + actions ) @@ -203,7 +204,7 @@ final class IssuesViewController: RefreshingTableViewController { } return UIMenu( title: "Labels", - image: context.symbol("tag"), + image: filterMenuImage("tag", active: !selected.isEmpty), children: actions.isEmpty ? [UIAction(title: "No labels", attributes: .disabled) { _ in }] : actions @@ -232,6 +233,32 @@ final class IssuesViewController: RefreshingTableViewController { ) } + private func clearFiltersMenu() -> UIMenu { + UIMenu( + options: .displayInline, + children: [ + UIAction( + title: "Clear Filters", + image: context.symbol("xmark.circle"), + attributes: filtersActive ? [] : .disabled + ) { [weak self] _ in self?.clearFilters() }, + ] + ) + } + + private func clearFilters() { + filterTask?.cancel() + do { + try context.core.clearIssueFilters(owner: owner, repository: repository) + filterOptions?.selectedMilestone = "" + filterOptions?.selectedLabels = [] + updateFilterMenu() + loadIssues(refreshing: false) + } catch { + show(error: error) + } + } + private var filtersActive: Bool { (try? context.core.issueFiltersActive(owner: owner, repository: repository)) ?? false } @@ -835,7 +862,7 @@ final class PullsViewController: RefreshingTableViewController { let current = context.core.settings().pullStatus let status = UIMenu( title: "Status", - image: context.symbol("circle.lefthalf.filled"), + image: filterMenuImage("circle.lefthalf.filled", active: current != "open"), options: .singleSelection, children: ["open", "closed"].map { status in UIAction(title: status.capitalized, state: current == status ? .on : .off) { @@ -856,7 +883,7 @@ final class PullsViewController: RefreshingTableViewController { let item = navigationItem.rightBarButtonItem ?? UIBarButtonItem( image: context.symbol("line.3.horizontal.decrease.circle") ) - item.menu = UIMenu(children: [status, milestone]) + item.menu = UIMenu(children: [status, milestone, clearFiltersMenu()]) item.accessibilityLabel = "Filter pull requests" navigationItem.rightBarButtonItem = item updateFilterTint() @@ -874,7 +901,7 @@ final class PullsViewController: RefreshingTableViewController { } return UIMenu( title: "Milestone", - image: context.symbol("flag"), + image: filterMenuImage("flag", active: !selected.isEmpty), options: .singleSelection, children: [all] + milestones ) @@ -892,6 +919,31 @@ final class PullsViewController: RefreshingTableViewController { } } + private func clearFiltersMenu() -> UIMenu { + UIMenu( + options: .displayInline, + children: [ + UIAction( + title: "Clear Filters", + image: context.symbol("xmark.circle"), + attributes: filtersActive ? [] : .disabled + ) { [weak self] _ in self?.clearFilters() }, + ] + ) + } + + private func clearFilters() { + filterTask?.cancel() + do { + try context.core.clearPullFilters() + filterOptions?.selectedMilestone = "" + updateFilterMenu() + loadPulls(refreshing: false) + } catch { + show(error: error) + } + } + private var filtersActive: Bool { (try? context.core.pullFiltersActive()) ?? false } diff --git a/ios/Sources/Support.swift b/ios/Sources/Support.swift index 2bc6900..85d434c 100644 --- a/ios/Sources/Support.swift +++ b/ios/Sources/Support.swift @@ -201,6 +201,13 @@ func configureTextCell(_ cell: UITableViewCell, title: String, detail: String, i cell.backgroundColor = .systemBackground } +func filterMenuImage(_ symbol: String, active: Bool) -> UIImage? { + let image = UIImage(systemName: symbol) + return active + ? image?.withTintColor(.systemBlue, renderingMode: .alwaysOriginal) + : image +} + func configureRepositoryContentCell(_ cell: UITableViewCell, row: RepositoryContentRow) { let directory = row.kind == "dir" let detail = directory