Enforce Rust and Swift ownership boundary
This commit is contained in:
28
AGENTS.md
28
AGENTS.md
@@ -27,6 +27,34 @@ cargo test --workspace
|
|||||||
Do not weaken or skip these gates to make a commit pass. Fix the underlying
|
Do not weaken or skip these gates to make a commit pass. Fix the underlying
|
||||||
warning, lint, formatting issue, or test failure.
|
warning, lint, formatting issue, or test failure.
|
||||||
|
|
||||||
|
## Rust and Swift ownership boundary
|
||||||
|
|
||||||
|
Rust owns all application behavior. Put networking, authentication, input
|
||||||
|
validation, persistence, preference changes, filtering, sorting, aggregation,
|
||||||
|
parsing, content classification, display-data formatting, and navigation target
|
||||||
|
derivation in `crates/app`. Cover non-trivial behavior there with Rust tests and
|
||||||
|
expose view-ready records and operations through UniFFI.
|
||||||
|
|
||||||
|
Swift is the native iOS presentation layer. Limit `ios/Sources` to UIKit and
|
||||||
|
SwiftUI lifecycle, view-controller navigation, native controls, layout, drawing,
|
||||||
|
fonts, colors, symbols, accessibility, task cancellation tied to view lifetime,
|
||||||
|
and Apple presentation integrations such as Quick Look. Swift may map
|
||||||
|
Rust-provided states to visual treatments and maintain transient control state;
|
||||||
|
it must not infer domain state from display strings, duplicate Rust
|
||||||
|
transformations, classify content, or implement persistence and API behavior.
|
||||||
|
|
||||||
|
When changing iOS functionality:
|
||||||
|
|
||||||
|
- Trace the complete flow before editing and implement behavior in Rust first.
|
||||||
|
- Prefer view-ready Rust records over exposing raw Gitea models or rebuilding
|
||||||
|
titles, summaries, selections, progress, and categories in Swift.
|
||||||
|
- Treat a pure Swift helper that does not require an Apple UI framework as a
|
||||||
|
boundary warning; move it to Rust unless it only calculates view geometry.
|
||||||
|
- Regenerate and commit the UniFFI Swift and C bindings whenever the exported
|
||||||
|
Rust interface changes.
|
||||||
|
- During review, inspect both sides of the bridge and reject new business logic
|
||||||
|
added to Swift merely because its caller is a view controller.
|
||||||
|
|
||||||
## UI verification conventions
|
## UI verification conventions
|
||||||
|
|
||||||
`TESTING.md` is the source of truth for iOS visual verification and the release
|
`TESTING.md` is the source of truth for iOS visual verification and the release
|
||||||
|
|||||||
@@ -80,6 +80,12 @@ pub struct IssueFilter {
|
|||||||
pub labels: BTreeSet<String>,
|
pub labels: BTreeSet<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl IssueFilter {
|
||||||
|
pub fn is_active(&self, status: &str) -> bool {
|
||||||
|
status != "open" || self != &Self::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct RepositoryData {
|
pub struct RepositoryData {
|
||||||
pub owner: String,
|
pub owner: String,
|
||||||
@@ -132,3 +138,21 @@ pub struct PullDetails {
|
|||||||
pub fn open_status() -> String {
|
pub fn open_status() -> String {
|
||||||
"open".into()
|
"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")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -259,6 +259,26 @@ impl GotchaCore {
|
|||||||
Ok(issue_filter_options(&labels, &milestones, &filter))
|
Ok(issue_filter_options(&labels, &milestones, &filter))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn issue_filters_active(
|
||||||
|
&self,
|
||||||
|
owner: String,
|
||||||
|
repository: String,
|
||||||
|
) -> Result<bool, GotchaError> {
|
||||||
|
let server = self.server()?;
|
||||||
|
let state = self.state.lock().unwrap();
|
||||||
|
let filter = state
|
||||||
|
.preferences
|
||||||
|
.issue_filters
|
||||||
|
.get(&repository_key(
|
||||||
|
&server.url,
|
||||||
|
owner.trim(),
|
||||||
|
repository.trim(),
|
||||||
|
))
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_default();
|
||||||
|
Ok(filter.is_active(&state.preferences.issue_status))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_issue_filters(
|
pub fn set_issue_filters(
|
||||||
&self,
|
&self,
|
||||||
owner: String,
|
owner: String,
|
||||||
@@ -381,8 +401,9 @@ impl GotchaCore {
|
|||||||
owner: String,
|
owner: String,
|
||||||
repository: String,
|
repository: String,
|
||||||
path: String,
|
path: String,
|
||||||
) -> Result<Vec<u8>, GotchaError> {
|
) -> Result<RepositoryFilePage, GotchaError> {
|
||||||
Ok(load_repository_file(&self.server()?, &owner, &repository, &path).await?)
|
let data = load_repository_file(&self.server()?, &owner, &repository, &path).await?;
|
||||||
|
Ok(repository_file_page(&path, data))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn commit_files(
|
pub async fn commit_files(
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ pub struct IssuePage {
|
|||||||
pub struct IssueFilterOptions {
|
pub struct IssueFilterOptions {
|
||||||
pub milestones: Vec<String>,
|
pub milestones: Vec<String>,
|
||||||
pub labels: Vec<String>,
|
pub labels: Vec<String>,
|
||||||
|
pub unavailable_labels: Vec<String>,
|
||||||
pub selected_milestone: String,
|
pub selected_milestone: String,
|
||||||
pub selected_labels: Vec<String>,
|
pub selected_labels: Vec<String>,
|
||||||
}
|
}
|
||||||
@@ -81,8 +82,9 @@ pub struct MilestoneRow {
|
|||||||
pub title: String,
|
pub title: String,
|
||||||
pub description: String,
|
pub description: String,
|
||||||
pub meta: String,
|
pub meta: String,
|
||||||
pub open_issues: i64,
|
pub has_issues: bool,
|
||||||
pub closed_issues: i64,
|
pub progress: f64,
|
||||||
|
pub progress_accessibility: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, uniffi::Record)]
|
#[derive(Clone, uniffi::Record)]
|
||||||
@@ -105,8 +107,7 @@ pub struct PullPage {
|
|||||||
pub struct CommitRow {
|
pub struct CommitRow {
|
||||||
pub sha: String,
|
pub sha: String,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub meta: String,
|
pub detail: String,
|
||||||
pub refs: String,
|
|
||||||
pub top_lanes: Vec<u32>,
|
pub top_lanes: Vec<u32>,
|
||||||
pub bottom_lanes: Vec<u32>,
|
pub bottom_lanes: Vec<u32>,
|
||||||
pub node_lane: Option<u32>,
|
pub node_lane: Option<u32>,
|
||||||
@@ -134,6 +135,15 @@ pub struct RepositoryContentRow {
|
|||||||
pub size: i64,
|
pub size: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, uniffi::Record)]
|
||||||
|
pub struct RepositoryFilePage {
|
||||||
|
pub name: String,
|
||||||
|
pub kind: String,
|
||||||
|
pub language: String,
|
||||||
|
pub text: String,
|
||||||
|
pub data: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, uniffi::Record)]
|
#[derive(Clone, uniffi::Record)]
|
||||||
pub struct DiffLine {
|
pub struct DiffLine {
|
||||||
pub old_number: String,
|
pub old_number: String,
|
||||||
@@ -164,17 +174,16 @@ pub struct ActivityRow {
|
|||||||
|
|
||||||
#[derive(Clone, uniffi::Record)]
|
#[derive(Clone, uniffi::Record)]
|
||||||
pub struct HeatCell {
|
pub struct HeatCell {
|
||||||
pub week: u32,
|
|
||||||
pub day: u32,
|
|
||||||
pub level: u32,
|
pub level: u32,
|
||||||
pub timestamp: i64,
|
pub timestamp: i64,
|
||||||
pub contributions: i64,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, uniffi::Record)]
|
#[derive(Clone, uniffi::Record)]
|
||||||
pub struct HomePage {
|
pub struct HomePage {
|
||||||
pub server_name: String,
|
pub server_name: String,
|
||||||
pub activities: Vec<ActivityRow>,
|
pub activities: Vec<ActivityRow>,
|
||||||
|
pub issue_activities: Vec<ActivityRow>,
|
||||||
|
pub pull_activities: Vec<ActivityRow>,
|
||||||
pub heat_cells: Vec<HeatCell>,
|
pub heat_cells: Vec<HeatCell>,
|
||||||
pub contribution_count: i64,
|
pub contribution_count: i64,
|
||||||
}
|
}
|
||||||
@@ -237,17 +246,28 @@ pub fn issue_filter_options(
|
|||||||
.iter()
|
.iter()
|
||||||
.filter_map(|label| label.name.clone())
|
.filter_map(|label| label.name.clone())
|
||||||
.collect();
|
.collect();
|
||||||
|
let unavailable_labels = filter
|
||||||
|
.labels
|
||||||
|
.iter()
|
||||||
|
.filter(|label| !labels.contains(label))
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
labels.extend(filter.labels.iter().cloned());
|
||||||
labels.sort_by_key(|label| label.to_lowercase());
|
labels.sort_by_key(|label| label.to_lowercase());
|
||||||
labels.dedup();
|
labels.dedup();
|
||||||
let mut milestones: Vec<_> = milestones
|
let mut milestones: Vec<_> = milestones
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|milestone| milestone.title.clone())
|
.filter_map(|milestone| milestone.title.clone())
|
||||||
.collect();
|
.collect();
|
||||||
|
if !filter.milestone.is_empty() {
|
||||||
|
milestones.push(filter.milestone.clone());
|
||||||
|
}
|
||||||
milestones.sort_by_key(|milestone| milestone.to_lowercase());
|
milestones.sort_by_key(|milestone| milestone.to_lowercase());
|
||||||
milestones.dedup();
|
milestones.dedup();
|
||||||
IssueFilterOptions {
|
IssueFilterOptions {
|
||||||
milestones,
|
milestones,
|
||||||
labels,
|
labels,
|
||||||
|
unavailable_labels,
|
||||||
selected_milestone: filter.milestone.clone(),
|
selected_milestone: filter.milestone.clone(),
|
||||||
selected_labels: filter.labels.iter().cloned().collect(),
|
selected_labels: filter.labels.iter().cloned().collect(),
|
||||||
}
|
}
|
||||||
@@ -299,10 +319,12 @@ pub fn pull_rows(pulls: &[models::Issue]) -> Vec<PullRow> {
|
|||||||
number: pull.number.unwrap_or_default(),
|
number: pull.number.unwrap_or_default(),
|
||||||
owner: repository.owner.clone()?,
|
owner: repository.owner.clone()?,
|
||||||
repository: repository.name.clone()?,
|
repository: repository.name.clone()?,
|
||||||
title: pull
|
title: format!(
|
||||||
.title
|
"{} #{}\n{}",
|
||||||
.clone()
|
repository.name.as_deref()?,
|
||||||
.unwrap_or_else(|| "Untitled pull request".into()),
|
pull.number.unwrap_or_default(),
|
||||||
|
pull.title.as_deref().unwrap_or("Untitled pull request")
|
||||||
|
),
|
||||||
summary: pull
|
summary: pull
|
||||||
.body
|
.body
|
||||||
.as_deref()
|
.as_deref()
|
||||||
@@ -367,8 +389,16 @@ pub fn commit_page(
|
|||||||
.map(|message| message.lines().next().unwrap_or(message))
|
.map(|message| message.lines().next().unwrap_or(message))
|
||||||
.unwrap_or("Commit")
|
.unwrap_or("Commit")
|
||||||
.into(),
|
.into(),
|
||||||
meta: format!("{author} · {}", compact_date(date)),
|
detail: if row.refs.is_empty() {
|
||||||
refs: row.refs.join(" · "),
|
format!("{author} · {}\n{}", compact_date(date), short_sha(sha))
|
||||||
|
} else {
|
||||||
|
format!(
|
||||||
|
"{}\n{author} · {} · {}",
|
||||||
|
row.refs.join(" · "),
|
||||||
|
compact_date(date),
|
||||||
|
short_sha(sha)
|
||||||
|
)
|
||||||
|
},
|
||||||
top_lanes: indices(&row.top_lanes),
|
top_lanes: indices(&row.top_lanes),
|
||||||
bottom_lanes: indices(&row.bottom_lanes),
|
bottom_lanes: indices(&row.bottom_lanes),
|
||||||
node_lane: row.node_lane.map(|lane| lane as u32),
|
node_lane: row.node_lane.map(|lane| lane as u32),
|
||||||
@@ -488,6 +518,66 @@ pub fn repository_content_rows(
|
|||||||
rows
|
rows
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn repository_file_page(path: &str, data: Vec<u8>) -> RepositoryFilePage {
|
||||||
|
let name = path.rsplit('/').next().unwrap_or("Preview").to_string();
|
||||||
|
let extension = path.rsplit('.').next().unwrap_or_default().to_lowercase();
|
||||||
|
let language = source_language(path).unwrap_or_default().to_string();
|
||||||
|
let markdown = matches!(extension.as_str(), "md" | "markdown" | "mdown" | "mkd");
|
||||||
|
let media = matches!(
|
||||||
|
extension.as_str(),
|
||||||
|
"apng"
|
||||||
|
| "avi"
|
||||||
|
| "bmp"
|
||||||
|
| "flac"
|
||||||
|
| "gif"
|
||||||
|
| "heic"
|
||||||
|
| "heif"
|
||||||
|
| "jpeg"
|
||||||
|
| "jpg"
|
||||||
|
| "m4a"
|
||||||
|
| "m4v"
|
||||||
|
| "mkv"
|
||||||
|
| "mov"
|
||||||
|
| "mp3"
|
||||||
|
| "mp4"
|
||||||
|
| "mpeg"
|
||||||
|
| "mpg"
|
||||||
|
| "ogg"
|
||||||
|
| "pdf"
|
||||||
|
| "png"
|
||||||
|
| "tif"
|
||||||
|
| "tiff"
|
||||||
|
| "wav"
|
||||||
|
| "webm"
|
||||||
|
| "webp"
|
||||||
|
);
|
||||||
|
if media {
|
||||||
|
return RepositoryFilePage {
|
||||||
|
name,
|
||||||
|
kind: "preview".into(),
|
||||||
|
language,
|
||||||
|
text: String::new(),
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
match String::from_utf8(data) {
|
||||||
|
Ok(text) => RepositoryFilePage {
|
||||||
|
name,
|
||||||
|
kind: if markdown { "markdown" } else { "source" }.into(),
|
||||||
|
language,
|
||||||
|
text,
|
||||||
|
data: Vec::new(),
|
||||||
|
},
|
||||||
|
Err(error) => RepositoryFilePage {
|
||||||
|
name,
|
||||||
|
kind: "preview".into(),
|
||||||
|
language,
|
||||||
|
text: String::new(),
|
||||||
|
data: error.into_bytes(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn diff_page(path: &str, diff: crate::diff::Parsed) -> DiffPage {
|
pub fn diff_page(path: &str, diff: crate::diff::Parsed) -> DiffPage {
|
||||||
DiffPage {
|
DiffPage {
|
||||||
title: path.rsplit('/').next().unwrap_or(path).into(),
|
title: path.rsplit('/').next().unwrap_or(path).into(),
|
||||||
@@ -509,12 +599,51 @@ pub fn home_page(server_name: String, home: HomeData) -> HomePage {
|
|||||||
let (heat_cells, contribution_count) = heat_cells(&home.heatmap);
|
let (heat_cells, contribution_count) = heat_cells(&home.heatmap);
|
||||||
HomePage {
|
HomePage {
|
||||||
server_name,
|
server_name,
|
||||||
|
issue_activities: home
|
||||||
|
.activities
|
||||||
|
.iter()
|
||||||
|
.filter(|activity| is_issue_activity(activity))
|
||||||
|
.map(activity_row)
|
||||||
|
.collect(),
|
||||||
|
pull_activities: home
|
||||||
|
.activities
|
||||||
|
.iter()
|
||||||
|
.filter(|activity| is_pull_activity(activity))
|
||||||
|
.map(activity_row)
|
||||||
|
.collect(),
|
||||||
activities: home.activities.iter().map(activity_row).collect(),
|
activities: home.activities.iter().map(activity_row).collect(),
|
||||||
heat_cells,
|
heat_cells,
|
||||||
contribution_count,
|
contribution_count,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_issue_activity(activity: &models::Activity) -> bool {
|
||||||
|
use models::activity::OpType;
|
||||||
|
|
||||||
|
matches!(
|
||||||
|
activity.op_type,
|
||||||
|
Some(OpType::CreateIssue | OpType::CloseIssue | OpType::ReopenIssue | OpType::CommentIssue)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_pull_activity(activity: &models::Activity) -> bool {
|
||||||
|
use models::activity::OpType;
|
||||||
|
|
||||||
|
matches!(
|
||||||
|
activity.op_type,
|
||||||
|
Some(
|
||||||
|
OpType::CreatePullRequest
|
||||||
|
| OpType::MergePullRequest
|
||||||
|
| OpType::AutoMergePullRequest
|
||||||
|
| OpType::ClosePullRequest
|
||||||
|
| OpType::ReopenPullRequest
|
||||||
|
| OpType::CommentPull
|
||||||
|
| OpType::ApprovePullRequest
|
||||||
|
| OpType::RejectPullRequest
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn activity_row(activity: &models::Activity) -> ActivityRow {
|
fn activity_row(activity: &models::Activity) -> ActivityRow {
|
||||||
use models::activity::OpType;
|
use models::activity::OpType;
|
||||||
|
|
||||||
@@ -653,15 +782,20 @@ fn activity_detail(activity: &models::Activity) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn heat_cells(data: &[models::UserHeatmapData]) -> (Vec<HeatCell>, i64) {
|
fn heat_cells(data: &[models::UserHeatmapData]) -> (Vec<HeatCell>, i64) {
|
||||||
const DAYS: i64 = 9 * 31;
|
|
||||||
let latest = data
|
let latest = data
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|entry| entry.timestamp)
|
.filter_map(|entry| entry.timestamp)
|
||||||
.max()
|
.max()
|
||||||
.unwrap_or(0)
|
.unwrap_or(0)
|
||||||
/ 86_400;
|
/ 86_400;
|
||||||
let start = latest - DAYS + 1;
|
let (year, month, _) = civil_from_days(latest);
|
||||||
let mut counts = vec![0_i64; DAYS as usize];
|
let first_month = year * 12 + month - 1 - 8;
|
||||||
|
let start = days_from_civil(
|
||||||
|
first_month.div_euclid(12),
|
||||||
|
first_month.rem_euclid(12) + 1,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
let mut counts = vec![0_i64; (latest - start + 1) as usize];
|
||||||
for entry in data {
|
for entry in data {
|
||||||
let day = entry.timestamp.unwrap_or_default() / 86_400;
|
let day = entry.timestamp.unwrap_or_default() / 86_400;
|
||||||
if (start..=latest).contains(&day) {
|
if (start..=latest).contains(&day) {
|
||||||
@@ -674,20 +808,42 @@ fn heat_cells(data: &[models::UserHeatmapData]) -> (Vec<HeatCell>, i64) {
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(index, count)| HeatCell {
|
.map(|(index, count)| HeatCell {
|
||||||
week: (index / 7) as u32,
|
|
||||||
day: (index % 7) as u32,
|
|
||||||
level: if count == 0 || maximum == 0 {
|
level: if count == 0 || maximum == 0 {
|
||||||
0
|
0
|
||||||
} else {
|
} else {
|
||||||
((count * 4 + maximum - 1) / maximum).clamp(1, 4) as u32
|
((count * 4 + maximum - 1) / maximum).clamp(1, 4) as u32
|
||||||
},
|
},
|
||||||
timestamp: (start + index as i64) * 86_400,
|
timestamp: (start + index as i64) * 86_400,
|
||||||
contributions: count,
|
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
(cells, contribution_count)
|
(cells, contribution_count)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn civil_from_days(days: i64) -> (i64, i64, i64) {
|
||||||
|
let days = days + 719_468;
|
||||||
|
let era = days.div_euclid(146_097);
|
||||||
|
let day_of_era = days - era * 146_097;
|
||||||
|
let year_of_era =
|
||||||
|
(day_of_era - day_of_era / 1_460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
|
||||||
|
let mut year = year_of_era + era * 400;
|
||||||
|
let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
|
||||||
|
let month_prime = (5 * day_of_year + 2) / 153;
|
||||||
|
let day = day_of_year - (153 * month_prime + 2) / 5 + 1;
|
||||||
|
let month = month_prime + if month_prime < 10 { 3 } else { -9 };
|
||||||
|
year += i64::from(month <= 2);
|
||||||
|
(year, month, day)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn days_from_civil(mut year: i64, month: i64, day: i64) -> i64 {
|
||||||
|
year -= i64::from(month <= 2);
|
||||||
|
let era = year.div_euclid(400);
|
||||||
|
let year_of_era = year - era * 400;
|
||||||
|
let month_prime = month + if month > 2 { -3 } else { 9 };
|
||||||
|
let day_of_year = (153 * month_prime + 2) / 5 + day - 1;
|
||||||
|
let day_of_era = year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year;
|
||||||
|
era * 146_097 + day_of_era - 719_468
|
||||||
|
}
|
||||||
|
|
||||||
fn label_row(label: &models::Label) -> Option<LabelRow> {
|
fn label_row(label: &models::Label) -> Option<LabelRow> {
|
||||||
let name = label.name.as_deref()?.trim();
|
let name = label.name.as_deref()?.trim();
|
||||||
let color = label.color.as_deref()?.trim().trim_start_matches('#');
|
let color = label.color.as_deref()?.trim().trim_start_matches('#');
|
||||||
@@ -777,6 +933,7 @@ fn issue_milestone(issue: &models::Issue) -> String {
|
|||||||
fn milestone_row(milestone: &models::Milestone) -> MilestoneRow {
|
fn milestone_row(milestone: &models::Milestone) -> MilestoneRow {
|
||||||
let open = milestone.open_issues.unwrap_or_default();
|
let open = milestone.open_issues.unwrap_or_default();
|
||||||
let closed = milestone.closed_issues.unwrap_or_default();
|
let closed = milestone.closed_issues.unwrap_or_default();
|
||||||
|
let total = open + closed;
|
||||||
let due = milestone
|
let due = milestone
|
||||||
.due_on
|
.due_on
|
||||||
.as_deref()
|
.as_deref()
|
||||||
@@ -798,8 +955,68 @@ fn milestone_row(milestone: &models::Milestone) -> MilestoneRow {
|
|||||||
milestone.state.as_deref().unwrap_or("unknown"),
|
milestone.state.as_deref().unwrap_or("unknown"),
|
||||||
open + closed
|
open + closed
|
||||||
),
|
),
|
||||||
open_issues: open,
|
has_issues: total > 0,
|
||||||
closed_issues: closed,
|
progress: if total == 0 {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
closed as f64 / total as f64
|
||||||
|
},
|
||||||
|
progress_accessibility: format!("{closed} closed, {open} open"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn short_sha(sha: &str) -> &str {
|
||||||
|
&sha[..sha.len().min(8)]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn source_language(path: &str) -> Option<&'static str> {
|
||||||
|
let filename = path.rsplit('/').next()?.to_lowercase();
|
||||||
|
match filename.as_str() {
|
||||||
|
"cmakelists.txt" => return Some("cmake"),
|
||||||
|
"dockerfile" => return Some("dockerfile"),
|
||||||
|
"gemfile" | "podfile" => return Some("ruby"),
|
||||||
|
"makefile" => return Some("makefile"),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
match filename.rsplit('.').next()? {
|
||||||
|
"asm" => Some("x86asm"),
|
||||||
|
"c" => Some("c"),
|
||||||
|
"cc" | "cpp" | "h" | "hpp" => Some("cpp"),
|
||||||
|
"clj" => Some("clojure"),
|
||||||
|
"cs" => Some("csharp"),
|
||||||
|
"css" => Some("css"),
|
||||||
|
"dart" => Some("dart"),
|
||||||
|
"ex" | "exs" => Some("elixir"),
|
||||||
|
"fs" => Some("fsharp"),
|
||||||
|
"go" => Some("go"),
|
||||||
|
"groovy" => Some("groovy"),
|
||||||
|
"hs" => Some("haskell"),
|
||||||
|
"html" | "plist" | "xml" => Some("xml"),
|
||||||
|
"java" => Some("java"),
|
||||||
|
"jl" => Some("julia"),
|
||||||
|
"js" | "jsx" => Some("javascript"),
|
||||||
|
"json" => Some("json"),
|
||||||
|
"kt" | "kts" => Some("kotlin"),
|
||||||
|
"lua" => Some("lua"),
|
||||||
|
"m" | "mm" => Some("objectivec"),
|
||||||
|
"md" | "markdown" | "mdown" | "mkd" => Some("markdown"),
|
||||||
|
"php" => Some("php"),
|
||||||
|
"pl" => Some("perl"),
|
||||||
|
"ps1" => Some("powershell"),
|
||||||
|
"py" => Some("python"),
|
||||||
|
"r" => Some("r"),
|
||||||
|
"rb" => Some("ruby"),
|
||||||
|
"rs" => Some("rust"),
|
||||||
|
"scala" => Some("scala"),
|
||||||
|
"scss" => Some("scss"),
|
||||||
|
"sh" => Some("bash"),
|
||||||
|
"sql" => Some("sql"),
|
||||||
|
"swift" => Some("swift"),
|
||||||
|
"toml" => Some("ini"),
|
||||||
|
"ts" | "tsx" => Some("typescript"),
|
||||||
|
"txt" => Some("plaintext"),
|
||||||
|
"yaml" | "yml" => Some("yaml"),
|
||||||
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -853,24 +1070,19 @@ mod tests {
|
|||||||
fn maps_heat_levels_and_labels() {
|
fn maps_heat_levels_and_labels() {
|
||||||
let heatmap = vec![
|
let heatmap = vec![
|
||||||
models::UserHeatmapData {
|
models::UserHeatmapData {
|
||||||
timestamp: Some(100 * 86_400),
|
timestamp: Some(20_393 * 86_400),
|
||||||
contributions: Some(1),
|
contributions: Some(1),
|
||||||
},
|
},
|
||||||
models::UserHeatmapData {
|
models::UserHeatmapData {
|
||||||
timestamp: Some(101 * 86_400),
|
timestamp: Some(20_665 * 86_400),
|
||||||
contributions: Some(4),
|
contributions: Some(4),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
let (cells, total) = heat_cells(&heatmap);
|
let (cells, total) = heat_cells(&heatmap);
|
||||||
assert_eq!((cells.len(), total), (279, 5));
|
assert_eq!((cells.len(), total), (273, 5));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
(
|
(cells[0].level, cells[0].timestamp, cells[272].level,),
|
||||||
cells[277].level,
|
(1, 20_393 * 86_400, 4)
|
||||||
cells[277].timestamp,
|
|
||||||
cells[277].contributions,
|
|
||||||
cells[278].level,
|
|
||||||
),
|
|
||||||
(1, 100 * 86_400, 1, 4)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let label = models::Label {
|
let label = models::Label {
|
||||||
@@ -890,10 +1102,8 @@ mod tests {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let milestone_row = milestone_rows(std::slice::from_ref(&milestone)).remove(0);
|
let milestone_row = milestone_rows(std::slice::from_ref(&milestone)).remove(0);
|
||||||
assert_eq!(
|
assert_eq!(milestone_row.progress, 0.4);
|
||||||
(milestone_row.open_issues, milestone_row.closed_issues),
|
assert_eq!(milestone_row.progress_accessibility, "2 closed, 3 open");
|
||||||
(3, 2)
|
|
||||||
);
|
|
||||||
let issue = models::Issue {
|
let issue = models::Issue {
|
||||||
milestone: Some(Box::new(milestone)),
|
milestone: Some(Box::new(milestone)),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
@@ -901,6 +1111,33 @@ mod tests {
|
|||||||
assert_eq!(issue_rows(&[issue])[0].milestone, "Version 1");
|
assert_eq!(issue_rows(&[issue])[0].milestone, "Version 1");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn categorizes_home_activity_before_crossing_the_bridge() {
|
||||||
|
use models::activity::OpType;
|
||||||
|
|
||||||
|
let page = home_page(
|
||||||
|
"Gitea".into(),
|
||||||
|
HomeData {
|
||||||
|
activities: [
|
||||||
|
OpType::CreateIssue,
|
||||||
|
OpType::CreatePullRequest,
|
||||||
|
OpType::CreateRepo,
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.map(|op_type| models::Activity {
|
||||||
|
op_type: Some(op_type),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
heatmap: Vec::new(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(page.activities.len(), 3);
|
||||||
|
assert_eq!(page.issue_activities.len(), 1);
|
||||||
|
assert_eq!(page.pull_activities.len(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn issue_page_exposes_state() {
|
fn issue_page_exposes_state() {
|
||||||
let page = issue_page(IssueDetails {
|
let page = issue_page(IssueDetails {
|
||||||
@@ -917,8 +1154,8 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn builds_sorted_issue_filter_options() {
|
fn builds_sorted_issue_filter_options() {
|
||||||
let filter = IssueFilter {
|
let filter = IssueFilter {
|
||||||
milestone: "v2".into(),
|
milestone: "v3".into(),
|
||||||
labels: ["bug".into(), "critical".into()].into(),
|
labels: ["bug".into(), "retired".into()].into(),
|
||||||
};
|
};
|
||||||
let options = issue_filter_options(
|
let options = issue_filter_options(
|
||||||
&[
|
&[
|
||||||
@@ -944,9 +1181,38 @@ mod tests {
|
|||||||
&filter,
|
&filter,
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(options.labels, ["bug", "critical"]);
|
assert_eq!(options.labels, ["bug", "critical", "retired"]);
|
||||||
assert_eq!(options.milestones, ["v1", "v2"]);
|
assert_eq!(options.unavailable_labels, ["retired"]);
|
||||||
assert_eq!(options.selected_labels, ["bug", "critical"]);
|
assert_eq!(options.milestones, ["v1", "v2", "v3"]);
|
||||||
assert_eq!(options.selected_milestone, "v2");
|
assert_eq!(options.selected_labels, ["bug", "retired"]);
|
||||||
|
assert_eq!(options.selected_milestone, "v3");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn classifies_repository_files_in_rust() {
|
||||||
|
let markdown = repository_file_page("docs/README.md", b"# Hello".to_vec());
|
||||||
|
assert_eq!(
|
||||||
|
(
|
||||||
|
markdown.name.as_str(),
|
||||||
|
markdown.kind.as_str(),
|
||||||
|
markdown.language.as_str()
|
||||||
|
),
|
||||||
|
("README.md", "markdown", "markdown")
|
||||||
|
);
|
||||||
|
assert_eq!(markdown.text, "# Hello");
|
||||||
|
|
||||||
|
let source = repository_file_page("Sources/App.swift", b"import UIKit".to_vec());
|
||||||
|
assert_eq!(
|
||||||
|
(source.kind.as_str(), source.language.as_str()),
|
||||||
|
("source", "swift")
|
||||||
|
);
|
||||||
|
|
||||||
|
let preview = repository_file_page("image.png", vec![0, 159]);
|
||||||
|
assert_eq!(preview.kind, "preview");
|
||||||
|
assert_eq!(preview.data, [0, 159]);
|
||||||
|
|
||||||
|
for path in ["manual.pdf", "sound.mp3", "movie.mp4"] {
|
||||||
|
assert_eq!(repository_file_page(path, Vec::new()).kind, "preview");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -494,6 +494,22 @@ fileprivate struct FfiConverterInt64: FfiConverterPrimitive {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
fileprivate struct FfiConverterDouble: FfiConverterPrimitive {
|
||||||
|
typealias FfiType = Double
|
||||||
|
typealias SwiftType = Double
|
||||||
|
|
||||||
|
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Double {
|
||||||
|
return try lift(readDouble(&buf))
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func write(_ value: Double, into buf: inout [UInt8]) {
|
||||||
|
writeDouble(&buf, lower(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#if swift(>=5.8)
|
#if swift(>=5.8)
|
||||||
@_documentation(visibility: private)
|
@_documentation(visibility: private)
|
||||||
#endif
|
#endif
|
||||||
@@ -605,6 +621,8 @@ public protocol GotchaCoreProtocol: AnyObject, Sendable {
|
|||||||
|
|
||||||
func issueFilters(owner: String, repository: String) async throws -> IssueFilterOptions
|
func issueFilters(owner: String, repository: String) async throws -> IssueFilterOptions
|
||||||
|
|
||||||
|
func issueFiltersActive(owner: String, repository: String) throws -> Bool
|
||||||
|
|
||||||
func issues(owner: String, repository: String) async throws -> [IssueRow]
|
func issues(owner: String, repository: String) async throws -> [IssueRow]
|
||||||
|
|
||||||
func milestone(owner: String, repository: String, id: Int64) async throws -> MilestonePage
|
func milestone(owner: String, repository: String, id: Int64) async throws -> MilestonePage
|
||||||
@@ -621,7 +639,7 @@ public protocol GotchaCoreProtocol: AnyObject, Sendable {
|
|||||||
|
|
||||||
func repositoryContents(owner: String, repository: String, path: String) async throws -> [RepositoryContentRow]
|
func repositoryContents(owner: String, repository: String, path: String) async throws -> [RepositoryContentRow]
|
||||||
|
|
||||||
func repositoryFile(owner: String, repository: String, path: String) async throws -> Data
|
func repositoryFile(owner: String, repository: String, path: String) async throws -> RepositoryFilePage
|
||||||
|
|
||||||
func selectServer(index: UInt32) throws
|
func selectServer(index: UInt32) throws
|
||||||
|
|
||||||
@@ -819,10 +837,10 @@ open func issue(owner: String, repository: String, number: Int64)async throws -
|
|||||||
|
|
||||||
open func issueFilters(owner: String, repository: String)async throws -> IssueFilterOptions {
|
open func issueFilters(owner: String, repository: String)async throws -> IssueFilterOptions {
|
||||||
return
|
return
|
||||||
try await uniffiRustCallAsync(
|
try await uniffiRustCallAsync(
|
||||||
rustFutureFunc: {
|
rustFutureFunc: {
|
||||||
uniffi_gotcha_core_fn_method_gotchacore_issue_filters(
|
uniffi_gotcha_core_fn_method_gotchacore_issue_filters(
|
||||||
self.uniffiCloneHandle(), FfiConverterString.lower(owner), FfiConverterString.lower(repository)
|
self.uniffiCloneHandle(),FfiConverterString.lower(owner),FfiConverterString.lower(repository)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
pollFunc: ffi_gotcha_core_rust_future_poll_rust_buffer,
|
pollFunc: ffi_gotcha_core_rust_future_poll_rust_buffer,
|
||||||
@@ -833,6 +851,17 @@ open func issueFilters(owner: String, repository: String)async throws -> IssueF
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
open func issueFiltersActive(owner: String, repository: String)throws -> Bool {
|
||||||
|
return try FfiConverterBool.lift(try rustCallWithError(FfiConverterTypeGotchaError_lift) {
|
||||||
|
uniffiCallStatus in
|
||||||
|
uniffi_gotcha_core_fn_method_gotchacore_issue_filters_active(
|
||||||
|
self.uniffiCloneHandle(),
|
||||||
|
FfiConverterString.lower(owner),
|
||||||
|
FfiConverterString.lower(repository),uniffiCallStatus
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
open func issues(owner: String, repository: String)async throws -> [IssueRow] {
|
open func issues(owner: String, repository: String)async throws -> [IssueRow] {
|
||||||
return
|
return
|
||||||
try await uniffiRustCallAsync(
|
try await uniffiRustCallAsync(
|
||||||
@@ -961,7 +990,7 @@ open func repositoryContents(owner: String, repository: String, path: String)asy
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
open func repositoryFile(owner: String, repository: String, path: String)async throws -> Data {
|
open func repositoryFile(owner: String, repository: String, path: String)async throws -> RepositoryFilePage {
|
||||||
return
|
return
|
||||||
try await uniffiRustCallAsync(
|
try await uniffiRustCallAsync(
|
||||||
rustFutureFunc: {
|
rustFutureFunc: {
|
||||||
@@ -972,7 +1001,7 @@ open func repositoryFile(owner: String, repository: String, path: String)async t
|
|||||||
pollFunc: ffi_gotcha_core_rust_future_poll_rust_buffer,
|
pollFunc: ffi_gotcha_core_rust_future_poll_rust_buffer,
|
||||||
completeFunc: ffi_gotcha_core_rust_future_complete_rust_buffer,
|
completeFunc: ffi_gotcha_core_rust_future_complete_rust_buffer,
|
||||||
freeFunc: ffi_gotcha_core_rust_future_free_rust_buffer,
|
freeFunc: ffi_gotcha_core_rust_future_free_rust_buffer,
|
||||||
liftFunc: FfiConverterData.lift,
|
liftFunc: FfiConverterTypeRepositoryFilePage_lift,
|
||||||
errorHandler: FfiConverterTypeGotchaError_lift
|
errorHandler: FfiConverterTypeGotchaError_lift
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1007,11 +1036,11 @@ open func setAppearance(index: UInt32)throws {try rustCallWithError(FfiConvert
|
|||||||
open func setIssueFilters(owner: String, repository: String, milestone: String, labels: [String])throws {try rustCallWithError(FfiConverterTypeGotchaError_lift) {
|
open func setIssueFilters(owner: String, repository: String, milestone: String, labels: [String])throws {try rustCallWithError(FfiConverterTypeGotchaError_lift) {
|
||||||
uniffiCallStatus in
|
uniffiCallStatus in
|
||||||
uniffi_gotcha_core_fn_method_gotchacore_set_issue_filters(
|
uniffi_gotcha_core_fn_method_gotchacore_set_issue_filters(
|
||||||
self.uniffiCloneHandle(),
|
self.uniffiCloneHandle(),
|
||||||
FfiConverterString.lower(owner),
|
FfiConverterString.lower(owner),
|
||||||
FfiConverterString.lower(repository),
|
FfiConverterString.lower(repository),
|
||||||
FfiConverterString.lower(milestone),
|
FfiConverterString.lower(milestone),
|
||||||
FfiConverterSequenceString.lower(labels), uniffiCallStatus
|
FfiConverterSequenceString.lower(labels),uniffiCallStatus
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1312,8 +1341,7 @@ public func FfiConverterTypeCommitPage_lower(_ value: CommitPage) -> RustBuffer
|
|||||||
public struct CommitRow: Equatable, Hashable {
|
public struct CommitRow: Equatable, Hashable {
|
||||||
public var sha: String
|
public var sha: String
|
||||||
public var title: String
|
public var title: String
|
||||||
public var meta: String
|
public var detail: String
|
||||||
public var refs: String
|
|
||||||
public var topLanes: [UInt32]
|
public var topLanes: [UInt32]
|
||||||
public var bottomLanes: [UInt32]
|
public var bottomLanes: [UInt32]
|
||||||
public var nodeLane: UInt32?
|
public var nodeLane: UInt32?
|
||||||
@@ -1321,11 +1349,10 @@ public struct CommitRow: Equatable, Hashable {
|
|||||||
|
|
||||||
// Default memberwise initializers are never public by default, so we
|
// Default memberwise initializers are never public by default, so we
|
||||||
// declare one manually.
|
// declare one manually.
|
||||||
public init(sha: String, title: String, meta: String, refs: String, topLanes: [UInt32], bottomLanes: [UInt32], nodeLane: UInt32?, connections: [UInt32]) {
|
public init(sha: String, title: String, detail: String, topLanes: [UInt32], bottomLanes: [UInt32], nodeLane: UInt32?, connections: [UInt32]) {
|
||||||
self.sha = sha
|
self.sha = sha
|
||||||
self.title = title
|
self.title = title
|
||||||
self.meta = meta
|
self.detail = detail
|
||||||
self.refs = refs
|
|
||||||
self.topLanes = topLanes
|
self.topLanes = topLanes
|
||||||
self.bottomLanes = bottomLanes
|
self.bottomLanes = bottomLanes
|
||||||
self.nodeLane = nodeLane
|
self.nodeLane = nodeLane
|
||||||
@@ -1350,8 +1377,7 @@ public struct FfiConverterTypeCommitRow: FfiConverterRustBuffer {
|
|||||||
try CommitRow(
|
try CommitRow(
|
||||||
sha: FfiConverterString.read(from: &buf),
|
sha: FfiConverterString.read(from: &buf),
|
||||||
title: FfiConverterString.read(from: &buf),
|
title: FfiConverterString.read(from: &buf),
|
||||||
meta: FfiConverterString.read(from: &buf),
|
detail: FfiConverterString.read(from: &buf),
|
||||||
refs: FfiConverterString.read(from: &buf),
|
|
||||||
topLanes: FfiConverterSequenceUInt32.read(from: &buf),
|
topLanes: FfiConverterSequenceUInt32.read(from: &buf),
|
||||||
bottomLanes: FfiConverterSequenceUInt32.read(from: &buf),
|
bottomLanes: FfiConverterSequenceUInt32.read(from: &buf),
|
||||||
nodeLane: FfiConverterOptionUInt32.read(from: &buf),
|
nodeLane: FfiConverterOptionUInt32.read(from: &buf),
|
||||||
@@ -1362,8 +1388,7 @@ public struct FfiConverterTypeCommitRow: FfiConverterRustBuffer {
|
|||||||
public static func write(_ value: CommitRow, into buf: inout [UInt8]) {
|
public static func write(_ value: CommitRow, into buf: inout [UInt8]) {
|
||||||
FfiConverterString.write(value.sha, into: &buf)
|
FfiConverterString.write(value.sha, into: &buf)
|
||||||
FfiConverterString.write(value.title, into: &buf)
|
FfiConverterString.write(value.title, into: &buf)
|
||||||
FfiConverterString.write(value.meta, into: &buf)
|
FfiConverterString.write(value.detail, into: &buf)
|
||||||
FfiConverterString.write(value.refs, into: &buf)
|
|
||||||
FfiConverterSequenceUInt32.write(value.topLanes, into: &buf)
|
FfiConverterSequenceUInt32.write(value.topLanes, into: &buf)
|
||||||
FfiConverterSequenceUInt32.write(value.bottomLanes, into: &buf)
|
FfiConverterSequenceUInt32.write(value.bottomLanes, into: &buf)
|
||||||
FfiConverterOptionUInt32.write(value.nodeLane, into: &buf)
|
FfiConverterOptionUInt32.write(value.nodeLane, into: &buf)
|
||||||
@@ -1562,20 +1587,14 @@ public func FfiConverterTypeFileRow_lower(_ value: FileRow) -> RustBuffer {
|
|||||||
|
|
||||||
|
|
||||||
public struct HeatCell: Equatable, Hashable {
|
public struct HeatCell: Equatable, Hashable {
|
||||||
public var week: UInt32
|
|
||||||
public var day: UInt32
|
|
||||||
public var level: UInt32
|
public var level: UInt32
|
||||||
public var timestamp: Int64
|
public var timestamp: Int64
|
||||||
public var contributions: Int64
|
|
||||||
|
|
||||||
// Default memberwise initializers are never public by default, so we
|
// Default memberwise initializers are never public by default, so we
|
||||||
// declare one manually.
|
// declare one manually.
|
||||||
public init(week: UInt32, day: UInt32, level: UInt32, timestamp: Int64, contributions: Int64) {
|
public init(level: UInt32, timestamp: Int64) {
|
||||||
self.week = week
|
|
||||||
self.day = day
|
|
||||||
self.level = level
|
self.level = level
|
||||||
self.timestamp = timestamp
|
self.timestamp = timestamp
|
||||||
self.contributions = contributions
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1594,20 +1613,14 @@ public struct FfiConverterTypeHeatCell: FfiConverterRustBuffer {
|
|||||||
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> HeatCell {
|
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> HeatCell {
|
||||||
return
|
return
|
||||||
try HeatCell(
|
try HeatCell(
|
||||||
week: FfiConverterUInt32.read(from: &buf),
|
|
||||||
day: FfiConverterUInt32.read(from: &buf),
|
|
||||||
level: FfiConverterUInt32.read(from: &buf),
|
level: FfiConverterUInt32.read(from: &buf),
|
||||||
timestamp: FfiConverterInt64.read(from: &buf),
|
timestamp: FfiConverterInt64.read(from: &buf)
|
||||||
contributions: FfiConverterInt64.read(from: &buf)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func write(_ value: HeatCell, into buf: inout [UInt8]) {
|
public static func write(_ value: HeatCell, into buf: inout [UInt8]) {
|
||||||
FfiConverterUInt32.write(value.week, into: &buf)
|
|
||||||
FfiConverterUInt32.write(value.day, into: &buf)
|
|
||||||
FfiConverterUInt32.write(value.level, into: &buf)
|
FfiConverterUInt32.write(value.level, into: &buf)
|
||||||
FfiConverterInt64.write(value.timestamp, into: &buf)
|
FfiConverterInt64.write(value.timestamp, into: &buf)
|
||||||
FfiConverterInt64.write(value.contributions, into: &buf)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1630,14 +1643,18 @@ public func FfiConverterTypeHeatCell_lower(_ value: HeatCell) -> RustBuffer {
|
|||||||
public struct HomePage: Equatable, Hashable {
|
public struct HomePage: Equatable, Hashable {
|
||||||
public var serverName: String
|
public var serverName: String
|
||||||
public var activities: [ActivityRow]
|
public var activities: [ActivityRow]
|
||||||
|
public var issueActivities: [ActivityRow]
|
||||||
|
public var pullActivities: [ActivityRow]
|
||||||
public var heatCells: [HeatCell]
|
public var heatCells: [HeatCell]
|
||||||
public var contributionCount: Int64
|
public var contributionCount: Int64
|
||||||
|
|
||||||
// Default memberwise initializers are never public by default, so we
|
// Default memberwise initializers are never public by default, so we
|
||||||
// declare one manually.
|
// declare one manually.
|
||||||
public init(serverName: String, activities: [ActivityRow], heatCells: [HeatCell], contributionCount: Int64) {
|
public init(serverName: String, activities: [ActivityRow], issueActivities: [ActivityRow], pullActivities: [ActivityRow], heatCells: [HeatCell], contributionCount: Int64) {
|
||||||
self.serverName = serverName
|
self.serverName = serverName
|
||||||
self.activities = activities
|
self.activities = activities
|
||||||
|
self.issueActivities = issueActivities
|
||||||
|
self.pullActivities = pullActivities
|
||||||
self.heatCells = heatCells
|
self.heatCells = heatCells
|
||||||
self.contributionCount = contributionCount
|
self.contributionCount = contributionCount
|
||||||
}
|
}
|
||||||
@@ -1660,6 +1677,8 @@ public struct FfiConverterTypeHomePage: FfiConverterRustBuffer {
|
|||||||
try HomePage(
|
try HomePage(
|
||||||
serverName: FfiConverterString.read(from: &buf),
|
serverName: FfiConverterString.read(from: &buf),
|
||||||
activities: FfiConverterSequenceTypeActivityRow.read(from: &buf),
|
activities: FfiConverterSequenceTypeActivityRow.read(from: &buf),
|
||||||
|
issueActivities: FfiConverterSequenceTypeActivityRow.read(from: &buf),
|
||||||
|
pullActivities: FfiConverterSequenceTypeActivityRow.read(from: &buf),
|
||||||
heatCells: FfiConverterSequenceTypeHeatCell.read(from: &buf),
|
heatCells: FfiConverterSequenceTypeHeatCell.read(from: &buf),
|
||||||
contributionCount: FfiConverterInt64.read(from: &buf)
|
contributionCount: FfiConverterInt64.read(from: &buf)
|
||||||
)
|
)
|
||||||
@@ -1668,6 +1687,8 @@ public struct FfiConverterTypeHomePage: FfiConverterRustBuffer {
|
|||||||
public static func write(_ value: HomePage, into buf: inout [UInt8]) {
|
public static func write(_ value: HomePage, into buf: inout [UInt8]) {
|
||||||
FfiConverterString.write(value.serverName, into: &buf)
|
FfiConverterString.write(value.serverName, into: &buf)
|
||||||
FfiConverterSequenceTypeActivityRow.write(value.activities, into: &buf)
|
FfiConverterSequenceTypeActivityRow.write(value.activities, into: &buf)
|
||||||
|
FfiConverterSequenceTypeActivityRow.write(value.issueActivities, into: &buf)
|
||||||
|
FfiConverterSequenceTypeActivityRow.write(value.pullActivities, into: &buf)
|
||||||
FfiConverterSequenceTypeHeatCell.write(value.heatCells, into: &buf)
|
FfiConverterSequenceTypeHeatCell.write(value.heatCells, into: &buf)
|
||||||
FfiConverterInt64.write(value.contributionCount, into: &buf)
|
FfiConverterInt64.write(value.contributionCount, into: &buf)
|
||||||
}
|
}
|
||||||
@@ -1692,17 +1713,23 @@ public func FfiConverterTypeHomePage_lower(_ value: HomePage) -> RustBuffer {
|
|||||||
public struct IssueFilterOptions: Equatable, Hashable {
|
public struct IssueFilterOptions: Equatable, Hashable {
|
||||||
public var milestones: [String]
|
public var milestones: [String]
|
||||||
public var labels: [String]
|
public var labels: [String]
|
||||||
|
public var unavailableLabels: [String]
|
||||||
public var selectedMilestone: String
|
public var selectedMilestone: String
|
||||||
public var selectedLabels: [String]
|
public var selectedLabels: [String]
|
||||||
|
|
||||||
// Default memberwise initializers are never public by default, so we
|
// Default memberwise initializers are never public by default, so we
|
||||||
// declare one manually.
|
// declare one manually.
|
||||||
public init(milestones: [String], labels: [String], selectedMilestone: String, selectedLabels: [String]) {
|
public init(milestones: [String], labels: [String], unavailableLabels: [String], selectedMilestone: String, selectedLabels: [String]) {
|
||||||
self.milestones = milestones
|
self.milestones = milestones
|
||||||
self.labels = labels
|
self.labels = labels
|
||||||
|
self.unavailableLabels = unavailableLabels
|
||||||
self.selectedMilestone = selectedMilestone
|
self.selectedMilestone = selectedMilestone
|
||||||
self.selectedLabels = selectedLabels
|
self.selectedLabels = selectedLabels
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#if compiler(>=6)
|
#if compiler(>=6)
|
||||||
@@ -1718,6 +1745,7 @@ public struct FfiConverterTypeIssueFilterOptions: FfiConverterRustBuffer {
|
|||||||
try IssueFilterOptions(
|
try IssueFilterOptions(
|
||||||
milestones: FfiConverterSequenceString.read(from: &buf),
|
milestones: FfiConverterSequenceString.read(from: &buf),
|
||||||
labels: FfiConverterSequenceString.read(from: &buf),
|
labels: FfiConverterSequenceString.read(from: &buf),
|
||||||
|
unavailableLabels: FfiConverterSequenceString.read(from: &buf),
|
||||||
selectedMilestone: FfiConverterString.read(from: &buf),
|
selectedMilestone: FfiConverterString.read(from: &buf),
|
||||||
selectedLabels: FfiConverterSequenceString.read(from: &buf)
|
selectedLabels: FfiConverterSequenceString.read(from: &buf)
|
||||||
)
|
)
|
||||||
@@ -1726,11 +1754,13 @@ public struct FfiConverterTypeIssueFilterOptions: FfiConverterRustBuffer {
|
|||||||
public static func write(_ value: IssueFilterOptions, into buf: inout [UInt8]) {
|
public static func write(_ value: IssueFilterOptions, into buf: inout [UInt8]) {
|
||||||
FfiConverterSequenceString.write(value.milestones, into: &buf)
|
FfiConverterSequenceString.write(value.milestones, into: &buf)
|
||||||
FfiConverterSequenceString.write(value.labels, into: &buf)
|
FfiConverterSequenceString.write(value.labels, into: &buf)
|
||||||
|
FfiConverterSequenceString.write(value.unavailableLabels, into: &buf)
|
||||||
FfiConverterString.write(value.selectedMilestone, into: &buf)
|
FfiConverterString.write(value.selectedMilestone, into: &buf)
|
||||||
FfiConverterSequenceString.write(value.selectedLabels, into: &buf)
|
FfiConverterSequenceString.write(value.selectedLabels, into: &buf)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#if swift(>=5.8)
|
#if swift(>=5.8)
|
||||||
@_documentation(visibility: private)
|
@_documentation(visibility: private)
|
||||||
#endif
|
#endif
|
||||||
@@ -2003,18 +2033,20 @@ public struct MilestoneRow: Equatable, Hashable {
|
|||||||
public var title: String
|
public var title: String
|
||||||
public var description: String
|
public var description: String
|
||||||
public var meta: String
|
public var meta: String
|
||||||
public var openIssues: Int64
|
public var hasIssues: Bool
|
||||||
public var closedIssues: Int64
|
public var progress: Double
|
||||||
|
public var progressAccessibility: String
|
||||||
|
|
||||||
// Default memberwise initializers are never public by default, so we
|
// Default memberwise initializers are never public by default, so we
|
||||||
// declare one manually.
|
// declare one manually.
|
||||||
public init(id: Int64, title: String, description: String, meta: String, openIssues: Int64, closedIssues: Int64) {
|
public init(id: Int64, title: String, description: String, meta: String, hasIssues: Bool, progress: Double, progressAccessibility: String) {
|
||||||
self.id = id
|
self.id = id
|
||||||
self.title = title
|
self.title = title
|
||||||
self.description = description
|
self.description = description
|
||||||
self.meta = meta
|
self.meta = meta
|
||||||
self.openIssues = openIssues
|
self.hasIssues = hasIssues
|
||||||
self.closedIssues = closedIssues
|
self.progress = progress
|
||||||
|
self.progressAccessibility = progressAccessibility
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -2037,8 +2069,9 @@ public struct FfiConverterTypeMilestoneRow: FfiConverterRustBuffer {
|
|||||||
title: FfiConverterString.read(from: &buf),
|
title: FfiConverterString.read(from: &buf),
|
||||||
description: FfiConverterString.read(from: &buf),
|
description: FfiConverterString.read(from: &buf),
|
||||||
meta: FfiConverterString.read(from: &buf),
|
meta: FfiConverterString.read(from: &buf),
|
||||||
openIssues: FfiConverterInt64.read(from: &buf),
|
hasIssues: FfiConverterBool.read(from: &buf),
|
||||||
closedIssues: FfiConverterInt64.read(from: &buf)
|
progress: FfiConverterDouble.read(from: &buf),
|
||||||
|
progressAccessibility: FfiConverterString.read(from: &buf)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2047,8 +2080,9 @@ public struct FfiConverterTypeMilestoneRow: FfiConverterRustBuffer {
|
|||||||
FfiConverterString.write(value.title, into: &buf)
|
FfiConverterString.write(value.title, into: &buf)
|
||||||
FfiConverterString.write(value.description, into: &buf)
|
FfiConverterString.write(value.description, into: &buf)
|
||||||
FfiConverterString.write(value.meta, into: &buf)
|
FfiConverterString.write(value.meta, into: &buf)
|
||||||
FfiConverterInt64.write(value.openIssues, into: &buf)
|
FfiConverterBool.write(value.hasIssues, into: &buf)
|
||||||
FfiConverterInt64.write(value.closedIssues, into: &buf)
|
FfiConverterDouble.write(value.progress, into: &buf)
|
||||||
|
FfiConverterString.write(value.progressAccessibility, into: &buf)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2270,6 +2304,72 @@ public func FfiConverterTypeRepositoryContentRow_lower(_ value: RepositoryConten
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public struct RepositoryFilePage: Equatable, Hashable {
|
||||||
|
public var name: String
|
||||||
|
public var kind: String
|
||||||
|
public var language: String
|
||||||
|
public var text: String
|
||||||
|
public var data: Data
|
||||||
|
|
||||||
|
// Default memberwise initializers are never public by default, so we
|
||||||
|
// declare one manually.
|
||||||
|
public init(name: String, kind: String, language: String, text: String, data: Data) {
|
||||||
|
self.name = name
|
||||||
|
self.kind = kind
|
||||||
|
self.language = language
|
||||||
|
self.text = text
|
||||||
|
self.data = data
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#if compiler(>=6)
|
||||||
|
extension RepositoryFilePage: Sendable {}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public struct FfiConverterTypeRepositoryFilePage: FfiConverterRustBuffer {
|
||||||
|
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> RepositoryFilePage {
|
||||||
|
return
|
||||||
|
try RepositoryFilePage(
|
||||||
|
name: FfiConverterString.read(from: &buf),
|
||||||
|
kind: FfiConverterString.read(from: &buf),
|
||||||
|
language: FfiConverterString.read(from: &buf),
|
||||||
|
text: FfiConverterString.read(from: &buf),
|
||||||
|
data: FfiConverterData.read(from: &buf)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func write(_ value: RepositoryFilePage, into buf: inout [UInt8]) {
|
||||||
|
FfiConverterString.write(value.name, into: &buf)
|
||||||
|
FfiConverterString.write(value.kind, into: &buf)
|
||||||
|
FfiConverterString.write(value.language, into: &buf)
|
||||||
|
FfiConverterString.write(value.text, into: &buf)
|
||||||
|
FfiConverterData.write(value.data, into: &buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public func FfiConverterTypeRepositoryFilePage_lift(_ buf: RustBuffer) throws -> RepositoryFilePage {
|
||||||
|
return try FfiConverterTypeRepositoryFilePage.lift(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public func FfiConverterTypeRepositoryFilePage_lower(_ value: RepositoryFilePage) -> RustBuffer {
|
||||||
|
return FfiConverterTypeRepositoryFilePage.lower(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public struct RepositoryRow: Equatable, Hashable {
|
public struct RepositoryRow: Equatable, Hashable {
|
||||||
public var name: String
|
public var name: String
|
||||||
public var owner: String
|
public var owner: String
|
||||||
@@ -3035,6 +3135,9 @@ private let initializationResult: InitializationResult = {
|
|||||||
if (uniffi_gotcha_core_checksum_method_gotchacore_issue_filters() != 24054) {
|
if (uniffi_gotcha_core_checksum_method_gotchacore_issue_filters() != 24054) {
|
||||||
return InitializationResult.apiChecksumMismatch
|
return InitializationResult.apiChecksumMismatch
|
||||||
}
|
}
|
||||||
|
if (uniffi_gotcha_core_checksum_method_gotchacore_issue_filters_active() != 51470) {
|
||||||
|
return InitializationResult.apiChecksumMismatch
|
||||||
|
}
|
||||||
if (uniffi_gotcha_core_checksum_method_gotchacore_issues() != 1316) {
|
if (uniffi_gotcha_core_checksum_method_gotchacore_issues() != 1316) {
|
||||||
return InitializationResult.apiChecksumMismatch
|
return InitializationResult.apiChecksumMismatch
|
||||||
}
|
}
|
||||||
@@ -3059,7 +3162,7 @@ private let initializationResult: InitializationResult = {
|
|||||||
if (uniffi_gotcha_core_checksum_method_gotchacore_repository_contents() != 8454) {
|
if (uniffi_gotcha_core_checksum_method_gotchacore_repository_contents() != 8454) {
|
||||||
return InitializationResult.apiChecksumMismatch
|
return InitializationResult.apiChecksumMismatch
|
||||||
}
|
}
|
||||||
if (uniffi_gotcha_core_checksum_method_gotchacore_repository_file() != 27399) {
|
if (uniffi_gotcha_core_checksum_method_gotchacore_repository_file() != 44948) {
|
||||||
return InitializationResult.apiChecksumMismatch
|
return InitializationResult.apiChecksumMismatch
|
||||||
}
|
}
|
||||||
if (uniffi_gotcha_core_checksum_method_gotchacore_select_server() != 22721) {
|
if (uniffi_gotcha_core_checksum_method_gotchacore_select_server() != 22721) {
|
||||||
@@ -3109,4 +3212,4 @@ public func uniffiEnsureGotchaCoreInitialized() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// swiftlint:enable all
|
// swiftlint:enable all
|
||||||
@@ -304,6 +304,11 @@ uint64_t uniffi_gotcha_core_fn_method_gotchacore_issue(uint64_t ptr, RustBuffer
|
|||||||
uint64_t uniffi_gotcha_core_fn_method_gotchacore_issue_filters(uint64_t ptr, RustBuffer owner, RustBuffer repository
|
uint64_t uniffi_gotcha_core_fn_method_gotchacore_issue_filters(uint64_t ptr, RustBuffer owner, RustBuffer repository
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_ISSUE_FILTERS_ACTIVE
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_ISSUE_FILTERS_ACTIVE
|
||||||
|
int8_t uniffi_gotcha_core_fn_method_gotchacore_issue_filters_active(uint64_t ptr, RustBuffer owner, RustBuffer repository, RustCallStatus *_Nonnull out_status
|
||||||
|
);
|
||||||
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_ISSUES
|
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_ISSUES
|
||||||
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_ISSUES
|
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_ISSUES
|
||||||
uint64_t uniffi_gotcha_core_fn_method_gotchacore_issues(uint64_t ptr, RustBuffer owner, RustBuffer repository
|
uint64_t uniffi_gotcha_core_fn_method_gotchacore_issues(uint64_t ptr, RustBuffer owner, RustBuffer repository
|
||||||
@@ -706,6 +711,12 @@ uint16_t uniffi_gotcha_core_checksum_method_gotchacore_issue(void
|
|||||||
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_ISSUE_FILTERS
|
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_ISSUE_FILTERS
|
||||||
uint16_t uniffi_gotcha_core_checksum_method_gotchacore_issue_filters(void
|
uint16_t uniffi_gotcha_core_checksum_method_gotchacore_issue_filters(void
|
||||||
|
|
||||||
|
);
|
||||||
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_ISSUE_FILTERS_ACTIVE
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_ISSUE_FILTERS_ACTIVE
|
||||||
|
uint16_t uniffi_gotcha_core_checksum_method_gotchacore_issue_filters_active(void
|
||||||
|
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_ISSUES
|
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_ISSUES
|
||||||
|
|||||||
@@ -135,13 +135,10 @@ final class IssuesViewController: RefreshingTableViewController {
|
|||||||
|
|
||||||
private func milestoneMenu(_ options: IssueFilterOptions) -> UIMenu {
|
private func milestoneMenu(_ options: IssueFilterOptions) -> UIMenu {
|
||||||
let selected = options.selectedMilestone
|
let selected = options.selectedMilestone
|
||||||
var milestones = options.milestones
|
|
||||||
if !selected.isEmpty, !milestones.contains(selected) { milestones.append(selected) }
|
|
||||||
let all = UIAction(title: "All Milestones", state: selected.isEmpty ? .on : .off) {
|
let all = UIAction(title: "All Milestones", state: selected.isEmpty ? .on : .off) {
|
||||||
[weak self] _ in self?.selectMilestone("")
|
[weak self] _ in self?.selectMilestone("")
|
||||||
}
|
}
|
||||||
let actions = milestones.sorted { $0.localizedCaseInsensitiveCompare($1) == .orderedAscending }
|
let actions = options.milestones.map { milestone in
|
||||||
.map { milestone in
|
|
||||||
UIAction(
|
UIAction(
|
||||||
title: milestone,
|
title: milestone,
|
||||||
state: selected == milestone ? .on : .off
|
state: selected == milestone ? .on : .off
|
||||||
@@ -156,13 +153,10 @@ final class IssuesViewController: RefreshingTableViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func labelMenu(_ options: IssueFilterOptions) -> UIMenu {
|
private func labelMenu(_ options: IssueFilterOptions) -> UIMenu {
|
||||||
let available = Set(options.labels)
|
|
||||||
let selected = Set(options.selectedLabels)
|
let selected = Set(options.selectedLabels)
|
||||||
let labels = available.union(selected)
|
let actions = options.labels.map { label in
|
||||||
.sorted { $0.localizedCaseInsensitiveCompare($1) == .orderedAscending }
|
|
||||||
let actions = labels.map { label in
|
|
||||||
UIAction(
|
UIAction(
|
||||||
title: available.contains(label) ? label : "\(label) (Unavailable)",
|
title: options.unavailableLabels.contains(label) ? "\(label) (Unavailable)" : label,
|
||||||
attributes: .keepsMenuPresented,
|
attributes: .keepsMenuPresented,
|
||||||
state: selected.contains(label) ? .on : .off
|
state: selected.contains(label) ? .on : .off
|
||||||
) { [weak self] action in
|
) { [weak self] action in
|
||||||
@@ -172,7 +166,7 @@ final class IssuesViewController: RefreshingTableViewController {
|
|||||||
if labels.remove(label) == nil { labels.insert(label) }
|
if labels.remove(label) == nil { labels.insert(label) }
|
||||||
do {
|
do {
|
||||||
try self.saveFilters(milestone: options.selectedMilestone, labels: labels)
|
try self.saveFilters(milestone: options.selectedMilestone, labels: labels)
|
||||||
self.filterOptions?.selectedLabels = labels.sorted()
|
self.filterOptions?.selectedLabels = Array(labels)
|
||||||
action.state = labels.contains(label) ? .on : .off
|
action.state = labels.contains(label) ? .on : .off
|
||||||
self.updateFilterMenu()
|
self.updateFilterMenu()
|
||||||
self.loadIssues(refreshing: false)
|
self.loadIssues(refreshing: false)
|
||||||
@@ -208,14 +202,12 @@ final class IssuesViewController: RefreshingTableViewController {
|
|||||||
owner: owner,
|
owner: owner,
|
||||||
repository: repository,
|
repository: repository,
|
||||||
milestone: milestone,
|
milestone: milestone,
|
||||||
labels: labels.sorted()
|
labels: Array(labels)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var filtersActive: Bool {
|
private var filtersActive: Bool {
|
||||||
context.core.settings().issueStatus != "open"
|
(try? context.core.issueFiltersActive(owner: owner, repository: repository)) ?? false
|
||||||
|| filterOptions?.selectedMilestone.isEmpty == false
|
|
||||||
|| filterOptions?.selectedLabels.isEmpty == false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateFilterTint() {
|
private func updateFilterTint() {
|
||||||
@@ -519,11 +511,10 @@ final class MilestoneCell: UITableViewCell {
|
|||||||
titleLabel.text = row.title
|
titleLabel.text = row.title
|
||||||
descriptionLabel.text = row.description
|
descriptionLabel.text = row.description
|
||||||
metaLabel.text = row.meta
|
metaLabel.text = row.meta
|
||||||
let total = row.openIssues + row.closedIssues
|
progress.progress = Float(row.progress)
|
||||||
progress.progress = total == 0 ? 0 : Float(row.closedIssues) / Float(total)
|
progress.trackTintColor = row.hasIssues ? .systemOrange : .systemGray5
|
||||||
progress.trackTintColor = total == 0 ? .systemGray5 : .systemOrange
|
|
||||||
progress.accessibilityLabel = "Milestone progress"
|
progress.accessibilityLabel = "Milestone progress"
|
||||||
progress.accessibilityValue = "\(row.closedIssues) closed, \(row.openIssues) open"
|
progress.accessibilityValue = row.progressAccessibility
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -599,7 +590,7 @@ final class PullsViewController: RefreshingTableViewController {
|
|||||||
let row = rows[indexPath.row]
|
let row = rows[indexPath.row]
|
||||||
configureTextCell(
|
configureTextCell(
|
||||||
cell,
|
cell,
|
||||||
title: "\(row.repository) #\(row.number)\n\(row.title)",
|
title: row.title,
|
||||||
detail: "\(row.summary)\n\(row.meta)"
|
detail: "\(row.summary)\n\(row.meta)"
|
||||||
)
|
)
|
||||||
cell.accessoryType = .disclosureIndicator
|
cell.accessoryType = .disclosureIndicator
|
||||||
@@ -819,9 +810,7 @@ final class CommitCell: UITableViewCell {
|
|||||||
var content = defaultContentConfiguration()
|
var content = defaultContentConfiguration()
|
||||||
content.directionalLayoutMargins.leading = laneCount == 0 ? 0 : min(72, CGFloat(laneCount) * 10 + 8)
|
content.directionalLayoutMargins.leading = laneCount == 0 ? 0 : min(72, CGFloat(laneCount) * 10 + 8)
|
||||||
content.text = row.title
|
content.text = row.title
|
||||||
content.secondaryText = row.refs.isEmpty
|
content.secondaryText = row.detail
|
||||||
? "\(row.meta)\n\(row.sha.prefix(8))"
|
|
||||||
: "\(row.refs)\n\(row.meta) · \(row.sha.prefix(8))"
|
|
||||||
content.secondaryTextProperties.numberOfLines = 2
|
content.secondaryTextProperties.numberOfLines = 2
|
||||||
contentConfiguration = content
|
contentConfiguration = content
|
||||||
setNeedsDisplay()
|
setNeedsDisplay()
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import MarkdownUI
|
|||||||
import QuickLook
|
import QuickLook
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import UIKit
|
import UIKit
|
||||||
import UniformTypeIdentifiers
|
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
class MarkdownPageViewController: UIViewController {
|
class MarkdownPageViewController: UIViewController {
|
||||||
@@ -273,13 +272,13 @@ final class RepositoryDirectoryViewController: RefreshingTableViewController {
|
|||||||
private let path: String
|
private let path: String
|
||||||
private var rows: [RepositoryContentRow] = []
|
private var rows: [RepositoryContentRow] = []
|
||||||
|
|
||||||
init(context: AppContext, owner: String, repository: String, path: String) {
|
init(context: AppContext, owner: String, repository: String, path: String, name: String) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.owner = owner
|
self.owner = owner
|
||||||
self.repository = repository
|
self.repository = repository
|
||||||
self.path = path
|
self.path = path
|
||||||
super.init()
|
super.init()
|
||||||
title = path.split(separator: "/").last.map(String.init) ?? repository
|
title = name
|
||||||
}
|
}
|
||||||
|
|
||||||
@available(*, unavailable)
|
@available(*, unavailable)
|
||||||
@@ -349,18 +348,19 @@ final class RepositoryFileViewController: UIViewController, QLPreviewControllerD
|
|||||||
private var loadingTask: Task<Void, Never>?
|
private var loadingTask: Task<Void, Never>?
|
||||||
private var previewURL: URL?
|
private var previewURL: URL?
|
||||||
private var markdownSource: String?
|
private var markdownSource: String?
|
||||||
|
private var fileLanguage: String?
|
||||||
private var displayedController: UIViewController?
|
private var displayedController: UIViewController?
|
||||||
private var displayedView: UIView?
|
private var displayedView: UIView?
|
||||||
private weak var sourceScrollView: UIScrollView?
|
private weak var sourceScrollView: UIScrollView?
|
||||||
private weak var sourceTextView: UITextView?
|
private weak var sourceTextView: UITextView?
|
||||||
|
|
||||||
init(context: AppContext, owner: String, repository: String, path: String) {
|
init(context: AppContext, owner: String, repository: String, path: String, name: String) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.owner = owner
|
self.owner = owner
|
||||||
self.repository = repository
|
self.repository = repository
|
||||||
self.path = path
|
self.path = path
|
||||||
super.init(nibName: nil, bundle: nil)
|
super.init(nibName: nil, bundle: nil)
|
||||||
title = path.split(separator: "/").last.map(String.init)
|
title = name
|
||||||
}
|
}
|
||||||
|
|
||||||
@available(*, unavailable)
|
@available(*, unavailable)
|
||||||
@@ -372,19 +372,17 @@ final class RepositoryFileViewController: UIViewController, QLPreviewControllerD
|
|||||||
beginNavigationLoading(spinner)
|
beginNavigationLoading(spinner)
|
||||||
loadingTask = Task {
|
loadingTask = Task {
|
||||||
do {
|
do {
|
||||||
let data = try await context.core.repositoryFile(
|
let page = try await context.core.repositoryFile(
|
||||||
owner: owner,
|
owner: owner,
|
||||||
repository: repository,
|
repository: repository,
|
||||||
path: path
|
path: path
|
||||||
)
|
)
|
||||||
if !isMediaFile(path), let source = String(data: data, encoding: .utf8) {
|
title = page.name
|
||||||
if isMarkdownFile {
|
fileLanguage = page.language.isEmpty ? nil : page.language
|
||||||
showMarkdown(source)
|
switch page.kind {
|
||||||
} else {
|
case "markdown": showMarkdown(page.text)
|
||||||
showSource(source)
|
case "source": showSource(page.text)
|
||||||
}
|
default: try showQuickLook(page.data, name: page.name)
|
||||||
} else {
|
|
||||||
try showQuickLook(data)
|
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
if !Task.isCancelled { show(error: error) }
|
if !Task.isCancelled { show(error: error) }
|
||||||
@@ -398,20 +396,6 @@ final class RepositoryFileViewController: UIViewController, QLPreviewControllerD
|
|||||||
updateSourceLayout()
|
updateSourceLayout()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func isMediaFile(_ path: String) -> Bool {
|
|
||||||
guard let type = UTType(filenameExtension: (path as NSString).pathExtension) else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return type.conforms(to: .image)
|
|
||||||
|| type.conforms(to: .audio)
|
|
||||||
|| type.conforms(to: .audiovisualContent)
|
|
||||||
|| type.conforms(to: .pdf)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var isMarkdownFile: Bool {
|
|
||||||
["md", "markdown", "mdown", "mkd"].contains((path as NSString).pathExtension.lowercased())
|
|
||||||
}
|
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
loadingTask?.cancel()
|
loadingTask?.cancel()
|
||||||
if let previewURL { try? FileManager.default.removeItem(at: previewURL) }
|
if let previewURL { try? FileManager.default.removeItem(at: previewURL) }
|
||||||
@@ -464,7 +448,7 @@ final class RepositoryFileViewController: UIViewController, QLPreviewControllerD
|
|||||||
traitCollection.userInterfaceStyle == .dark ? "atom-one-dark" : "atom-one-light"
|
traitCollection.userInterfaceStyle == .dark ? "atom-one-dark" : "atom-one-light"
|
||||||
)
|
)
|
||||||
highlighter?.theme.setCodeFont(.monospacedSystemFont(ofSize: 13, weight: .regular))
|
highlighter?.theme.setCodeFont(.monospacedSystemFont(ofSize: 13, weight: .regular))
|
||||||
let highlighted = highlighter?.highlight(source, as: sourceLanguage(path))
|
let highlighted = highlighter?.highlight(source, as: fileLanguage)
|
||||||
?? NSAttributedString(string: source, attributes: [
|
?? NSAttributedString(string: source, attributes: [
|
||||||
.font: UIFont.monospacedSystemFont(ofSize: 13, weight: .regular),
|
.font: UIFont.monospacedSystemFont(ofSize: 13, weight: .regular),
|
||||||
.foregroundColor: UIColor.label,
|
.foregroundColor: UIColor.label,
|
||||||
@@ -532,8 +516,7 @@ final class RepositoryFileViewController: UIViewController, QLPreviewControllerD
|
|||||||
scrollView.contentSize = contentSize
|
scrollView.contentSize = contentSize
|
||||||
}
|
}
|
||||||
|
|
||||||
private func showQuickLook(_ data: Data) throws {
|
private func showQuickLook(_ data: Data, name: String) throws {
|
||||||
let name = path.split(separator: "/").last.map(String.init) ?? "Preview"
|
|
||||||
let url = FileManager.default.temporaryDirectory
|
let url = FileManager.default.temporaryDirectory
|
||||||
.appendingPathComponent("\(UUID().uuidString)-\(name)")
|
.appendingPathComponent("\(UUID().uuidString)-\(name)")
|
||||||
try data.write(to: url, options: .atomic)
|
try data.write(to: url, options: .atomic)
|
||||||
|
|||||||
@@ -303,17 +303,6 @@ final class HomeViewController: RefreshingTableViewController {
|
|||||||
case all
|
case all
|
||||||
case issues
|
case issues
|
||||||
case pulls
|
case pulls
|
||||||
|
|
||||||
func includes(_ row: ActivityRow) -> Bool {
|
|
||||||
switch self {
|
|
||||||
case .all:
|
|
||||||
return true
|
|
||||||
case .issues:
|
|
||||||
return row.icon.contains("issue")
|
|
||||||
case .pulls:
|
|
||||||
return row.icon.contains("pull")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private let context: AppContext
|
private let context: AppContext
|
||||||
@@ -321,7 +310,12 @@ final class HomeViewController: RefreshingTableViewController {
|
|||||||
private var filter = ActivityFilter.all
|
private var filter = ActivityFilter.all
|
||||||
|
|
||||||
private var activities: [ActivityRow] {
|
private var activities: [ActivityRow] {
|
||||||
page?.activities.filter(filter.includes) ?? []
|
guard let page else { return [] }
|
||||||
|
switch filter {
|
||||||
|
case .all: return page.activities
|
||||||
|
case .issues: return page.issueActivities
|
||||||
|
case .pulls: return page.pullActivities
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init(context: AppContext) {
|
init(context: AppContext) {
|
||||||
@@ -443,20 +437,7 @@ final class HeatmapView: UIView {
|
|||||||
private let calendar = Calendar(identifier: .gregorian)
|
private let calendar = Calendar(identifier: .gregorian)
|
||||||
|
|
||||||
init(page: HomePage, selectedFilter: Int, onFilter: @escaping (Int) -> Void) {
|
init(page: HomePage, selectedFilter: Int, onFilter: @escaping (Int) -> Void) {
|
||||||
let latest = page.heatCells.last.map {
|
cells = page.heatCells
|
||||||
Date(timeIntervalSince1970: TimeInterval($0.timestamp))
|
|
||||||
} ?? Date()
|
|
||||||
let latestMonth = Calendar(identifier: .gregorian).date(
|
|
||||||
from: Calendar(identifier: .gregorian).dateComponents([.year, .month], from: latest)
|
|
||||||
) ?? latest
|
|
||||||
let firstMonth = Calendar(identifier: .gregorian).date(
|
|
||||||
byAdding: .month,
|
|
||||||
value: -8,
|
|
||||||
to: latestMonth
|
|
||||||
) ?? latestMonth
|
|
||||||
cells = page.heatCells.filter {
|
|
||||||
Date(timeIntervalSince1970: TimeInterval($0.timestamp)) >= firstMonth
|
|
||||||
}
|
|
||||||
super.init(frame: CGRect(x: 0, y: 0, width: 0, height: 180))
|
super.init(frame: CGRect(x: 0, y: 0, width: 0, height: 180))
|
||||||
backgroundColor = .systemBackground
|
backgroundColor = .systemBackground
|
||||||
let title = UILabel(frame: CGRect(x: 16, y: 12, width: 300, height: 22))
|
let title = UILabel(frame: CGRect(x: 16, y: 12, width: 300, height: 22))
|
||||||
@@ -465,7 +446,7 @@ final class HeatmapView: UIView {
|
|||||||
title.textColor = .secondaryLabel
|
title.textColor = .secondaryLabel
|
||||||
addSubview(title)
|
addSubview(title)
|
||||||
let total = UILabel(frame: CGRect(x: 16, y: 108, width: 300, height: 18))
|
let total = UILabel(frame: CGRect(x: 16, y: 108, width: 300, height: 18))
|
||||||
total.text = "\(cells.reduce(0) { $0 + $1.contributions }) contributions"
|
total.text = "\(page.contributionCount) contributions"
|
||||||
total.font = .preferredFont(forTextStyle: .caption1)
|
total.font = .preferredFont(forTextStyle: .caption1)
|
||||||
total.textColor = .tertiaryLabel
|
total.textColor = .tertiaryLabel
|
||||||
addSubview(total)
|
addSubview(total)
|
||||||
|
|||||||
@@ -151,39 +151,19 @@ func showRepositoryContent(
|
|||||||
context: context,
|
context: context,
|
||||||
owner: owner,
|
owner: owner,
|
||||||
repository: repository,
|
repository: repository,
|
||||||
path: row.path
|
path: row.path,
|
||||||
|
name: row.name
|
||||||
)
|
)
|
||||||
: RepositoryFileViewController(
|
: RepositoryFileViewController(
|
||||||
context: context,
|
context: context,
|
||||||
owner: owner,
|
owner: owner,
|
||||||
repository: repository,
|
repository: repository,
|
||||||
path: row.path
|
path: row.path,
|
||||||
|
name: row.name
|
||||||
)
|
)
|
||||||
navigationController?.pushViewController(destination, animated: true)
|
navigationController?.pushViewController(destination, animated: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func sourceLanguage(_ path: String) -> String? {
|
|
||||||
let filename = (path as NSString).lastPathComponent.lowercased()
|
|
||||||
let filenames = [
|
|
||||||
"cmakelists.txt": "cmake", "dockerfile": "dockerfile", "gemfile": "ruby",
|
|
||||||
"makefile": "makefile", "podfile": "ruby",
|
|
||||||
]
|
|
||||||
if let language = filenames[filename] { return language }
|
|
||||||
let languages = [
|
|
||||||
"asm": "x86asm", "c": "c", "cc": "cpp", "clj": "clojure", "cpp": "cpp",
|
|
||||||
"cs": "csharp", "css": "css", "dart": "dart", "ex": "elixir", "exs": "elixir",
|
|
||||||
"fs": "fsharp", "go": "go", "groovy": "groovy", "h": "cpp", "hpp": "cpp",
|
|
||||||
"hs": "haskell", "html": "xml", "java": "java", "jl": "julia", "js": "javascript",
|
|
||||||
"json": "json", "jsx": "javascript", "kt": "kotlin", "kts": "kotlin", "lua": "lua",
|
|
||||||
"m": "objectivec", "md": "markdown", "mm": "objectivec", "php": "php", "pl": "perl",
|
|
||||||
"plist": "xml", "ps1": "powershell", "py": "python", "r": "r", "rb": "ruby",
|
|
||||||
"rs": "rust", "scala": "scala", "scss": "scss", "sh": "bash", "sql": "sql",
|
|
||||||
"swift": "swift", "toml": "ini", "ts": "typescript", "tsx": "typescript",
|
|
||||||
"txt": "plaintext", "xml": "xml", "yaml": "yaml", "yml": "yaml",
|
|
||||||
]
|
|
||||||
return languages[(path as NSString).pathExtension.lowercased()]
|
|
||||||
}
|
|
||||||
|
|
||||||
func symbolText(_ symbol: String, text: String, font: UIFont, color: UIColor) -> NSAttributedString {
|
func symbolText(_ symbol: String, text: String, font: UIFont, color: UIColor) -> NSAttributedString {
|
||||||
let output = NSMutableAttributedString()
|
let output = NSMutableAttributedString()
|
||||||
if let image = UIImage(systemName: symbol)?.withTintColor(color, renderingMode: .alwaysOriginal) {
|
if let image = UIImage(systemName: symbol)?.withTintColor(color, renderingMode: .alwaysOriginal) {
|
||||||
|
|||||||
Reference in New Issue
Block a user