Files
Gotcha/crates/app/src/domain.rs
2026-08-15 20:06:52 +02:00

278 lines
7.4 KiB
Rust

use std::{
collections::{BTreeMap, BTreeSet},
path::PathBuf,
};
pub use gotcha_gitea::{
HistoryCommit, HomeData, IssueDetails, IssueEditorData, MilestoneDetails, Page, PullDetails,
civil_from_days, days_from_civil, parse_api_date, parse_api_timestamp,
};
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum AppearanceMode {
Light,
Dark,
#[default]
#[serde(other)]
Auto,
}
impl AppearanceMode {
pub fn index(self) -> i32 {
match self {
Self::Auto => 0,
Self::Light => 1,
Self::Dark => 2,
}
}
pub fn from_index(index: i32) -> Option<Self> {
match index {
0 => Some(Self::Auto),
1 => Some(Self::Light),
2 => Some(Self::Dark),
_ => None,
}
}
}
#[derive(Clone, Deserialize, Serialize)]
pub struct Server {
pub name: String,
pub url: String,
#[serde(default)]
pub provider: gotcha_gitea::Provider,
#[serde(default)]
pub credential_account: String,
#[serde(skip)]
pub token: String,
}
#[derive(Clone, Deserialize, Serialize)]
pub struct Preferences {
#[serde(skip)]
pub path: PathBuf,
#[serde(default)]
pub servers: Vec<Server>,
#[serde(default)]
pub favorites: BTreeSet<String>,
#[serde(default)]
pub last_server: Option<usize>,
#[serde(default = "open_status")]
pub issue_status: String,
#[serde(default)]
pub issue_filters: BTreeMap<String, IssueFilter>,
#[serde(default = "open_status")]
pub pull_status: String,
#[serde(default)]
pub pull_filters: BTreeMap<String, PullFilter>,
#[serde(default)]
pub appearance: AppearanceMode,
#[serde(default)]
pub notifications_enabled: bool,
#[serde(default)]
pub notification_cursors: BTreeMap<String, String>,
#[serde(default = "default_primary_destinations")]
pub primary_destinations: Vec<crate::PrimaryDestination>,
}
impl Default for Preferences {
fn default() -> Self {
Self {
path: PathBuf::new(),
servers: Vec::new(),
favorites: BTreeSet::new(),
last_server: None,
issue_status: open_status(),
issue_filters: BTreeMap::new(),
pull_status: open_status(),
pull_filters: BTreeMap::new(),
appearance: AppearanceMode::default(),
notifications_enabled: false,
notification_cursors: BTreeMap::new(),
primary_destinations: default_primary_destinations(),
}
}
}
pub fn default_primary_destinations() -> Vec<crate::PrimaryDestination> {
vec![
crate::PrimaryDestination::Issues,
crate::PrimaryDestination::Repositories,
crate::PrimaryDestination::PullRequests,
crate::PrimaryDestination::Milestones,
]
}
pub fn normalize_primary_destinations(destinations: &mut Vec<crate::PrimaryDestination>) -> bool {
let original = destinations.clone();
let mut unique = Vec::with_capacity(destinations.len().min(4));
for destination in destinations.drain(..) {
if !unique.contains(&destination) && unique.len() < 4 {
unique.push(destination);
}
}
*destinations = unique;
*destinations != original
}
#[derive(Clone, Default, Deserialize, Eq, PartialEq, Serialize)]
pub struct IssueFilter {
#[serde(default)]
pub milestone: String,
#[serde(default)]
pub labels: BTreeSet<String>,
#[serde(skip)]
pub search_text: String,
}
impl IssueFilter {
pub fn is_active(&self, status: &str) -> bool {
status != "open" || self != &Self::default()
}
}
#[derive(Clone, Default, Deserialize, Eq, PartialEq, Serialize)]
pub struct PullFilter {
#[serde(default)]
pub milestone: String,
#[serde(skip)]
pub search_text: String,
}
impl PullFilter {
pub fn is_active(&self, status: &str) -> bool {
status != "open" || self != &Self::default()
}
}
#[derive(Clone)]
pub struct RepositoryData {
pub owner: String,
pub name: String,
pub description: String,
pub language: String,
pub open_issues: i64,
pub updated: String,
pub default_branch: String,
}
#[derive(Default)]
pub struct State {
pub preferences: Preferences,
pub active_server: Option<usize>,
pub repositories: Vec<RepositoryData>,
}
pub struct IssueDraft {
pub title: String,
pub body: String,
pub label_ids: Vec<i64>,
pub milestone_id: Option<i64>,
pub due_date: Option<i64>,
pub closed: bool,
}
pub struct MilestoneDraft {
pub title: String,
pub description: String,
pub due_date: Option<i64>,
pub closed: bool,
}
pub fn open_status() -> String {
"open".into()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn issue_filter_default_state_is_owned_by_rust() {
assert!(!IssueFilter::default().is_active("open"));
assert!(IssueFilter::default().is_active("closed"));
assert!(
IssueFilter {
milestone: "v1".into(),
..Default::default()
}
.is_active("open")
);
assert!(!PullFilter::default().is_active("open"));
assert!(PullFilter::default().is_active("closed"));
assert!(
PullFilter {
milestone: "v1".into(),
..Default::default()
}
.is_active("open")
);
assert!(
IssueFilter {
search_text: "needle".into(),
..Default::default()
}
.is_active("open")
);
assert!(
PullFilter {
search_text: "needle".into(),
..Default::default()
}
.is_active("open")
);
}
#[test]
fn search_text_is_session_only() {
let issue = IssueFilter {
search_text: "needle".into(),
..Default::default()
};
let pull = PullFilter {
search_text: "needle".into(),
..Default::default()
};
assert!(!serde_json::to_string(&issue).unwrap().contains("needle"));
assert!(!serde_json::to_string(&pull).unwrap().contains("needle"));
assert_eq!(
serde_json::from_str::<IssueFilter>(r#"{"milestone":"v1"}"#)
.unwrap()
.search_text,
""
);
}
#[test]
fn primary_destinations_are_ordered_unique_and_limited() {
let migrated: Preferences = serde_json::from_str("{}").unwrap();
assert_eq!(
migrated.primary_destinations,
default_primary_destinations()
);
let mut destinations = vec![
crate::PrimaryDestination::Actions,
crate::PrimaryDestination::Actions,
crate::PrimaryDestination::Issues,
crate::PrimaryDestination::Repositories,
crate::PrimaryDestination::PullRequests,
crate::PrimaryDestination::Milestones,
];
assert!(normalize_primary_destinations(&mut destinations));
assert_eq!(
destinations,
[
crate::PrimaryDestination::Actions,
crate::PrimaryDestination::Issues,
crate::PrimaryDestination::Repositories,
crate::PrimaryDestination::PullRequests,
]
);
}
}