Scope repository favorites by pane

This commit is contained in:
Georg Bauer
2026-07-31 18:19:02 +02:00
parent 9c7cef3869
commit 0a38dbe9f2
7 changed files with 187 additions and 33 deletions

View File

@@ -15,6 +15,25 @@ use storage::*;
uniffi::setup_scaffolding!();
#[derive(Clone, Copy, Debug, uniffi::Enum)]
pub enum RepositoryPane {
Issues,
Commits,
Milestones,
}
impl RepositoryPane {
const ALL: [Self; 3] = [Self::Issues, Self::Commits, Self::Milestones];
const fn key(self) -> &'static str {
match self {
Self::Issues => "issues",
Self::Commits => "commits",
Self::Milestones => "milestones",
}
}
}
#[derive(Debug, Error, uniffi::Error)]
pub enum GotchaError {
#[error("{message}")]
@@ -184,7 +203,11 @@ impl GotchaCore {
))
}
pub async fn repositories(&self, page: u32) -> Result<RepositoryListPage, GotchaError> {
pub async fn repositories(
&self,
page: u32,
pane: RepositoryPane,
) -> Result<RepositoryListPage, GotchaError> {
let server = self.server()?;
let page_number = valid_page(page)?;
let repositories = load_repositories(&server, page_number).await?;
@@ -202,7 +225,7 @@ impl GotchaCore {
}
}
Ok(RepositoryListPage {
rows: self.repository_rows(&state),
rows: self.repository_rows(&state, pane),
has_more: repositories.has_more,
})
}
@@ -211,18 +234,19 @@ impl GotchaCore {
&self,
owner: String,
repository: String,
pane: RepositoryPane,
) -> Result<Vec<RepositoryRow>, 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 = favorite_key(&server.url, &owner, &repository);
let key = favorite_key(pane, &server.url, &owner, &repository);
if !state.preferences.favorites.remove(&key) {
state.preferences.favorites.insert(key);
}
save_preferences(&state.preferences)?;
Ok(self.repository_rows(&state))
Ok(self.repository_rows(&state, pane))
}
pub async fn issues(
@@ -755,7 +779,7 @@ impl GotchaCore {
.ok_or_else(|| "Select a server first.".to_string().into())
}
fn repository_rows(&self, state: &State) -> Vec<RepositoryRow> {
fn repository_rows(&self, state: &State, pane: RepositoryPane) -> Vec<RepositoryRow> {
let server_url = state
.active_server
.and_then(|index| state.preferences.servers.get(index))
@@ -763,6 +787,7 @@ impl GotchaCore {
.unwrap_or_default();
repository_rows(&state.repositories, |repository| {
state.preferences.favorites.contains(&favorite_key(
pane,
server_url,
&repository.owner,
&repository.name,

View File

@@ -3,10 +3,17 @@ use std::{env, fs, path::PathBuf};
use gotcha_gitea::Client;
use security_framework::passwords::{get_generic_password, set_generic_password};
use crate::domain::{Preferences, Server, open_status};
use crate::{
RepositoryPane,
domain::{Preferences, Server, open_status},
};
pub fn favorite_key(server: &str, owner: &str, repository: &str) -> String {
repository_key(server, owner, repository)
pub fn favorite_key(pane: RepositoryPane, server: &str, owner: &str, repository: &str) -> String {
format!(
"{}|{}",
pane.key(),
repository_key(server, owner, repository)
)
}
pub fn repository_key(server: &str, owner: &str, repository: &str) -> String {
@@ -39,6 +46,7 @@ pub fn load_preferences() -> Result<Preferences, String> {
let mut preferences: Preferences =
serde_json::from_slice(&fs::read(&path).map_err(|error| error.to_string())?)
.map_err(|error| format!("Cannot read {}: {error}", path.display()))?;
let favorites_migrated = migrate_favorites(&mut preferences.favorites);
if !matches!(preferences.issue_status.as_str(), "open" | "closed") {
preferences.issue_status = open_status();
}
@@ -52,9 +60,29 @@ pub fn load_preferences() -> Result<Preferences, String> {
)
.map_err(|_| format!("The token for {} is not valid text.", server.name))?;
}
if favorites_migrated {
save_preferences(&preferences)?;
}
Ok(preferences)
}
fn migrate_favorites(favorites: &mut std::collections::BTreeSet<String>) -> bool {
let legacy: Vec<_> = favorites
.iter()
.filter(|favorite| {
!RepositoryPane::ALL
.iter()
.any(|pane| favorite.starts_with(&format!("{}|", pane.key())))
})
.cloned()
.collect();
for favorite in &legacy {
favorites.remove(favorite);
favorites.extend(RepositoryPane::ALL.map(|pane| format!("{}|{favorite}", pane.key())));
}
!legacy.is_empty()
}
pub fn save_preferences(preferences: &Preferences) -> Result<(), String> {
let path = preferences_path()?;
let parent = path.parent().ok_or("Invalid app data directory.")?;
@@ -96,8 +124,13 @@ mod tests {
assert!(validate_server("", "https://gitea.example.com", "secret").is_err());
assert!(validate_server("Work", "file:///tmp/gitea", "secret").is_err());
assert_eq!(
favorite_key("https://gitea.example.com", "octo", "demo"),
"https://gitea.example.com|octo/demo"
favorite_key(
RepositoryPane::Issues,
"https://gitea.example.com",
"octo",
"demo"
),
"issues|https://gitea.example.com|octo/demo"
);
let preferences: Preferences = serde_json::from_str(
r#"{"issue_filters":{"server|octo/demo":{"milestone":"v1","labels":["bug"]}},"pull_filters":{"server":{"milestone":"v2"}}}"#,
@@ -136,4 +169,24 @@ mod tests {
);
assert_eq!(crate::domain::AppearanceMode::from_index(3), None);
}
#[test]
fn migrates_global_favorites_to_each_repository_pane() {
let mut favorites = ["https://gitea.example.com|octo/demo".to_string()]
.into_iter()
.collect();
assert!(migrate_favorites(&mut favorites));
assert_eq!(
favorites,
[
"commits|https://gitea.example.com|octo/demo".to_string(),
"issues|https://gitea.example.com|octo/demo".to_string(),
"milestones|https://gitea.example.com|octo/demo".to_string(),
]
.into_iter()
.collect()
);
assert!(!migrate_favorites(&mut favorites));
}
}