Add paginated panel loading

This commit is contained in:
Georg Bauer
2026-07-31 15:02:21 +02:00
parent 228879da15
commit 33f9eb809c
11 changed files with 1104 additions and 290 deletions

View File

@@ -99,6 +99,20 @@ xcrun simctl launch booted de.rfc1437.gotcha
- [ ] Pull past the top of Home, repository lists, issue lists, pull lists, - [ ] Pull past the top of Home, repository lists, issue lists, pull lists,
commit history, changed files, issue details, pull details, and diffs. commit history, changed files, issue details, pull details, and diffs.
The native refresh control appears, reloads data, and disappears. The native refresh control appears, reloads data, and disappears.
- [ ] On accounts/repositories with more than 30 results, Home activity,
repositories, issues, pull requests, milestones, milestone contents,
commit history, and pull-request files initially show one page plus a
**Pull up or tap to load more**
footer. Pull upward past the bottom and verify the footer changes to a
native loading indicator, the next page appends without duplicates, and
the footer disappears after the final page.
- [ ] Tap the load-more footer as an accessibility alternative and repeat while
VoiceOver is running. It announces loading, ignores repeated activation
while a request is active, preserves the current scroll position, and
allows retry after an error.
- [ ] Pull down to refresh after loading multiple pages. The content resets to
the new first page, filtering/branch changes also reset pagination, and
switching History/Files never shows a stale load-more footer.
- [ ] Tap a row, scroll its detail, and use both the navigation-bar Back button - [ ] Tap a row, scroll its detail, and use both the navigation-bar Back button
and the left-edge interactive swipe to return. and the left-edge interactive swipe to return.
- [ ] From a list scrolled well away from the top, open a detail and go Back. - [ ] From a list scrolled well away from the top, open a detail and go Back.

View File

