1181 lines
39 KiB
Rust
1181 lines
39 KiB
Rust
use std::{
|
|
future::Future,
|
|
sync::{Arc, Mutex, OnceLock},
|
|
};
|
|
|
|
use gotcha_gitea::{Client, apis};
|
|
use slint::{ModelRc, VecModel};
|
|
|
|
mod activity;
|
|
mod api;
|
|
mod diff;
|
|
mod domain;
|
|
mod markdown;
|
|
mod storage;
|
|
mod ui;
|
|
mod view;
|
|
|
|
use api::*;
|
|
use domain::*;
|
|
use storage::*;
|
|
use ui::*;
|
|
use view::*;
|
|
|
|
static RUNTIME: OnceLock<tokio::runtime::Runtime> = OnceLock::new();
|
|
|
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
let ui = AppWindow::new()?;
|
|
let (preferences, startup_error) = match load_preferences() {
|
|
Ok(preferences) => (preferences, None),
|
|
Err(error) => (Preferences::default(), Some(error)),
|
|
};
|
|
let state = Arc::new(Mutex::new(State {
|
|
preferences,
|
|
..Default::default()
|
|
}));
|
|
|
|
refresh_servers(&ui, &state.lock().unwrap().preferences);
|
|
{
|
|
let state = state.lock().unwrap();
|
|
ui.set_issue_filter(state.preferences.issue_status.clone().into());
|
|
ui.set_pull_filter(state.preferences.pull_status.clone().into());
|
|
}
|
|
if let Some(error) = startup_error {
|
|
ui.set_error(error.into());
|
|
}
|
|
wire_callbacks(&ui, state.clone());
|
|
|
|
let startup_server = {
|
|
let state = state.lock().unwrap();
|
|
state
|
|
.preferences
|
|
.last_server
|
|
.filter(|index| *index < state.preferences.servers.len())
|
|
.or_else(|| (state.preferences.servers.len() == 1).then_some(0))
|
|
};
|
|
if let Some(index) = startup_server {
|
|
open_server(&ui, state.clone(), index);
|
|
open_home(&ui, state);
|
|
}
|
|
|
|
ui.run()?;
|
|
Ok(())
|
|
}
|
|
|
|
fn wire_callbacks(ui: &AppWindow, state: Arc<Mutex<State>>) {
|
|
ui.on_select_tab({
|
|
let weak = ui.as_weak();
|
|
let state = state.clone();
|
|
move |tab| {
|
|
let Some(ui) = weak.upgrade() else { return };
|
|
let changed = ui.get_tab() != tab;
|
|
if changed {
|
|
state.lock().unwrap().opened_from_home = false;
|
|
}
|
|
ui.set_error("".into());
|
|
ui.set_tab(tab.clone());
|
|
if changed {
|
|
match tab.as_str() {
|
|
"home" => open_home(&ui, state.clone()),
|
|
"pulls" => open_pulls(&ui, state.clone()),
|
|
"issues" | "repositories" => {
|
|
ui.set_page(if state.lock().unwrap().active_server.is_some() {
|
|
"repositories".into()
|
|
} else {
|
|
"servers".into()
|
|
});
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
ui.on_refresh_home({
|
|
let weak = ui.as_weak();
|
|
let state = state.clone();
|
|
move || {
|
|
if let Some(ui) = weak.upgrade() {
|
|
open_home(&ui, state.clone());
|
|
}
|
|
}
|
|
});
|
|
|
|
ui.on_refresh_repositories({
|
|
let weak = ui.as_weak();
|
|
let state = state.clone();
|
|
move || {
|
|
let server = {
|
|
let state = state.lock().unwrap();
|
|
state
|
|
.active_server
|
|
.and_then(|index| state.preferences.servers.get(index))
|
|
.cloned()
|
|
};
|
|
if let (Some(ui), Some(server)) = (weak.upgrade(), server) {
|
|
fetch_repositories(&ui, state.clone(), server, true);
|
|
}
|
|
}
|
|
});
|
|
|
|
ui.on_refresh_issues({
|
|
let weak = ui.as_weak();
|
|
let state = state.clone();
|
|
move || {
|
|
let target = {
|
|
let state = state.lock().unwrap();
|
|
state
|
|
.active_server
|
|
.and_then(|index| state.preferences.servers.get(index))
|
|
.cloned()
|
|
.zip(state.active_repository.clone())
|
|
};
|
|
if let (Some(ui), Some((server, (owner, repository)))) = (weak.upgrade(), target) {
|
|
fetch_issues(&ui, state.clone(), server, owner, repository, true);
|
|
}
|
|
}
|
|
});
|
|
|
|
ui.on_refresh_issue({
|
|
let weak = ui.as_weak();
|
|
let state = state.clone();
|
|
move || {
|
|
let target = {
|
|
let state = state.lock().unwrap();
|
|
state
|
|
.active_server
|
|
.and_then(|index| state.preferences.servers.get(index))
|
|
.cloned()
|
|
.zip(state.active_repository.clone())
|
|
.zip(state.active_issue)
|
|
};
|
|
if let (Some(ui), Some(((server, (owner, repository)), number))) =
|
|
(weak.upgrade(), target)
|
|
{
|
|
fetch_issue(&ui, server, owner, repository, number, true);
|
|
}
|
|
}
|
|
});
|
|
|
|
ui.on_refresh_pulls({
|
|
let weak = ui.as_weak();
|
|
let state = state.clone();
|
|
move || {
|
|
if let Some(ui) = weak.upgrade() {
|
|
open_pulls_with_refresh(&ui, state.clone(), true);
|
|
}
|
|
}
|
|
});
|
|
|
|
ui.on_refresh_pull({
|
|
let weak = ui.as_weak();
|
|
let state = state.clone();
|
|
move || {
|
|
let target = {
|
|
let state = state.lock().unwrap();
|
|
state
|
|
.active_server
|
|
.and_then(|index| state.preferences.servers.get(index))
|
|
.cloned()
|
|
.zip(state.active_pull.clone())
|
|
};
|
|
if let (Some(ui), Some((server, (owner, repository, number)))) =
|
|
(weak.upgrade(), target)
|
|
{
|
|
fetch_pull(&ui, server, owner, repository, number, true);
|
|
}
|
|
}
|
|
});
|
|
|
|
ui.on_refresh_commits({
|
|
let weak = ui.as_weak();
|
|
let state = state.clone();
|
|
move || {
|
|
let target = {
|
|
let state = state.lock().unwrap();
|
|
state
|
|
.active_server
|
|
.and_then(|index| state.preferences.servers.get(index))
|
|
.cloned()
|
|
.zip(state.active_repository.clone())
|
|
};
|
|
if let (Some(ui), Some((server, (owner, repository)))) = (weak.upgrade(), target) {
|
|
fetch_commits(&ui, state.clone(), server, owner, repository, true);
|
|
}
|
|
}
|
|
});
|
|
|
|
ui.on_show_add_server({
|
|
let weak = ui.as_weak();
|
|
move || {
|
|
if let Some(ui) = weak.upgrade() {
|
|
ui.set_error("".into());
|
|
ui.set_page("add".into());
|
|
}
|
|
}
|
|
});
|
|
|
|
ui.on_save_server({
|
|
let weak = ui.as_weak();
|
|
let state = state.clone();
|
|
move |name, url, token| {
|
|
let server = match validate_server(name.as_str(), url.as_str(), token.as_str()) {
|
|
Ok(server) => server,
|
|
Err(error) => {
|
|
if let Some(ui) = weak.upgrade() {
|
|
ui.set_error(error.into());
|
|
}
|
|
return;
|
|
}
|
|
};
|
|
if let Some(ui) = weak.upgrade() {
|
|
ui.set_error("".into());
|
|
ui.set_loading(true);
|
|
}
|
|
let weak = weak.clone();
|
|
let state = state.clone();
|
|
spawn(async move {
|
|
let result = match Client::new(&server.url, Some(&server.token)) {
|
|
Ok(client) => client
|
|
.current_user()
|
|
.await
|
|
.map_err(|error| error.to_string()),
|
|
Err(error) => Err(error.to_string()),
|
|
};
|
|
post(move || {
|
|
let Some(ui) = weak.upgrade() else { return };
|
|
ui.set_loading(false);
|
|
match result {
|
|
Ok(_) => {
|
|
if let Err(error) = save_server_token(&server) {
|
|
ui.set_error(error.into());
|
|
return;
|
|
}
|
|
let index = {
|
|
let mut state = state.lock().unwrap();
|
|
state.preferences.servers.push(server);
|
|
let index = state.preferences.servers.len() - 1;
|
|
state.preferences.last_server = Some(index);
|
|
if let Err(error) = save_preferences(&state.preferences) {
|
|
ui.set_error(error.into());
|
|
return;
|
|
}
|
|
refresh_servers(&ui, &state.preferences);
|
|
index
|
|
};
|
|
open_server(&ui, state, index);
|
|
}
|
|
Err(error) => ui.set_error(error.into()),
|
|
}
|
|
});
|
|
});
|
|
}
|
|
});
|
|
|
|
ui.on_select_server({
|
|
let weak = ui.as_weak();
|
|
let state = state.clone();
|
|
move |index| {
|
|
if let Some(ui) = weak.upgrade() {
|
|
open_server(&ui, state.clone(), index as usize);
|
|
}
|
|
}
|
|
});
|
|
|
|
ui.on_select_repository({
|
|
let weak = ui.as_weak();
|
|
let state = state.clone();
|
|
move |owner, name| {
|
|
if let Some(ui) = weak.upgrade() {
|
|
if ui.get_tab().as_str() == "repositories" {
|
|
open_commit_history(&ui, state.clone(), owner.to_string(), name.to_string());
|
|
} else {
|
|
open_repository(&ui, state.clone(), owner.to_string(), name.to_string());
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
ui.on_toggle_favorite({
|
|
let weak = ui.as_weak();
|
|
let state = state.clone();
|
|
move |owner, name| {
|
|
let mut state = state.lock().unwrap();
|
|
let Some(server) = state
|
|
.active_server
|
|
.and_then(|i| state.preferences.servers.get(i))
|
|
else {
|
|
return;
|
|
};
|
|
let key = favorite_key(&server.url, owner.as_str(), name.as_str());
|
|
if !state.preferences.favorites.remove(&key) {
|
|
state.preferences.favorites.insert(key);
|
|
}
|
|
if let Err(error) = save_preferences(&state.preferences) {
|
|
if let Some(ui) = weak.upgrade() {
|
|
ui.set_error(error.into());
|
|
}
|
|
return;
|
|
}
|
|
if let Some(ui) = weak.upgrade() {
|
|
refresh_repositories(&ui, &state);
|
|
}
|
|
}
|
|
});
|
|
|
|
ui.on_select_issue({
|
|
let weak = ui.as_weak();
|
|
let state = state.clone();
|
|
move |number| {
|
|
let target = {
|
|
let mut state = state.lock().unwrap();
|
|
state.opened_from_home = false;
|
|
state.active_issue = Some(number as i64);
|
|
state
|
|
.active_server
|
|
.and_then(|index| state.preferences.servers.get(index))
|
|
.cloned()
|
|
.zip(state.active_repository.clone())
|
|
};
|
|
if let (Some(ui), Some((server, (owner, repository)))) = (weak.upgrade(), target) {
|
|
fetch_issue(&ui, server, owner, repository, number as i64, false);
|
|
}
|
|
}
|
|
});
|
|
|
|
ui.on_select_pull({
|
|
let weak = ui.as_weak();
|
|
let state = state.clone();
|
|
move |owner, repository, number| {
|
|
let server = {
|
|
let mut state = state.lock().unwrap();
|
|
state.opened_from_home = false;
|
|
state.active_pull =
|
|
Some((owner.to_string(), repository.to_string(), number as i64));
|
|
state
|
|
.active_server
|
|
.and_then(|index| state.preferences.servers.get(index))
|
|
.cloned()
|
|
};
|
|
if let (Some(ui), Some(server)) = (weak.upgrade(), server) {
|
|
fetch_pull(
|
|
&ui,
|
|
server,
|
|
owner.to_string(),
|
|
repository.to_string(),
|
|
number as i64,
|
|
false,
|
|
);
|
|
}
|
|
}
|
|
});
|
|
|
|
ui.on_select_commit({
|
|
let weak = ui.as_weak();
|
|
let state = state.clone();
|
|
move |sha| {
|
|
let target = {
|
|
let mut state = state.lock().unwrap();
|
|
state.opened_from_home = false;
|
|
state.active_commit = Some(sha.to_string());
|
|
state
|
|
.active_server
|
|
.and_then(|index| state.preferences.servers.get(index))
|
|
.cloned()
|
|
.zip(state.active_repository.clone())
|
|
};
|
|
if let (Some(ui), Some((server, (owner, repository)))) = (weak.upgrade(), target) {
|
|
fetch_commit(&ui, server, owner, repository, sha.to_string());
|
|
}
|
|
}
|
|
});
|
|
|
|
ui.on_select_branch({
|
|
let weak = ui.as_weak();
|
|
let state = state.clone();
|
|
move |selection| {
|
|
let target = {
|
|
let mut state = state.lock().unwrap();
|
|
let branch = (selection.as_str() != "All").then(|| selection.to_string());
|
|
if branch
|
|
.as_ref()
|
|
.is_some_and(|branch| !state.branches.contains(branch))
|
|
{
|
|
return;
|
|
}
|
|
state.active_branch = branch;
|
|
state.commits.clear();
|
|
state
|
|
.active_server
|
|
.and_then(|index| state.preferences.servers.get(index))
|
|
.cloned()
|
|
.zip(state.active_repository.clone())
|
|
};
|
|
if let (Some(ui), Some((server, (owner, repository)))) = (weak.upgrade(), target) {
|
|
ui.set_commits(empty_model());
|
|
fetch_commits(&ui, state.clone(), server, owner, repository, false);
|
|
}
|
|
}
|
|
});
|
|
|
|
ui.on_select_file({
|
|
let weak = ui.as_weak();
|
|
let state = state.clone();
|
|
move |path| {
|
|
let target = {
|
|
let state = state.lock().unwrap();
|
|
state
|
|
.active_server
|
|
.and_then(|index| state.preferences.servers.get(index))
|
|
.cloned()
|
|
.zip(state.active_repository.clone())
|
|
.zip(state.active_commit.clone())
|
|
};
|
|
if let (Some(ui), Some(((server, (owner, repository)), sha))) = (weak.upgrade(), target)
|
|
{
|
|
fetch_file_diff(&ui, server, owner, repository, sha, path.to_string());
|
|
}
|
|
}
|
|
});
|
|
|
|
ui.on_select_pull_file({
|
|
let weak = ui.as_weak();
|
|
let state = state.clone();
|
|
move |path| {
|
|
let target = {
|
|
let state = state.lock().unwrap();
|
|
state
|
|
.active_server
|
|
.and_then(|index| state.preferences.servers.get(index))
|
|
.cloned()
|
|
.zip(state.active_pull.clone())
|
|
};
|
|
if let (Some(ui), Some((server, (owner, repository, number)))) =
|
|
(weak.upgrade(), target)
|
|
{
|
|
fetch_pull_file_diff(&ui, server, owner, repository, number, path.to_string());
|
|
}
|
|
}
|
|
});
|
|
|
|
ui.on_select_activity({
|
|
let weak = ui.as_weak();
|
|
let state = state.clone();
|
|
move |target, owner, repository, number, sha| {
|
|
let server = {
|
|
let mut state = state.lock().unwrap();
|
|
state.opened_from_home = true;
|
|
state.active_repository = Some((owner.to_string(), repository.to_string()));
|
|
state
|
|
.active_server
|
|
.and_then(|index| state.preferences.servers.get(index))
|
|
.cloned()
|
|
};
|
|
let (Some(ui), Some(server)) = (weak.upgrade(), server) else {
|
|
return;
|
|
};
|
|
match target.as_str() {
|
|
"repository" => {
|
|
ui.set_tab("repositories".into());
|
|
open_commit_history(
|
|
&ui,
|
|
state.clone(),
|
|
owner.to_string(),
|
|
repository.to_string(),
|
|
);
|
|
}
|
|
"issue" => {
|
|
state.lock().unwrap().active_issue = Some(number as i64);
|
|
ui.set_tab("issues".into());
|
|
fetch_issue(
|
|
&ui,
|
|
server,
|
|
owner.to_string(),
|
|
repository.to_string(),
|
|
number as i64,
|
|
false,
|
|
);
|
|
}
|
|
"pull" => {
|
|
state.lock().unwrap().active_pull =
|
|
Some((owner.to_string(), repository.to_string(), number as i64));
|
|
ui.set_tab("pulls".into());
|
|
fetch_pull(
|
|
&ui,
|
|
server,
|
|
owner.to_string(),
|
|
repository.to_string(),
|
|
number as i64,
|
|
false,
|
|
);
|
|
}
|
|
"commit" => {
|
|
state.lock().unwrap().active_commit = Some(sha.to_string());
|
|
ui.set_tab("repositories".into());
|
|
ui.set_repository_title(repository.clone());
|
|
fetch_commit(
|
|
&ui,
|
|
server,
|
|
owner.to_string(),
|
|
repository.to_string(),
|
|
sha.to_string(),
|
|
);
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
});
|
|
|
|
ui.on_choose_filter({
|
|
let weak = ui.as_weak();
|
|
let state = state.clone();
|
|
move |status| {
|
|
if !matches!(status.as_str(), "open" | "closed") {
|
|
return;
|
|
}
|
|
let Some(ui) = weak.upgrade() else { return };
|
|
{
|
|
let mut state = state.lock().unwrap();
|
|
if ui.get_tab().as_str() == "pulls" {
|
|
state.preferences.pull_status = status.to_string();
|
|
ui.set_pull_filter(status.clone());
|
|
} else {
|
|
state.preferences.issue_status = status.to_string();
|
|
ui.set_issue_filter(status.clone());
|
|
}
|
|
if let Err(error) = save_preferences(&state.preferences) {
|
|
ui.set_error(error.into());
|
|
return;
|
|
}
|
|
}
|
|
if ui.get_tab().as_str() == "pulls" {
|
|
open_pulls(&ui, state.clone());
|
|
} else {
|
|
let target = {
|
|
let state = state.lock().unwrap();
|
|
state
|
|
.active_server
|
|
.and_then(|index| state.preferences.servers.get(index))
|
|
.cloned()
|
|
.zip(state.active_repository.clone())
|
|
};
|
|
if let Some((server, (owner, repository))) = target {
|
|
fetch_issues(&ui, state.clone(), server, owner, repository, false);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
ui.on_back({
|
|
let weak = ui.as_weak();
|
|
let state = state.clone();
|
|
move || {
|
|
let Some(ui) = weak.upgrade() else { return };
|
|
ui.set_error("".into());
|
|
if state.lock().unwrap().opened_from_home
|
|
&& matches!(ui.get_page().as_str(), "issue" | "pull" | "files")
|
|
{
|
|
state.lock().unwrap().opened_from_home = false;
|
|
ui.set_tab("home".into());
|
|
open_home(&ui, state.clone());
|
|
return;
|
|
}
|
|
ui.set_page(match (ui.get_tab().as_str(), ui.get_page().as_str()) {
|
|
("pulls", "pull") => "pulls".into(),
|
|
("pulls", "diff") => "pull".into(),
|
|
("repositories", "diff") => "files".into(),
|
|
("repositories", "files") => "commits".into(),
|
|
("repositories", "commits") => "repositories".into(),
|
|
(_, "issue") => "issues".into(),
|
|
(_, "issues") => "repositories".into(),
|
|
(_, "repositories" | "add") => "servers".into(),
|
|
_ => "servers".into(),
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
fn open_server(ui: &AppWindow, state: Arc<Mutex<State>>, index: usize) {
|
|
let server = {
|
|
let mut state = state.lock().unwrap();
|
|
let Some(server) = state.preferences.servers.get(index).cloned() else {
|
|
return;
|
|
};
|
|
state.active_server = Some(index);
|
|
state.preferences.last_server = Some(index);
|
|
state.repositories.clear();
|
|
if let Err(error) = save_preferences(&state.preferences) {
|
|
ui.set_error(error.into());
|
|
}
|
|
server
|
|
};
|
|
ui.set_has_active_server(true);
|
|
ui.set_home_server(server.name.clone().into());
|
|
ui.set_server_title(server.name.clone().into());
|
|
ui.set_repositories(empty_model());
|
|
ui.set_page("repositories".into());
|
|
ui.set_error("".into());
|
|
fetch_repositories(ui, state, server, false);
|
|
}
|
|
|
|
fn fetch_repositories(ui: &AppWindow, state: Arc<Mutex<State>>, server: Server, refreshing: bool) {
|
|
if refreshing {
|
|
ui.set_refreshing(true);
|
|
} else {
|
|
ui.set_loading(true);
|
|
}
|
|
let weak = ui.as_weak();
|
|
spawn(async move {
|
|
let result = load_repositories(&server).await;
|
|
post(move || {
|
|
let Some(ui) = weak.upgrade() else { return };
|
|
if refreshing {
|
|
ui.set_refreshing(false);
|
|
} else {
|
|
ui.set_loading(false);
|
|
}
|
|
match result {
|
|
Ok(repositories) => {
|
|
let mut state = state.lock().unwrap();
|
|
state.repositories = repositories;
|
|
refresh_repositories(&ui, &state);
|
|
}
|
|
Err(error) => ui.set_error(error.into()),
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
fn open_home(ui: &AppWindow, state: Arc<Mutex<State>>) {
|
|
let server = {
|
|
let state = state.lock().unwrap();
|
|
state
|
|
.active_server
|
|
.and_then(|index| state.preferences.servers.get(index))
|
|
.cloned()
|
|
};
|
|
let Some(server) = server else {
|
|
ui.set_has_active_server(false);
|
|
ui.set_activities(empty_model());
|
|
ui.set_heat_cells(empty_model());
|
|
ui.set_contribution_count(0);
|
|
return;
|
|
};
|
|
|
|
ui.set_has_active_server(true);
|
|
ui.set_home_server(server.name.clone().into());
|
|
ui.set_home_loading(true);
|
|
let server_url = server.url.clone();
|
|
let weak = ui.as_weak();
|
|
spawn(async move {
|
|
let result = load_home(&server).await;
|
|
post(move || {
|
|
let Some(ui) = weak.upgrade() else { return };
|
|
let is_current = {
|
|
let state = state.lock().unwrap();
|
|
state
|
|
.active_server
|
|
.and_then(|index| state.preferences.servers.get(index))
|
|
.is_some_and(|server| server.url == server_url)
|
|
};
|
|
if !is_current {
|
|
return;
|
|
}
|
|
ui.set_home_loading(false);
|
|
match result {
|
|
Ok(home) => refresh_home(&ui, home),
|
|
Err(error) => ui.set_error(error.into()),
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
fn open_repository(ui: &AppWindow, state: Arc<Mutex<State>>, owner: String, name: String) {
|
|
let server = {
|
|
let mut state = state.lock().unwrap();
|
|
state.active_repository = Some((owner.clone(), name.clone()));
|
|
state.issues.clear();
|
|
state
|
|
.active_server
|
|
.and_then(|i| state.preferences.servers.get(i))
|
|
.cloned()
|
|
};
|
|
let Some(server) = server else { return };
|
|
ui.set_repository_title(name.clone().into());
|
|
ui.set_issues(empty_model());
|
|
ui.set_page("issues".into());
|
|
ui.set_error("".into());
|
|
fetch_issues(ui, state, server, owner, name, false);
|
|
}
|
|
|
|
fn fetch_issues(
|
|
ui: &AppWindow,
|
|
state: Arc<Mutex<State>>,
|
|
server: Server,
|
|
owner: String,
|
|
repository: String,
|
|
refreshing: bool,
|
|
) {
|
|
if refreshing {
|
|
ui.set_refreshing(true);
|
|
} else {
|
|
ui.set_loading(true);
|
|
}
|
|
let weak = ui.as_weak();
|
|
let status = state.lock().unwrap().preferences.issue_status.clone();
|
|
spawn(async move {
|
|
let result = load_issues(&server, &owner, &repository, &status).await;
|
|
post(move || {
|
|
let Some(ui) = weak.upgrade() else { return };
|
|
if refreshing {
|
|
ui.set_refreshing(false);
|
|
} else {
|
|
ui.set_loading(false);
|
|
}
|
|
match result {
|
|
Ok(issues) => {
|
|
let mut state = state.lock().unwrap();
|
|
state.issues = issues;
|
|
refresh_issues(&ui, &state.issues);
|
|
}
|
|
Err(error) => ui.set_error(error.into()),
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
fn fetch_issue(
|
|
ui: &AppWindow,
|
|
server: Server,
|
|
owner: String,
|
|
repository: String,
|
|
number: i64,
|
|
refreshing: bool,
|
|
) {
|
|
if refreshing {
|
|
ui.set_refreshing(true);
|
|
} else {
|
|
ui.set_loading(true);
|
|
}
|
|
let weak = ui.as_weak();
|
|
spawn(async move {
|
|
let result = async {
|
|
let client =
|
|
Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?;
|
|
let configuration = client.configuration();
|
|
let (issue, comments) = tokio::join!(
|
|
apis::issue_api::issue_get_issue(&configuration, &owner, &repository, number),
|
|
apis::issue_api::issue_get_comments(
|
|
&configuration,
|
|
&owner,
|
|
&repository,
|
|
number,
|
|
None,
|
|
None,
|
|
),
|
|
);
|
|
Ok::<_, String>(IssueDetails {
|
|
issue: issue.map_err(|error| error.to_string())?,
|
|
comments: comments.map_err(|error| error.to_string())?,
|
|
})
|
|
}
|
|
.await;
|
|
post(move || {
|
|
let Some(ui) = weak.upgrade() else { return };
|
|
if refreshing {
|
|
ui.set_refreshing(false);
|
|
} else {
|
|
ui.set_loading(false);
|
|
}
|
|
match result {
|
|
Ok(details) => show_issue(&ui, &details),
|
|
Err(error) => ui.set_error(error.into()),
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
fn open_pulls(ui: &AppWindow, state: Arc<Mutex<State>>) {
|
|
open_pulls_with_refresh(ui, state, false);
|
|
}
|
|
|
|
fn open_pulls_with_refresh(ui: &AppWindow, state: Arc<Mutex<State>>, refreshing: bool) {
|
|
let target = {
|
|
let state = state.lock().unwrap();
|
|
state
|
|
.active_server
|
|
.and_then(|index| state.preferences.servers.get(index))
|
|
.cloned()
|
|
.map(|server| (server, state.preferences.pull_status.clone()))
|
|
};
|
|
let Some((server, status)) = target else {
|
|
ui.set_has_active_server(false);
|
|
ui.set_pulls(empty_model());
|
|
return;
|
|
};
|
|
ui.set_has_active_server(true);
|
|
ui.set_page("pulls".into());
|
|
if refreshing {
|
|
ui.set_refreshing(true);
|
|
} else {
|
|
ui.set_loading(true);
|
|
}
|
|
let weak = ui.as_weak();
|
|
spawn(async move {
|
|
let result = load_pulls(&server, &status).await;
|
|
post(move || {
|
|
let Some(ui) = weak.upgrade() else { return };
|
|
if refreshing {
|
|
ui.set_refreshing(false);
|
|
} else {
|
|
ui.set_loading(false);
|
|
}
|
|
match result {
|
|
Ok(pulls) => {
|
|
state.lock().unwrap().pulls = pulls;
|
|
refresh_pulls(&ui, &state.lock().unwrap().pulls);
|
|
}
|
|
Err(error) => ui.set_error(error.into()),
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
fn fetch_pull(
|
|
ui: &AppWindow,
|
|
server: Server,
|
|
owner: String,
|
|
repository: String,
|
|
number: i64,
|
|
refreshing: bool,
|
|
) {
|
|
if refreshing {
|
|
ui.set_refreshing(true);
|
|
} else {
|
|
ui.set_loading(true);
|
|
}
|
|
let weak = ui.as_weak();
|
|
spawn(async move {
|
|
let result = async {
|
|
let client =
|
|
Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?;
|
|
let configuration = client.configuration();
|
|
let (pull, comments, files) = tokio::join!(
|
|
apis::repository_api::repo_get_pull_request(
|
|
&configuration,
|
|
&owner,
|
|
&repository,
|
|
number,
|
|
),
|
|
apis::issue_api::issue_get_comments(
|
|
&configuration,
|
|
&owner,
|
|
&repository,
|
|
number,
|
|
None,
|
|
None,
|
|
),
|
|
load_pull_files(&configuration, &owner, &repository, number,),
|
|
);
|
|
Ok::<_, String>(PullDetails {
|
|
pull: pull.map_err(|error| error.to_string())?,
|
|
comments: comments.map_err(|error| error.to_string())?,
|
|
files: files.map_err(|error| error.to_string())?,
|
|
})
|
|
}
|
|
.await;
|
|
post(move || {
|
|
let Some(ui) = weak.upgrade() else { return };
|
|
if refreshing {
|
|
ui.set_refreshing(false);
|
|
} else {
|
|
ui.set_loading(false);
|
|
}
|
|
match result {
|
|
Ok(details) => show_pull(&ui, &details),
|
|
Err(error) => ui.set_error(error.into()),
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
fn open_commit_history(
|
|
ui: &AppWindow,
|
|
state: Arc<Mutex<State>>,
|
|
owner: String,
|
|
repository: String,
|
|
) {
|
|
let server = {
|
|
let mut state = state.lock().unwrap();
|
|
let default_branch = state
|
|
.repositories
|
|
.iter()
|
|
.find(|candidate| candidate.owner == owner && candidate.name == repository)
|
|
.map(|candidate| candidate.default_branch.clone())
|
|
.unwrap_or_else(|| "main".into());
|
|
state.active_repository = Some((owner.clone(), repository.clone()));
|
|
state.active_branch = Some(default_branch.clone());
|
|
state.branches.clear();
|
|
state.commits.clear();
|
|
ui.set_branch_choices(model(vec!["All".into(), default_branch.into()]));
|
|
ui.set_branch_index(1);
|
|
state
|
|
.active_server
|
|
.and_then(|index| state.preferences.servers.get(index))
|
|
.cloned()
|
|
};
|
|
let Some(server) = server else { return };
|
|
ui.set_repository_title(repository.clone().into());
|
|
ui.set_commits(empty_model());
|
|
ui.set_page("commits".into());
|
|
ui.set_error("".into());
|
|
fetch_commits(ui, state, server, owner, repository, false);
|
|
}
|
|
|
|
fn fetch_commits(
|
|
ui: &AppWindow,
|
|
state: Arc<Mutex<State>>,
|
|
server: Server,
|
|
owner: String,
|
|
repository: String,
|
|
refreshing: bool,
|
|
) {
|
|
if refreshing {
|
|
ui.set_refreshing(true);
|
|
} else {
|
|
ui.set_loading(true);
|
|
}
|
|
let weak = ui.as_weak();
|
|
let (active_branch, branches) = {
|
|
let state = state.lock().unwrap();
|
|
(state.active_branch.clone(), state.branches.clone())
|
|
};
|
|
spawn(async move {
|
|
let requested_branch = active_branch.clone();
|
|
let result = async {
|
|
let default_branch = active_branch
|
|
.as_deref()
|
|
.or(branches.first().map(String::as_str))
|
|
.unwrap_or("main");
|
|
let branches = if branches.is_empty() {
|
|
load_branches(&server, &owner, &repository, default_branch).await?
|
|
} else {
|
|
branches
|
|
};
|
|
let commits = match active_branch.as_deref() {
|
|
Some(branch) => load_branch_commits(&server, &owner, &repository, branch).await?,
|
|
None => load_all_commits(&server, &owner, &repository, &branches).await?,
|
|
};
|
|
Ok::<_, String>((requested_branch, branches, commits))
|
|
}
|
|
.await;
|
|
post(move || {
|
|
let Some(ui) = weak.upgrade() else { return };
|
|
if refreshing {
|
|
ui.set_refreshing(false);
|
|
} else {
|
|
ui.set_loading(false);
|
|
}
|
|
match result {
|
|
Ok((requested_branch, branches, commits)) => {
|
|
let mut state = state.lock().unwrap();
|
|
if state.active_branch != requested_branch {
|
|
return;
|
|
}
|
|
state.branches = branches;
|
|
state.commits = commits;
|
|
let mut choices: Vec<slint::SharedString> = vec!["All".into()];
|
|
choices.extend(state.branches.iter().map(Into::into));
|
|
ui.set_branch_choices(model(choices));
|
|
ui.set_branch_index(
|
|
state
|
|
.active_branch
|
|
.as_ref()
|
|
.and_then(|active| {
|
|
state.branches.iter().position(|branch| branch == active)
|
|
})
|
|
.map_or(0, |index| index as i32 + 1),
|
|
);
|
|
let lane_count = if state.active_branch.is_none() {
|
|
state
|
|
.commits
|
|
.iter()
|
|
.flat_map(|commit| {
|
|
commit
|
|
.top_lanes
|
|
.iter()
|
|
.chain(commit.bottom_lanes.iter())
|
|
.chain(commit.connections.iter())
|
|
.chain(commit.node_lane.iter())
|
|
})
|
|
.max()
|
|
.map_or(0, |lane| lane + 1)
|
|
} else {
|
|
0
|
|
};
|
|
refresh_commits(&ui, &state.commits, lane_count);
|
|
}
|
|
Err(error) => ui.set_error(error.into()),
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
fn fetch_commit(ui: &AppWindow, server: Server, owner: String, repository: String, sha: String) {
|
|
ui.set_loading(true);
|
|
let weak = ui.as_weak();
|
|
spawn(async move {
|
|
let result = async {
|
|
let client =
|
|
Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?;
|
|
apis::repository_api::repo_get_single_commit(
|
|
&client.configuration(),
|
|
&owner,
|
|
&repository,
|
|
&sha,
|
|
Some(true),
|
|
None,
|
|
Some(true),
|
|
)
|
|
.await
|
|
.map_err(|error| error.to_string())
|
|
}
|
|
.await;
|
|
post(move || {
|
|
let Some(ui) = weak.upgrade() else { return };
|
|
ui.set_loading(false);
|
|
match result {
|
|
Ok(commit) => {
|
|
ui.set_files(model(
|
|
commit
|
|
.files
|
|
.unwrap_or_default()
|
|
.into_iter()
|
|
.filter_map(|file| {
|
|
Some(FileRow {
|
|
path: file.filename?.into(),
|
|
status: file.status.unwrap_or_else(|| "modified".into()).into(),
|
|
})
|
|
})
|
|
.collect(),
|
|
));
|
|
ui.set_page("files".into());
|
|
}
|
|
Err(error) => ui.set_error(error.into()),
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
fn fetch_file_diff(
|
|
ui: &AppWindow,
|
|
server: Server,
|
|
owner: String,
|
|
repository: String,
|
|
sha: String,
|
|
path: String,
|
|
) {
|
|
ui.set_loading(true);
|
|
let weak = ui.as_weak();
|
|
spawn(async move {
|
|
let result = async {
|
|
let client =
|
|
Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?;
|
|
apis::repository_api::repo_download_commit_diff_or_patch(
|
|
&client.configuration(),
|
|
&owner,
|
|
&repository,
|
|
&sha,
|
|
"diff",
|
|
)
|
|
.await
|
|
.map(|text| diff::parse_file(&text, &path))
|
|
.map_err(|error| error.to_string())
|
|
}
|
|
.await;
|
|
post(move || {
|
|
let Some(ui) = weak.upgrade() else { return };
|
|
ui.set_loading(false);
|
|
match result {
|
|
Ok(diff) => show_file_diff(&ui, &path, diff),
|
|
Err(error) => ui.set_error(error.into()),
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
fn fetch_pull_file_diff(
|
|
ui: &AppWindow,
|
|
server: Server,
|
|
owner: String,
|
|
repository: String,
|
|
number: i64,
|
|
path: String,
|
|
) {
|
|
ui.set_loading(true);
|
|
let weak = ui.as_weak();
|
|
spawn(async move {
|
|
let result = async {
|
|
let client =
|
|
Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?;
|
|
apis::repository_api::repo_download_pull_diff_or_patch(
|
|
&client.configuration(),
|
|
&owner,
|
|
&repository,
|
|
number,
|
|
"diff",
|
|
Some(false),
|
|
)
|
|
.await
|
|
.map(|text| diff::parse_file(&text, &path))
|
|
.map_err(|error| error.to_string())
|
|
}
|
|
.await;
|
|
post(move || {
|
|
let Some(ui) = weak.upgrade() else { return };
|
|
ui.set_loading(false);
|
|
match result {
|
|
Ok(diff) => show_file_diff(&ui, &path, diff),
|
|
Err(error) => ui.set_error(error.into()),
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
fn show_file_diff(ui: &AppWindow, path: &str, diff: diff::Parsed) {
|
|
ui.set_detail_title(path.rsplit('/').next().unwrap_or(path).into());
|
|
ui.set_diff_columns(diff.columns.min(i32::MAX as usize) as i32);
|
|
ui.set_diff_lines(model(
|
|
diff.lines
|
|
.into_iter()
|
|
.map(|line| DiffLine {
|
|
old_number: line.old_number.into(),
|
|
new_number: line.new_number.into(),
|
|
text: line.text.into(),
|
|
kind: line.kind.into(),
|
|
})
|
|
.collect(),
|
|
));
|
|
ui.set_page("diff".into());
|
|
}
|
|
|
|
fn runtime() -> &'static tokio::runtime::Runtime {
|
|
RUNTIME.get_or_init(|| tokio::runtime::Runtime::new().expect("Tokio runtime"))
|
|
}
|
|
|
|
fn spawn(future: impl Future<Output = ()> + Send + 'static) {
|
|
runtime().spawn(future);
|
|
}
|
|
|
|
fn post(callback: impl FnOnce() + Send + 'static) {
|
|
let _ = slint::invoke_from_event_loop(callback);
|
|
}
|
|
|
|
fn model<T: Clone + 'static>(rows: Vec<T>) -> ModelRc<T> {
|
|
ModelRc::new(VecModel::from(rows))
|
|
}
|
|
|
|
fn empty_model<T: Clone + 'static>() -> ModelRc<T> {
|
|
model(Vec::new())
|
|
}
|