Add configurable iPhone widgets
This commit is contained in:
24
TESTING.md
24
TESTING.md
@@ -230,6 +230,30 @@ answer before uploading it to App Store Connect.
|
|||||||
matching native tab and destination.
|
matching native tab and destination.
|
||||||
- [ ] Non-linkable server activity does not navigate or appear tappable.
|
- [ ] Non-linkable server activity does not navigate or appear tappable.
|
||||||
|
|
||||||
|
## Home-screen widgets
|
||||||
|
|
||||||
|
- [ ] Upgrade over a build with an existing server. Launch Gotcha once, then
|
||||||
|
add both **Recent Activity** and **Open Pull Requests** from the system
|
||||||
|
widget gallery; the saved server and Keychain token work without re-entry.
|
||||||
|
- [ ] Both widgets are offered only in the large system size and use native
|
||||||
|
widget margins, typography, tint, relative update time, and light/dark
|
||||||
|
appearances without clipping at the largest accessibility text size.
|
||||||
|
- [ ] Recent Activity shows up to six latest activity rows for its configured
|
||||||
|
server. Open Pull Requests shows up to five open pull requests. Loading,
|
||||||
|
empty, missing-server, and API-error states remain legible and do not
|
||||||
|
expose account data while the device is locked.
|
||||||
|
- [ ] Add a second server profile, long-press each widget, choose **Edit
|
||||||
|
Widget**, and assign a different server to each. The displayed server
|
||||||
|
name and rows update independently; deleting a selected server changes
|
||||||
|
that widget to its missing-server state instead of showing another
|
||||||
|
server's data.
|
||||||
|
- [ ] Tap Recent Activity; Gotcha opens or foregrounds at the Home root. Tap
|
||||||
|
Open Pull Requests; Gotcha opens or foregrounds at the PR root. Both
|
||||||
|
routes discard a stale detail stack in the destination tab.
|
||||||
|
- [ ] Add, edit, select, and delete server profiles in Gotcha, then return to
|
||||||
|
the Home Screen. Widget configuration choices and timelines refresh to
|
||||||
|
match the persisted server list.
|
||||||
|
|
||||||
## Issues
|
## Issues
|
||||||
|
|
||||||
- [ ] The repository list shows name, description, language, open count, update
|
- [ ] The repository list shows name, description, language, open count, update
|
||||||
|
|||||||
@@ -423,6 +423,13 @@ pub async fn load_home(
|
|||||||
client(server)?.home(page, filter).await.map_err(message)
|
client(server)?.home(page, filter).await.map_err(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn load_activities(server: &Server, page: i32) -> Result<Page<models::Activity>, String> {
|
||||||
|
client(server)?
|
||||||
|
.activities(page, gotcha_gitea::ActivityFilter::All)
|
||||||
|
.await
|
||||||
|
.map_err(message)
|
||||||
|
}
|
||||||
|
|
||||||
fn client(server: &Server) -> Result<Client, String> {
|
fn client(server: &Server) -> Result<Client, String> {
|
||||||
Client::with_provider(&server.url, Some(&server.token), server.provider).map_err(message)
|
Client::with_provider(&server.url, Some(&server.token), server.provider).map_err(message)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,3 +4,4 @@ mod milestones;
|
|||||||
mod pulls;
|
mod pulls;
|
||||||
mod repositories;
|
mod repositories;
|
||||||
mod servers;
|
mod servers;
|
||||||
|
mod widgets;
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ use crate::*;
|
|||||||
#[uniffi::export(async_runtime = "tokio")]
|
#[uniffi::export(async_runtime = "tokio")]
|
||||||
impl GotchaCore {
|
impl GotchaCore {
|
||||||
#[uniffi::constructor]
|
#[uniffi::constructor]
|
||||||
pub fn new() -> Arc<Self> {
|
pub fn new(storage_directory: Option<String>) -> Arc<Self> {
|
||||||
let (preferences, startup_error) = match load_preferences() {
|
let (preferences, startup_error) = match load_preferences(storage_directory.as_deref()) {
|
||||||
Ok(preferences) => (preferences, None),
|
Ok(preferences) => (preferences, None),
|
||||||
Err(error) => (Preferences::default(), Some(error)),
|
Err(error) => (Preferences::default(), Some(error)),
|
||||||
};
|
};
|
||||||
@@ -38,6 +38,7 @@ impl GotchaCore {
|
|||||||
.servers
|
.servers
|
||||||
.iter()
|
.iter()
|
||||||
.map(|server| ServerRow {
|
.map(|server| ServerRow {
|
||||||
|
id: server.credential_account.clone(),
|
||||||
name: server.name.clone(),
|
name: server.name.clone(),
|
||||||
url: server.url.clone(),
|
url: server.url.clone(),
|
||||||
})
|
})
|
||||||
|
|||||||
55
crates/app/src/core/widgets.rs
Normal file
55
crates/app/src/core/widgets.rs
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
use crate::*;
|
||||||
|
|
||||||
|
const MAX_WIDGET_ROWS: u32 = 10;
|
||||||
|
|
||||||
|
#[uniffi::export(async_runtime = "tokio")]
|
||||||
|
impl GotchaCore {
|
||||||
|
pub async fn widget_activity(
|
||||||
|
&self,
|
||||||
|
server_id: String,
|
||||||
|
limit: u32,
|
||||||
|
) -> Result<WidgetActivityPage, GotchaError> {
|
||||||
|
let server = self.server_by_id(&server_id)?;
|
||||||
|
let mut rows = activity_rows(&load_activities(&server, 1).await?.items);
|
||||||
|
rows.truncate(widget_limit(limit)?);
|
||||||
|
Ok(WidgetActivityPage {
|
||||||
|
server_name: server.name,
|
||||||
|
rows,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn widget_pulls(
|
||||||
|
&self,
|
||||||
|
server_id: String,
|
||||||
|
limit: u32,
|
||||||
|
) -> Result<WidgetPullPage, GotchaError> {
|
||||||
|
let server = self.server_by_id(&server_id)?;
|
||||||
|
let pulls = load_pulls(&server, "open", "", "", 1).await?;
|
||||||
|
let mut rows = pull_rows(&pulls.items);
|
||||||
|
rows.truncate(widget_limit(limit)?);
|
||||||
|
Ok(WidgetPullPage {
|
||||||
|
server_name: server.name,
|
||||||
|
rows,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn widget_limit(limit: u32) -> Result<usize, GotchaError> {
|
||||||
|
(1..=MAX_WIDGET_ROWS)
|
||||||
|
.contains(&limit)
|
||||||
|
.then_some(limit as usize)
|
||||||
|
.ok_or_else(|| "Widget row count must be between 1 and 10.".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn validates_widget_row_limits() {
|
||||||
|
assert_eq!(widget_limit(1).unwrap(), 1);
|
||||||
|
assert_eq!(widget_limit(10).unwrap(), 10);
|
||||||
|
assert!(widget_limit(0).is_err());
|
||||||
|
assert!(widget_limit(11).is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
use std::collections::{BTreeMap, BTreeSet};
|
use std::{
|
||||||
|
collections::{BTreeMap, BTreeSet},
|
||||||
|
path::PathBuf,
|
||||||
|
};
|
||||||
|
|
||||||
pub use gotcha_gitea::{
|
pub use gotcha_gitea::{
|
||||||
HistoryCommit, HomeData, IssueDetails, IssueEditorData, MilestoneDetails, Page, PullDetails,
|
HistoryCommit, HomeData, IssueDetails, IssueEditorData, MilestoneDetails, Page, PullDetails,
|
||||||
@@ -49,6 +52,8 @@ pub struct Server {
|
|||||||
|
|
||||||
#[derive(Clone, Deserialize, Serialize)]
|
#[derive(Clone, Deserialize, Serialize)]
|
||||||
pub struct Preferences {
|
pub struct Preferences {
|
||||||
|
#[serde(skip)]
|
||||||
|
pub path: PathBuf,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub servers: Vec<Server>,
|
pub servers: Vec<Server>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -70,6 +75,7 @@ pub struct Preferences {
|
|||||||
impl Default for Preferences {
|
impl Default for Preferences {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
path: PathBuf::new(),
|
||||||
servers: Vec::new(),
|
servers: Vec::new(),
|
||||||
favorites: BTreeSet::new(),
|
favorites: BTreeSet::new(),
|
||||||
last_server: None,
|
last_server: None,
|
||||||
|
|||||||
@@ -117,6 +117,18 @@ impl GotchaCore {
|
|||||||
.ok_or_else(|| "Select a server first.".to_string().into())
|
.ok_or_else(|| "Select a server first.".to_string().into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn server_by_id(&self, id: &str) -> Result<Server, GotchaError> {
|
||||||
|
self.state
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.preferences
|
||||||
|
.servers
|
||||||
|
.iter()
|
||||||
|
.find(|server| server.credential_account == id)
|
||||||
|
.cloned()
|
||||||
|
.ok_or_else(|| "That server is no longer configured.".into())
|
||||||
|
}
|
||||||
|
|
||||||
fn repository_rows(&self, state: &State, pane: RepositoryPane) -> Vec<RepositoryRow> {
|
fn repository_rows(&self, state: &State, pane: RepositoryPane) -> Vec<RepositoryRow> {
|
||||||
let server_url = state
|
let server_url = state
|
||||||
.active_server
|
.active_server
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ pub enum WorkItemState {
|
|||||||
|
|
||||||
#[derive(Clone, uniffi::Record)]
|
#[derive(Clone, uniffi::Record)]
|
||||||
pub struct ServerRow {
|
pub struct ServerRow {
|
||||||
|
pub id: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub url: String,
|
pub url: String,
|
||||||
}
|
}
|
||||||
@@ -358,6 +359,18 @@ pub struct HomePage {
|
|||||||
pub next_page: Option<u32>,
|
pub next_page: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, uniffi::Record)]
|
||||||
|
pub struct WidgetActivityPage {
|
||||||
|
pub server_name: String,
|
||||||
|
pub rows: Vec<ActivityRow>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, uniffi::Record)]
|
||||||
|
pub struct WidgetPullPage {
|
||||||
|
pub server_name: String,
|
||||||
|
pub rows: Vec<PullRow>,
|
||||||
|
}
|
||||||
|
|
||||||
mod details;
|
mod details;
|
||||||
mod files;
|
mod files;
|
||||||
mod helpers;
|
mod helpers;
|
||||||
|
|||||||
@@ -4,13 +4,17 @@ pub fn home_page(server_name: String, home: HomeData) -> HomePage {
|
|||||||
let (heat_cells, contribution_count) = heat_cells(&home.heatmap);
|
let (heat_cells, contribution_count) = heat_cells(&home.heatmap);
|
||||||
HomePage {
|
HomePage {
|
||||||
server_name,
|
server_name,
|
||||||
activities: home.activities.iter().map(activity_row).collect(),
|
activities: activity_rows(&home.activities),
|
||||||
heat_cells,
|
heat_cells,
|
||||||
contribution_count,
|
contribution_count,
|
||||||
next_page: home.next_page.map(|page| page as u32),
|
next_page: home.next_page.map(|page| page as u32),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn activity_rows(activities: &[models::Activity]) -> Vec<ActivityRow> {
|
||||||
|
activities.iter().map(activity_row).collect()
|
||||||
|
}
|
||||||
|
|
||||||
fn activity_row(activity: &models::Activity) -> ActivityRow {
|
fn activity_row(activity: &models::Activity) -> ActivityRow {
|
||||||
use models::activity::OpType;
|
use models::activity::OpType;
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ use std::{
|
|||||||
|
|
||||||
use gotcha_gitea::{Client, Provider};
|
use gotcha_gitea::{Client, Provider};
|
||||||
use security_framework::passwords::{
|
use security_framework::passwords::{
|
||||||
delete_generic_password, get_generic_password, set_generic_password,
|
PasswordOptions, delete_generic_password_options, generic_password, get_generic_password,
|
||||||
|
set_generic_password_options,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -52,14 +53,27 @@ pub fn validate_server(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_preferences() -> Result<Preferences, String> {
|
const APP_GROUP: &str = "group.de.rfc1437.gotcha";
|
||||||
let path = preferences_path()?;
|
const KEYCHAIN_SERVICE: &str = "de.rfc1437.gotcha";
|
||||||
if !path.exists() {
|
|
||||||
return Ok(Preferences::default());
|
pub fn load_preferences(storage_directory: Option<&str>) -> Result<Preferences, String> {
|
||||||
}
|
let path = preferences_path(storage_directory)?;
|
||||||
|
let legacy = preferences_path(None)?;
|
||||||
|
let source = if path.exists() {
|
||||||
|
path.clone()
|
||||||
|
} else if path != legacy && legacy.exists() {
|
||||||
|
legacy
|
||||||
|
} else {
|
||||||
|
return Ok(Preferences {
|
||||||
|
path,
|
||||||
|
..Preferences::default()
|
||||||
|
});
|
||||||
|
};
|
||||||
let mut preferences: Preferences =
|
let mut preferences: Preferences =
|
||||||
serde_json::from_slice(&fs::read(&path).map_err(|error| error.to_string())?)
|
serde_json::from_slice(&fs::read(&source).map_err(|error| error.to_string())?)
|
||||||
.map_err(|error| format!("Cannot read {}: {error}", path.display()))?;
|
.map_err(|error| format!("Cannot read {}: {error}", source.display()))?;
|
||||||
|
preferences.path = path;
|
||||||
|
let storage_migrated = source != preferences.path;
|
||||||
let favorites_migrated = migrate_favorites(&mut preferences.favorites);
|
let favorites_migrated = migrate_favorites(&mut preferences.favorites);
|
||||||
if !matches!(preferences.issue_status.as_str(), "open" | "closed") {
|
if !matches!(preferences.issue_status.as_str(), "open" | "closed") {
|
||||||
preferences.issue_status = open_status();
|
preferences.issue_status = open_status();
|
||||||
@@ -73,13 +87,10 @@ pub fn load_preferences() -> Result<Preferences, String> {
|
|||||||
server.credential_account = format!("{}|{}", server.name, server.url);
|
server.credential_account = format!("{}|{}", server.name, server.url);
|
||||||
credentials_migrated = true;
|
credentials_migrated = true;
|
||||||
}
|
}
|
||||||
server.token = String::from_utf8(
|
server.token = String::from_utf8(load_server_token(server)?)
|
||||||
get_generic_password("de.rfc1437.gotcha", &keychain_account(server))
|
|
||||||
.map_err(|error| format!("Cannot read the token for {}: {error}", server.name))?,
|
|
||||||
)
|
|
||||||
.map_err(|_| format!("The token for {} is not valid text.", server.name))?;
|
.map_err(|_| format!("The token for {} is not valid text.", server.name))?;
|
||||||
}
|
}
|
||||||
if favorites_migrated || credentials_migrated {
|
if storage_migrated || favorites_migrated || credentials_migrated {
|
||||||
save_preferences(&preferences)?;
|
save_preferences(&preferences)?;
|
||||||
}
|
}
|
||||||
Ok(preferences)
|
Ok(preferences)
|
||||||
@@ -103,7 +114,11 @@ fn migrate_favorites(favorites: &mut std::collections::BTreeSet<String>) -> bool
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn save_preferences(preferences: &Preferences) -> Result<(), String> {
|
pub fn save_preferences(preferences: &Preferences) -> Result<(), String> {
|
||||||
let path = preferences_path()?;
|
let path = if preferences.path.as_os_str().is_empty() {
|
||||||
|
preferences_path(None)?
|
||||||
|
} else {
|
||||||
|
preferences.path.clone()
|
||||||
|
};
|
||||||
let parent = path.parent().ok_or("Invalid app data directory.")?;
|
let parent = path.parent().ok_or("Invalid app data directory.")?;
|
||||||
fs::create_dir_all(parent).map_err(|error| error.to_string())?;
|
fs::create_dir_all(parent).map_err(|error| error.to_string())?;
|
||||||
let temporary = path.with_extension("tmp");
|
let temporary = path.with_extension("tmp");
|
||||||
@@ -116,16 +131,12 @@ pub fn save_preferences(preferences: &Preferences) -> Result<(), String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn save_server_token(server: &Server) -> Result<(), String> {
|
pub fn save_server_token(server: &Server) -> Result<(), String> {
|
||||||
set_generic_password(
|
set_generic_password_options(server.token.as_bytes(), shared_password_options(server))
|
||||||
"de.rfc1437.gotcha",
|
|
||||||
&keychain_account(server),
|
|
||||||
server.token.as_bytes(),
|
|
||||||
)
|
|
||||||
.map_err(|error| format!("Cannot save the token for {}: {error}", server.name))
|
.map_err(|error| format!("Cannot save the token for {}: {error}", server.name))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete_server_token(server: &Server) -> Result<(), String> {
|
pub fn delete_server_token(server: &Server) -> Result<(), String> {
|
||||||
match delete_generic_password("de.rfc1437.gotcha", &keychain_account(server)) {
|
match delete_generic_password_options(shared_password_options(server)) {
|
||||||
Ok(()) => Ok(()),
|
Ok(()) => Ok(()),
|
||||||
// A missing Keychain item must not make an otherwise valid profile undeletable.
|
// A missing Keychain item must not make an otherwise valid profile undeletable.
|
||||||
Err(error) if error.code() == -25300 => Ok(()), // errSecItemNotFound
|
Err(error) if error.code() == -25300 => Ok(()), // errSecItemNotFound
|
||||||
@@ -161,11 +172,42 @@ pub fn assign_server_credential_account(server: &mut Server, existing: &[Server]
|
|||||||
.expect("a unique Keychain account must exist");
|
.expect("a unique Keychain account must exist");
|
||||||
}
|
}
|
||||||
|
|
||||||
fn preferences_path() -> Result<PathBuf, String> {
|
fn preferences_path(storage_directory: Option<&str>) -> Result<PathBuf, String> {
|
||||||
|
if let Some(directory) = storage_directory {
|
||||||
|
let directory = directory.trim();
|
||||||
|
if directory.is_empty() {
|
||||||
|
return Err("Invalid shared app data directory.".into());
|
||||||
|
}
|
||||||
|
return Ok(PathBuf::from(directory).join("preferences.json"));
|
||||||
|
}
|
||||||
let home = env::var_os("HOME").ok_or("Cannot find the app data directory.")?;
|
let home = env::var_os("HOME").ok_or("Cannot find the app data directory.")?;
|
||||||
Ok(PathBuf::from(home).join("Library/Application Support/Gotcha/preferences.json"))
|
Ok(PathBuf::from(home).join("Library/Application Support/Gotcha/preferences.json"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn load_server_token(server: &Server) -> Result<Vec<u8>, String> {
|
||||||
|
match generic_password(shared_password_options(server)) {
|
||||||
|
Ok(token) => Ok(token),
|
||||||
|
Err(error) if error.code() == -25300 => {
|
||||||
|
let token = get_generic_password(KEYCHAIN_SERVICE, &keychain_account(server))
|
||||||
|
.map_err(|error| format!("Cannot read the token for {}: {error}", server.name))?;
|
||||||
|
set_generic_password_options(&token, shared_password_options(server))
|
||||||
|
.map_err(|error| format!("Cannot share the token for {}: {error}", server.name))?;
|
||||||
|
Ok(token)
|
||||||
|
}
|
||||||
|
Err(error) => Err(format!(
|
||||||
|
"Cannot read the token for {}: {error}",
|
||||||
|
server.name
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shared_password_options(server: &Server) -> PasswordOptions {
|
||||||
|
let mut options =
|
||||||
|
PasswordOptions::new_generic_password(KEYCHAIN_SERVICE, &keychain_account(server));
|
||||||
|
options.set_access_group(APP_GROUP);
|
||||||
|
options
|
||||||
|
}
|
||||||
|
|
||||||
fn keychain_account(server: &Server) -> String {
|
fn keychain_account(server: &Server) -> String {
|
||||||
if server.credential_account.is_empty() {
|
if server.credential_account.is_empty() {
|
||||||
format!("{}|{}", server.name, server.url)
|
format!("{}|{}", server.name, server.url)
|
||||||
@@ -271,4 +313,35 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert!(!migrate_favorites(&mut favorites));
|
assert!(!migrate_favorites(&mut favorites));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn saves_preferences_to_the_selected_storage_directory() {
|
||||||
|
let directory = env::temp_dir().join(format!(
|
||||||
|
"gotcha-preferences-{}-{}",
|
||||||
|
process::id(),
|
||||||
|
SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_nanos()
|
||||||
|
));
|
||||||
|
let path = directory.join("preferences.json");
|
||||||
|
let preferences = Preferences {
|
||||||
|
path: path.clone(),
|
||||||
|
pull_status: "closed".into(),
|
||||||
|
..Preferences::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
save_preferences(&preferences).unwrap();
|
||||||
|
let stored = fs::read_to_string(&path).unwrap();
|
||||||
|
assert!(!stored.contains(path.to_string_lossy().as_ref()));
|
||||||
|
assert_eq!(
|
||||||
|
serde_json::from_str::<Preferences>(&stored)
|
||||||
|
.unwrap()
|
||||||
|
.pull_status,
|
||||||
|
"closed"
|
||||||
|
);
|
||||||
|
|
||||||
|
fs::remove_file(path).unwrap();
|
||||||
|
fs::remove_dir(directory).unwrap();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
Client, Error, Result,
|
Client, Error, Result,
|
||||||
domain::{DEFAULT_PAGE_SIZE, HomeData},
|
domain::{DEFAULT_PAGE_SIZE, HomeData, Page},
|
||||||
models,
|
models,
|
||||||
};
|
};
|
||||||
use gitea_openapi::apis;
|
use gitea_openapi::apis;
|
||||||
@@ -71,6 +71,23 @@ pub enum Target {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Client {
|
impl Client {
|
||||||
|
pub async fn activities(
|
||||||
|
&self,
|
||||||
|
page: i32,
|
||||||
|
filter: ActivityFilter,
|
||||||
|
) -> Result<Page<models::Activity>> {
|
||||||
|
if page < 1 {
|
||||||
|
return Err(Error::InvalidInput("page must be positive".into()));
|
||||||
|
}
|
||||||
|
let configuration = self.configuration();
|
||||||
|
let login = self
|
||||||
|
.current_user()
|
||||||
|
.await?
|
||||||
|
.login
|
||||||
|
.ok_or_else(|| Error::Generated("The server account has no username.".into()))?;
|
||||||
|
filtered_activity_page(&configuration, &login, page, filter).await
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn home(&self, page: i32, filter: ActivityFilter) -> Result<HomeData> {
|
pub async fn home(&self, page: i32, filter: ActivityFilter) -> Result<HomeData> {
|
||||||
if page < 1 {
|
if page < 1 {
|
||||||
return Err(Error::InvalidInput("page must be positive".into()));
|
return Err(Error::InvalidInput("page must be positive".into()));
|
||||||
@@ -81,14 +98,26 @@ impl Client {
|
|||||||
.await?
|
.await?
|
||||||
.login
|
.login
|
||||||
.ok_or_else(|| Error::Generated("The server account has no username.".into()))?;
|
.ok_or_else(|| Error::Generated("The server account has no username.".into()))?;
|
||||||
let activities = activity_page(&configuration, &login, page);
|
|
||||||
let (activities, heatmap) = tokio::join!(
|
let (activities, heatmap) = tokio::join!(
|
||||||
activities,
|
filtered_activity_page(&configuration, &login, page, filter),
|
||||||
apis::user_api::user_get_heatmap_data(&configuration, &login),
|
apis::user_api::user_get_heatmap_data(&configuration, &login),
|
||||||
);
|
);
|
||||||
let mut activities = activities?;
|
let activities = activities?;
|
||||||
let heatmap = heatmap.map_err(Error::generated)?;
|
Ok(HomeData {
|
||||||
let mut page = page;
|
activities: activities.items,
|
||||||
|
heatmap: heatmap.map_err(Error::generated)?,
|
||||||
|
next_page: activities.has_more.then_some(page + 1),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn filtered_activity_page(
|
||||||
|
configuration: &apis::configuration::Configuration,
|
||||||
|
login: &str,
|
||||||
|
mut page: i32,
|
||||||
|
filter: ActivityFilter,
|
||||||
|
) -> Result<Page<models::Activity>> {
|
||||||
|
let mut activities = activity_page(configuration, login, page).await?;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let has_more = activities.len() == DEFAULT_PAGE_SIZE as usize;
|
let has_more = activities.len() == DEFAULT_PAGE_SIZE as usize;
|
||||||
@@ -97,15 +126,13 @@ impl Client {
|
|||||||
.filter(|activity| filter.matches(activity))
|
.filter(|activity| filter.matches(activity))
|
||||||
.collect();
|
.collect();
|
||||||
if filter == ActivityFilter::All || !filtered.is_empty() || !has_more {
|
if filter == ActivityFilter::All || !filtered.is_empty() || !has_more {
|
||||||
return Ok(HomeData {
|
return Ok(Page {
|
||||||
activities: filtered,
|
items: filtered,
|
||||||
heatmap,
|
has_more,
|
||||||
next_page: has_more.then_some(page + 1),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
page += 1;
|
page += 1;
|
||||||
activities = activity_page(&configuration, &login, page).await?;
|
activities = activity_page(configuration, login, page).await?;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -693,6 +693,10 @@ public protocol GotchaCoreProtocol: AnyObject, Sendable {
|
|||||||
|
|
||||||
func updateServer(index: UInt32, name: String, url: String, token: String, provider: ServerProvider) async throws
|
func updateServer(index: UInt32, name: String, url: String, token: String, provider: ServerProvider) async throws
|
||||||
|
|
||||||
|
func widgetActivity(serverId: String, limit: UInt32) async throws -> WidgetActivityPage
|
||||||
|
|
||||||
|
func widgetPulls(serverId: String, limit: UInt32) async throws -> WidgetPullPage
|
||||||
|
|
||||||
}
|
}
|
||||||
open class GotchaCore: GotchaCoreProtocol, @unchecked Sendable {
|
open class GotchaCore: GotchaCoreProtocol, @unchecked Sendable {
|
||||||
fileprivate let handle: UInt64
|
fileprivate let handle: UInt64
|
||||||
@@ -733,11 +737,12 @@ open class GotchaCore: GotchaCoreProtocol, @unchecked Sendable {
|
|||||||
public func uniffiCloneHandle() -> UInt64 {
|
public func uniffiCloneHandle() -> UInt64 {
|
||||||
return try! rustCall { uniffi_gotcha_core_fn_clone_gotchacore(self.handle, $0) }
|
return try! rustCall { uniffi_gotcha_core_fn_clone_gotchacore(self.handle, $0) }
|
||||||
}
|
}
|
||||||
public convenience init() {
|
public convenience init(storageDirectory: String?) {
|
||||||
let handle =
|
let handle =
|
||||||
try! rustCall() {
|
try! rustCall() {
|
||||||
uniffiCallStatus in
|
uniffiCallStatus in
|
||||||
uniffi_gotcha_core_fn_constructor_gotchacore_new(uniffiCallStatus
|
uniffi_gotcha_core_fn_constructor_gotchacore_new(
|
||||||
|
FfiConverterOptionString.lower(storageDirectory),uniffiCallStatus
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
self.init(unsafeFromHandle: handle)
|
self.init(unsafeFromHandle: handle)
|
||||||
@@ -1367,6 +1372,38 @@ open func updateServer(index: UInt32, name: String, url: String, token: String,
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
open func widgetActivity(serverId: String, limit: UInt32)async throws -> WidgetActivityPage {
|
||||||
|
return
|
||||||
|
try await uniffiRustCallAsync(
|
||||||
|
rustFutureFunc: {
|
||||||
|
uniffi_gotcha_core_fn_method_gotchacore_widget_activity(
|
||||||
|
self.uniffiCloneHandle(),FfiConverterString.lower(serverId),FfiConverterUInt32.lower(limit)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
pollFunc: ffi_gotcha_core_rust_future_poll_rust_buffer,
|
||||||
|
completeFunc: ffi_gotcha_core_rust_future_complete_rust_buffer,
|
||||||
|
freeFunc: ffi_gotcha_core_rust_future_free_rust_buffer,
|
||||||
|
liftFunc: FfiConverterTypeWidgetActivityPage_lift,
|
||||||
|
errorHandler: FfiConverterTypeGotchaError_lift
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
open func widgetPulls(serverId: String, limit: UInt32)async throws -> WidgetPullPage {
|
||||||
|
return
|
||||||
|
try await uniffiRustCallAsync(
|
||||||
|
rustFutureFunc: {
|
||||||
|
uniffi_gotcha_core_fn_method_gotchacore_widget_pulls(
|
||||||
|
self.uniffiCloneHandle(),FfiConverterString.lower(serverId),FfiConverterUInt32.lower(limit)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
pollFunc: ffi_gotcha_core_rust_future_poll_rust_buffer,
|
||||||
|
completeFunc: ffi_gotcha_core_rust_future_complete_rust_buffer,
|
||||||
|
freeFunc: ffi_gotcha_core_rust_future_free_rust_buffer,
|
||||||
|
liftFunc: FfiConverterTypeWidgetPullPage_lift,
|
||||||
|
errorHandler: FfiConverterTypeGotchaError_lift
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -3475,6 +3512,10 @@ public struct ServerEditor: Equatable, Hashable {
|
|||||||
self.url = url
|
self.url = url
|
||||||
self.provider = provider
|
self.provider = provider
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#if compiler(>=6)
|
#if compiler(>=6)
|
||||||
@@ -3501,6 +3542,7 @@ public struct FfiConverterTypeServerEditor: FfiConverterRustBuffer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#if swift(>=5.8)
|
#if swift(>=5.8)
|
||||||
@_documentation(visibility: private)
|
@_documentation(visibility: private)
|
||||||
#endif
|
#endif
|
||||||
@@ -3515,13 +3557,16 @@ public func FfiConverterTypeServerEditor_lower(_ value: ServerEditor) -> RustBuf
|
|||||||
return FfiConverterTypeServerEditor.lower(value)
|
return FfiConverterTypeServerEditor.lower(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public struct ServerRow: Equatable, Hashable {
|
public struct ServerRow: Equatable, Hashable {
|
||||||
|
public var id: String
|
||||||
public var name: String
|
public var name: String
|
||||||
public var url: String
|
public var url: String
|
||||||
|
|
||||||
// Default memberwise initializers are never public by default, so we
|
// Default memberwise initializers are never public by default, so we
|
||||||
// declare one manually.
|
// declare one manually.
|
||||||
public init(name: String, url: String) {
|
public init(id: String, name: String, url: String) {
|
||||||
|
self.id = id
|
||||||
self.name = name
|
self.name = name
|
||||||
self.url = url
|
self.url = url
|
||||||
}
|
}
|
||||||
@@ -3542,12 +3587,14 @@ public struct FfiConverterTypeServerRow: FfiConverterRustBuffer {
|
|||||||
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> ServerRow {
|
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> ServerRow {
|
||||||
return
|
return
|
||||||
try ServerRow(
|
try ServerRow(
|
||||||
|
id: FfiConverterString.read(from: &buf),
|
||||||
name: FfiConverterString.read(from: &buf),
|
name: FfiConverterString.read(from: &buf),
|
||||||
url: FfiConverterString.read(from: &buf)
|
url: FfiConverterString.read(from: &buf)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func write(_ value: ServerRow, into buf: inout [UInt8]) {
|
public static func write(_ value: ServerRow, into buf: inout [UInt8]) {
|
||||||
|
FfiConverterString.write(value.id, into: &buf)
|
||||||
FfiConverterString.write(value.name, into: &buf)
|
FfiConverterString.write(value.name, into: &buf)
|
||||||
FfiConverterString.write(value.url, into: &buf)
|
FfiConverterString.write(value.url, into: &buf)
|
||||||
}
|
}
|
||||||
@@ -3627,6 +3674,114 @@ public func FfiConverterTypeSettings_lower(_ value: Settings) -> RustBuffer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public struct WidgetActivityPage: Equatable, Hashable {
|
||||||
|
public var serverName: String
|
||||||
|
public var rows: [ActivityRow]
|
||||||
|
|
||||||
|
// Default memberwise initializers are never public by default, so we
|
||||||
|
// declare one manually.
|
||||||
|
public init(serverName: String, rows: [ActivityRow]) {
|
||||||
|
self.serverName = serverName
|
||||||
|
self.rows = rows
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#if compiler(>=6)
|
||||||
|
extension WidgetActivityPage: Sendable {}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public struct FfiConverterTypeWidgetActivityPage: FfiConverterRustBuffer {
|
||||||
|
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> WidgetActivityPage {
|
||||||
|
return
|
||||||
|
try WidgetActivityPage(
|
||||||
|
serverName: FfiConverterString.read(from: &buf),
|
||||||
|
rows: FfiConverterSequenceTypeActivityRow.read(from: &buf)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func write(_ value: WidgetActivityPage, into buf: inout [UInt8]) {
|
||||||
|
FfiConverterString.write(value.serverName, into: &buf)
|
||||||
|
FfiConverterSequenceTypeActivityRow.write(value.rows, into: &buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public func FfiConverterTypeWidgetActivityPage_lift(_ buf: RustBuffer) throws -> WidgetActivityPage {
|
||||||
|
return try FfiConverterTypeWidgetActivityPage.lift(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public func FfiConverterTypeWidgetActivityPage_lower(_ value: WidgetActivityPage) -> RustBuffer {
|
||||||
|
return FfiConverterTypeWidgetActivityPage.lower(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public struct WidgetPullPage: Equatable, Hashable {
|
||||||
|
public var serverName: String
|
||||||
|
public var rows: [PullRow]
|
||||||
|
|
||||||
|
// Default memberwise initializers are never public by default, so we
|
||||||
|
// declare one manually.
|
||||||
|
public init(serverName: String, rows: [PullRow]) {
|
||||||
|
self.serverName = serverName
|
||||||
|
self.rows = rows
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#if compiler(>=6)
|
||||||
|
extension WidgetPullPage: Sendable {}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public struct FfiConverterTypeWidgetPullPage: FfiConverterRustBuffer {
|
||||||
|
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> WidgetPullPage {
|
||||||
|
return
|
||||||
|
try WidgetPullPage(
|
||||||
|
serverName: FfiConverterString.read(from: &buf),
|
||||||
|
rows: FfiConverterSequenceTypePullRow.read(from: &buf)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func write(_ value: WidgetPullPage, into buf: inout [UInt8]) {
|
||||||
|
FfiConverterString.write(value.serverName, into: &buf)
|
||||||
|
FfiConverterSequenceTypePullRow.write(value.rows, into: &buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public func FfiConverterTypeWidgetPullPage_lift(_ buf: RustBuffer) throws -> WidgetPullPage {
|
||||||
|
return try FfiConverterTypeWidgetPullPage.lift(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public func FfiConverterTypeWidgetPullPage_lower(_ value: WidgetPullPage) -> RustBuffer {
|
||||||
|
return FfiConverterTypeWidgetPullPage.lower(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public enum ActivityIcon: Equatable, Hashable {
|
public enum ActivityIcon: Equatable, Hashable {
|
||||||
|
|
||||||
@@ -5145,7 +5300,13 @@ private let initializationResult: InitializationResult = {
|
|||||||
if (uniffi_gotcha_core_checksum_method_gotchacore_update_server() != 31001) {
|
if (uniffi_gotcha_core_checksum_method_gotchacore_update_server() != 31001) {
|
||||||
return InitializationResult.apiChecksumMismatch
|
return InitializationResult.apiChecksumMismatch
|
||||||
}
|
}
|
||||||
if (uniffi_gotcha_core_checksum_constructor_gotchacore_new() != 35044) {
|
if (uniffi_gotcha_core_checksum_method_gotchacore_widget_activity() != 29431) {
|
||||||
|
return InitializationResult.apiChecksumMismatch
|
||||||
|
}
|
||||||
|
if (uniffi_gotcha_core_checksum_method_gotchacore_widget_pulls() != 23227) {
|
||||||
|
return InitializationResult.apiChecksumMismatch
|
||||||
|
}
|
||||||
|
if (uniffi_gotcha_core_checksum_constructor_gotchacore_new() != 64965) {
|
||||||
return InitializationResult.apiChecksumMismatch
|
return InitializationResult.apiChecksumMismatch
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -255,8 +255,7 @@ void uniffi_gotcha_core_fn_free_gotchacore(uint64_t handle, RustCallStatus *_Non
|
|||||||
#endif
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_CONSTRUCTOR_GOTCHACORE_NEW
|
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_CONSTRUCTOR_GOTCHACORE_NEW
|
||||||
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_CONSTRUCTOR_GOTCHACORE_NEW
|
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_CONSTRUCTOR_GOTCHACORE_NEW
|
||||||
uint64_t uniffi_gotcha_core_fn_constructor_gotchacore_new(RustCallStatus *_Nonnull out_status
|
uint64_t uniffi_gotcha_core_fn_constructor_gotchacore_new(RustBuffer storage_directory, RustCallStatus *_Nonnull out_status
|
||||||
|
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_COMMIT_DETAILS
|
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_COMMIT_DETAILS
|
||||||
@@ -484,6 +483,16 @@ RustBuffer uniffi_gotcha_core_fn_method_gotchacore_startup_error(uint64_t ptr, R
|
|||||||
uint64_t uniffi_gotcha_core_fn_method_gotchacore_update_server(uint64_t ptr, uint32_t index, RustBuffer name, RustBuffer url, RustBuffer token, RustBuffer provider
|
uint64_t uniffi_gotcha_core_fn_method_gotchacore_update_server(uint64_t ptr, uint32_t index, RustBuffer name, RustBuffer url, RustBuffer token, RustBuffer provider
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_WIDGET_ACTIVITY
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_WIDGET_ACTIVITY
|
||||||
|
uint64_t uniffi_gotcha_core_fn_method_gotchacore_widget_activity(uint64_t ptr, RustBuffer server_id, uint32_t limit
|
||||||
|
);
|
||||||
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_WIDGET_PULLS
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_WIDGET_PULLS
|
||||||
|
uint64_t uniffi_gotcha_core_fn_method_gotchacore_widget_pulls(uint64_t ptr, RustBuffer server_id, uint32_t limit
|
||||||
|
);
|
||||||
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUSTBUFFER_ALLOC
|
#ifndef UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUSTBUFFER_ALLOC
|
||||||
#define UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUSTBUFFER_ALLOC
|
#define UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUSTBUFFER_ALLOC
|
||||||
RustBuffer ffi_gotcha_core_rustbuffer_alloc(uint64_t size, RustCallStatus *_Nonnull out_status
|
RustBuffer ffi_gotcha_core_rustbuffer_alloc(uint64_t size, RustCallStatus *_Nonnull out_status
|
||||||
@@ -1012,6 +1021,18 @@ uint16_t uniffi_gotcha_core_checksum_method_gotchacore_startup_error(void
|
|||||||
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_UPDATE_SERVER
|
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_UPDATE_SERVER
|
||||||
uint16_t uniffi_gotcha_core_checksum_method_gotchacore_update_server(void
|
uint16_t uniffi_gotcha_core_checksum_method_gotchacore_update_server(void
|
||||||
|
|
||||||
|
);
|
||||||
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_WIDGET_ACTIVITY
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_WIDGET_ACTIVITY
|
||||||
|
uint16_t uniffi_gotcha_core_checksum_method_gotchacore_widget_activity(void
|
||||||
|
|
||||||
|
);
|
||||||
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_WIDGET_PULLS
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_WIDGET_PULLS
|
||||||
|
uint16_t uniffi_gotcha_core_checksum_method_gotchacore_widget_pulls(void
|
||||||
|
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_CONSTRUCTOR_GOTCHACORE_NEW
|
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_CONSTRUCTOR_GOTCHACORE_NEW
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
|
<key>com.apple.security.application-groups</key>
|
||||||
|
<array>
|
||||||
|
<string>group.de.rfc1437.gotcha</string>
|
||||||
|
</array>
|
||||||
<key>keychain-access-groups</key>
|
<key>keychain-access-groups</key>
|
||||||
<array>
|
<array>
|
||||||
<string>$(AppIdentifierPrefix)$(CFBundleIdentifier)</string>
|
<string>$(AppIdentifierPrefix)$(CFBundleIdentifier)</string>
|
||||||
|
|||||||
@@ -14,14 +14,19 @@
|
|||||||
267E72DD7E12E3082974337E /* IssueActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17264C5C909501D8ADEC00B7 /* IssueActions.swift */; };
|
267E72DD7E12E3082974337E /* IssueActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17264C5C909501D8ADEC00B7 /* IssueActions.swift */; };
|
||||||
33D3E65C9E50522B2039816A /* PullScreens.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99B8279189276A084B69D7D0 /* PullScreens.swift */; };
|
33D3E65C9E50522B2039816A /* PullScreens.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99B8279189276A084B69D7D0 /* PullScreens.swift */; };
|
||||||
381A27D70EA30C1A3BA1BBC1 /* CommentEditorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F9FFCC06E0313760524EBD2 /* CommentEditorViewController.swift */; };
|
381A27D70EA30C1A3BA1BBC1 /* CommentEditorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F9FFCC06E0313760524EBD2 /* CommentEditorViewController.swift */; };
|
||||||
|
39C36B0D5FB260C920D466DC /* GotchaWidgets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5121A6F14A9C9F6EA144BB2F /* GotchaWidgets.swift */; };
|
||||||
4652515AE4CB10963D995143 /* CommitScreens.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7588A44A93B8DB4B78C3B2BB /* CommitScreens.swift */; };
|
4652515AE4CB10963D995143 /* CommitScreens.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7588A44A93B8DB4B78C3B2BB /* CommitScreens.swift */; };
|
||||||
|
74626C144E9214BF89F821D2 /* WidgetIntents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77AE1D594C38D2BDAA2E86AD /* WidgetIntents.swift */; };
|
||||||
|
7A9D1ADD6623C89A7D5E016F /* GotchaWidgets.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 52ABD1B07CEEF00263038653 /* GotchaWidgets.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||||
7BBE64F66374221F1743BC24 /* IssueScreens.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACD6449327E1A51FCD8E3BD4 /* IssueScreens.swift */; };
|
7BBE64F66374221F1743BC24 /* IssueScreens.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACD6449327E1A51FCD8E3BD4 /* IssueScreens.swift */; };
|
||||||
7DD332583B169B3CA1CA46E0 /* Highlighter in Frameworks */ = {isa = PBXBuildFile; productRef = EC5F999F50905E3801E8A71A /* Highlighter */; };
|
7DD332583B169B3CA1CA46E0 /* Highlighter in Frameworks */ = {isa = PBXBuildFile; productRef = EC5F999F50905E3801E8A71A /* Highlighter */; };
|
||||||
8A42B4319AF343566D77D12A /* WorkItemDetailScreens.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BB99EB32ECA1F8C55293EE3 /* WorkItemDetailScreens.swift */; };
|
8A42B4319AF343566D77D12A /* WorkItemDetailScreens.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BB99EB32ECA1F8C55293EE3 /* WorkItemDetailScreens.swift */; };
|
||||||
|
8BDF1E1259A237221FDDB822 /* WidgetIntents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77AE1D594C38D2BDAA2E86AD /* WidgetIntents.swift */; };
|
||||||
950C584D58E80106DF350A22 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C0921C76676A14A024BA417 /* AppDelegate.swift */; };
|
950C584D58E80106DF350A22 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C0921C76676A14A024BA417 /* AppDelegate.swift */; };
|
||||||
96C4AFC206A1DBAE3D3E00CE /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F145F13B8ED83A5AB0009A4 /* SettingsViewController.swift */; };
|
96C4AFC206A1DBAE3D3E00CE /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F145F13B8ED83A5AB0009A4 /* SettingsViewController.swift */; };
|
||||||
9B2206DF1263080B9B25B82C /* ServerScreens.swift in Sources */ = {isa = PBXBuildFile; fileRef = C13A39F3C39C1F353D58C307 /* ServerScreens.swift */; };
|
9B2206DF1263080B9B25B82C /* ServerScreens.swift in Sources */ = {isa = PBXBuildFile; fileRef = C13A39F3C39C1F353D58C307 /* ServerScreens.swift */; };
|
||||||
A2AF2C1F3C8B0716E1EF36B2 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = F121BE52F7C9C8780341F988 /* PrivacyInfo.xcprivacy */; };
|
A2AF2C1F3C8B0716E1EF36B2 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = F121BE52F7C9C8780341F988 /* PrivacyInfo.xcprivacy */; };
|
||||||
|
BDFE5D7A54541A109B35BF45 /* gotcha_core.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE12480725C13293FAFDDA52 /* gotcha_core.swift */; };
|
||||||
C33BA07C5F7DA72CBF72CEAE /* MilestoneScreens.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64B529D84926EEEDC163A929 /* MilestoneScreens.swift */; };
|
C33BA07C5F7DA72CBF72CEAE /* MilestoneScreens.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64B529D84926EEEDC163A929 /* MilestoneScreens.swift */; };
|
||||||
CD50D82AC4B92576A2ADE163 /* CommitDetailScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81A55AAC9DB99FEB79F56DAF /* CommitDetailScreen.swift */; };
|
CD50D82AC4B92576A2ADE163 /* CommitDetailScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81A55AAC9DB99FEB79F56DAF /* CommitDetailScreen.swift */; };
|
||||||
D2B9B033E92BF8E7C0BDCDB0 /* IssueEditorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 722405F999984C7CBE838711 /* IssueEditorViewController.swift */; };
|
D2B9B033E92BF8E7C0BDCDB0 /* IssueEditorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 722405F999984C7CBE838711 /* IssueEditorViewController.swift */; };
|
||||||
@@ -32,18 +37,46 @@
|
|||||||
E7AC0B140F5CFC5EF226D174 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DDAABE6B13ADC6D08D9438AF /* Assets.xcassets */; };
|
E7AC0B140F5CFC5EF226D174 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DDAABE6B13ADC6D08D9438AF /* Assets.xcassets */; };
|
||||||
EC7F8B4703DDE37A0B10CD9B /* AppContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = F61515849F6AACD721FE915C /* AppContext.swift */; };
|
EC7F8B4703DDE37A0B10CD9B /* AppContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = F61515849F6AACD721FE915C /* AppContext.swift */; };
|
||||||
F55A89489B2758D694F3B27D /* Support.swift in Sources */ = {isa = PBXBuildFile; fileRef = F75B3E4FFB9C9992517C4D69 /* Support.swift */; };
|
F55A89489B2758D694F3B27D /* Support.swift in Sources */ = {isa = PBXBuildFile; fileRef = F75B3E4FFB9C9992517C4D69 /* Support.swift */; };
|
||||||
|
FF6BD7B3AAE19FDB107AF9F8 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = F121BE52F7C9C8780341F988 /* PrivacyInfo.xcprivacy */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
|
/* Begin PBXContainerItemProxy section */
|
||||||
|
EFB9473AB945D16F3AA3B49B /* PBXContainerItemProxy */ = {
|
||||||
|
isa = PBXContainerItemProxy;
|
||||||
|
containerPortal = C30A6F1B592A907D96D6AB40 /* Project object */;
|
||||||
|
proxyType = 1;
|
||||||
|
remoteGlobalIDString = AC111BD5DA5739822FCD7926;
|
||||||
|
remoteInfo = GotchaWidgets;
|
||||||
|
};
|
||||||
|
/* End PBXContainerItemProxy section */
|
||||||
|
|
||||||
|
/* Begin PBXCopyFilesBuildPhase section */
|
||||||
|
E567315EBFF59B424AE3BCC5 /* Embed Foundation Extensions */ = {
|
||||||
|
isa = PBXCopyFilesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
dstPath = "";
|
||||||
|
dstSubfolderSpec = 13;
|
||||||
|
files = (
|
||||||
|
7A9D1ADD6623C89A7D5E016F /* GotchaWidgets.appex in Embed Foundation Extensions */,
|
||||||
|
);
|
||||||
|
name = "Embed Foundation Extensions";
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXCopyFilesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
000E810FCB39B9916CA5CAB8 /* RepositoryListScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepositoryListScreen.swift; sourceTree = "<group>"; };
|
000E810FCB39B9916CA5CAB8 /* RepositoryListScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepositoryListScreen.swift; sourceTree = "<group>"; };
|
||||||
0BB99EB32ECA1F8C55293EE3 /* WorkItemDetailScreens.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkItemDetailScreens.swift; sourceTree = "<group>"; };
|
0BB99EB32ECA1F8C55293EE3 /* WorkItemDetailScreens.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkItemDetailScreens.swift; sourceTree = "<group>"; };
|
||||||
17264C5C909501D8ADEC00B7 /* IssueActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IssueActions.swift; sourceTree = "<group>"; };
|
17264C5C909501D8ADEC00B7 /* IssueActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IssueActions.swift; sourceTree = "<group>"; };
|
||||||
181AE294D07DB4EAC9C9A0FF /* RepositoryFileScreens.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepositoryFileScreens.swift; sourceTree = "<group>"; };
|
181AE294D07DB4EAC9C9A0FF /* RepositoryFileScreens.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepositoryFileScreens.swift; sourceTree = "<group>"; };
|
||||||
41E0F726C9668B50418D9ADF /* DiffScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiffScreen.swift; sourceTree = "<group>"; };
|
41E0F726C9668B50418D9ADF /* DiffScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiffScreen.swift; sourceTree = "<group>"; };
|
||||||
|
5121A6F14A9C9F6EA144BB2F /* GotchaWidgets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GotchaWidgets.swift; sourceTree = "<group>"; };
|
||||||
|
52ABD1B07CEEF00263038653 /* GotchaWidgets.appex */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = "wrapper.app-extension"; path = GotchaWidgets.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
5FB3250A93766966A60A685E /* Gotcha.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = Gotcha.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
5FB3250A93766966A60A685E /* Gotcha.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = Gotcha.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
64B529D84926EEEDC163A929 /* MilestoneScreens.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MilestoneScreens.swift; sourceTree = "<group>"; };
|
64B529D84926EEEDC163A929 /* MilestoneScreens.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MilestoneScreens.swift; sourceTree = "<group>"; };
|
||||||
722405F999984C7CBE838711 /* IssueEditorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IssueEditorViewController.swift; sourceTree = "<group>"; };
|
722405F999984C7CBE838711 /* IssueEditorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IssueEditorViewController.swift; sourceTree = "<group>"; };
|
||||||
7588A44A93B8DB4B78C3B2BB /* CommitScreens.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommitScreens.swift; sourceTree = "<group>"; };
|
7588A44A93B8DB4B78C3B2BB /* CommitScreens.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommitScreens.swift; sourceTree = "<group>"; };
|
||||||
|
77AE1D594C38D2BDAA2E86AD /* WidgetIntents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetIntents.swift; sourceTree = "<group>"; };
|
||||||
81A55AAC9DB99FEB79F56DAF /* CommitDetailScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommitDetailScreen.swift; sourceTree = "<group>"; };
|
81A55AAC9DB99FEB79F56DAF /* CommitDetailScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommitDetailScreen.swift; sourceTree = "<group>"; };
|
||||||
8F145F13B8ED83A5AB0009A4 /* SettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = "<group>"; };
|
8F145F13B8ED83A5AB0009A4 /* SettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = "<group>"; };
|
||||||
8F9FFCC06E0313760524EBD2 /* CommentEditorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentEditorViewController.swift; sourceTree = "<group>"; };
|
8F9FFCC06E0313760524EBD2 /* CommentEditorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentEditorViewController.swift; sourceTree = "<group>"; };
|
||||||
@@ -74,6 +107,14 @@
|
|||||||
/* End PBXFrameworksBuildPhase section */
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXGroup section */
|
/* Begin PBXGroup section */
|
||||||
|
1E34F71186DBE1669D42B546 /* Widgets */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
5121A6F14A9C9F6EA144BB2F /* GotchaWidgets.swift */,
|
||||||
|
);
|
||||||
|
path = Widgets;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
710A50F51478401FC642E6E3 /* Sources */ = {
|
710A50F51478401FC642E6E3 /* Sources */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -96,6 +137,7 @@
|
|||||||
C13A39F3C39C1F353D58C307 /* ServerScreens.swift */,
|
C13A39F3C39C1F353D58C307 /* ServerScreens.swift */,
|
||||||
8F145F13B8ED83A5AB0009A4 /* SettingsViewController.swift */,
|
8F145F13B8ED83A5AB0009A4 /* SettingsViewController.swift */,
|
||||||
F75B3E4FFB9C9992517C4D69 /* Support.swift */,
|
F75B3E4FFB9C9992517C4D69 /* Support.swift */,
|
||||||
|
77AE1D594C38D2BDAA2E86AD /* WidgetIntents.swift */,
|
||||||
0BB99EB32ECA1F8C55293EE3 /* WorkItemDetailScreens.swift */,
|
0BB99EB32ECA1F8C55293EE3 /* WorkItemDetailScreens.swift */,
|
||||||
);
|
);
|
||||||
path = Sources;
|
path = Sources;
|
||||||
@@ -116,6 +158,7 @@
|
|||||||
F121BE52F7C9C8780341F988 /* PrivacyInfo.xcprivacy */,
|
F121BE52F7C9C8780341F988 /* PrivacyInfo.xcprivacy */,
|
||||||
94721140EFE7F8E7CD0F5C0B /* Generated */,
|
94721140EFE7F8E7CD0F5C0B /* Generated */,
|
||||||
710A50F51478401FC642E6E3 /* Sources */,
|
710A50F51478401FC642E6E3 /* Sources */,
|
||||||
|
1E34F71186DBE1669D42B546 /* Widgets */,
|
||||||
F059299C038F3CAFCE470831 /* Products */,
|
F059299C038F3CAFCE470831 /* Products */,
|
||||||
);
|
);
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -124,6 +167,7 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
5FB3250A93766966A60A685E /* Gotcha.app */,
|
5FB3250A93766966A60A685E /* Gotcha.app */,
|
||||||
|
52ABD1B07CEEF00263038653 /* GotchaWidgets.appex */,
|
||||||
);
|
);
|
||||||
name = Products;
|
name = Products;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -139,10 +183,12 @@
|
|||||||
409DBF67E9C2743801B8F8A4 /* Sources */,
|
409DBF67E9C2743801B8F8A4 /* Sources */,
|
||||||
9EDD4380917C29EE67EE344B /* Resources */,
|
9EDD4380917C29EE67EE344B /* Resources */,
|
||||||
9BC71F568A96324A1942A0AC /* Frameworks */,
|
9BC71F568A96324A1942A0AC /* Frameworks */,
|
||||||
|
E567315EBFF59B424AE3BCC5 /* Embed Foundation Extensions */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
dependencies = (
|
dependencies = (
|
||||||
|
D934DE8051607244EA19B70A /* PBXTargetDependency */,
|
||||||
);
|
);
|
||||||
name = Gotcha;
|
name = Gotcha;
|
||||||
packageProductDependencies = (
|
packageProductDependencies = (
|
||||||
@@ -153,6 +199,25 @@
|
|||||||
productReference = 5FB3250A93766966A60A685E /* Gotcha.app */;
|
productReference = 5FB3250A93766966A60A685E /* Gotcha.app */;
|
||||||
productType = "com.apple.product-type.application";
|
productType = "com.apple.product-type.application";
|
||||||
};
|
};
|
||||||
|
AC111BD5DA5739822FCD7926 /* GotchaWidgets */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = 09AF074399FFB04451DE5C3A /* Build configuration list for PBXNativeTarget "GotchaWidgets" */;
|
||||||
|
buildPhases = (
|
||||||
|
E451101CA0ABC24348B8641A /* Build Rust core */,
|
||||||
|
3850C0D57620418DE0FDE303 /* Sources */,
|
||||||
|
C2E7E52485E196A093E8124C /* Resources */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
);
|
||||||
|
name = GotchaWidgets;
|
||||||
|
packageProductDependencies = (
|
||||||
|
);
|
||||||
|
productName = GotchaWidgets;
|
||||||
|
productReference = 52ABD1B07CEEF00263038653 /* GotchaWidgets.appex */;
|
||||||
|
productType = "com.apple.product-type.app-extension";
|
||||||
|
};
|
||||||
/* End PBXNativeTarget section */
|
/* End PBXNativeTarget section */
|
||||||
|
|
||||||
/* Begin PBXProject section */
|
/* Begin PBXProject section */
|
||||||
@@ -166,6 +231,10 @@
|
|||||||
DevelopmentTeam = MU22FMRGK8;
|
DevelopmentTeam = MU22FMRGK8;
|
||||||
ProvisioningStyle = Automatic;
|
ProvisioningStyle = Automatic;
|
||||||
};
|
};
|
||||||
|
AC111BD5DA5739822FCD7926 = {
|
||||||
|
DevelopmentTeam = MU22FMRGK8;
|
||||||
|
ProvisioningStyle = Automatic;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
buildConfigurationList = 807BC8918EC1B0F31384F2B7 /* Build configuration list for PBXProject "Gotcha" */;
|
buildConfigurationList = 807BC8918EC1B0F31384F2B7 /* Build configuration list for PBXProject "Gotcha" */;
|
||||||
@@ -187,6 +256,7 @@
|
|||||||
projectRoot = "";
|
projectRoot = "";
|
||||||
targets = (
|
targets = (
|
||||||
60C9DF1AB4A7858833302BA5 /* Gotcha */,
|
60C9DF1AB4A7858833302BA5 /* Gotcha */,
|
||||||
|
AC111BD5DA5739822FCD7926 /* GotchaWidgets */,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
/* End PBXProject section */
|
/* End PBXProject section */
|
||||||
@@ -201,6 +271,14 @@
|
|||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
C2E7E52485E196A093E8124C /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
FF6BD7B3AAE19FDB107AF9F8 /* PrivacyInfo.xcprivacy in Resources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
/* End PBXResourcesBuildPhase section */
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXShellScriptBuildPhase section */
|
/* Begin PBXShellScriptBuildPhase section */
|
||||||
@@ -224,9 +302,39 @@
|
|||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "./build_rust_core.bash\n";
|
shellScript = "./build_rust_core.bash\n";
|
||||||
};
|
};
|
||||||
|
E451101CA0ABC24348B8641A /* Build Rust core */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
alwaysOutOfDate = 1;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
|
name = "Build Rust core";
|
||||||
|
outputFileListPaths = (
|
||||||
|
);
|
||||||
|
outputPaths = (
|
||||||
|
"$(DERIVED_FILE_DIR)/rust/libgotcha_core.a",
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "./build_rust_core.bash\n";
|
||||||
|
};
|
||||||
/* End PBXShellScriptBuildPhase section */
|
/* End PBXShellScriptBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
|
3850C0D57620418DE0FDE303 /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
39C36B0D5FB260C920D466DC /* GotchaWidgets.swift in Sources */,
|
||||||
|
8BDF1E1259A237221FDDB822 /* WidgetIntents.swift in Sources */,
|
||||||
|
BDFE5D7A54541A109B35BF45 /* gotcha_core.swift in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
409DBF67E9C2743801B8F8A4 /* Sources */ = {
|
409DBF67E9C2743801B8F8A4 /* Sources */ = {
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@@ -250,6 +358,7 @@
|
|||||||
9B2206DF1263080B9B25B82C /* ServerScreens.swift in Sources */,
|
9B2206DF1263080B9B25B82C /* ServerScreens.swift in Sources */,
|
||||||
96C4AFC206A1DBAE3D3E00CE /* SettingsViewController.swift in Sources */,
|
96C4AFC206A1DBAE3D3E00CE /* SettingsViewController.swift in Sources */,
|
||||||
F55A89489B2758D694F3B27D /* Support.swift in Sources */,
|
F55A89489B2758D694F3B27D /* Support.swift in Sources */,
|
||||||
|
74626C144E9214BF89F821D2 /* WidgetIntents.swift in Sources */,
|
||||||
8A42B4319AF343566D77D12A /* WorkItemDetailScreens.swift in Sources */,
|
8A42B4319AF343566D77D12A /* WorkItemDetailScreens.swift in Sources */,
|
||||||
D59D3ED36ABCC1D8690A9088 /* gotcha_core.swift in Sources */,
|
D59D3ED36ABCC1D8690A9088 /* gotcha_core.swift in Sources */,
|
||||||
);
|
);
|
||||||
@@ -257,6 +366,14 @@
|
|||||||
};
|
};
|
||||||
/* End PBXSourcesBuildPhase section */
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXTargetDependency section */
|
||||||
|
D934DE8051607244EA19B70A /* PBXTargetDependency */ = {
|
||||||
|
isa = PBXTargetDependency;
|
||||||
|
target = AC111BD5DA5739822FCD7926 /* GotchaWidgets */;
|
||||||
|
targetProxy = EFB9473AB945D16F3AA3B49B /* PBXContainerItemProxy */;
|
||||||
|
};
|
||||||
|
/* End PBXTargetDependency section */
|
||||||
|
|
||||||
/* Begin XCBuildConfiguration section */
|
/* Begin XCBuildConfiguration section */
|
||||||
2F34E9C13F86A9A4A187A561 /* Debug */ = {
|
2F34E9C13F86A9A4A187A561 /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
@@ -373,6 +490,31 @@
|
|||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
};
|
};
|
||||||
|
9A0532DE2DD19A5F00EE4829 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = GotchaWidgets.entitlements;
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
DEVELOPMENT_TEAM = MU22FMRGK8;
|
||||||
|
INFOPLIST_FILE = "GotchaWidgets-Info.plist";
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
"@executable_path/../../Frameworks",
|
||||||
|
);
|
||||||
|
LIBRARY_SEARCH_PATHS = "$(inherited) $(DERIVED_FILE_DIR)/rust";
|
||||||
|
OTHER_LDFLAGS = "$(inherited) -lgotcha_core";
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = de.rfc1437.gotcha.widgets;
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
SKIP_INSTALL = YES;
|
||||||
|
SWIFT_OBJC_BRIDGING_HEADER = "Gotcha-Bridging-Header.h";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
AA31619080EBF75C58A16A96 /* Release */ = {
|
AA31619080EBF75C58A16A96 /* Release */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
@@ -429,9 +571,43 @@
|
|||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
|
B2DBC3D9C80A2BC962A8FA91 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = GotchaWidgets.entitlements;
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
DEVELOPMENT_TEAM = MU22FMRGK8;
|
||||||
|
INFOPLIST_FILE = "GotchaWidgets-Info.plist";
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
"@executable_path/../../Frameworks",
|
||||||
|
);
|
||||||
|
LIBRARY_SEARCH_PATHS = "$(inherited) $(DERIVED_FILE_DIR)/rust";
|
||||||
|
OTHER_LDFLAGS = "$(inherited) -lgotcha_core";
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = de.rfc1437.gotcha.widgets;
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
SKIP_INSTALL = YES;
|
||||||
|
SWIFT_OBJC_BRIDGING_HEADER = "Gotcha-Bridging-Header.h";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
/* End XCBuildConfiguration section */
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
/* Begin XCConfigurationList section */
|
/* Begin XCConfigurationList section */
|
||||||
|
09AF074399FFB04451DE5C3A /* Build configuration list for PBXNativeTarget "GotchaWidgets" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
9A0532DE2DD19A5F00EE4829 /* Debug */,
|
||||||
|
B2DBC3D9C80A2BC962A8FA91 /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Debug;
|
||||||
|
};
|
||||||
7E8DF3DDA64C8C998FF3BB1D /* Build configuration list for PBXNativeTarget "Gotcha" */ = {
|
7E8DF3DDA64C8C998FF3BB1D /* Build configuration list for PBXNativeTarget "Gotcha" */ = {
|
||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
|
|||||||
31
ios/GotchaWidgets-Info.plist
Normal file
31
ios/GotchaWidgets-Info.plist
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
|
<key>CFBundleDisplayName</key>
|
||||||
|
<string>Gotcha Widgets</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>$(PRODUCT_NAME)</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>XPC!</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>1.0</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>1</string>
|
||||||
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
|
<false/>
|
||||||
|
<key>NSExtension</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSExtensionPointIdentifier</key>
|
||||||
|
<string>com.apple.widgetkit-extension</string>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
14
ios/GotchaWidgets.entitlements
Normal file
14
ios/GotchaWidgets.entitlements
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>com.apple.security.application-groups</key>
|
||||||
|
<array>
|
||||||
|
<string>group.de.rfc1437.gotcha</string>
|
||||||
|
</array>
|
||||||
|
<key>keychain-access-groups</key>
|
||||||
|
<array>
|
||||||
|
<string>$(AppIdentifierPrefix)$(CFBundleIdentifier)</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -18,6 +18,17 @@
|
|||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>$(MARKETING_VERSION)</string>
|
<string>$(MARKETING_VERSION)</string>
|
||||||
|
<key>CFBundleURLTypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleURLName</key>
|
||||||
|
<string>de.rfc1437.gotcha</string>
|
||||||
|
<key>CFBundleURLSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>gotcha</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
|
|||||||
@@ -1,14 +1,19 @@
|
|||||||
import UIKit
|
import UIKit
|
||||||
|
import WidgetKit
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
final class AppContext {
|
final class AppContext {
|
||||||
let core = GotchaCore()
|
let core: GotchaCore
|
||||||
private let window: UIWindow
|
private let window: UIWindow
|
||||||
private(set) var tabs = UITabBarController()
|
private(set) var tabs = UITabBarController()
|
||||||
private(set) var navigationControllers: [UINavigationController] = []
|
private(set) var navigationControllers: [UINavigationController] = []
|
||||||
|
|
||||||
init(window: UIWindow) {
|
init(window: UIWindow) {
|
||||||
self.window = window
|
self.window = window
|
||||||
|
let storageDirectory = FileManager.default.containerURL(
|
||||||
|
forSecurityApplicationGroupIdentifier: "group.de.rfc1437.gotcha"
|
||||||
|
)?.path
|
||||||
|
core = GotchaCore(storageDirectory: storageDirectory)
|
||||||
applyAppearance()
|
applyAppearance()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,6 +66,21 @@ final class AppContext {
|
|||||||
for (index, root) in replacements {
|
for (index, root) in replacements {
|
||||||
navigationControllers[index].setViewControllers([root], animated: false)
|
navigationControllers[index].setViewControllers([root], animated: false)
|
||||||
}
|
}
|
||||||
|
WidgetCenter.shared.reloadAllTimelines()
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func route(widgetURL: URL) -> Bool {
|
||||||
|
guard widgetURL.scheme == "gotcha" else { return false }
|
||||||
|
let index: Int
|
||||||
|
switch widgetURL.host {
|
||||||
|
case "home": index = 0
|
||||||
|
case "pulls": index = 3
|
||||||
|
default: return false
|
||||||
|
}
|
||||||
|
tabs.selectedIndex = index
|
||||||
|
navigationControllers[index].popToRootViewController(animated: false)
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func didAddServer(index: UInt32) throws {
|
func didAddServer(index: UInt32) throws {
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import UIKit
|
import UIKit
|
||||||
|
import WidgetKit
|
||||||
|
|
||||||
@main
|
@main
|
||||||
final class AppDelegate: UIResponder, UIApplicationDelegate {
|
final class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
var window: UIWindow?
|
var window: UIWindow?
|
||||||
|
private var context: AppContext?
|
||||||
|
|
||||||
func application(
|
func application(
|
||||||
_ application: UIApplication,
|
_ application: UIApplication,
|
||||||
@@ -10,10 +12,26 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||||||
) -> Bool {
|
) -> Bool {
|
||||||
let window = UIWindow(frame: UIScreen.main.bounds)
|
let window = UIWindow(frame: UIScreen.main.bounds)
|
||||||
let context = AppContext(window: window)
|
let context = AppContext(window: window)
|
||||||
|
self.context = context
|
||||||
window.rootViewController = context.makeRootController()
|
window.rootViewController = context.makeRootController()
|
||||||
window.makeKeyAndVisible()
|
window.makeKeyAndVisible()
|
||||||
self.window = window
|
self.window = window
|
||||||
context.showStartupErrorIfNeeded()
|
context.showStartupErrorIfNeeded()
|
||||||
|
if let url = launchOptions?[.url] as? URL {
|
||||||
|
context.route(widgetURL: url)
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func application(
|
||||||
|
_ application: UIApplication,
|
||||||
|
open url: URL,
|
||||||
|
options: [UIApplication.OpenURLOptionsKey: Any] = [:]
|
||||||
|
) -> Bool {
|
||||||
|
context?.route(widgetURL: url) ?? false
|
||||||
|
}
|
||||||
|
|
||||||
|
func applicationDidBecomeActive(_ application: UIApplication) {
|
||||||
|
WidgetCenter.shared.reloadAllTimelines()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
52
ios/Sources/WidgetIntents.swift
Normal file
52
ios/Sources/WidgetIntents.swift
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import AppIntents
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum WidgetEnvironment {
|
||||||
|
static let appGroup = "group.de.rfc1437.gotcha"
|
||||||
|
|
||||||
|
static func core() -> GotchaCore {
|
||||||
|
GotchaCore(
|
||||||
|
storageDirectory: FileManager.default.containerURL(
|
||||||
|
forSecurityApplicationGroupIdentifier: appGroup
|
||||||
|
)?.path
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func servers() -> [ServerRow] {
|
||||||
|
core().servers()
|
||||||
|
}
|
||||||
|
|
||||||
|
static func selection(for server: ServerRow) -> String {
|
||||||
|
"\(server.name) · \(server.url)"
|
||||||
|
}
|
||||||
|
|
||||||
|
static func server(for selection: String) -> ServerRow? {
|
||||||
|
servers().first { self.selection(for: $0) == selection }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct WidgetServerOptionsProvider: DynamicOptionsProvider {
|
||||||
|
func results() async throws -> [String] {
|
||||||
|
WidgetEnvironment.servers().map(WidgetEnvironment.selection)
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultResult() async -> String? {
|
||||||
|
WidgetEnvironment.servers().first.map(WidgetEnvironment.selection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ActivityWidgetIntent: WidgetConfigurationIntent {
|
||||||
|
static let title: LocalizedStringResource = "Activity Server"
|
||||||
|
static let description = IntentDescription("Choose the server whose recent activity appears.")
|
||||||
|
|
||||||
|
@Parameter(title: "Server", optionsProvider: WidgetServerOptionsProvider())
|
||||||
|
var server: String?
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PullsWidgetIntent: WidgetConfigurationIntent {
|
||||||
|
static let title: LocalizedStringResource = "Pull Request Server"
|
||||||
|
static let description = IntentDescription("Choose the server whose open pull requests appear.")
|
||||||
|
|
||||||
|
@Parameter(title: "Server", optionsProvider: WidgetServerOptionsProvider())
|
||||||
|
var server: String?
|
||||||
|
}
|
||||||
324
ios/Widgets/GotchaWidgets.swift
Normal file
324
ios/Widgets/GotchaWidgets.swift
Normal file
@@ -0,0 +1,324 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import WidgetKit
|
||||||
|
|
||||||
|
private struct ActivityEntry: TimelineEntry {
|
||||||
|
let date: Date
|
||||||
|
let serverName: String
|
||||||
|
let rows: [ActivityRow]
|
||||||
|
let message: String?
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct PullsEntry: TimelineEntry {
|
||||||
|
let date: Date
|
||||||
|
let serverName: String
|
||||||
|
let rows: [PullRow]
|
||||||
|
let message: String?
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct ActivityProvider: AppIntentTimelineProvider {
|
||||||
|
func placeholder(in context: Context) -> ActivityEntry {
|
||||||
|
ActivityEntry(
|
||||||
|
date: .now,
|
||||||
|
serverName: "My Server",
|
||||||
|
rows: [
|
||||||
|
ActivityRow(
|
||||||
|
icon: .push,
|
||||||
|
title: "Pushed to main in owner/project",
|
||||||
|
detail: "",
|
||||||
|
meta: "Just now",
|
||||||
|
target: .commit,
|
||||||
|
owner: "owner",
|
||||||
|
repository: "project",
|
||||||
|
number: 0,
|
||||||
|
sha: ""
|
||||||
|
),
|
||||||
|
ActivityRow(
|
||||||
|
icon: .issue,
|
||||||
|
title: "Opened an issue in owner/project",
|
||||||
|
detail: "",
|
||||||
|
meta: "5m ago",
|
||||||
|
target: .issue,
|
||||||
|
owner: "owner",
|
||||||
|
repository: "project",
|
||||||
|
number: 1,
|
||||||
|
sha: ""
|
||||||
|
),
|
||||||
|
],
|
||||||
|
message: nil
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func snapshot(for configuration: ActivityWidgetIntent, in context: Context) async -> ActivityEntry {
|
||||||
|
context.isPreview ? placeholder(in: context) : await load(configuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
func timeline(for configuration: ActivityWidgetIntent, in context: Context) async -> Timeline<ActivityEntry> {
|
||||||
|
Timeline(
|
||||||
|
entries: [await load(configuration)],
|
||||||
|
policy: .after(Date.now.addingTimeInterval(15 * 60))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func load(_ configuration: ActivityWidgetIntent) async -> ActivityEntry {
|
||||||
|
guard
|
||||||
|
let selection = configuration.server,
|
||||||
|
let server = WidgetEnvironment.server(for: selection)
|
||||||
|
else {
|
||||||
|
return ActivityEntry(
|
||||||
|
date: .now,
|
||||||
|
serverName: "Activity",
|
||||||
|
rows: [],
|
||||||
|
message: "Add a server in Gotcha, then configure this widget."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
let page = try await WidgetEnvironment.core().widgetActivity(serverId: server.id, limit: 6)
|
||||||
|
return ActivityEntry(
|
||||||
|
date: .now,
|
||||||
|
serverName: page.serverName,
|
||||||
|
rows: page.rows,
|
||||||
|
message: page.rows.isEmpty ? "No recent activity." : nil
|
||||||
|
)
|
||||||
|
} catch {
|
||||||
|
return ActivityEntry(
|
||||||
|
date: .now,
|
||||||
|
serverName: server.name,
|
||||||
|
rows: [],
|
||||||
|
message: error.localizedDescription
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct PullsProvider: AppIntentTimelineProvider {
|
||||||
|
func placeholder(in context: Context) -> PullsEntry {
|
||||||
|
PullsEntry(
|
||||||
|
date: .now,
|
||||||
|
serverName: "My Server",
|
||||||
|
rows: [
|
||||||
|
PullRow(
|
||||||
|
number: 42,
|
||||||
|
owner: "owner",
|
||||||
|
repository: "project",
|
||||||
|
state: .open,
|
||||||
|
title: "project #42\nImprove the activity screen",
|
||||||
|
summary: "",
|
||||||
|
meta: "Open · developer · Just now"
|
||||||
|
),
|
||||||
|
PullRow(
|
||||||
|
number: 41,
|
||||||
|
owner: "owner",
|
||||||
|
repository: "project",
|
||||||
|
state: .open,
|
||||||
|
title: "project #41\nFix repository navigation",
|
||||||
|
summary: "",
|
||||||
|
meta: "Open · developer · 10m ago"
|
||||||
|
),
|
||||||
|
],
|
||||||
|
message: nil
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func snapshot(for configuration: PullsWidgetIntent, in context: Context) async -> PullsEntry {
|
||||||
|
context.isPreview ? placeholder(in: context) : await load(configuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
func timeline(for configuration: PullsWidgetIntent, in context: Context) async -> Timeline<PullsEntry> {
|
||||||
|
Timeline(
|
||||||
|
entries: [await load(configuration)],
|
||||||
|
policy: .after(Date.now.addingTimeInterval(15 * 60))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func load(_ configuration: PullsWidgetIntent) async -> PullsEntry {
|
||||||
|
guard
|
||||||
|
let selection = configuration.server,
|
||||||
|
let server = WidgetEnvironment.server(for: selection)
|
||||||
|
else {
|
||||||
|
return PullsEntry(
|
||||||
|
date: .now,
|
||||||
|
serverName: "Pull Requests",
|
||||||
|
rows: [],
|
||||||
|
message: "Add a server in Gotcha, then configure this widget."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
let page = try await WidgetEnvironment.core().widgetPulls(serverId: server.id, limit: 5)
|
||||||
|
return PullsEntry(
|
||||||
|
date: .now,
|
||||||
|
serverName: page.serverName,
|
||||||
|
rows: page.rows,
|
||||||
|
message: page.rows.isEmpty ? "No open pull requests." : nil
|
||||||
|
)
|
||||||
|
} catch {
|
||||||
|
return PullsEntry(
|
||||||
|
date: .now,
|
||||||
|
serverName: server.name,
|
||||||
|
rows: [],
|
||||||
|
message: error.localizedDescription
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct WidgetHeader: View {
|
||||||
|
let title: String
|
||||||
|
let symbol: String
|
||||||
|
let server: String
|
||||||
|
let updated: Date
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(alignment: .firstTextBaseline) {
|
||||||
|
Label(title, systemImage: symbol)
|
||||||
|
.font(.headline)
|
||||||
|
Spacer(minLength: 8)
|
||||||
|
VStack(alignment: .trailing, spacing: 1) {
|
||||||
|
Text(server)
|
||||||
|
.font(.caption.weight(.semibold))
|
||||||
|
.lineLimit(1)
|
||||||
|
Text(updated, style: .relative)
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct ActivityWidgetView: View {
|
||||||
|
let entry: ActivityEntry
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
WidgetHeader(
|
||||||
|
title: "Activity",
|
||||||
|
symbol: "waveform.path.ecg",
|
||||||
|
server: entry.serverName,
|
||||||
|
updated: entry.date
|
||||||
|
)
|
||||||
|
Divider()
|
||||||
|
if let message = entry.message {
|
||||||
|
emptyState(message, symbol: "clock.arrow.circlepath")
|
||||||
|
} else {
|
||||||
|
VStack(alignment: .leading, spacing: 7) {
|
||||||
|
ForEach(Array(entry.rows.enumerated()), id: \.offset) { _, row in
|
||||||
|
HStack(alignment: .firstTextBaseline, spacing: 8) {
|
||||||
|
Image(systemName: symbol(for: row.icon))
|
||||||
|
.foregroundStyle(.tint)
|
||||||
|
.frame(width: 16)
|
||||||
|
Text(row.title)
|
||||||
|
.font(.caption)
|
||||||
|
.lineLimit(1)
|
||||||
|
Spacer(minLength: 4)
|
||||||
|
Text(row.meta)
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.lineLimit(1)
|
||||||
|
}
|
||||||
|
.accessibilityElement(children: .combine)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.privacySensitive()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.containerBackground(.background, for: .widget)
|
||||||
|
.widgetURL(URL(string: "gotcha://home"))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func symbol(for icon: ActivityIcon) -> String {
|
||||||
|
switch icon {
|
||||||
|
case .repository: "shippingbox"
|
||||||
|
case .issue: "exclamationmark.circle"
|
||||||
|
case .pullRequest: "arrow.triangle.pull"
|
||||||
|
case .branch: "arrow.triangle.branch"
|
||||||
|
case .tag: "tag"
|
||||||
|
case .push: "arrow.up.circle"
|
||||||
|
case .release: "shippingbox.fill"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct PullsWidgetView: View {
|
||||||
|
let entry: PullsEntry
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
WidgetHeader(
|
||||||
|
title: "Open Pull Requests",
|
||||||
|
symbol: "arrow.triangle.pull",
|
||||||
|
server: entry.serverName,
|
||||||
|
updated: entry.date
|
||||||
|
)
|
||||||
|
Divider()
|
||||||
|
if let message = entry.message {
|
||||||
|
emptyState(message, symbol: "arrow.triangle.pull")
|
||||||
|
} else {
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
ForEach(Array(entry.rows.enumerated()), id: \.offset) { _, row in
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
Text(row.title)
|
||||||
|
.font(.caption.weight(.semibold))
|
||||||
|
.lineLimit(2)
|
||||||
|
Text(row.meta)
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.lineLimit(1)
|
||||||
|
}
|
||||||
|
.accessibilityElement(children: .combine)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.privacySensitive()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.containerBackground(.background, for: .widget)
|
||||||
|
.widgetURL(URL(string: "gotcha://pulls"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func emptyState(_ message: String, symbol: String) -> some View {
|
||||||
|
VStack(spacing: 8) {
|
||||||
|
Spacer()
|
||||||
|
Image(systemName: symbol)
|
||||||
|
.font(.title2)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
Text(message)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ActivityWidget: Widget {
|
||||||
|
let kind = "GotchaActivityWidget"
|
||||||
|
|
||||||
|
var body: some WidgetConfiguration {
|
||||||
|
AppIntentConfiguration(kind: kind, intent: ActivityWidgetIntent.self, provider: ActivityProvider()) {
|
||||||
|
ActivityWidgetView(entry: $0)
|
||||||
|
}
|
||||||
|
.configurationDisplayName("Recent Activity")
|
||||||
|
.description("See the latest activity from a configured Gitea or Forgejo server.")
|
||||||
|
.supportedFamilies([.systemLarge])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PullsWidget: Widget {
|
||||||
|
let kind = "GotchaPullsWidget"
|
||||||
|
|
||||||
|
var body: some WidgetConfiguration {
|
||||||
|
AppIntentConfiguration(kind: kind, intent: PullsWidgetIntent.self, provider: PullsProvider()) {
|
||||||
|
PullsWidgetView(entry: $0)
|
||||||
|
}
|
||||||
|
.configurationDisplayName("Open Pull Requests")
|
||||||
|
.description("See open pull requests from a configured Gitea or Forgejo server.")
|
||||||
|
.supportedFamilies([.systemLarge])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@main
|
||||||
|
struct GotchaWidgetBundle: WidgetBundle {
|
||||||
|
var body: some Widget {
|
||||||
|
ActivityWidget()
|
||||||
|
PullsWidget()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,6 +34,10 @@ targets:
|
|||||||
CFBundleShortVersionString: "$(MARKETING_VERSION)"
|
CFBundleShortVersionString: "$(MARKETING_VERSION)"
|
||||||
CFBundleVersion: "$(CURRENT_PROJECT_VERSION)"
|
CFBundleVersion: "$(CURRENT_PROJECT_VERSION)"
|
||||||
ITSAppUsesNonExemptEncryption: false
|
ITSAppUsesNonExemptEncryption: false
|
||||||
|
CFBundleURLTypes:
|
||||||
|
- CFBundleURLName: de.rfc1437.gotcha
|
||||||
|
CFBundleURLSchemes:
|
||||||
|
- gotcha
|
||||||
UILaunchScreen: {}
|
UILaunchScreen: {}
|
||||||
UISupportedInterfaceOrientations:
|
UISupportedInterfaceOrientations:
|
||||||
- UIInterfaceOrientationPortrait
|
- UIInterfaceOrientationPortrait
|
||||||
@@ -45,6 +49,42 @@ targets:
|
|||||||
dependencies:
|
dependencies:
|
||||||
- package: Highlighter
|
- package: Highlighter
|
||||||
- package: MarkdownUI
|
- package: MarkdownUI
|
||||||
|
- target: GotchaWidgets
|
||||||
|
embed: true
|
||||||
|
preBuildScripts:
|
||||||
|
- name: Build Rust core
|
||||||
|
basedOnDependencyAnalysis: false
|
||||||
|
script: |
|
||||||
|
./build_rust_core.bash
|
||||||
|
outputFiles:
|
||||||
|
- $(DERIVED_FILE_DIR)/rust/libgotcha_core.a
|
||||||
|
GotchaWidgets:
|
||||||
|
type: app-extension
|
||||||
|
platform: iOS
|
||||||
|
deploymentTarget: "17.0"
|
||||||
|
settings:
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER: de.rfc1437.gotcha.widgets
|
||||||
|
DEVELOPMENT_TEAM: MU22FMRGK8
|
||||||
|
CODE_SIGN_STYLE: Automatic
|
||||||
|
CODE_SIGN_ENTITLEMENTS: GotchaWidgets.entitlements
|
||||||
|
SWIFT_VERSION: "5.0"
|
||||||
|
SWIFT_OBJC_BRIDGING_HEADER: Gotcha-Bridging-Header.h
|
||||||
|
LIBRARY_SEARCH_PATHS: "$(inherited) $(DERIVED_FILE_DIR)/rust"
|
||||||
|
OTHER_LDFLAGS: "$(inherited) -lgotcha_core"
|
||||||
|
APPLICATION_EXTENSION_API_ONLY: YES
|
||||||
|
SKIP_INSTALL: YES
|
||||||
|
info:
|
||||||
|
path: GotchaWidgets-Info.plist
|
||||||
|
properties:
|
||||||
|
CFBundleDisplayName: Gotcha Widgets
|
||||||
|
ITSAppUsesNonExemptEncryption: false
|
||||||
|
NSExtension:
|
||||||
|
NSExtensionPointIdentifier: com.apple.widgetkit-extension
|
||||||
|
sources:
|
||||||
|
- Widgets
|
||||||
|
- Sources/WidgetIntents.swift
|
||||||
|
- PrivacyInfo.xcprivacy
|
||||||
|
- Generated/gotcha_core.swift
|
||||||
preBuildScripts:
|
preBuildScripts:
|
||||||
- name: Build Rust core
|
- name: Build Rust core
|
||||||
basedOnDependencyAnalysis: false
|
basedOnDependencyAnalysis: false
|
||||||
|
|||||||
Reference in New Issue
Block a user