@@ -10,12 +10,12 @@ use crate::{
diff, diff,
domain::{ domain::{
HistoryCommit, HomeData, IssueDetails, IssueDraft, IssueEditorData, MilestoneDetails, HistoryCommit, HomeData, IssueDetails, IssueDraft, IssueEditorData, MilestoneDetails,
PullDetails, RepositoryData, Server, api_date, PAGE_SIZE, Page, PullDetails, RepositoryData, Server, api_date,
}, },
presentation::compact_date, presentation::compact_date,
}; };
pub async fn load_repositories(server: &Server) -> Result<Vec<RepositoryData>, String> { pub async fn load_repositories(server: &Server, page: i32) -> Result<Page<RepositoryData>, String> {
let client = let client =
Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?; Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?;
let configuration = client.configuration(); let configuration = client.configuration();
@@ -24,36 +24,41 @@ pub async fn load_repositories(server: &Server) -> Result<Vec<RepositoryData>, S
.await .await
.map_err(|error| error.to_string())?; .map_err(|error| error.to_string())?;
let login = user.login.unwrap_or_default(); let login = user.login.unwrap_or_default();
let repositories = apis::user_api::user_current_list_repos(&configuration, Some(1), Some(100)) let repositories =
.await apis::user_api::user_current_list_repos(&configuration, Some(page), Some(PAGE_SIZE))
.map_err(|error| error.to_string())?; .await
Ok(repositories .map_err(|error| error.to_string())?;
.into_iter() let has_more = repositories.len() == PAGE_SIZE as usize;
.filter(|repository| { Ok(Page {
repository items: repositories
.owner .into_iter()
.as_ref() .filter(|repository| {
.and_then(|owner| owner.login.as_deref()) repository
== Some(login.as_str()) .owner
}) .as_ref()
.filter_map(|repository| { .and_then(|owner| owner.login.as_deref())
Some(RepositoryData { == Some(login.as_str())
owner: repository.owner?.login?,
name: repository.name?,
description: repository
.description
.filter(|text| !text.is_empty())
.unwrap_or_else(|| "No description".into()),
language: repository
.language
.filter(|text| !text.is_empty())
.unwrap_or_else(|| "Unknown language".into()),
open_issues: repository.open_issues_count.unwrap_or_default(),
updated: compact_date(repository.updated_at.as_deref()),
default_branch: repository.default_branch.unwrap_or_else(|| "main".into()),
}) })
}) .filter_map(|repository| {
.collect()) Some(RepositoryData {
owner: repository.owner?.login?,
name: repository.name?,
description: repository
.description
.filter(|text| !text.is_empty())
.unwrap_or_else(|| "No description".into()),
language: repository
.language
.filter(|text| !text.is_empty())
.unwrap_or_else(|| "Unknown language".into()),
open_issues: repository.open_issues_count.unwrap_or_default(),
updated: compact_date(repository.updated_at.as_deref()),
default_branch: repository.default_branch.unwrap_or_else(|| "main".into()),
})
})
.collect(),
has_more,
})
} }
pub async fn load_issues( pub async fn load_issues(
@@ -63,7 +68,8 @@ pub async fn load_issues(
status: &str, status: &str,
labels: &[String], labels: &[String],
milestone: &str, milestone: &str,
) -> Result<Vec<models::Issue>, String> { page: i32,
) -> Result<Page<models::Issue>, String> {
let client = let client =
Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?; Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?;
let labels = (!labels.is_empty()).then(|| labels.join(",")); let labels = (!labels.is_empty()).then(|| labels.join(","));
@@ -81,10 +87,33 @@ pub async fn load_issues(
None, None,
None, None,
None, None,
Some(1), Some(page),
Some(100), Some(PAGE_SIZE),
) )
.await .await
.map(Page::from_items)
.map_err(|error| error.to_string())
}
pub async fn load_milestones_page(
server: &Server,
owner: &str,
repository: &str,
page: i32,
) -> Result<Page<models::Milestone>, String> {
let client =
Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?;
apis::issue_api::issue_get_milestones_list(
&client.configuration(),
owner,
repository,
Some("all"),
None,
Some(page),
Some(PAGE_SIZE),
)
.await
.map(Page::from_items)
.map_err(|error| error.to_string()) .map_err(|error| error.to_string())
} }
@@ -151,6 +180,7 @@ pub async fn load_milestone(
owner: &str, owner: &str,
repository: &str, repository: &str,
id: i64, id: i64,
page: i32,
) -> Result<MilestoneDetails, String> { ) -> Result<MilestoneDetails, String> {
let client = let client =
Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?; Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?;
@@ -174,12 +204,14 @@ pub async fn load_milestone(
None, None,
None, None,
None, None,
Some(1), Some(page),
Some(100), Some(PAGE_SIZE),
) )
}; };
let (issues, pulls) = tokio::join!(load("issues"), load("pulls")); let (issues, pulls) = tokio::join!(load("issues"), load("pulls"));
let issues = issues.map_err(|error| error.to_string())?;
let mut pulls = pulls.map_err(|error| error.to_string())?; let mut pulls = pulls.map_err(|error| error.to_string())?;
let has_more = issues.len() == PAGE_SIZE as usize || pulls.len() == PAGE_SIZE as usize;
for pull in &mut pulls { for pull in &mut pulls {
pull.repository.get_or_insert_with(|| { pull.repository.get_or_insert_with(|| {
Box::new(models::RepositoryMeta { Box::new(models::RepositoryMeta {
@@ -191,8 +223,9 @@ pub async fn load_milestone(
} }
Ok(MilestoneDetails { Ok(MilestoneDetails {
milestone, milestone,
issues: issues.map_err(|error| error.to_string())?, issues,
pulls, pulls,
has_more,
}) })
} }
@@ -200,7 +233,8 @@ pub async fn load_pulls(
server: &Server, server: &Server,
status: &str, status: &str,
milestone: &str, milestone: &str,
) -> Result<Vec<models::Issue>, String> { page: i32,
) -> Result<Page<models::Issue>, String> {
let client = let client =
Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?; Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?;
let configuration = client.configuration(); let configuration = client.configuration();
@@ -210,42 +244,35 @@ pub async fn load_pulls(
.map_err(|error| error.to_string())? .map_err(|error| error.to_string())?
.login .login
.ok_or("The server account has no username.")?; .ok_or("The server account has no username.")?;
let mut pulls = Vec::new(); apis::issue_api::issue_search_issues(
for page in 1.. { &configuration,
let batch = apis::issue_api::issue_search_issues( Some(status),
&configuration, None,
Some(status), (!milestone.is_empty()).then_some(milestone),
None, None,
(!milestone.is_empty()).then_some(milestone), None,
None, Some("pulls"),
None, None,
Some("pulls"), None,
None, None,
None, None,
None, None,
None, None,
None, None,
None, Some(&owner),
None, None,
Some(&owner), Some(page),
None, Some(PAGE_SIZE),
Some(page), )
Some(100), .await
) .map(Page::from_items)
.await .map_err(|error| error.to_string())
.map_err(|error| error.to_string())?;
if batch.is_empty() {
break;
}
pulls.extend(batch);
}
Ok(pulls)
} }
pub async fn load_pull_milestones(server: &Server) -> Result<Vec<String>, String> { pub async fn load_pull_milestones(server: &Server) -> Result<Vec<String>, String> {
let (open, closed) = tokio::try_join!( let (open, closed) = tokio::try_join!(
load_pulls(server, "open", ""), load_all_pulls(server, "open"),
load_pulls(server, "closed", "") load_all_pulls(server, "closed")
)?; )?;
Ok(open Ok(open
.into_iter() .into_iter()
@@ -256,6 +283,18 @@ pub async fn load_pull_milestones(server: &Server) -> Result<Vec<String>, String
.collect()) .collect())
} }
async fn load_all_pulls(server: &Server, status: &str) -> Result<Vec<models::Issue>, String> {
let mut pulls = Vec::new();
for page in 1.. {
let batch = load_pulls(server, status, "", page).await?;
pulls.extend(batch.items);
if !batch.has_more {
break;
}
}
Ok(pulls)
}
pub async fn load_branches( pub async fn load_branches(
server: &Server, server: &Server,
owner: &str, owner: &str,
@@ -336,11 +375,14 @@ pub async fn load_branch_commits(
owner: &str, owner: &str,
repository: &str, repository: &str,
branch: &str, branch: &str,
) -> Result<Vec<HistoryCommit>, String> { pages: u32,
load_commits_for_ref(server, owner, repository, Some(branch)) ) -> Result<Page<HistoryCommit>, String> {
load_commits_for_ref(server, owner, repository, Some(branch), pages)
.await .await
.map(|commits| { .map(|page| Page {
commits has_more: page.has_more,
items: page
.items
.into_iter() .into_iter()
.map(|commit| HistoryCommit { .map(|commit| HistoryCommit {
commit, commit,
@@ -350,7 +392,7 @@ pub async fn load_branch_commits(
connections: Vec::new(), connections: Vec::new(),
refs: Vec::new(), refs: Vec::new(),
}) })
.collect() .collect(),
}) })
} }
@@ -359,13 +401,15 @@ pub async fn load_all_commits(
owner: &str, owner: &str,
repository: &str, repository: &str,
branches: &[String], branches: &[String],
) -> Result<Vec<HistoryCommit>, String> { pages: u32,
) -> Result<Page<HistoryCommit>, String> {
let mut tasks = JoinSet::new(); let mut tasks = JoinSet::new();
for (lane, branch) in branches.iter().cloned().enumerate() { for (lane, branch) in branches.iter().cloned().enumerate() {
let (server, owner, repository) = let (server, owner, repository) =
(server.clone(), owner.to_string(), repository.to_string()); (server.clone(), owner.to_string(), repository.to_string());
tasks.spawn(async move { tasks.spawn(async move {
let commits = load_commits_for_ref(&server, &owner, &repository, Some(&branch)).await?; let commits =
load_commits_for_ref(&server, &owner, &repository, Some(&branch), pages).await?;
Ok::<_, String>((lane, branch, commits)) Ok::<_, String>((lane, branch, commits))
}); });
} }
@@ -375,11 +419,19 @@ pub async fn load_all_commits(
histories.push(result.map_err(|error| error.to_string())??); histories.push(result.map_err(|error| error.to_string())??);
} }
histories.sort_by_key(|(lane, _, _)| *lane); histories.sort_by_key(|(lane, _, _)| *lane);
let has_more = histories.iter().any(|(_, _, page)| page.has_more);
let histories = histories
.into_iter()
.map(|(lane, branch, page)| (lane, branch, page.items))
.collect();
let pull_refs = load_pull_refs(server, owner, repository) let pull_refs = load_pull_refs(server, owner, repository)
.await .await
.unwrap_or_default(); .unwrap_or_default();
Ok(build_graph(histories, pull_refs)) Ok(Page {
items: build_graph(histories, pull_refs),
has_more,
})
} }
pub async fn load_pull_files( pub async fn load_pull_files(
@@ -387,28 +439,21 @@ pub async fn load_pull_files(
owner: &str, owner: &str,
repository: &str, repository: &str,
number: i64, number: i64,
) -> Result<Vec<models::ChangedFile>, String> { page: i32,
let mut files = Vec::new(); ) -> Result<Page<models::ChangedFile>, String> {
for page in 1.. { apis::repository_api::repo_get_pull_request_files(
let batch = apis::repository_api::repo_get_pull_request_files( configuration,
configuration, owner,
owner, repository,
repository, number,
number, None,
None, None,
None, Some(page),
Some(page), Some(PAGE_SIZE),
Some(100), )
) .await
.await .map(Page::from_items)
.map_err(|error| error.to_string())?; .map_err(|error| error.to_string())
let done = batch.len() < 100;
files.extend(batch);
if done {
break;
}
}
Ok(files)
} }
pub async fn load_issue( pub async fn load_issue(
@@ -416,20 +461,37 @@ pub async fn load_issue(
owner: &str, owner: &str,
repository: &str, repository: &str,
number: i64, number: i64,
page: i32,
) -> Result<IssueDetails, String> { ) -> Result<IssueDetails, String> {
let client = let client =
Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?; Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?;
let configuration = client.configuration(); let configuration = client.configuration();
let (issue, comments) = tokio::join!( let (issue, comments) = tokio::join!(
apis::issue_api::issue_get_issue(&configuration, owner, repository, number), apis::issue_api::issue_get_issue(&configuration, owner, repository, number),
apis::issue_api::issue_get_comments(&configuration, owner, repository, number, None, None,), load_issue_comments(&configuration, owner, repository, number, page),
); );
Ok(IssueDetails { Ok(IssueDetails {
issue: issue.map_err(|error| error.to_string())?, issue: issue.map_err(|error| error.to_string())?,
comments: comments.map_err(|error| error.to_string())?, has_more: false,
comments: comments?,
}) })
} }
async fn load_issue_comments(
configuration: &apis::configuration::Configuration,
owner: &str,
repository: &str,
number: i64,
page: i32,
) -> Result<Vec<models::Comment>, String> {
if page > 1 {
return Ok(Vec::new());
}
apis::issue_api::issue_get_comments(configuration, owner, repository, number, None, None)
.await
.map_err(|error| error.to_string())
}
pub async fn load_issue_editor( pub async fn load_issue_editor(
server: &Server, server: &Server,
owner: &str, owner: &str,
@@ -544,19 +606,22 @@ pub async fn load_pull(
owner: &str, owner: &str,
repository: &str, repository: &str,
number: i64, number: i64,
page: i32,
) -> Result<PullDetails, String> { ) -> Result<PullDetails, String> {
let client = let client =
Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?; Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?;
let configuration = client.configuration(); let configuration = client.configuration();
let (pull, comments, files) = tokio::join!( let (pull, comments, files) = tokio::join!(
apis::repository_api::repo_get_pull_request(&configuration, owner, repository, number,), apis::repository_api::repo_get_pull_request(&configuration, owner, repository, number,),
apis::issue_api::issue_get_comments(&configuration, owner, repository, number, None, None,), load_issue_comments(&configuration, owner, repository, number, page),
load_pull_files(&configuration, owner, repository, number), load_pull_files(&configuration, owner, repository, number, page),
); );
let files = files?;
Ok(PullDetails { Ok(PullDetails {
pull: pull.map_err(|error| error.to_string())?, pull: pull.map_err(|error| error.to_string())?,
comments: comments.map_err(|error| error.to_string())?, has_more: files.has_more,
files: files.map_err(|error| error.to_string())?, comments: comments?,
files: files.items,
}) })
} }
@@ -821,12 +886,14 @@ async fn load_commits_for_ref(
owner: &str, owner: &str,
repository: &str, repository: &str,
branch: Option<&str>, branch: Option<&str>,
) -> Result<Vec<models::Commit>, String> { pages: u32,
) -> Result<Page<models::Commit>, String> {
let client = let client =
Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?; Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?;
let configuration = client.configuration(); let configuration = client.configuration();
let mut commits = Vec::new(); let mut commits = Vec::new();
for page in 1.. { let mut has_more = false;
for page in 1..=pages.max(1) {
let batch = apis::repository_api::repo_get_all_commits( let batch = apis::repository_api::repo_get_all_commits(
&configuration, &configuration,
owner, owner,
@@ -838,22 +905,25 @@ async fn load_commits_for_ref(
None, None,
None, None,
Some(false), Some(false),
Some(page), Some(page as i32),
Some(100), Some(PAGE_SIZE),
None, None,
) )
.await .await
.map_err(|error| error.to_string())?; .map_err(|error| error.to_string())?;
let done = batch.len() < 100; has_more = batch.len() == PAGE_SIZE as usize;
commits.extend(batch); commits.extend(batch);
if done { if !has_more {
break; break;
} }
} }
Ok(commits) Ok(Page {
items: commits,
has_more,
})
} }
pub async fn load_home(server: &Server) -> Result<HomeData, String> { pub async fn load_home(server: &Server, page: i32) -> Result<HomeData, String> {
let client = let client =
Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?; Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?;
let configuration = client.configuration(); let configuration = client.configuration();
@@ -863,34 +933,23 @@ pub async fn load_home(server: &Server) -> Result<HomeData, String> {
.map_err(|error| error.to_string())? .map_err(|error| error.to_string())?
.login .login
.ok_or("The server account has no username.")?; .ok_or("The server account has no username.")?;
let activity_configuration = configuration.clone(); let activities = apis::user_api::user_list_activity_feeds(
let activity_login = login.clone(); &configuration,
let activities = async move { &login,
let mut activities = Vec::new(); Some(true),
for page in 1.. { None,
let batch = apis::user_api::user_list_activity_feeds( Some(page),
&activity_configuration, Some(PAGE_SIZE),
&activity_login, );
Some(true),
None,
Some(page),
Some(100),
)
.await
.map_err(|error| error.to_string())?;
if batch.is_empty() {
break;
}
activities.extend(batch);
}
Ok::<_, String>(activities)
};
let (activities, heatmap) = tokio::join!( let (activities, heatmap) = tokio::join!(
activities, activities,
apis::user_api::user_get_heatmap_data(&configuration, &login), apis::user_api::user_get_heatmap_data(&configuration, &login),
); );
Ok(HomeData { Ok(HomeData {
activities: activities?, has_more: activities
.as_ref()
.is_ok_and(|items| items.len() == PAGE_SIZE as usize),
activities: activities.map_err(|error| error.to_string())?,
heatmap: heatmap.map_err(|error| error.to_string())?, heatmap: heatmap.map_err(|error| error.to_string())?,
}) })
} }

View File

@@ -3,6 +3,22 @@ use std::collections::{BTreeMap, BTreeSet};
use gotcha_gitea::models; use gotcha_gitea::models;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
pub const PAGE_SIZE: i32 = 30;
pub struct Page<T> {
pub items: Vec<T>,
pub has_more: bool,
}
impl<T> Page<T> {
pub fn from_items(items: Vec<T>) -> Self {
Self {
has_more: items.len() == PAGE_SIZE as usize,
items,
}
}
}
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
pub enum AppearanceMode { pub enum AppearanceMode {
@@ -132,11 +148,13 @@ pub struct State {
pub struct HomeData { pub struct HomeData {
pub activities: Vec<models::Activity>, pub activities: Vec<models::Activity>,
pub heatmap: Vec<models::UserHeatmapData>, pub heatmap: Vec<models::UserHeatmapData>,
pub has_more: bool,
} }
pub struct IssueDetails { pub struct IssueDetails {
pub issue: models::Issue, pub issue: models::Issue,
pub comments: Vec<models::Comment>, pub comments: Vec<models::Comment>,
pub has_more: bool,
} }
pub struct IssueEditorData { pub struct IssueEditorData {
@@ -157,12 +175,14 @@ pub struct MilestoneDetails {
pub milestone: models::Milestone, pub milestone: models::Milestone,
pub issues: Vec<models::Issue>, pub issues: Vec<models::Issue>,
pub pulls: Vec<models::Issue>, pub pulls: Vec<models::Issue>,
pub has_more: bool,
} }
pub struct PullDetails { pub struct PullDetails {
pub pull: models::PullRequest, pub pull: models::PullRequest,
pub comments: Vec<models::Comment>, pub comments: Vec<models::Comment>,
pub files: Vec<models::ChangedFile>, pub files: Vec<models::ChangedFile>,
pub has_more: bool,
} }
pub fn open_status() -> String { pub fn open_status() -> String {
@@ -244,4 +264,10 @@ mod tests {
assert_eq!(parse_api_date("2023-02-29T00:00:00Z"), None); assert_eq!(parse_api_date("2023-02-29T00:00:00Z"), None);
assert_eq!(parse_api_date("not-a-date"), None); assert_eq!(parse_api_date("not-a-date"), None);
} }
#[test]
fn full_api_pages_expose_more_results() {
assert!(Page::from_items(vec![(); PAGE_SIZE as usize]).has_more);
assert!(!Page::from_items(vec![(); PAGE_SIZE as usize - 1]).has_more);
}
} }

View File

@@ -173,18 +173,36 @@ impl GotchaCore {
save_preferences(&state.preferences).map_err(Into::into) save_preferences(&state.preferences).map_err(Into::into)
} }
pub async fn home(&self) -> Result<HomePage, GotchaError> { pub async fn home(&self, page: u32) -> Result<HomePage, GotchaError> {
let server = self.server()?; let server = self.server()?;
let name = server.name.clone(); let name = server.name.clone();
Ok(home_page(name, load_home(&server).await?)) Ok(home_page(
name,
load_home(&server, valid_page(page)?).await?,
))
} }
pub async fn repositories(&self) -> Result<Vec<RepositoryRow>, GotchaError> { pub async fn repositories(&self, page: u32) -> Result<RepositoryListPage, GotchaError> {
let server = self.server()?; let server = self.server()?;
let repositories = load_repositories(&server).await?; let page_number = valid_page(page)?;
let repositories = load_repositories(&server, page_number).await?;
let mut state = self.state.lock().unwrap(); let mut state = self.state.lock().unwrap();
state.repositories = repositories; if page_number == 1 {
Ok(self.repository_rows(&state)) state.repositories.clear();
}
for repository in repositories.items {
if let Some(existing) = state.repositories.iter_mut().find(|candidate| {
candidate.owner == repository.owner && candidate.name == repository.name
}) {
*existing = repository;
} else {
state.repositories.push(repository);
}
}
Ok(RepositoryListPage {
rows: self.repository_rows(&state),
has_more: repositories.has_more,
})
} }
pub fn toggle_favorite( pub fn toggle_favorite(
@@ -209,7 +227,8 @@ impl GotchaCore {
&self, &self,
owner: String, owner: String,
repository: String, repository: String,
) -> Result<Vec<IssueRow>, GotchaError> { page: u32,
) -> Result<IssueListPage, GotchaError> {
let server = self.server()?; let server = self.server()?;
let (status, filter) = { let (status, filter) = {
let state = self.state.lock().unwrap(); let state = self.state.lock().unwrap();
@@ -224,17 +243,20 @@ impl GotchaCore {
) )
}; };
let labels: Vec<_> = filter.labels.into_iter().collect(); let labels: Vec<_> = filter.labels.into_iter().collect();
Ok(issue_rows( let page = load_issues(
&load_issues( &server,
&server, &owner,
&owner, &repository,
&repository, &status,
&status, &labels,
&labels, &filter.milestone,
&filter.milestone, valid_page(page)?,
) )
.await?, .await?;
)) Ok(IssueListPage {
rows: issue_rows(&page.items),
has_more: page.has_more,
})
} }
pub async fn issue_filters( pub async fn issue_filters(
@@ -317,9 +339,17 @@ impl GotchaCore {
owner: String, owner: String,
repository: String, repository: String,
number: i64, number: i64,
page: u32,
) -> Result<IssuePage, GotchaError> { ) -> Result<IssuePage, GotchaError> {
Ok(issue_page( Ok(issue_page(
load_issue(&self.server()?, &owner, &repository, number).await?, load_issue(
&self.server()?,
&owner,
&repository,
number,
valid_page(page)?,
)
.await?,
)) ))
} }
@@ -387,10 +417,14 @@ impl GotchaCore {
&self, &self,
owner: String, owner: String,
repository: String, repository: String,
) -> Result<Vec<MilestoneRow>, GotchaError> { page: u32,
Ok(milestone_rows( ) -> Result<MilestoneListPage, GotchaError> {
&load_milestones(&self.server()?, &owner, &repository).await?, let page =
)) load_milestones_page(&self.server()?, &owner, &repository, valid_page(page)?).await?;
Ok(MilestoneListPage {
rows: milestone_rows(&page.items),
has_more: page.has_more,
})
} }
pub async fn milestone( pub async fn milestone(
@@ -398,9 +432,10 @@ impl GotchaCore {
owner: String, owner: String,
repository: String, repository: String,
id: i64, id: i64,
page: u32,
) -> Result<MilestonePage, GotchaError> { ) -> Result<MilestonePage, GotchaError> {
Ok(milestone_page( Ok(milestone_page(
load_milestone(&self.server()?, &owner, &repository, id).await?, load_milestone(&self.server()?, &owner, &repository, id, valid_page(page)?).await?,
)) ))
} }
@@ -449,7 +484,7 @@ impl GotchaCore {
save_preferences(&state.preferences).map_err(Into::into) save_preferences(&state.preferences).map_err(Into::into)
} }
pub async fn pulls(&self) -> Result<Vec<PullRow>, GotchaError> { pub async fn pulls(&self, page: u32) -> Result<PullListPage, GotchaError> {
let server = self.server()?; let server = self.server()?;
let (status, filter) = { let (status, filter) = {
let state = self.state.lock().unwrap(); let state = self.state.lock().unwrap();
@@ -463,9 +498,11 @@ impl GotchaCore {
.unwrap_or_default(), .unwrap_or_default(),
) )
}; };
Ok(pull_rows( let page = load_pulls(&server, &status, &filter.milestone, valid_page(page)?).await?;
&load_pulls(&server, &status, &filter.milestone).await?, Ok(PullListPage {
)) rows: pull_rows(&page.items),
has_more: page.has_more,
})
} }
pub async fn pull( pub async fn pull(
@@ -473,9 +510,17 @@ impl GotchaCore {
owner: String, owner: String,
repository: String, repository: String,
number: i64, number: i64,
page: u32,
) -> Result<PullPage, GotchaError> { ) -> Result<PullPage, GotchaError> {
Ok(pull_page( Ok(pull_page(
load_pull(&self.server()?, &owner, &repository, number).await?, load_pull(
&self.server()?,
&owner,
&repository,
number,
valid_page(page)?,
)
.await?,
)) ))
} }
@@ -484,6 +529,7 @@ impl GotchaCore {
owner: String, owner: String,
repository: String, repository: String,
branch: Option<String>, branch: Option<String>,
pages: u32,
) -> Result<CommitPage, GotchaError> { ) -> Result<CommitPage, GotchaError> {
let server = self.server()?; let server = self.server()?;
let default_branch = self let default_branch = self
@@ -497,10 +543,17 @@ impl GotchaCore {
.unwrap_or_else(|| "main".into()); .unwrap_or_else(|| "main".into());
let branches = load_branches(&server, &owner, &repository, &default_branch).await?; let branches = load_branches(&server, &owner, &repository, &default_branch).await?;
let commits = match branch.as_deref() { let commits = match branch.as_deref() {
Some(branch) => load_branch_commits(&server, &owner, &repository, branch).await?, Some(branch) => {
None => load_all_commits(&server, &owner, &repository, &branches).await?, load_branch_commits(&server, &owner, &repository, branch, pages.max(1)).await?
}
None => load_all_commits(&server, &owner, &repository, &branches, pages.max(1)).await?,
}; };
Ok(commit_page(branches, &commits, branch.is_none())) Ok(commit_page(
branches,
&commits.items,
branch.is_none(),
commits.has_more,
))
} }
pub async fn repository_contents( pub async fn repository_contents(
@@ -574,6 +627,13 @@ fn validate_repository<'a>(
Ok((owner, repository)) Ok((owner, repository))
} }
fn valid_page(page: u32) -> Result<i32, GotchaError> {
(page > 0)
.then(|| i32::try_from(page).ok())
.flatten()
.ok_or_else(|| "Invalid page number.".into())
}
impl GotchaCore { impl GotchaCore {
fn server(&self) -> Result<Server, GotchaError> { fn server(&self) -> Result<Server, GotchaError> {
let state = self.state.lock().unwrap(); let state = self.state.lock().unwrap();

View File

@@ -23,6 +23,12 @@ pub struct RepositoryRow {
pub favorite: bool, pub favorite: bool,
} }
#[derive(Clone, uniffi::Record)]
pub struct RepositoryListPage {
pub rows: Vec<RepositoryRow>,
pub has_more: bool,
}
#[derive(Clone, uniffi::Record)] #[derive(Clone, uniffi::Record)]
pub struct LabelRow { pub struct LabelRow {
pub name: String, pub name: String,
@@ -40,6 +46,12 @@ pub struct IssueRow {
pub labels: Vec<LabelRow>, pub labels: Vec<LabelRow>,
} }
#[derive(Clone, uniffi::Record)]
pub struct IssueListPage {
pub rows: Vec<IssueRow>,
pub has_more: bool,
}
#[derive(Clone, uniffi::Record)] #[derive(Clone, uniffi::Record)]
pub struct PullRow { pub struct PullRow {
pub number: i64, pub number: i64,
@@ -65,6 +77,7 @@ pub struct IssuePage {
pub milestone: String, pub milestone: String,
pub body: String, pub body: String,
pub comments: Vec<CommentRow>, pub comments: Vec<CommentRow>,
pub has_more: bool,
} }
#[derive(Clone, uniffi::Record)] #[derive(Clone, uniffi::Record)]
@@ -115,6 +128,13 @@ pub struct MilestonePage {
pub milestone: MilestoneRow, pub milestone: MilestoneRow,
pub issues: Vec<IssueRow>, pub issues: Vec<IssueRow>,
pub pulls: Vec<PullRow>, pub pulls: Vec<PullRow>,
pub has_more: bool,
}
#[derive(Clone, uniffi::Record)]
pub struct MilestoneListPage {
pub rows: Vec<MilestoneRow>,
pub has_more: bool,
} }
#[derive(Clone, uniffi::Record)] #[derive(Clone, uniffi::Record)]
@@ -131,6 +151,13 @@ pub struct PullPage {
pub files_ref: String, pub files_ref: String,
pub files: Vec<FileRow>, pub files: Vec<FileRow>,
pub comments: Vec<CommentRow>, pub comments: Vec<CommentRow>,
pub has_more: bool,
}
#[derive(Clone, uniffi::Record)]
pub struct PullListPage {
pub rows: Vec<PullRow>,
pub has_more: bool,
} }
#[derive(Clone, uniffi::Record)] #[derive(Clone, uniffi::Record)]
@@ -149,6 +176,7 @@ pub struct CommitPage {
pub branches: Vec<String>, pub branches: Vec<String>,
pub commits: Vec<CommitRow>, pub commits: Vec<CommitRow>,
pub lane_count: u32, pub lane_count: u32,
pub has_more: bool,
} }
#[derive(Clone, uniffi::Record)] #[derive(Clone, uniffi::Record)]
@@ -157,6 +185,12 @@ pub struct FileRow {
pub status: String, pub status: String,
} }
#[derive(Clone, uniffi::Record)]
pub struct FileListPage {
pub rows: Vec<FileRow>,
pub has_more: bool,
}
#[derive(Clone, uniffi::Record)] #[derive(Clone, uniffi::Record)]
pub struct RepositoryContentRow { pub struct RepositoryContentRow {
pub name: String, pub name: String,
@@ -216,6 +250,7 @@ pub struct HomePage {
pub pull_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,
pub has_more: bool,
} }
pub fn repository_rows( pub fn repository_rows(
@@ -323,6 +358,7 @@ pub fn milestone_page(details: MilestoneDetails) -> MilestonePage {
milestone: milestone_row(&details.milestone), milestone: milestone_row(&details.milestone),
issues: issue_rows(&details.issues), issues: issue_rows(&details.issues),
pulls: pull_rows(&details.pulls), pulls: pull_rows(&details.pulls),
has_more: details.has_more,
} }
} }
@@ -389,6 +425,7 @@ pub fn commit_page(
branches: Vec<String>, branches: Vec<String>,
commits: &[HistoryCommit], commits: &[HistoryCommit],
show_graph: bool, show_graph: bool,
has_more: bool,
) -> CommitPage { ) -> CommitPage {
let lane_count = if show_graph { let lane_count = if show_graph {
commits commits
@@ -454,6 +491,7 @@ pub fn commit_page(
branches, branches,
commits, commits,
lane_count: lane_count as u32, lane_count: lane_count as u32,
has_more,
} }
} }
@@ -477,6 +515,7 @@ pub fn issue_page(details: IssueDetails) -> IssuePage {
.filter(|body| !body.is_empty()) .filter(|body| !body.is_empty())
.unwrap_or_else(|| "No description provided.".into()), .unwrap_or_else(|| "No description provided.".into()),
comments: details.comments.iter().map(comment_row).collect(), comments: details.comments.iter().map(comment_row).collect(),
has_more: details.has_more,
} }
} }
@@ -589,6 +628,7 @@ pub fn pull_page(details: PullDetails) -> PullPage {
files_ref: pull_files_ref(pull), files_ref: pull_files_ref(pull),
files: details.files.iter().filter_map(file_row).collect(), files: details.files.iter().filter_map(file_row).collect(),
comments: details.comments.iter().map(comment_row).collect(), comments: details.comments.iter().map(comment_row).collect(),
has_more: details.has_more,
} }
} }
@@ -722,6 +762,7 @@ pub fn home_page(server_name: String, home: HomeData) -> HomePage {
activities: home.activities.iter().map(activity_row).collect(), activities: home.activities.iter().map(activity_row).collect(),
heat_cells, heat_cells,
contribution_count, contribution_count,
has_more: home.has_more,
} }
} }
@@ -1213,6 +1254,7 @@ mod tests {
}) })
.collect(), .collect(),
heatmap: Vec::new(), heatmap: Vec::new(),
has_more: false,
}, },
); );
@@ -1229,6 +1271,7 @@ mod tests {
..Default::default() ..Default::default()
}, },
comments: Vec::new(), comments: Vec::new(),
has_more: false,
}); });
assert_eq!(page.state, "closed"); assert_eq!(page.state, "closed");
@@ -1331,6 +1374,7 @@ mod tests {
})), })),
..Default::default() ..Default::default()
}], }],
has_more: false,
}); });
assert!(page.issues.is_empty()); assert!(page.issues.is_empty());

View File

@@ -613,11 +613,11 @@ public protocol GotchaCoreProtocol: AnyObject, Sendable {
func commitFiles(owner: String, repository: String, sha: String) async throws -> [FileRow] func commitFiles(owner: String, repository: String, sha: String) async throws -> [FileRow]
func commits(owner: String, repository: String, branch: String?) async throws -> CommitPage func commits(owner: String, repository: String, branch: String?, pages: UInt32) async throws -> CommitPage
func home() async throws -> HomePage func home(page: UInt32) async throws -> HomePage
func issue(owner: String, repository: String, number: Int64) async throws -> IssuePage func issue(owner: String, repository: String, number: Int64, page: UInt32) async throws -> IssuePage
func issueEditor(owner: String, repository: String, number: Int64?) async throws -> IssueEditorPage func issueEditor(owner: String, repository: String, number: Int64?) async throws -> IssueEditorPage
@@ -625,13 +625,13 @@ public protocol GotchaCoreProtocol: AnyObject, Sendable {
func issueFiltersActive(owner: String, repository: String) throws -> Bool func issueFiltersActive(owner: String, repository: String) throws -> Bool
func issues(owner: String, repository: String) async throws -> [IssueRow] func issues(owner: String, repository: String, page: UInt32) async throws -> IssueListPage
func milestone(owner: String, repository: String, id: Int64) async throws -> MilestonePage func milestone(owner: String, repository: String, id: Int64, page: UInt32) async throws -> MilestonePage
func milestones(owner: String, repository: String) async throws -> [MilestoneRow] func milestones(owner: String, repository: String, page: UInt32) async throws -> MilestoneListPage
func pull(owner: String, repository: String, number: Int64) async throws -> PullPage func pull(owner: String, repository: String, number: Int64, page: UInt32) async throws -> PullPage
func pullDiff(owner: String, repository: String, number: Int64, path: String) async throws -> DiffPage func pullDiff(owner: String, repository: String, number: Int64, path: String) async throws -> DiffPage
@@ -639,9 +639,9 @@ public protocol GotchaCoreProtocol: AnyObject, Sendable {
func pullFiltersActive() throws -> Bool func pullFiltersActive() throws -> Bool
func pulls() async throws -> [PullRow] func pulls(page: UInt32) async throws -> PullListPage
func repositories() async throws -> [RepositoryRow] func repositories(page: UInt32) async throws -> RepositoryListPage
func repositoryContents(owner: String, repository: String, path: String) async throws -> [RepositoryContentRow] func repositoryContents(owner: String, repository: String, path: String) async throws -> [RepositoryContentRow]
@@ -797,12 +797,12 @@ open func commitFiles(owner: String, repository: String, sha: String)async throw
) )
} }
open func commits(owner: String, repository: String, branch: String?)async throws -> CommitPage { open func commits(owner: String, repository: String, branch: String?, pages: UInt32)async throws -> CommitPage {
return return
try await uniffiRustCallAsync( try await uniffiRustCallAsync(
rustFutureFunc: { rustFutureFunc: {
uniffi_gotcha_core_fn_method_gotchacore_commits( uniffi_gotcha_core_fn_method_gotchacore_commits(
self.uniffiCloneHandle(),FfiConverterString.lower(owner),FfiConverterString.lower(repository),FfiConverterOptionString.lower(branch) self.uniffiCloneHandle(),FfiConverterString.lower(owner),FfiConverterString.lower(repository),FfiConverterOptionString.lower(branch),FfiConverterUInt32.lower(pages)
) )
}, },
pollFunc: ffi_gotcha_core_rust_future_poll_rust_buffer, pollFunc: ffi_gotcha_core_rust_future_poll_rust_buffer,
@@ -813,12 +813,12 @@ open func commits(owner: String, repository: String, branch: String?)async throw
) )
} }
open func home()async throws -> HomePage { open func home(page: UInt32)async throws -> HomePage {
return return
try await uniffiRustCallAsync( try await uniffiRustCallAsync(
rustFutureFunc: { rustFutureFunc: {
uniffi_gotcha_core_fn_method_gotchacore_home( uniffi_gotcha_core_fn_method_gotchacore_home(
self.uniffiCloneHandle() self.uniffiCloneHandle(),FfiConverterUInt32.lower(page)
) )
}, },
pollFunc: ffi_gotcha_core_rust_future_poll_rust_buffer, pollFunc: ffi_gotcha_core_rust_future_poll_rust_buffer,
@@ -829,12 +829,12 @@ open func home()async throws -> HomePage {
) )
} }
open func issue(owner: String, repository: String, number: Int64)async throws -> IssuePage { open func issue(owner: String, repository: String, number: Int64, page: UInt32)async throws -> IssuePage {
return return
try await uniffiRustCallAsync( try await uniffiRustCallAsync(
rustFutureFunc: { rustFutureFunc: {
uniffi_gotcha_core_fn_method_gotchacore_issue( uniffi_gotcha_core_fn_method_gotchacore_issue(
self.uniffiCloneHandle(),FfiConverterString.lower(owner),FfiConverterString.lower(repository),FfiConverterInt64.lower(number) self.uniffiCloneHandle(),FfiConverterString.lower(owner),FfiConverterString.lower(repository),FfiConverterInt64.lower(number),FfiConverterUInt32.lower(page)
) )
}, },
pollFunc: ffi_gotcha_core_rust_future_poll_rust_buffer, pollFunc: ffi_gotcha_core_rust_future_poll_rust_buffer,
@@ -888,28 +888,28 @@ open func issueFiltersActive(owner: String, repository: String)throws -> Bool
}) })
} }
open func issues(owner: String, repository: String)async throws -> [IssueRow] { open func issues(owner: String, repository: String, page: UInt32)async throws -> IssueListPage {
return return
try await uniffiRustCallAsync( try await uniffiRustCallAsync(
rustFutureFunc: { rustFutureFunc: {
uniffi_gotcha_core_fn_method_gotchacore_issues( uniffi_gotcha_core_fn_method_gotchacore_issues(
self.uniffiCloneHandle(),FfiConverterString.lower(owner),FfiConverterString.lower(repository) self.uniffiCloneHandle(),FfiConverterString.lower(owner),FfiConverterString.lower(repository),FfiConverterUInt32.lower(page)
) )
}, },
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: FfiConverterSequenceTypeIssueRow.lift, liftFunc: FfiConverterTypeIssueListPage_lift,
errorHandler: FfiConverterTypeGotchaError_lift errorHandler: FfiConverterTypeGotchaError_lift
) )
} }
open func milestone(owner: String, repository: String, id: Int64)async throws -> MilestonePage { open func milestone(owner: String, repository: String, id: Int64, page: UInt32)async throws -> MilestonePage {
return return
try await uniffiRustCallAsync( try await uniffiRustCallAsync(
rustFutureFunc: { rustFutureFunc: {
uniffi_gotcha_core_fn_method_gotchacore_milestone( uniffi_gotcha_core_fn_method_gotchacore_milestone(
self.uniffiCloneHandle(),FfiConverterString.lower(owner),FfiConverterString.lower(repository),FfiConverterInt64.lower(id) self.uniffiCloneHandle(),FfiConverterString.lower(owner),FfiConverterString.lower(repository),FfiConverterInt64.lower(id),FfiConverterUInt32.lower(page)
) )
}, },
pollFunc: ffi_gotcha_core_rust_future_poll_rust_buffer, pollFunc: ffi_gotcha_core_rust_future_poll_rust_buffer,
@@ -920,28 +920,28 @@ open func milestone(owner: String, repository: String, id: Int64)async throws -
) )
} }
open func milestones(owner: String, repository: String)async throws -> [MilestoneRow] { open func milestones(owner: String, repository: String, page: UInt32)async throws -> MilestoneListPage {
return return
try await uniffiRustCallAsync( try await uniffiRustCallAsync(
rustFutureFunc: { rustFutureFunc: {
uniffi_gotcha_core_fn_method_gotchacore_milestones( uniffi_gotcha_core_fn_method_gotchacore_milestones(
self.uniffiCloneHandle(),FfiConverterString.lower(owner),FfiConverterString.lower(repository) self.uniffiCloneHandle(),FfiConverterString.lower(owner),FfiConverterString.lower(repository),FfiConverterUInt32.lower(page)
) )
}, },
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: FfiConverterSequenceTypeMilestoneRow.lift, liftFunc: FfiConverterTypeMilestoneListPage_lift,
errorHandler: FfiConverterTypeGotchaError_lift errorHandler: FfiConverterTypeGotchaError_lift
) )
} }
open func pull(owner: String, repository: String, number: Int64)async throws -> PullPage { open func pull(owner: String, repository: String, number: Int64, page: UInt32)async throws -> PullPage {
return return
try await uniffiRustCallAsync( try await uniffiRustCallAsync(
rustFutureFunc: { rustFutureFunc: {
uniffi_gotcha_core_fn_method_gotchacore_pull( uniffi_gotcha_core_fn_method_gotchacore_pull(
self.uniffiCloneHandle(),FfiConverterString.lower(owner),FfiConverterString.lower(repository),FfiConverterInt64.lower(number) self.uniffiCloneHandle(),FfiConverterString.lower(owner),FfiConverterString.lower(repository),FfiConverterInt64.lower(number),FfiConverterUInt32.lower(page)
) )
}, },
pollFunc: ffi_gotcha_core_rust_future_poll_rust_buffer, pollFunc: ffi_gotcha_core_rust_future_poll_rust_buffer,
@@ -993,34 +993,34 @@ open func pullFiltersActive()throws -> Bool {
}) })
} }
open func pulls()async throws -> [PullRow] { open func pulls(page: UInt32)async throws -> PullListPage {
return return
try await uniffiRustCallAsync( try await uniffiRustCallAsync(
rustFutureFunc: { rustFutureFunc: {
uniffi_gotcha_core_fn_method_gotchacore_pulls( uniffi_gotcha_core_fn_method_gotchacore_pulls(
self.uniffiCloneHandle() self.uniffiCloneHandle(),FfiConverterUInt32.lower(page)
) )
}, },
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: FfiConverterSequenceTypePullRow.lift, liftFunc: FfiConverterTypePullListPage_lift,
errorHandler: FfiConverterTypeGotchaError_lift errorHandler: FfiConverterTypeGotchaError_lift
) )
} }
open func repositories()async throws -> [RepositoryRow] { open func repositories(page: UInt32)async throws -> RepositoryListPage {
return return
try await uniffiRustCallAsync( try await uniffiRustCallAsync(
rustFutureFunc: { rustFutureFunc: {
uniffi_gotcha_core_fn_method_gotchacore_repositories( uniffi_gotcha_core_fn_method_gotchacore_repositories(
self.uniffiCloneHandle() self.uniffiCloneHandle(),FfiConverterUInt32.lower(page)
) )
}, },
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: FfiConverterSequenceTypeRepositoryRow.lift, liftFunc: FfiConverterTypeRepositoryListPage_lift,
errorHandler: FfiConverterTypeGotchaError_lift errorHandler: FfiConverterTypeGotchaError_lift
) )
} }
@@ -1360,13 +1360,15 @@ public struct CommitPage: Equatable, Hashable {
public var branches: [String] public var branches: [String]
public var commits: [CommitRow] public var commits: [CommitRow]
public var laneCount: UInt32 public var laneCount: UInt32
public var hasMore: Bool
// 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(branches: [String], commits: [CommitRow], laneCount: UInt32) { public init(branches: [String], commits: [CommitRow], laneCount: UInt32, hasMore: Bool) {
self.branches = branches self.branches = branches
self.commits = commits self.commits = commits
self.laneCount = laneCount self.laneCount = laneCount
self.hasMore = hasMore
} }
@@ -1387,7 +1389,8 @@ public struct FfiConverterTypeCommitPage: FfiConverterRustBuffer {
try CommitPage( try CommitPage(
branches: FfiConverterSequenceString.read(from: &buf), branches: FfiConverterSequenceString.read(from: &buf),
commits: FfiConverterSequenceTypeCommitRow.read(from: &buf), commits: FfiConverterSequenceTypeCommitRow.read(from: &buf),
laneCount: FfiConverterUInt32.read(from: &buf) laneCount: FfiConverterUInt32.read(from: &buf),
hasMore: FfiConverterBool.read(from: &buf)
) )
} }
@@ -1395,6 +1398,7 @@ public struct FfiConverterTypeCommitPage: FfiConverterRustBuffer {
FfiConverterSequenceString.write(value.branches, into: &buf) FfiConverterSequenceString.write(value.branches, into: &buf)
FfiConverterSequenceTypeCommitRow.write(value.commits, into: &buf) FfiConverterSequenceTypeCommitRow.write(value.commits, into: &buf)
FfiConverterUInt32.write(value.laneCount, into: &buf) FfiConverterUInt32.write(value.laneCount, into: &buf)
FfiConverterBool.write(value.hasMore, into: &buf)
} }
} }
@@ -1608,6 +1612,60 @@ public func FfiConverterTypeDiffPage_lower(_ value: DiffPage) -> RustBuffer {
} }
public struct FileListPage: Equatable, Hashable {
public var rows: [FileRow]
public var hasMore: Bool
// Default memberwise initializers are never public by default, so we
// declare one manually.
public init(rows: [FileRow], hasMore: Bool) {
self.rows = rows
self.hasMore = hasMore
}
}
#if compiler(>=6)
extension FileListPage: Sendable {}
#endif
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public struct FfiConverterTypeFileListPage: FfiConverterRustBuffer {
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> FileListPage {
return
try FileListPage(
rows: FfiConverterSequenceTypeFileRow.read(from: &buf),
hasMore: FfiConverterBool.read(from: &buf)
)
}
public static func write(_ value: FileListPage, into buf: inout [UInt8]) {
FfiConverterSequenceTypeFileRow.write(value.rows, into: &buf)
FfiConverterBool.write(value.hasMore, into: &buf)
}
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeFileListPage_lift(_ buf: RustBuffer) throws -> FileListPage {
return try FfiConverterTypeFileListPage.lift(buf)
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeFileListPage_lower(_ value: FileListPage) -> RustBuffer {
return FfiConverterTypeFileListPage.lower(value)
}
public struct FileRow: Equatable, Hashable { public struct FileRow: Equatable, Hashable {
public var path: String public var path: String
public var status: String public var status: String
@@ -1723,16 +1781,18 @@ public struct HomePage: Equatable, Hashable {
public var pullActivities: [ActivityRow] public var pullActivities: [ActivityRow]
public var heatCells: [HeatCell] public var heatCells: [HeatCell]
public var contributionCount: Int64 public var contributionCount: Int64
public var hasMore: Bool
// 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], issueActivities: [ActivityRow], pullActivities: [ActivityRow], heatCells: [HeatCell], contributionCount: Int64) { public init(serverName: String, activities: [ActivityRow], issueActivities: [ActivityRow], pullActivities: [ActivityRow], heatCells: [HeatCell], contributionCount: Int64, hasMore: Bool) {
self.serverName = serverName self.serverName = serverName
self.activities = activities self.activities = activities
self.issueActivities = issueActivities self.issueActivities = issueActivities
self.pullActivities = pullActivities self.pullActivities = pullActivities
self.heatCells = heatCells self.heatCells = heatCells
self.contributionCount = contributionCount self.contributionCount = contributionCount
self.hasMore = hasMore
} }
@@ -1756,7 +1816,8 @@ public struct FfiConverterTypeHomePage: FfiConverterRustBuffer {
issueActivities: FfiConverterSequenceTypeActivityRow.read(from: &buf), issueActivities: FfiConverterSequenceTypeActivityRow.read(from: &buf),
pullActivities: 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),
hasMore: FfiConverterBool.read(from: &buf)
) )
} }
@@ -1767,6 +1828,7 @@ public struct FfiConverterTypeHomePage: FfiConverterRustBuffer {
FfiConverterSequenceTypeActivityRow.write(value.pullActivities, 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)
FfiConverterBool.write(value.hasMore, into: &buf)
} }
} }
@@ -2034,6 +2096,60 @@ public func FfiConverterTypeIssueFilterOptions_lower(_ value: IssueFilterOptions
} }
public struct IssueListPage: Equatable, Hashable {
public var rows: [IssueRow]
public var hasMore: Bool
// Default memberwise initializers are never public by default, so we
// declare one manually.
public init(rows: [IssueRow], hasMore: Bool) {
self.rows = rows
self.hasMore = hasMore
}
}
#if compiler(>=6)
extension IssueListPage: Sendable {}
#endif
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public struct FfiConverterTypeIssueListPage: FfiConverterRustBuffer {
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> IssueListPage {
return
try IssueListPage(
rows: FfiConverterSequenceTypeIssueRow.read(from: &buf),
hasMore: FfiConverterBool.read(from: &buf)
)
}
public static func write(_ value: IssueListPage, into buf: inout [UInt8]) {
FfiConverterSequenceTypeIssueRow.write(value.rows, into: &buf)
FfiConverterBool.write(value.hasMore, into: &buf)
}
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeIssueListPage_lift(_ buf: RustBuffer) throws -> IssueListPage {
return try FfiConverterTypeIssueListPage.lift(buf)
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeIssueListPage_lower(_ value: IssueListPage) -> RustBuffer {
return FfiConverterTypeIssueListPage.lower(value)
}
public struct IssuePage: Equatable, Hashable { public struct IssuePage: Equatable, Hashable {
public var title: String public var title: String
public var state: String public var state: String
@@ -2041,16 +2157,18 @@ public struct IssuePage: Equatable, Hashable {
public var milestone: String public var milestone: String
public var body: String public var body: String
public var comments: [CommentRow] public var comments: [CommentRow]
public var hasMore: Bool
// 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(title: String, state: String, meta: String, milestone: String, body: String, comments: [CommentRow]) { public init(title: String, state: String, meta: String, milestone: String, body: String, comments: [CommentRow], hasMore: Bool) {
self.title = title self.title = title
self.state = state self.state = state
self.meta = meta self.meta = meta
self.milestone = milestone self.milestone = milestone
self.body = body self.body = body
self.comments = comments self.comments = comments
self.hasMore = hasMore
} }
@@ -2074,7 +2192,8 @@ public struct FfiConverterTypeIssuePage: FfiConverterRustBuffer {
meta: FfiConverterString.read(from: &buf), meta: FfiConverterString.read(from: &buf),
milestone: FfiConverterString.read(from: &buf), milestone: FfiConverterString.read(from: &buf),
body: FfiConverterString.read(from: &buf), body: FfiConverterString.read(from: &buf),
comments: FfiConverterSequenceTypeCommentRow.read(from: &buf) comments: FfiConverterSequenceTypeCommentRow.read(from: &buf),
hasMore: FfiConverterBool.read(from: &buf)
) )
} }
@@ -2085,6 +2204,7 @@ public struct FfiConverterTypeIssuePage: FfiConverterRustBuffer {
FfiConverterString.write(value.milestone, into: &buf) FfiConverterString.write(value.milestone, into: &buf)
FfiConverterString.write(value.body, into: &buf) FfiConverterString.write(value.body, into: &buf)
FfiConverterSequenceTypeCommentRow.write(value.comments, into: &buf) FfiConverterSequenceTypeCommentRow.write(value.comments, into: &buf)
FfiConverterBool.write(value.hasMore, into: &buf)
} }
} }
@@ -2232,17 +2352,73 @@ public func FfiConverterTypeLabelRow_lower(_ value: LabelRow) -> RustBuffer {
} }
public struct MilestoneListPage: Equatable, Hashable {
public var rows: [MilestoneRow]
public var hasMore: Bool
// Default memberwise initializers are never public by default, so we
// declare one manually.
public init(rows: [MilestoneRow], hasMore: Bool) {
self.rows = rows
self.hasMore = hasMore
}
}
#if compiler(>=6)
extension MilestoneListPage: Sendable {}
#endif
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public struct FfiConverterTypeMilestoneListPage: FfiConverterRustBuffer {
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MilestoneListPage {
return
try MilestoneListPage(
rows: FfiConverterSequenceTypeMilestoneRow.read(from: &buf),
hasMore: FfiConverterBool.read(from: &buf)
)
}
public static func write(_ value: MilestoneListPage, into buf: inout [UInt8]) {
FfiConverterSequenceTypeMilestoneRow.write(value.rows, into: &buf)
FfiConverterBool.write(value.hasMore, into: &buf)
}
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeMilestoneListPage_lift(_ buf: RustBuffer) throws -> MilestoneListPage {
return try FfiConverterTypeMilestoneListPage.lift(buf)
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeMilestoneListPage_lower(_ value: MilestoneListPage) -> RustBuffer {
return FfiConverterTypeMilestoneListPage.lower(value)
}
public struct MilestonePage: Equatable, Hashable { public struct MilestonePage: Equatable, Hashable {
public var milestone: MilestoneRow public var milestone: MilestoneRow
public var issues: [IssueRow] public var issues: [IssueRow]
public var pulls: [PullRow] public var pulls: [PullRow]
public var hasMore: Bool
// 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(milestone: MilestoneRow, issues: [IssueRow], pulls: [PullRow]) { public init(milestone: MilestoneRow, issues: [IssueRow], pulls: [PullRow], hasMore: Bool) {
self.milestone = milestone self.milestone = milestone
self.issues = issues self.issues = issues
self.pulls = pulls self.pulls = pulls
self.hasMore = hasMore
} }
@@ -2263,7 +2439,8 @@ public struct FfiConverterTypeMilestonePage: FfiConverterRustBuffer {
try MilestonePage( try MilestonePage(
milestone: FfiConverterTypeMilestoneRow.read(from: &buf), milestone: FfiConverterTypeMilestoneRow.read(from: &buf),
issues: FfiConverterSequenceTypeIssueRow.read(from: &buf), issues: FfiConverterSequenceTypeIssueRow.read(from: &buf),
pulls: FfiConverterSequenceTypePullRow.read(from: &buf) pulls: FfiConverterSequenceTypePullRow.read(from: &buf),
hasMore: FfiConverterBool.read(from: &buf)
) )
} }
@@ -2271,6 +2448,7 @@ public struct FfiConverterTypeMilestonePage: FfiConverterRustBuffer {
FfiConverterTypeMilestoneRow.write(value.milestone, into: &buf) FfiConverterTypeMilestoneRow.write(value.milestone, into: &buf)
FfiConverterSequenceTypeIssueRow.write(value.issues, into: &buf) FfiConverterSequenceTypeIssueRow.write(value.issues, into: &buf)
FfiConverterSequenceTypePullRow.write(value.pulls, into: &buf) FfiConverterSequenceTypePullRow.write(value.pulls, into: &buf)
FfiConverterBool.write(value.hasMore, into: &buf)
} }
} }
@@ -2418,6 +2596,60 @@ public func FfiConverterTypePullFilterOptions_lower(_ value: PullFilterOptions)
} }
public struct PullListPage: Equatable, Hashable {
public var rows: [PullRow]
public var hasMore: Bool
// Default memberwise initializers are never public by default, so we
// declare one manually.
public init(rows: [PullRow], hasMore: Bool) {
self.rows = rows
self.hasMore = hasMore
}
}
#if compiler(>=6)
extension PullListPage: Sendable {}
#endif
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public struct FfiConverterTypePullListPage: FfiConverterRustBuffer {
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> PullListPage {
return
try PullListPage(
rows: FfiConverterSequenceTypePullRow.read(from: &buf),
hasMore: FfiConverterBool.read(from: &buf)
)
}
public static func write(_ value: PullListPage, into buf: inout [UInt8]) {
FfiConverterSequenceTypePullRow.write(value.rows, into: &buf)
FfiConverterBool.write(value.hasMore, into: &buf)
}
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypePullListPage_lift(_ buf: RustBuffer) throws -> PullListPage {
return try FfiConverterTypePullListPage.lift(buf)
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypePullListPage_lower(_ value: PullListPage) -> RustBuffer {
return FfiConverterTypePullListPage.lower(value)
}
public struct PullPage: Equatable, Hashable { public struct PullPage: Equatable, Hashable {
public var title: String public var title: String
public var meta: String public var meta: String
@@ -2425,16 +2657,18 @@ public struct PullPage: Equatable, Hashable {
public var filesRef: String public var filesRef: String
public var files: [FileRow] public var files: [FileRow]
public var comments: [CommentRow] public var comments: [CommentRow]
public var hasMore: Bool
// 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(title: String, meta: String, body: String, filesRef: String, files: [FileRow], comments: [CommentRow]) { public init(title: String, meta: String, body: String, filesRef: String, files: [FileRow], comments: [CommentRow], hasMore: Bool) {
self.title = title self.title = title
self.meta = meta self.meta = meta
self.body = body self.body = body
self.filesRef = filesRef self.filesRef = filesRef
self.files = files self.files = files
self.comments = comments self.comments = comments
self.hasMore = hasMore
} }
@@ -2458,7 +2692,8 @@ public struct FfiConverterTypePullPage: FfiConverterRustBuffer {
body: FfiConverterString.read(from: &buf), body: FfiConverterString.read(from: &buf),
filesRef: FfiConverterString.read(from: &buf), filesRef: FfiConverterString.read(from: &buf),
files: FfiConverterSequenceTypeFileRow.read(from: &buf), files: FfiConverterSequenceTypeFileRow.read(from: &buf),
comments: FfiConverterSequenceTypeCommentRow.read(from: &buf) comments: FfiConverterSequenceTypeCommentRow.read(from: &buf),
hasMore: FfiConverterBool.read(from: &buf)
) )
} }
@@ -2469,6 +2704,7 @@ public struct FfiConverterTypePullPage: FfiConverterRustBuffer {
FfiConverterString.write(value.filesRef, into: &buf) FfiConverterString.write(value.filesRef, into: &buf)
FfiConverterSequenceTypeFileRow.write(value.files, into: &buf) FfiConverterSequenceTypeFileRow.write(value.files, into: &buf)
FfiConverterSequenceTypeCommentRow.write(value.comments, into: &buf) FfiConverterSequenceTypeCommentRow.write(value.comments, into: &buf)
FfiConverterBool.write(value.hasMore, into: &buf)
} }
} }
@@ -2686,6 +2922,60 @@ public func FfiConverterTypeRepositoryFilePage_lower(_ value: RepositoryFilePage
} }
public struct RepositoryListPage: Equatable, Hashable {
public var rows: [RepositoryRow]
public var hasMore: Bool
// Default memberwise initializers are never public by default, so we
// declare one manually.
public init(rows: [RepositoryRow], hasMore: Bool) {
self.rows = rows
self.hasMore = hasMore
}
}
#if compiler(>=6)
extension RepositoryListPage: Sendable {}
#endif
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public struct FfiConverterTypeRepositoryListPage: FfiConverterRustBuffer {
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> RepositoryListPage {
return
try RepositoryListPage(
rows: FfiConverterSequenceTypeRepositoryRow.read(from: &buf),
hasMore: FfiConverterBool.read(from: &buf)
)
}
public static func write(_ value: RepositoryListPage, into buf: inout [UInt8]) {
FfiConverterSequenceTypeRepositoryRow.write(value.rows, into: &buf)
FfiConverterBool.write(value.hasMore, into: &buf)
}
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeRepositoryListPage_lift(_ buf: RustBuffer) throws -> RepositoryListPage {
return try FfiConverterTypeRepositoryListPage.lift(buf)
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeRepositoryListPage_lower(_ value: RepositoryListPage) -> RustBuffer {
return FfiConverterTypeRepositoryListPage.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
@@ -3538,13 +3828,13 @@ private let initializationResult: InitializationResult = {
if (uniffi_gotcha_core_checksum_method_gotchacore_commit_files() != 43926) { if (uniffi_gotcha_core_checksum_method_gotchacore_commit_files() != 43926) {
return InitializationResult.apiChecksumMismatch return InitializationResult.apiChecksumMismatch
} }
if (uniffi_gotcha_core_checksum_method_gotchacore_commits() != 472) { if (uniffi_gotcha_core_checksum_method_gotchacore_commits() != 59625) {
return InitializationResult.apiChecksumMismatch return InitializationResult.apiChecksumMismatch
} }
if (uniffi_gotcha_core_checksum_method_gotchacore_home() != 5983) { if (uniffi_gotcha_core_checksum_method_gotchacore_home() != 38990) {
return InitializationResult.apiChecksumMismatch return InitializationResult.apiChecksumMismatch
} }
if (uniffi_gotcha_core_checksum_method_gotchacore_issue() != 56735) { if (uniffi_gotcha_core_checksum_method_gotchacore_issue() != 3108) {
return InitializationResult.apiChecksumMismatch return InitializationResult.apiChecksumMismatch
} }
if (uniffi_gotcha_core_checksum_method_gotchacore_issue_editor() != 10078) { if (uniffi_gotcha_core_checksum_method_gotchacore_issue_editor() != 10078) {
@@ -3556,16 +3846,16 @@ private let initializationResult: InitializationResult = {
if (uniffi_gotcha_core_checksum_method_gotchacore_issue_filters_active() != 51470) { if (uniffi_gotcha_core_checksum_method_gotchacore_issue_filters_active() != 51470) {
return InitializationResult.apiChecksumMismatch return InitializationResult.apiChecksumMismatch
} }
if (uniffi_gotcha_core_checksum_method_gotchacore_issues() != 1316) { if (uniffi_gotcha_core_checksum_method_gotchacore_issues() != 8566) {
return InitializationResult.apiChecksumMismatch return InitializationResult.apiChecksumMismatch
} }
if (uniffi_gotcha_core_checksum_method_gotchacore_milestone() != 26979) { if (uniffi_gotcha_core_checksum_method_gotchacore_milestone() != 57186) {
return InitializationResult.apiChecksumMismatch return InitializationResult.apiChecksumMismatch
} }
if (uniffi_gotcha_core_checksum_method_gotchacore_milestones() != 27282) { if (uniffi_gotcha_core_checksum_method_gotchacore_milestones() != 50431) {
return InitializationResult.apiChecksumMismatch return InitializationResult.apiChecksumMismatch
} }
if (uniffi_gotcha_core_checksum_method_gotchacore_pull() != 46939) { if (uniffi_gotcha_core_checksum_method_gotchacore_pull() != 12919) {
return InitializationResult.apiChecksumMismatch return InitializationResult.apiChecksumMismatch
} }
if (uniffi_gotcha_core_checksum_method_gotchacore_pull_diff() != 61384) { if (uniffi_gotcha_core_checksum_method_gotchacore_pull_diff() != 61384) {
@@ -3577,10 +3867,10 @@ private let initializationResult: InitializationResult = {
if (uniffi_gotcha_core_checksum_method_gotchacore_pull_filters_active() != 20260) { if (uniffi_gotcha_core_checksum_method_gotchacore_pull_filters_active() != 20260) {
return InitializationResult.apiChecksumMismatch return InitializationResult.apiChecksumMismatch
} }
if (uniffi_gotcha_core_checksum_method_gotchacore_pulls() != 13092) { if (uniffi_gotcha_core_checksum_method_gotchacore_pulls() != 37027) {
return InitializationResult.apiChecksumMismatch return InitializationResult.apiChecksumMismatch
} }
if (uniffi_gotcha_core_checksum_method_gotchacore_repositories() != 12256) { if (uniffi_gotcha_core_checksum_method_gotchacore_repositories() != 31164) {
return InitializationResult.apiChecksumMismatch return InitializationResult.apiChecksumMismatch
} }
if (uniffi_gotcha_core_checksum_method_gotchacore_repository_contents() != 8454) { if (uniffi_gotcha_core_checksum_method_gotchacore_repository_contents() != 8454) {

View File

@@ -286,17 +286,17 @@ uint64_t uniffi_gotcha_core_fn_method_gotchacore_commit_files(uint64_t ptr, Rust
#endif #endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_COMMITS #ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_COMMITS
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_COMMITS #define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_COMMITS
uint64_t uniffi_gotcha_core_fn_method_gotchacore_commits(uint64_t ptr, RustBuffer owner, RustBuffer repository, RustBuffer branch uint64_t uniffi_gotcha_core_fn_method_gotchacore_commits(uint64_t ptr, RustBuffer owner, RustBuffer repository, RustBuffer branch, uint32_t pages
); );
#endif #endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_HOME #ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_HOME
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_HOME #define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_HOME
uint64_t uniffi_gotcha_core_fn_method_gotchacore_home(uint64_t ptr uint64_t uniffi_gotcha_core_fn_method_gotchacore_home(uint64_t ptr, uint32_t page
); );
#endif #endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_ISSUE #ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_ISSUE
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_ISSUE #define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_ISSUE
uint64_t uniffi_gotcha_core_fn_method_gotchacore_issue(uint64_t ptr, RustBuffer owner, RustBuffer repository, int64_t number uint64_t uniffi_gotcha_core_fn_method_gotchacore_issue(uint64_t ptr, RustBuffer owner, RustBuffer repository, int64_t number, uint32_t page
); );
#endif #endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_ISSUE_EDITOR #ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_ISSUE_EDITOR
@@ -316,22 +316,22 @@ int8_t uniffi_gotcha_core_fn_method_gotchacore_issue_filters_active(uint64_t ptr
#endif #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, uint32_t page
); );
#endif #endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_MILESTONE #ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_MILESTONE
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_MILESTONE #define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_MILESTONE
uint64_t uniffi_gotcha_core_fn_method_gotchacore_milestone(uint64_t ptr, RustBuffer owner, RustBuffer repository, int64_t id uint64_t uniffi_gotcha_core_fn_method_gotchacore_milestone(uint64_t ptr, RustBuffer owner, RustBuffer repository, int64_t id, uint32_t page
); );
#endif #endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_MILESTONES #ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_MILESTONES
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_MILESTONES #define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_MILESTONES
uint64_t uniffi_gotcha_core_fn_method_gotchacore_milestones(uint64_t ptr, RustBuffer owner, RustBuffer repository uint64_t uniffi_gotcha_core_fn_method_gotchacore_milestones(uint64_t ptr, RustBuffer owner, RustBuffer repository, uint32_t page
); );
#endif #endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_PULL #ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_PULL
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_PULL #define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_PULL
uint64_t uniffi_gotcha_core_fn_method_gotchacore_pull(uint64_t ptr, RustBuffer owner, RustBuffer repository, int64_t number uint64_t uniffi_gotcha_core_fn_method_gotchacore_pull(uint64_t ptr, RustBuffer owner, RustBuffer repository, int64_t number, uint32_t page
); );
#endif #endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_PULL_DIFF #ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_PULL_DIFF
@@ -351,12 +351,12 @@ int8_t uniffi_gotcha_core_fn_method_gotchacore_pull_filters_active(uint64_t ptr,
#endif #endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_PULLS #ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_PULLS
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_PULLS #define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_PULLS
uint64_t uniffi_gotcha_core_fn_method_gotchacore_pulls(uint64_t ptr uint64_t uniffi_gotcha_core_fn_method_gotchacore_pulls(uint64_t ptr, uint32_t page
); );
#endif #endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_REPOSITORIES #ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_REPOSITORIES
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_REPOSITORIES #define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_REPOSITORIES
uint64_t uniffi_gotcha_core_fn_method_gotchacore_repositories(uint64_t ptr uint64_t uniffi_gotcha_core_fn_method_gotchacore_repositories(uint64_t ptr, uint32_t page
); );
#endif #endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_REPOSITORY_CONTENTS #ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_REPOSITORY_CONTENTS

View File

@@ -8,6 +8,7 @@ final class IssuesViewController: RefreshingTableViewController {
private var rows: [IssueRow] = [] private var rows: [IssueRow] = []
private var filterOptions: IssueFilterOptions? private var filterOptions: IssueFilterOptions?
private var filterTask: Task<Void, Never>? private var filterTask: Task<Void, Never>?
private var currentPage: UInt32 = 0
private lazy var filterButton = UIBarButtonItem( private lazy var filterButton = UIBarButtonItem(
image: context.symbol("line.3.horizontal.decrease.circle") image: context.symbol("line.3.horizontal.decrease.circle")
) )
@@ -45,17 +46,38 @@ final class IssuesViewController: RefreshingTableViewController {
} }
private func loadIssues(refreshing: Bool) { private func loadIssues(refreshing: Bool) {
beginLoading(refreshing: refreshing) loadIssues(page: 1, refreshing: refreshing)
}
override func loadMoreContent() {
loadIssues(page: currentPage + 1, refreshing: false)
}
private func loadIssues(page: UInt32, refreshing: Bool) {
if page == 1 {
resetPagination()
beginLoading(refreshing: refreshing)
}
loadingTask?.cancel() loadingTask?.cancel()
loadingTask = Task { loadingTask = Task {
do { do {
rows = try await context.core.issues(owner: owner, repository: repository) let result = try await context.core.issues(
owner: owner,
repository: repository,
page: page
)
if page == 1 { rows = result.rows } else { rows.append(contentsOf: result.rows) }
currentPage = page
finishPagination(hasMore: result.hasMore)
tableView.reloadData() tableView.reloadData()
updateEmptyState() updateEmptyState()
} catch { } catch {
if !Task.isCancelled { show(error: error) } if !Task.isCancelled {
show(error: error)
failPagination()
}
} }
endLoading() if page == 1 { endLoading() }
} }
} }
@@ -335,6 +357,7 @@ final class MilestonesViewController: RefreshingTableViewController {
private let owner: String private let owner: String
private let repository: String private let repository: String
private var rows: [MilestoneRow] = [] private var rows: [MilestoneRow] = []
private var currentPage: UInt32 = 0
init(context: AppContext, owner: String, repository: String) { init(context: AppContext, owner: String, repository: String) {
self.context = context self.context = context
@@ -356,11 +379,29 @@ final class MilestonesViewController: RefreshingTableViewController {
} }
override func loadContent(refreshing: Bool) { override func loadContent(refreshing: Bool) {
beginLoading(refreshing: refreshing) loadPage(1, refreshing: refreshing)
}
override func loadMoreContent() {
loadPage(currentPage + 1, refreshing: false)
}
private func loadPage(_ page: UInt32, refreshing: Bool) {
if page == 1 {
resetPagination()
beginLoading(refreshing: refreshing)
}
loadingTask?.cancel() loadingTask?.cancel()
loadingTask = Task { loadingTask = Task {
do { do {
rows = try await context.core.milestones(owner: owner, repository: repository) let result = try await context.core.milestones(
owner: owner,
repository: repository,
page: page
)
if page == 1 { rows = result.rows } else { rows.append(contentsOf: result.rows) }
currentPage = page
finishPagination(hasMore: result.hasMore)
tableView.reloadData() tableView.reloadData()
tableView.backgroundView = rows.isEmpty tableView.backgroundView = rows.isEmpty
? EmptyBackgroundView( ? EmptyBackgroundView(
@@ -369,9 +410,12 @@ final class MilestonesViewController: RefreshingTableViewController {
) )
: nil : nil
} catch { } catch {
if !Task.isCancelled { show(error: error) } if !Task.isCancelled {
show(error: error)
failPagination()
}
} }
endLoading() if page == 1 { endLoading() }
} }
} }
@@ -412,6 +456,7 @@ final class MilestoneViewController: RefreshingTableViewController {
private let repository: String private let repository: String
private let id: Int64 private let id: Int64
private var page: MilestonePage? private var page: MilestonePage?
private var currentPage: UInt32 = 0
init(context: AppContext, owner: String, repository: String, id: Int64) { init(context: AppContext, owner: String, repository: String, id: Int64) {
self.context = context self.context = context
@@ -435,21 +480,45 @@ final class MilestoneViewController: RefreshingTableViewController {
} }
override func loadContent(refreshing: Bool) { override func loadContent(refreshing: Bool) {
beginLoading(refreshing: refreshing) loadPage(1, refreshing: refreshing)
}
override func loadMoreContent() {
loadPage(currentPage + 1, refreshing: false)
}
private func loadPage(_ requestedPage: UInt32, refreshing: Bool) {
if requestedPage == 1 {
resetPagination()
beginLoading(refreshing: refreshing)
}
loadingTask?.cancel() loadingTask?.cancel()
loadingTask = Task { loadingTask = Task {
do { do {
page = try await context.core.milestone( let result = try await context.core.milestone(
owner: owner, owner: owner,
repository: repository, repository: repository,
id: id id: id,
page: requestedPage
) )
if requestedPage == 1 {
page = result
} else {
page?.issues.append(contentsOf: result.issues)
page?.pulls.append(contentsOf: result.pulls)
page?.hasMore = result.hasMore
}
currentPage = requestedPage
finishPagination(hasMore: result.hasMore)
title = page?.milestone.title title = page?.milestone.title
tableView.reloadData() tableView.reloadData()
} catch { } catch {
if !Task.isCancelled { show(error: error) } if !Task.isCancelled {
show(error: error)
failPagination()
}
} }
endLoading() if requestedPage == 1 { endLoading() }
} }
} }
@@ -584,6 +653,7 @@ final class PullsViewController: RefreshingTableViewController {
private var rows: [PullRow] = [] private var rows: [PullRow] = []
private var filterOptions: PullFilterOptions? private var filterOptions: PullFilterOptions?
private var filterTask: Task<Void, Never>? private var filterTask: Task<Void, Never>?
private var currentPage: UInt32 = 0
init(context: AppContext) { init(context: AppContext) {
self.context = context self.context = context
@@ -626,11 +696,25 @@ final class PullsViewController: RefreshingTableViewController {
} }
private func loadPulls(refreshing: Bool) { private func loadPulls(refreshing: Bool) {
beginLoading(refreshing: refreshing) loadPulls(page: 1, refreshing: refreshing)
}
override func loadMoreContent() {
loadPulls(page: currentPage + 1, refreshing: false)
}
private func loadPulls(page: UInt32, refreshing: Bool) {
if page == 1 {
resetPagination()
beginLoading(refreshing: refreshing)
}
loadingTask?.cancel() loadingTask?.cancel()
loadingTask = Task { loadingTask = Task {
do { do {
rows = try await context.core.pulls() let result = try await context.core.pulls(page: page)
if page == 1 { rows = result.rows } else { rows.append(contentsOf: result.rows) }
currentPage = page
finishPagination(hasMore: result.hasMore)
tableView.reloadData() tableView.reloadData()
let status = context.core.settings().pullStatus let status = context.core.settings().pullStatus
tableView.backgroundView = rows.isEmpty tableView.backgroundView = rows.isEmpty
@@ -642,9 +726,12 @@ final class PullsViewController: RefreshingTableViewController {
) )
: nil : nil
} catch { } catch {
if !Task.isCancelled { show(error: error) } if !Task.isCancelled {
show(error: error)
failPagination()
}
} }
endLoading() if page == 1 { endLoading() }
} }
} }
@@ -783,6 +870,7 @@ final class CommitsViewController: RefreshingTableViewController {
private var contents: [RepositoryContentRow] = [] private var contents: [RepositoryContentRow] = []
private var branch: String? private var branch: String?
private var mode = Mode.history private var mode = Mode.history
private var currentPage: UInt32 = 0
init(context: AppContext, owner: String, repository: String) { init(context: AppContext, owner: String, repository: String) {
self.context = context self.context = context
@@ -808,7 +896,19 @@ final class CommitsViewController: RefreshingTableViewController {
} }
override func loadContent(refreshing: Bool) { override func loadContent(refreshing: Bool) {
beginLoading(refreshing: refreshing) loadPage(1, refreshing: refreshing)
}
override func loadMoreContent() {
guard mode == .history else { return }
loadPage(currentPage + 1, refreshing: false)
}
private func loadPage(_ requestedPage: UInt32, refreshing: Bool) {
if requestedPage == 1 {
resetPagination()
beginLoading(refreshing: refreshing)
}
loadingTask?.cancel() loadingTask?.cancel()
loadingTask = Task { loadingTask = Task {
do { do {
@@ -817,8 +917,11 @@ final class CommitsViewController: RefreshingTableViewController {
page = try await context.core.commits( page = try await context.core.commits(
owner: owner, owner: owner,
repository: repository, repository: repository,
branch: branch branch: branch,
pages: requestedPage
) )
currentPage = requestedPage
finishPagination(hasMore: page?.hasMore ?? false)
updateBranchMenu() updateBranchMenu()
case .files: case .files:
contents = try await context.core.repositoryContents( contents = try await context.core.repositoryContents(
@@ -830,9 +933,12 @@ final class CommitsViewController: RefreshingTableViewController {
tableView.reloadData() tableView.reloadData()
updateEmptyView() updateEmptyView()
} catch { } catch {
if !Task.isCancelled { show(error: error) } if !Task.isCancelled {
show(error: error)
failPagination()
}
} }
endLoading() if requestedPage == 1 { endLoading() }
} }
} }

View File

@@ -5,12 +5,18 @@ import SwiftUI
import UIKit import UIKit
@MainActor @MainActor
class MarkdownPageViewController: UIViewController { class MarkdownPageViewController: UIViewController, UIScrollViewDelegate {
let context: AppContext let context: AppContext
let scrollView = UIScrollView() let scrollView = UIScrollView()
let stack = UIStackView() let stack = UIStackView()
private let spinner = UIActivityIndicatorView(style: .medium) private let spinner = UIActivityIndicatorView(style: .medium)
var loadingTask: Task<Void, Never>? var loadingTask: Task<Void, Never>?
private lazy var moreButton = UIButton(
configuration: .plain(),
primaryAction: UIAction { [weak self] _ in self?.requestMoreContent() }
)
private var hasMoreContent = false
private var loadingMore = false
init(context: AppContext) { init(context: AppContext) {
self.context = context self.context = context
@@ -25,6 +31,7 @@ class MarkdownPageViewController: UIViewController {
view.backgroundColor = .systemGroupedBackground view.backgroundColor = .systemGroupedBackground
scrollView.translatesAutoresizingMaskIntoConstraints = false scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.alwaysBounceVertical = true scrollView.alwaysBounceVertical = true
scrollView.delegate = self
scrollView.refreshControl = UIRefreshControl() scrollView.refreshControl = UIRefreshControl()
scrollView.refreshControl?.addTarget(self, action: #selector(refreshRequested), for: .valueChanged) scrollView.refreshControl?.addTarget(self, action: #selector(refreshRequested), for: .valueChanged)
view.addSubview(scrollView) view.addSubview(scrollView)
@@ -49,6 +56,8 @@ class MarkdownPageViewController: UIViewController {
func loadContent(refreshing: Bool) {} func loadContent(refreshing: Bool) {}
func loadMoreContent() {}
func beginLoading(refreshing: Bool) { func beginLoading(refreshing: Bool) {
if !refreshing { beginNavigationLoading(spinner) } if !refreshing { beginNavigationLoading(spinner) }
} }
@@ -63,6 +72,54 @@ class MarkdownPageViewController: UIViewController {
views.forEach(stack.addArrangedSubview) views.forEach(stack.addArrangedSubview)
} }
func resetPagination() {
hasMoreContent = false
loadingMore = false
stack.removeArrangedSubview(moreButton)
moreButton.removeFromSuperview()
}
func finishPagination(hasMore: Bool) {
hasMoreContent = hasMore
loadingMore = false
guard hasMore else { return }
var configuration = moreButton.configuration
configuration?.title = "Pull up or tap to load more"
configuration?.showsActivityIndicator = false
moreButton.configuration = configuration
moreButton.accessibilityLabel = "Load more results"
if moreButton.superview !== stack { stack.addArrangedSubview(moreButton) }
if moreButton.constraints.isEmpty {
moreButton.heightAnchor.constraint(equalToConstant: 50).isActive = true
}
}
func failPagination() {
loadingMore = false
finishPagination(hasMore: hasMoreContent)
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard scrollView.isDragging, hasMoreContent, !loadingMore else { return }
let bottom = max(
-scrollView.adjustedContentInset.top,
scrollView.contentSize.height
+ scrollView.adjustedContentInset.bottom
- scrollView.bounds.height
)
if scrollView.contentOffset.y > bottom + 60 { requestMoreContent() }
}
private func requestMoreContent() {
guard hasMoreContent, !loadingMore else { return }
loadingMore = true
var configuration = moreButton.configuration
configuration?.title = "Loading more…"
configuration?.showsActivityIndicator = true
moreButton.configuration = configuration
loadMoreContent()
}
@objc private func refreshRequested() { @objc private func refreshRequested() {
loadContent(refreshing: true) loadContent(refreshing: true)
} }
@@ -73,6 +130,8 @@ final class IssueViewController: MarkdownPageViewController {
private let owner: String private let owner: String
private let repository: String private let repository: String
private let number: Int64 private let number: Int64
private var page: IssuePage?
private var currentPage: UInt32 = 0
init(context: AppContext, owner: String, repository: String, number: Int64) { init(context: AppContext, owner: String, repository: String, number: Int64) {
self.owner = owner self.owner = owner
@@ -110,27 +169,52 @@ final class IssueViewController: MarkdownPageViewController {
} }
override func loadContent(refreshing: Bool) { override func loadContent(refreshing: Bool) {
beginLoading(refreshing: refreshing) loadPage(1, refreshing: refreshing)
}
override func loadMoreContent() {
loadPage(currentPage + 1, refreshing: false)
}
private func loadPage(_ requestedPage: UInt32, refreshing: Bool) {
if requestedPage == 1 {
resetPagination()
beginLoading(refreshing: refreshing)
}
loadingTask?.cancel() loadingTask?.cancel()
loadingTask = Task { loadingTask = Task {
do { do {
let page = try await context.core.issue( let result = try await context.core.issue(
owner: owner, owner: owner,
repository: repository, repository: repository,
number: number number: number,
page: requestedPage
) )
replaceContent(detailViews( if requestedPage == 1 {
title: page.title, page = result
issueState: page.state, } else {
meta: page.meta, page?.comments.append(contentsOf: result.comments)
body: page.body, page?.hasMore = result.hasMore
comments: page.comments, }
milestone: page.milestone currentPage = requestedPage
)) if let page {
replaceContent(detailViews(
title: page.title,
issueState: page.state,
meta: page.meta,
body: page.body,
comments: page.comments,
milestone: page.milestone
))
finishPagination(hasMore: page.hasMore)
}
} catch { } catch {
if !Task.isCancelled { show(error: error) } if !Task.isCancelled {
show(error: error)
failPagination()
}
} }
endLoading() if requestedPage == 1 { endLoading() }
} }
} }
} }
@@ -140,6 +224,8 @@ final class PullViewController: MarkdownPageViewController {
private let owner: String private let owner: String
private let repository: String private let repository: String
private let number: Int64 private let number: Int64
private var page: PullPage?
private var currentPage: UInt32 = 0
init(context: AppContext, owner: String, repository: String, number: Int64) { init(context: AppContext, owner: String, repository: String, number: Int64) {
self.owner = owner self.owner = owner
@@ -158,24 +244,45 @@ final class PullViewController: MarkdownPageViewController {
} }
override func loadContent(refreshing: Bool) { override func loadContent(refreshing: Bool) {
beginLoading(refreshing: refreshing) loadPage(1, refreshing: refreshing)
}
override func loadMoreContent() {
loadPage(currentPage + 1, refreshing: false)
}
private func loadPage(_ requestedPage: UInt32, refreshing: Bool) {
if requestedPage == 1 {
resetPagination()
beginLoading(refreshing: refreshing)
}
loadingTask?.cancel() loadingTask?.cancel()
loadingTask = Task { loadingTask = Task {
do { do {
let page = try await context.core.pull( let result = try await context.core.pull(
owner: owner, owner: owner,
repository: repository, repository: repository,
number: number number: number,
page: requestedPage
) )
var views = detailViews( if requestedPage == 1 {
title: page.title, page = result
meta: page.meta, } else {
body: page.body, page?.files.append(contentsOf: result.files)
comments: [] page?.comments.append(contentsOf: result.comments)
) page?.hasMore = result.hasMore
if !page.files.isEmpty { }
views.append(sectionLabel(page.filesRef)) currentPage = requestedPage
views.append(contentsOf: page.files.map { file in if let page {
var views = detailViews(
title: page.title,
meta: page.meta,
body: page.body,
comments: []
)
if !page.files.isEmpty {
views.append(sectionLabel(page.filesRef))
views.append(contentsOf: page.files.map { file in
detailButton(title: file.path, detail: file.status) { [weak self] in detailButton(title: file.path, detail: file.status) { [weak self] in
guard let self else { return } guard let self else { return }
self.navigationController?.pushViewController( self.navigationController?.pushViewController(
@@ -191,14 +298,19 @@ final class PullViewController: MarkdownPageViewController {
animated: true animated: true
) )
} }
}) })
}
views.append(contentsOf: commentViews(page.comments))
replaceContent(views)
finishPagination(hasMore: page.hasMore)
} }
views.append(contentsOf: commentViews(page.comments))
replaceContent(views)
} catch { } catch {
if !Task.isCancelled { show(error: error) } if !Task.isCancelled {
show(error: error)
failPagination()
}
} }
endLoading() if requestedPage == 1 { endLoading() }
} }
} }
} }

View File

@@ -188,6 +188,7 @@ final class RepositoriesViewController: RefreshingTableViewController {
private let context: AppContext private let context: AppContext
private let mode: RepositoryMode private let mode: RepositoryMode
private var rows: [RepositoryRow] = [] private var rows: [RepositoryRow] = []
private var currentPage: UInt32 = 0
init(context: AppContext, mode: RepositoryMode) { init(context: AppContext, mode: RepositoryMode) {
self.context = context self.context = context
@@ -215,11 +216,25 @@ final class RepositoriesViewController: RefreshingTableViewController {
} }
override func loadContent(refreshing: Bool) { override func loadContent(refreshing: Bool) {
beginLoading(refreshing: refreshing) loadPage(1, refreshing: refreshing)
}
override func loadMoreContent() {
loadPage(currentPage + 1, refreshing: false)
}
private func loadPage(_ page: UInt32, refreshing: Bool) {
if page == 1 {
resetPagination()
beginLoading(refreshing: refreshing)
}
loadingTask?.cancel() loadingTask?.cancel()
loadingTask = Task { loadingTask = Task {
do { do {
rows = try await context.core.repositories() let result = try await context.core.repositories(page: page)
rows = result.rows
currentPage = page
finishPagination(hasMore: result.hasMore)
tableView.reloadData() tableView.reloadData()
tableView.backgroundView = rows.isEmpty tableView.backgroundView = rows.isEmpty
? EmptyBackgroundView( ? EmptyBackgroundView(
@@ -228,9 +243,12 @@ final class RepositoriesViewController: RefreshingTableViewController {
) )
: nil : nil
} catch { } catch {
if !Task.isCancelled { show(error: error) } if !Task.isCancelled {
show(error: error)
failPagination()
}
} }
endLoading() if page == 1 { endLoading() }
} }
} }
@@ -308,6 +326,7 @@ final class HomeViewController: RefreshingTableViewController {
private let context: AppContext private let context: AppContext
private var page: HomePage? private var page: HomePage?
private var filter = ActivityFilter.all private var filter = ActivityFilter.all
private var currentPage: UInt32 = 0
private var activities: [ActivityRow] { private var activities: [ActivityRow] {
guard let page else { return [] } guard let page else { return [] }
@@ -358,24 +377,48 @@ final class HomeViewController: RefreshingTableViewController {
refreshControl?.endRefreshing() refreshControl?.endRefreshing()
return return
} }
beginLoading(refreshing: refreshing) loadPage(1, refreshing: refreshing)
}
override func loadMoreContent() {
loadPage(currentPage + 1, refreshing: false)
}
private func loadPage(_ requestedPage: UInt32, refreshing: Bool) {
if requestedPage == 1 {
resetPagination()
beginLoading(refreshing: refreshing)
}
loadingTask?.cancel() loadingTask?.cancel()
loadingTask = Task { loadingTask = Task {
do { do {
page = try await context.core.home() let result = try await context.core.home(page: requestedPage)
if requestedPage == 1 {
page = result
} else {
page?.activities.append(contentsOf: result.activities)
page?.issueActivities.append(contentsOf: result.issueActivities)
page?.pullActivities.append(contentsOf: result.pullActivities)
page?.hasMore = result.hasMore
}
currentPage = requestedPage
finishPagination(hasMore: result.hasMore)
title = page?.serverName title = page?.serverName
tableView.tableHeaderView = page.map { page in if requestedPage == 1 { tableView.tableHeaderView = page.map { page in
HeatmapView(page: page, selectedFilter: filter.rawValue) { [weak self] index in HeatmapView(page: page, selectedFilter: filter.rawValue) { [weak self] index in
guard let self, let filter = ActivityFilter(rawValue: index) else { return } guard let self, let filter = ActivityFilter(rawValue: index) else { return }
self.filter = filter self.filter = filter
self.updateActivities() self.updateActivities()
} }
} } }
updateActivities() updateActivities()
} catch { } catch {
if !Task.isCancelled { show(error: error) } if !Task.isCancelled {
show(error: error)
failPagination()
}
} }
endLoading() if requestedPage == 1 { endLoading() }
} }
} }

View File

@@ -29,6 +29,12 @@ extension UIViewController {
class RefreshingTableViewController: UITableViewController { class RefreshingTableViewController: UITableViewController {
private let spinner = UIActivityIndicatorView(style: .medium) private let spinner = UIActivityIndicatorView(style: .medium)
var loadingTask: Task<Void, Never>? var loadingTask: Task<Void, Never>?
private lazy var moreButton = UIButton(
configuration: .plain(),
primaryAction: UIAction { [weak self] _ in self?.requestMoreContent() }
)
private var hasMoreContent = false
private var loadingMore = false
init() { init() {
super.init(style: .plain) super.init(style: .plain)
@@ -46,6 +52,8 @@ class RefreshingTableViewController: UITableViewController {
func loadContent(refreshing: Bool) {} func loadContent(refreshing: Bool) {}
func loadMoreContent() {}
func beginLoading(refreshing: Bool) { func beginLoading(refreshing: Bool) {
if !refreshing { beginNavigationLoading(spinner) } if !refreshing { beginNavigationLoading(spinner) }
} }
@@ -55,6 +63,58 @@ class RefreshingTableViewController: UITableViewController {
refreshControl?.endRefreshing() refreshControl?.endRefreshing()
} }
func resetPagination() {
hasMoreContent = false
loadingMore = false
tableView.tableFooterView = nil
}
func finishPagination(hasMore: Bool) {
hasMoreContent = hasMore
loadingMore = false
var configuration = moreButton.configuration
configuration?.title = "Pull up or tap to load more"
configuration?.showsActivityIndicator = false
moreButton.configuration = configuration
moreButton.accessibilityLabel = "Load more results"
tableView.tableFooterView = hasMore ? paginationFooter() : nil
}
func failPagination() {
loadingMore = false
finishPagination(hasMore: hasMoreContent)
}
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard scrollView.isDragging, hasMoreContent, !loadingMore else { return }
let bottom = max(
-scrollView.adjustedContentInset.top,
scrollView.contentSize.height
+ scrollView.adjustedContentInset.bottom
- scrollView.bounds.height
)
if scrollView.contentOffset.y > bottom + 60 { requestMoreContent() }
}
private func requestMoreContent() {
guard hasMoreContent, !loadingMore else { return }
loadingMore = true
var configuration = moreButton.configuration
configuration?.title = "Loading more…"
configuration?.showsActivityIndicator = true
moreButton.configuration = configuration
moreButton.accessibilityLabel = "Loading more results"
loadMoreContent()
}
private func paginationFooter() -> UIView {
let footer = UIView(frame: CGRect(x: 0, y: 0, width: tableView.bounds.width, height: 56))
moreButton.frame = footer.bounds
moreButton.autoresizingMask = [.flexibleWidth, .flexibleHeight]
footer.addSubview(moreButton)
return footer
}
@objc private func refreshRequested() { @objc private func refreshRequested() {
loadContent(refreshing: true) loadContent(refreshing: true)
} }