Replace Slint UI with UIKit and add milestones

Fixes #2.\nFixes #9.
This commit is contained in:
Georg Bauer
2026-07-31 09:52:24 +02:00
parent 8f9a4dfc00
commit f201814d54
33 changed files with 7455 additions and 7878 deletions

View File

@@ -0,0 +1,7 @@
module gotcha_core {
header "gotcha_coreFFI.h"
export *
use "Darwin"
use "_Builtin_stdbool"
use "_Builtin_stdint"
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,786 @@
// This file was autogenerated by some hot garbage in the `uniffi` crate.
// Trust me, you don't want to mess with it!
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
// The following structs are used to implement the lowest level
// of the FFI, and thus useful to multiple uniffied crates.
// We ensure they are declared exactly once, with a header guard, UNIFFI_SHARED_H.
#ifdef UNIFFI_SHARED_H
// We also try to prevent mixing versions of shared uniffi header structs.
// If you add anything to the #else block, you must increment the version suffix in UNIFFI_SHARED_HEADER_V4
#ifndef UNIFFI_SHARED_HEADER_V4
#error Combining helper code from multiple versions of uniffi is not supported
#endif // ndef UNIFFI_SHARED_HEADER_V4
#else
#define UNIFFI_SHARED_H
#define UNIFFI_SHARED_HEADER_V4
// ⚠️ Attention: If you change this #else block (ending in `#endif // def UNIFFI_SHARED_H`) you *must* ⚠️
// ⚠️ increment the version suffix in all instances of UNIFFI_SHARED_HEADER_V4 in this file. ⚠️
typedef struct RustBuffer
{
uint64_t capacity;
uint64_t len;
uint8_t *_Nullable data;
} RustBuffer;
typedef struct ForeignBytes
{
int32_t len;
const uint8_t *_Nullable data;
} ForeignBytes;
// Error definitions
typedef struct RustCallStatus {
int8_t code;
RustBuffer errorBuf;
} RustCallStatus;
// ⚠️ Attention: If you change this #else block (ending in `#endif // def UNIFFI_SHARED_H`) you *must* ⚠️
// ⚠️ increment the version suffix in all instances of UNIFFI_SHARED_HEADER_V4 in this file. ⚠️
#endif // def UNIFFI_SHARED_H
#ifndef UNIFFI_FFIDEF_RUST_FUTURE_CONTINUATION_CALLBACK
#define UNIFFI_FFIDEF_RUST_FUTURE_CONTINUATION_CALLBACK
typedef void (*UniffiRustFutureContinuationCallback)(uint64_t, int8_t
);
#endif
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_DROPPED_CALLBACK
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_DROPPED_CALLBACK
typedef void (*UniffiForeignFutureDroppedCallback)(uint64_t
);
#endif
#ifndef UNIFFI_FFIDEF_CALLBACK_INTERFACE_FREE
#define UNIFFI_FFIDEF_CALLBACK_INTERFACE_FREE
typedef void (*UniffiCallbackInterfaceFree)(uint64_t
);
#endif
#ifndef UNIFFI_FFIDEF_CALLBACK_INTERFACE_CLONE
#define UNIFFI_FFIDEF_CALLBACK_INTERFACE_CLONE
typedef uint64_t (*UniffiCallbackInterfaceClone)(uint64_t
);
#endif
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_DROPPED_CALLBACK_STRUCT
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_DROPPED_CALLBACK_STRUCT
typedef struct UniffiForeignFutureDroppedCallbackStruct {
uint64_t handle;
UniffiForeignFutureDroppedCallback _Nonnull free;
} UniffiForeignFutureDroppedCallbackStruct;
#endif
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_U8
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_U8
typedef struct UniffiForeignFutureResultU8 {
uint8_t returnValue;
RustCallStatus callStatus;
} UniffiForeignFutureResultU8;
#endif
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_U8
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_U8
typedef void (*UniffiForeignFutureCompleteU8)(uint64_t, UniffiForeignFutureResultU8
);
#endif
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_I8
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_I8
typedef struct UniffiForeignFutureResultI8 {
int8_t returnValue;
RustCallStatus callStatus;
} UniffiForeignFutureResultI8;
#endif
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_I8
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_I8
typedef void (*UniffiForeignFutureCompleteI8)(uint64_t, UniffiForeignFutureResultI8
);
#endif
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_U16
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_U16
typedef struct UniffiForeignFutureResultU16 {
uint16_t returnValue;
RustCallStatus callStatus;
} UniffiForeignFutureResultU16;
#endif
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_U16
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_U16
typedef void (*UniffiForeignFutureCompleteU16)(uint64_t, UniffiForeignFutureResultU16
);
#endif
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_I16
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_I16
typedef struct UniffiForeignFutureResultI16 {
int16_t returnValue;
RustCallStatus callStatus;
} UniffiForeignFutureResultI16;
#endif
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_I16
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_I16
typedef void (*UniffiForeignFutureCompleteI16)(uint64_t, UniffiForeignFutureResultI16
);
#endif
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_U32
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_U32
typedef struct UniffiForeignFutureResultU32 {
uint32_t returnValue;
RustCallStatus callStatus;
} UniffiForeignFutureResultU32;
#endif
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_U32
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_U32
typedef void (*UniffiForeignFutureCompleteU32)(uint64_t, UniffiForeignFutureResultU32
);
#endif
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_I32
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_I32
typedef struct UniffiForeignFutureResultI32 {
int32_t returnValue;
RustCallStatus callStatus;
} UniffiForeignFutureResultI32;
#endif
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_I32
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_I32
typedef void (*UniffiForeignFutureCompleteI32)(uint64_t, UniffiForeignFutureResultI32
);
#endif
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_U64
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_U64
typedef struct UniffiForeignFutureResultU64 {
uint64_t returnValue;
RustCallStatus callStatus;
} UniffiForeignFutureResultU64;
#endif
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_U64
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_U64
typedef void (*UniffiForeignFutureCompleteU64)(uint64_t, UniffiForeignFutureResultU64
);
#endif
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_I64
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_I64
typedef struct UniffiForeignFutureResultI64 {
int64_t returnValue;
RustCallStatus callStatus;
} UniffiForeignFutureResultI64;
#endif
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_I64
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_I64
typedef void (*UniffiForeignFutureCompleteI64)(uint64_t, UniffiForeignFutureResultI64
);
#endif
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_F32
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_F32
typedef struct UniffiForeignFutureResultF32 {
float returnValue;
RustCallStatus callStatus;
} UniffiForeignFutureResultF32;
#endif
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_F32
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_F32
typedef void (*UniffiForeignFutureCompleteF32)(uint64_t, UniffiForeignFutureResultF32
);
#endif
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_F64
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_F64
typedef struct UniffiForeignFutureResultF64 {
double returnValue;
RustCallStatus callStatus;
} UniffiForeignFutureResultF64;
#endif
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_F64
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_F64
typedef void (*UniffiForeignFutureCompleteF64)(uint64_t, UniffiForeignFutureResultF64
);
#endif
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_RUST_BUFFER
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_RUST_BUFFER
typedef struct UniffiForeignFutureResultRustBuffer {
RustBuffer returnValue;
RustCallStatus callStatus;
} UniffiForeignFutureResultRustBuffer;
#endif
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_RUST_BUFFER
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_RUST_BUFFER
typedef void (*UniffiForeignFutureCompleteRustBuffer)(uint64_t, UniffiForeignFutureResultRustBuffer
);
#endif
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_VOID
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_VOID
typedef struct UniffiForeignFutureResultVoid {
RustCallStatus callStatus;
} UniffiForeignFutureResultVoid;
#endif
#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_VOID
#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_VOID
typedef void (*UniffiForeignFutureCompleteVoid)(uint64_t, UniffiForeignFutureResultVoid
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_CLONE_GOTCHACORE
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_CLONE_GOTCHACORE
uint64_t uniffi_gotcha_core_fn_clone_gotchacore(uint64_t handle, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_FREE_GOTCHACORE
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_FREE_GOTCHACORE
void uniffi_gotcha_core_fn_free_gotchacore(uint64_t handle, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef 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
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_ACTIVE_SERVER_INDEX
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_ACTIVE_SERVER_INDEX
RustBuffer uniffi_gotcha_core_fn_method_gotchacore_active_server_index(uint64_t ptr, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_ACTIVE_SERVER_NAME
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_ACTIVE_SERVER_NAME
RustBuffer uniffi_gotcha_core_fn_method_gotchacore_active_server_name(uint64_t ptr, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_ADD_SERVER
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_ADD_SERVER
uint64_t uniffi_gotcha_core_fn_method_gotchacore_add_server(uint64_t ptr, RustBuffer name, RustBuffer url, RustBuffer token
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_COMMIT_DIFF
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_COMMIT_DIFF
uint64_t uniffi_gotcha_core_fn_method_gotchacore_commit_diff(uint64_t ptr, RustBuffer owner, RustBuffer repository, RustBuffer sha, RustBuffer path
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_COMMIT_FILES
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_COMMIT_FILES
uint64_t uniffi_gotcha_core_fn_method_gotchacore_commit_files(uint64_t ptr, RustBuffer owner, RustBuffer repository, RustBuffer sha
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_COMMITS
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_COMMITS
uint64_t uniffi_gotcha_core_fn_method_gotchacore_commits(uint64_t ptr, RustBuffer owner, RustBuffer repository, RustBuffer branch
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_HOME
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_HOME
uint64_t uniffi_gotcha_core_fn_method_gotchacore_home(uint64_t ptr
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_ISSUE
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_ISSUE
uint64_t uniffi_gotcha_core_fn_method_gotchacore_issue(uint64_t ptr, RustBuffer owner, RustBuffer repository, int64_t number
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_ISSUES
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_ISSUES
uint64_t uniffi_gotcha_core_fn_method_gotchacore_issues(uint64_t ptr, RustBuffer owner, RustBuffer repository
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_MILESTONE
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_MILESTONE
uint64_t uniffi_gotcha_core_fn_method_gotchacore_milestone(uint64_t ptr, RustBuffer owner, RustBuffer repository, int64_t id
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_MILESTONES
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_MILESTONES
uint64_t uniffi_gotcha_core_fn_method_gotchacore_milestones(uint64_t ptr, RustBuffer owner, RustBuffer repository
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_PULL
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_PULL
uint64_t uniffi_gotcha_core_fn_method_gotchacore_pull(uint64_t ptr, RustBuffer owner, RustBuffer repository, int64_t number
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_PULL_DIFF
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_PULL_DIFF
uint64_t uniffi_gotcha_core_fn_method_gotchacore_pull_diff(uint64_t ptr, RustBuffer owner, RustBuffer repository, int64_t number, RustBuffer path
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_PULLS
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_PULLS
uint64_t uniffi_gotcha_core_fn_method_gotchacore_pulls(uint64_t ptr
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_REPOSITORIES
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_REPOSITORIES
uint64_t uniffi_gotcha_core_fn_method_gotchacore_repositories(uint64_t ptr
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_SELECT_SERVER
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_SELECT_SERVER
void uniffi_gotcha_core_fn_method_gotchacore_select_server(uint64_t ptr, uint32_t index, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_SERVERS
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_SERVERS
RustBuffer uniffi_gotcha_core_fn_method_gotchacore_servers(uint64_t ptr, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_SET_APPEARANCE
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_SET_APPEARANCE
void uniffi_gotcha_core_fn_method_gotchacore_set_appearance(uint64_t ptr, uint32_t index, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_SET_ISSUE_STATUS
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_SET_ISSUE_STATUS
void uniffi_gotcha_core_fn_method_gotchacore_set_issue_status(uint64_t ptr, RustBuffer status, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_SET_PULL_STATUS
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_SET_PULL_STATUS
void uniffi_gotcha_core_fn_method_gotchacore_set_pull_status(uint64_t ptr, RustBuffer status, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_SETTINGS
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_SETTINGS
RustBuffer uniffi_gotcha_core_fn_method_gotchacore_settings(uint64_t ptr, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_STARTUP_ERROR
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_STARTUP_ERROR
RustBuffer uniffi_gotcha_core_fn_method_gotchacore_startup_error(uint64_t ptr, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_TOGGLE_FAVORITE
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_TOGGLE_FAVORITE
RustBuffer uniffi_gotcha_core_fn_method_gotchacore_toggle_favorite(uint64_t ptr, RustBuffer owner, RustBuffer repository, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef 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
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUSTBUFFER_FROM_BYTES
#define UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUSTBUFFER_FROM_BYTES
RustBuffer ffi_gotcha_core_rustbuffer_from_bytes(ForeignBytes bytes, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUSTBUFFER_FREE
#define UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUSTBUFFER_FREE
void ffi_gotcha_core_rustbuffer_free(RustBuffer buf, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUSTBUFFER_RESERVE
#define UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUSTBUFFER_RESERVE
RustBuffer ffi_gotcha_core_rustbuffer_reserve(RustBuffer buf, uint64_t additional, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_POLL_U8
#define UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_POLL_U8
void ffi_gotcha_core_rust_future_poll_u8(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_CANCEL_U8
#define UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_CANCEL_U8
void ffi_gotcha_core_rust_future_cancel_u8(uint64_t handle
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_FREE_U8
#define UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_FREE_U8
void ffi_gotcha_core_rust_future_free_u8(uint64_t handle
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_COMPLETE_U8
#define UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_COMPLETE_U8
uint8_t ffi_gotcha_core_rust_future_complete_u8(uint64_t handle, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_POLL_I8
#define UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_POLL_I8
void ffi_gotcha_core_rust_future_poll_i8(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_CANCEL_I8
#define UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_CANCEL_I8
void ffi_gotcha_core_rust_future_cancel_i8(uint64_t handle
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_FREE_I8
#define UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_FREE_I8
void ffi_gotcha_core_rust_future_free_i8(uint64_t handle
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_COMPLETE_I8
#define UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_COMPLETE_I8
int8_t ffi_gotcha_core_rust_future_complete_i8(uint64_t handle, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_POLL_U16
#define UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_POLL_U16
void ffi_gotcha_core_rust_future_poll_u16(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_CANCEL_U16
#define UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_CANCEL_U16
void ffi_gotcha_core_rust_future_cancel_u16(uint64_t handle
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_FREE_U16
#define UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_FREE_U16
void ffi_gotcha_core_rust_future_free_u16(uint64_t handle
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_COMPLETE_U16
#define UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_COMPLETE_U16
uint16_t ffi_gotcha_core_rust_future_complete_u16(uint64_t handle, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_POLL_I16
#define UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_POLL_I16
void ffi_gotcha_core_rust_future_poll_i16(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_CANCEL_I16
#define UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_CANCEL_I16
void ffi_gotcha_core_rust_future_cancel_i16(uint64_t handle
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_FREE_I16
#define UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_FREE_I16
void ffi_gotcha_core_rust_future_free_i16(uint64_t handle
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_COMPLETE_I16
#define UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_COMPLETE_I16
int16_t ffi_gotcha_core_rust_future_complete_i16(uint64_t handle, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_POLL_U32
#define UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_POLL_U32
void ffi_gotcha_core_rust_future_poll_u32(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_CANCEL_U32
#define UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_CANCEL_U32
void ffi_gotcha_core_rust_future_cancel_u32(uint64_t handle
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_FREE_U32
#define UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_FREE_U32
void ffi_gotcha_core_rust_future_free_u32(uint64_t handle
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_COMPLETE_U32
#define UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_COMPLETE_U32
uint32_t ffi_gotcha_core_rust_future_complete_u32(uint64_t handle, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_POLL_I32
#define UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_POLL_I32
void ffi_gotcha_core_rust_future_poll_i32(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_CANCEL_I32
#define UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_CANCEL_I32
void ffi_gotcha_core_rust_future_cancel_i32(uint64_t handle
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_FREE_I32
#define UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_FREE_I32
void ffi_gotcha_core_rust_future_free_i32(uint64_t handle
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_COMPLETE_I32
#define UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_COMPLETE_I32
int32_t ffi_gotcha_core_rust_future_complete_i32(uint64_t handle, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_POLL_U64
#define UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_POLL_U64
void ffi_gotcha_core_rust_future_poll_u64(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_CANCEL_U64
#define UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_CANCEL_U64
void ffi_gotcha_core_rust_future_cancel_u64(uint64_t handle
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_FREE_U64
#define UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_FREE_U64
void ffi_gotcha_core_rust_future_free_u64(uint64_t handle
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_COMPLETE_U64
#define UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_COMPLETE_U64
uint64_t ffi_gotcha_core_rust_future_complete_u64(uint64_t handle, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_POLL_I64
#define UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_POLL_I64
void ffi_gotcha_core_rust_future_poll_i64(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_CANCEL_I64
#define UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_CANCEL_I64
void ffi_gotcha_core_rust_future_cancel_i64(uint64_t handle
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_FREE_I64
#define UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_FREE_I64
void ffi_gotcha_core_rust_future_free_i64(uint64_t handle
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_COMPLETE_I64
#define UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_COMPLETE_I64
int64_t ffi_gotcha_core_rust_future_complete_i64(uint64_t handle, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_POLL_F32
#define UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_POLL_F32
void ffi_gotcha_core_rust_future_poll_f32(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_CANCEL_F32
#define UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_CANCEL_F32
void ffi_gotcha_core_rust_future_cancel_f32(uint64_t handle
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_FREE_F32
#define UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_FREE_F32
void ffi_gotcha_core_rust_future_free_f32(uint64_t handle
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_COMPLETE_F32
#define UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_COMPLETE_F32
float ffi_gotcha_core_rust_future_complete_f32(uint64_t handle, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_POLL_F64
#define UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_POLL_F64
void ffi_gotcha_core_rust_future_poll_f64(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_CANCEL_F64
#define UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_CANCEL_F64
void ffi_gotcha_core_rust_future_cancel_f64(uint64_t handle
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_FREE_F64
#define UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_FREE_F64
void ffi_gotcha_core_rust_future_free_f64(uint64_t handle
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_COMPLETE_F64
#define UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_COMPLETE_F64
double ffi_gotcha_core_rust_future_complete_f64(uint64_t handle, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_POLL_RUST_BUFFER
#define UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_POLL_RUST_BUFFER
void ffi_gotcha_core_rust_future_poll_rust_buffer(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_CANCEL_RUST_BUFFER
#define UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_CANCEL_RUST_BUFFER
void ffi_gotcha_core_rust_future_cancel_rust_buffer(uint64_t handle
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_FREE_RUST_BUFFER
#define UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_FREE_RUST_BUFFER
void ffi_gotcha_core_rust_future_free_rust_buffer(uint64_t handle
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_COMPLETE_RUST_BUFFER
#define UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_COMPLETE_RUST_BUFFER
RustBuffer ffi_gotcha_core_rust_future_complete_rust_buffer(uint64_t handle, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_POLL_VOID
#define UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_POLL_VOID
void ffi_gotcha_core_rust_future_poll_void(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_CANCEL_VOID
#define UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_CANCEL_VOID
void ffi_gotcha_core_rust_future_cancel_void(uint64_t handle
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_FREE_VOID
#define UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_FREE_VOID
void ffi_gotcha_core_rust_future_free_void(uint64_t handle
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_COMPLETE_VOID
#define UNIFFI_FFIDEF_FFI_GOTCHA_CORE_RUST_FUTURE_COMPLETE_VOID
void ffi_gotcha_core_rust_future_complete_void(uint64_t handle, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_ACTIVE_SERVER_INDEX
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_ACTIVE_SERVER_INDEX
uint16_t uniffi_gotcha_core_checksum_method_gotchacore_active_server_index(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_ACTIVE_SERVER_NAME
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_ACTIVE_SERVER_NAME
uint16_t uniffi_gotcha_core_checksum_method_gotchacore_active_server_name(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_ADD_SERVER
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_ADD_SERVER
uint16_t uniffi_gotcha_core_checksum_method_gotchacore_add_server(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_COMMIT_DIFF
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_COMMIT_DIFF
uint16_t uniffi_gotcha_core_checksum_method_gotchacore_commit_diff(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_COMMIT_FILES
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_COMMIT_FILES
uint16_t uniffi_gotcha_core_checksum_method_gotchacore_commit_files(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_COMMITS
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_COMMITS
uint16_t uniffi_gotcha_core_checksum_method_gotchacore_commits(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_HOME
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_HOME
uint16_t uniffi_gotcha_core_checksum_method_gotchacore_home(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_ISSUE
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_ISSUE
uint16_t uniffi_gotcha_core_checksum_method_gotchacore_issue(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_ISSUES
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_ISSUES
uint16_t uniffi_gotcha_core_checksum_method_gotchacore_issues(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_MILESTONE
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_MILESTONE
uint16_t uniffi_gotcha_core_checksum_method_gotchacore_milestone(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_MILESTONES
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_MILESTONES
uint16_t uniffi_gotcha_core_checksum_method_gotchacore_milestones(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_PULL
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_PULL
uint16_t uniffi_gotcha_core_checksum_method_gotchacore_pull(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_PULL_DIFF
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_PULL_DIFF
uint16_t uniffi_gotcha_core_checksum_method_gotchacore_pull_diff(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_PULLS
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_PULLS
uint16_t uniffi_gotcha_core_checksum_method_gotchacore_pulls(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_REPOSITORIES
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_REPOSITORIES
uint16_t uniffi_gotcha_core_checksum_method_gotchacore_repositories(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_SELECT_SERVER
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_SELECT_SERVER
uint16_t uniffi_gotcha_core_checksum_method_gotchacore_select_server(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_SERVERS
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_SERVERS
uint16_t uniffi_gotcha_core_checksum_method_gotchacore_servers(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_SET_APPEARANCE
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_SET_APPEARANCE
uint16_t uniffi_gotcha_core_checksum_method_gotchacore_set_appearance(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_SET_ISSUE_STATUS
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_SET_ISSUE_STATUS
uint16_t uniffi_gotcha_core_checksum_method_gotchacore_set_issue_status(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_SET_PULL_STATUS
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_SET_PULL_STATUS
uint16_t uniffi_gotcha_core_checksum_method_gotchacore_set_pull_status(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_SETTINGS
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_SETTINGS
uint16_t uniffi_gotcha_core_checksum_method_gotchacore_settings(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_STARTUP_ERROR
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_STARTUP_ERROR
uint16_t uniffi_gotcha_core_checksum_method_gotchacore_startup_error(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_TOGGLE_FAVORITE
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_TOGGLE_FAVORITE
uint16_t uniffi_gotcha_core_checksum_method_gotchacore_toggle_favorite(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_CONSTRUCTOR_GOTCHACORE_NEW
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_CONSTRUCTOR_GOTCHACORE_NEW
uint16_t uniffi_gotcha_core_checksum_constructor_gotchacore_new(void
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_GOTCHA_CORE_UNIFFI_CONTRACT_VERSION
#define UNIFFI_FFIDEF_FFI_GOTCHA_CORE_UNIFFI_CONTRACT_VERSION
uint32_t ffi_gotcha_core_uniffi_contract_version(void
);
#endif

View File

@@ -0,0 +1 @@
#include "Generated/gotcha_coreFFI.h"

View File

@@ -7,19 +7,59 @@
objects = {
/* Begin PBXBuildFile section */
1AED04075E363FBC0FF55773 /* ContentScreens.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE5E10787062D1175125BB4C /* ContentScreens.swift */; };
25BDBDED14B3B886E19F511C /* DetailScreens.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13EFB48F40AEB3016D3CF643 /* DetailScreens.swift */; };
374320EB00185A0C8E40D985 /* ListScreens.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC828923A1E11A58AE348A74 /* ListScreens.swift */; };
950C584D58E80106DF350A22 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C0921C76676A14A024BA417 /* AppDelegate.swift */; };
96C4AFC206A1DBAE3D3E00CE /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F145F13B8ED83A5AB0009A4 /* SettingsViewController.swift */; };
D59D3ED36ABCC1D8690A9088 /* gotcha_core.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE12480725C13293FAFDDA52 /* gotcha_core.swift */; };
E7AC0B140F5CFC5EF226D174 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DDAABE6B13ADC6D08D9438AF /* Assets.xcassets */; };
EC7F8B4703DDE37A0B10CD9B /* AppContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = F61515849F6AACD721FE915C /* AppContext.swift */; };
F55A89489B2758D694F3B27D /* Support.swift in Sources */ = {isa = PBXBuildFile; fileRef = F75B3E4FFB9C9992517C4D69 /* Support.swift */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
13EFB48F40AEB3016D3CF643 /* DetailScreens.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailScreens.swift; sourceTree = "<group>"; };
5FB3250A93766966A60A685E /* Gotcha.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = Gotcha.app; sourceTree = BUILT_PRODUCTS_DIR; };
8F145F13B8ED83A5AB0009A4 /* SettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = "<group>"; };
9C0921C76676A14A024BA417 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
AC828923A1E11A58AE348A74 /* ListScreens.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListScreens.swift; sourceTree = "<group>"; };
CE5E10787062D1175125BB4C /* ContentScreens.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentScreens.swift; sourceTree = "<group>"; };
DDAABE6B13ADC6D08D9438AF /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
F61515849F6AACD721FE915C /* AppContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppContext.swift; sourceTree = "<group>"; };
F75B3E4FFB9C9992517C4D69 /* Support.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Support.swift; sourceTree = "<group>"; };
FE12480725C13293FAFDDA52 /* gotcha_core.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = gotcha_core.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXGroup section */
710A50F51478401FC642E6E3 /* Sources */ = {
isa = PBXGroup;
children = (
F61515849F6AACD721FE915C /* AppContext.swift */,
9C0921C76676A14A024BA417 /* AppDelegate.swift */,
CE5E10787062D1175125BB4C /* ContentScreens.swift */,
13EFB48F40AEB3016D3CF643 /* DetailScreens.swift */,
AC828923A1E11A58AE348A74 /* ListScreens.swift */,
8F145F13B8ED83A5AB0009A4 /* SettingsViewController.swift */,
F75B3E4FFB9C9992517C4D69 /* Support.swift */,
);
path = Sources;
sourceTree = "<group>";
};
94721140EFE7F8E7CD0F5C0B /* Generated */ = {
isa = PBXGroup;
children = (
FE12480725C13293FAFDDA52 /* gotcha_core.swift */,
);
path = Generated;
sourceTree = "<group>";
};
A8558BC8DD12191B80F52573 = {
isa = PBXGroup;
children = (
DDAABE6B13ADC6D08D9438AF /* Assets.xcassets */,
94721140EFE7F8E7CD0F5C0B /* Generated */,
710A50F51478401FC642E6E3 /* Sources */,
F059299C038F3CAFCE470831 /* Products */,
);
sourceTree = "<group>";
@@ -39,8 +79,8 @@
isa = PBXNativeTarget;
buildConfigurationList = 7E8DF3DDA64C8C998FF3BB1D /* Build configuration list for PBXNativeTarget "Gotcha" */;
buildPhases = (
E01FE2CE8A64C51D25060BE9 /* Build Rust core */,
409DBF67E9C2743801B8F8A4 /* Sources */,
6334AD54CF86DAD2EC201EBC /* Build Rust app */,
9EDD4380917C29EE67EE344B /* Resources */,
);
buildRules = (
@@ -63,6 +103,10 @@
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1430;
TargetAttributes = {
60C9DF1AB4A7858833302BA5 = {
DevelopmentTeam = MU22FMRGK8;
ProvisioningStyle = Automatic;
};
};
};
buildConfigurationList = 807BC8918EC1B0F31384F2B7 /* Build configuration list for PBXProject "Gotcha" */;
@@ -96,7 +140,7 @@
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
6334AD54CF86DAD2EC201EBC /* Build Rust app */ = {
E01FE2CE8A64C51D25060BE9 /* Build Rust core */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
@@ -106,15 +150,15 @@
);
inputPaths = (
);
name = "Build Rust app";
name = "Build Rust core";
outputFileListPaths = (
);
outputPaths = (
"$(TARGET_BUILD_DIR)/$(EXECUTABLE_PATH)",
"$(DERIVED_FILE_DIR)/rust/libgotcha_core.a",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "./build_for_ios_with_cargo.bash gotcha-app\n";
shellScript = "./build_rust_core.bash\n";
};
/* End PBXShellScriptBuildPhase section */
@@ -123,6 +167,14 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
EC7F8B4703DDE37A0B10CD9B /* AppContext.swift in Sources */,
950C584D58E80106DF350A22 /* AppDelegate.swift in Sources */,
1AED04075E363FBC0FF55773 /* ContentScreens.swift in Sources */,
25BDBDED14B3B886E19F511C /* DetailScreens.swift in Sources */,
374320EB00185A0C8E40D985 /* ListScreens.swift in Sources */,
96C4AFC206A1DBAE3D3E00CE /* SettingsViewController.swift in Sources */,
F55A89489B2758D694F3B27D /* Support.swift in Sources */,
D59D3ED36ABCC1D8690A9088 /* gotcha_core.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -135,15 +187,21 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = Gotcha.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = MU22FMRGK8;
INFOPLIST_FILE = Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
LIBRARY_SEARCH_PATHS = "$(inherited) $(DERIVED_FILE_DIR)/rust";
OTHER_LDFLAGS = "$(inherited) -lgotcha_core";
PRODUCT_BUNDLE_IDENTIFIER = de.rfc1437.gotcha;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
SWIFT_OBJC_BRIDGING_HEADER = "Gotcha-Bridging-Header.h";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
};
name = Debug;
};
@@ -153,15 +211,21 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = Gotcha.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = MU22FMRGK8;
INFOPLIST_FILE = Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
LIBRARY_SEARCH_PATHS = "$(inherited) $(DERIVED_FILE_DIR)/rust";
OTHER_LDFLAGS = "$(inherited) -lgotcha_core";
PRODUCT_BUNDLE_IDENTIFIER = de.rfc1437.gotcha;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
SWIFT_OBJC_BRIDGING_HEADER = "Gotcha-Bridging-Header.h";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
};
name = Release;
};

View File

@@ -0,0 +1,134 @@
import UIKit
@MainActor
final class AppContext {
let core = GotchaCore()
private let window: UIWindow
private(set) var tabs = UITabBarController()
private(set) var navigationControllers: [UINavigationController] = []
init(window: UIWindow) {
self.window = window
applyAppearance()
}
func makeRootController() -> UIViewController {
let roots: [UIViewController] = [
HomeViewController(context: self),
repositoryRoot(mode: .issues),
repositoryRoot(mode: .commits),
PullsViewController(context: self),
repositoryRoot(mode: .milestones),
]
let items = [
("Home", "house", "house.fill"),
("Issues", "exclamationmark.circle", "exclamationmark.circle.fill"),
("Repos", "books.vertical", "books.vertical.fill"),
("PRs", "arrow.triangle.pull", "arrow.triangle.pull"),
("Milestones", "flag", "flag.fill"),
]
navigationControllers = zip(roots, items).map { root, item in
let navigation = UINavigationController(rootViewController: root)
navigation.tabBarItem = UITabBarItem(
title: item.0,
image: UIImage(systemName: item.1),
selectedImage: UIImage(systemName: item.2)
)
return navigation
}
tabs.viewControllers = navigationControllers
return tabs
}
func showStartupErrorIfNeeded() {
guard let message = core.startupError() else { return }
tabs.present(errorAlert(message), animated: true)
}
func selectServer(index: UInt32) throws {
try core.selectServer(index: index)
let replacements: [(Int, UIViewController)] = [
(0, HomeViewController(context: self)),
(1, RepositoriesViewController(context: self, mode: .issues)),
(2, RepositoriesViewController(context: self, mode: .commits)),
(3, PullsViewController(context: self)),
(4, RepositoriesViewController(context: self, mode: .milestones)),
]
for (index, root) in replacements {
navigationControllers[index].setViewControllers([root], animated: false)
}
}
func didAddServer(index: UInt32) throws {
try selectServer(index: index)
}
func applyAppearance() {
switch core.settings().appearance {
case 1: window.overrideUserInterfaceStyle = .light
case 2: window.overrideUserInterfaceStyle = .dark
default: window.overrideUserInterfaceStyle = .unspecified
}
}
func route(_ activity: ActivityRow) {
switch activity.target {
case "repository":
tabs.selectedIndex = 1
navigationControllers[1].pushViewController(
IssuesViewController(
context: self,
owner: activity.owner,
repository: activity.repository
),
animated: true
)
case "issue":
tabs.selectedIndex = 1
navigationControllers[1].pushViewController(
IssueViewController(
context: self,
owner: activity.owner,
repository: activity.repository,
number: activity.number
),
animated: true
)
case "pull":
tabs.selectedIndex = 3
navigationControllers[3].pushViewController(
PullViewController(
context: self,
owner: activity.owner,
repository: activity.repository,
number: activity.number
),
animated: true
)
case "commit":
tabs.selectedIndex = 2
navigationControllers[2].pushViewController(
FilesViewController(
context: self,
owner: activity.owner,
repository: activity.repository,
sha: activity.sha
),
animated: true
)
default:
break
}
}
func symbol(_ name: String) -> UIImage? {
UIImage(systemName: name)
}
private func repositoryRoot(mode: RepositoryMode) -> UIViewController {
if core.activeServerIndex() == nil {
return ServersViewController(context: self, destination: mode)
}
return RepositoriesViewController(context: self, mode: mode)
}
}

View File

@@ -0,0 +1,19 @@
import UIKit
@main
final class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
let window = UIWindow(frame: UIScreen.main.bounds)
let context = AppContext(window: window)
window.rootViewController = context.makeRootController()
window.makeKeyAndVisible()
self.window = window
context.showStartupErrorIfNeeded()
return true
}
}

View File

@@ -0,0 +1,654 @@
import UIKit
@MainActor
final class IssuesViewController: RefreshingTableViewController {
private let context: AppContext
private let owner: String
private let repository: String
private var rows: [IssueRow] = []
init(context: AppContext, owner: String, repository: String) {
self.context = context
self.owner = owner
self.repository = repository
super.init()
title = repository
}
@available(*, unavailable)
required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(IssueCell.self, forCellReuseIdentifier: "issue")
updateFilterMenu()
loadContent(refreshing: false)
}
override func loadContent(refreshing: Bool) {
beginLoading(refreshing: refreshing)
loadingTask?.cancel()
loadingTask = Task {
do {
rows = try await context.core.issues(owner: owner, repository: repository)
tableView.reloadData()
let status = context.core.settings().issueStatus
tableView.backgroundView = rows.isEmpty
? EmptyBackgroundView(
title: "No \(status) issues",
detail: "No issues match the selected status."
)
: nil
} catch {
if !Task.isCancelled { show(error: error) }
}
endLoading()
}
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
rows.count
}
override func tableView(
_ tableView: UITableView,
cellForRowAt indexPath: IndexPath
) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "issue", for: indexPath) as! IssueCell
cell.configure(rows[indexPath.row])
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
navigationController?.pushViewController(
IssueViewController(
context: context,
owner: owner,
repository: repository,
number: rows[indexPath.row].number
),
animated: true
)
}
private func updateFilterMenu() {
let current = context.core.settings().issueStatus
navigationItem.rightBarButtonItem = UIBarButtonItem(
image: context.symbol("line.3.horizontal.decrease.circle"),
menu: UIMenu(children: ["open", "closed"].map { status in
UIAction(
title: status.capitalized,
state: current == status ? .on : .off
) { [weak self] _ in
guard let self else { return }
do {
try self.context.core.setIssueStatus(status: status)
self.updateFilterMenu()
self.loadContent(refreshing: false)
} catch {
self.show(error: error)
}
}
})
)
}
}
final class IssueCell: UITableViewCell {
private let titleLabel = UILabel()
private let summaryLabel = UILabel()
private let labels = UIStackView()
private let metaLabel = UILabel()
private let milestoneLabel = UILabel()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
accessoryType = .disclosureIndicator
titleLabel.font = .preferredFont(forTextStyle: .headline)
titleLabel.numberOfLines = 2
summaryLabel.font = .preferredFont(forTextStyle: .subheadline)
summaryLabel.textColor = .secondaryLabel
summaryLabel.numberOfLines = 2
labels.axis = .horizontal
labels.spacing = 5
metaLabel.font = .preferredFont(forTextStyle: .caption1)
metaLabel.textColor = .tertiaryLabel
metaLabel.numberOfLines = 2
milestoneLabel.font = .preferredFont(forTextStyle: .caption1)
milestoneLabel.adjustsFontForContentSizeCategory = true
let stack = UIStackView(
arrangedSubviews: [titleLabel, summaryLabel, labels, metaLabel, milestoneLabel]
)
stack.axis = .vertical
stack.spacing = 6
stack.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(stack)
NSLayoutConstraint.activate([
stack.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
stack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -8),
stack.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10),
stack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -10),
])
}
@available(*, unavailable)
required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
func configure(_ row: IssueRow) {
titleLabel.text = row.title
summaryLabel.text = row.summary
metaLabel.text = row.meta
milestoneLabel.isHidden = row.milestone.isEmpty
milestoneLabel.attributedText = symbolText(
"flag.fill",
text: row.milestone,
font: milestoneLabel.font,
color: .tertiaryLabel
)
milestoneLabel.accessibilityLabel = row.milestone.isEmpty
? nil
: "Milestone \(row.milestone)"
labels.arrangedSubviews.forEach { $0.removeFromSuperview() }
labels.isHidden = row.labels.isEmpty
for label in row.labels.prefix(3) {
let view = UILabel()
view.text = " \(label.name) "
view.font = .preferredFont(forTextStyle: .caption2)
view.textColor = label.light ? .black : .white
view.backgroundColor = UIColor(hex: label.color)
view.layer.cornerRadius = 9
view.clipsToBounds = true
labels.addArrangedSubview(view)
}
labels.addArrangedSubview(UIView())
}
}
@MainActor
final class MilestonesViewController: RefreshingTableViewController {
private let context: AppContext
private let owner: String
private let repository: String
private var rows: [MilestoneRow] = []
init(context: AppContext, owner: String, repository: String) {
self.context = context
self.owner = owner
self.repository = repository
super.init()
title = repository
}
@available(*, unavailable)
required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(MilestoneCell.self, forCellReuseIdentifier: "milestone")
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 118
loadContent(refreshing: false)
}
override func loadContent(refreshing: Bool) {
beginLoading(refreshing: refreshing)
loadingTask?.cancel()
loadingTask = Task {
do {
rows = try await context.core.milestones(owner: owner, repository: repository)
tableView.reloadData()
tableView.backgroundView = rows.isEmpty
? EmptyBackgroundView(
title: "No milestones",
detail: "This repository does not have any milestones."
)
: nil
} catch {
if !Task.isCancelled { show(error: error) }
}
endLoading()
}
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
rows.count
}
override func tableView(
_ tableView: UITableView,
cellForRowAt indexPath: IndexPath
) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(
withIdentifier: "milestone",
for: indexPath
) as! MilestoneCell
cell.configure(rows[indexPath.row], disclosure: true)
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
navigationController?.pushViewController(
MilestoneViewController(
context: context,
owner: owner,
repository: repository,
id: rows[indexPath.row].id
),
animated: true
)
}
}
@MainActor
final class MilestoneViewController: RefreshingTableViewController {
private let context: AppContext
private let owner: String
private let repository: String
private let id: Int64
private var page: MilestonePage?
init(context: AppContext, owner: String, repository: String, id: Int64) {
self.context = context
self.owner = owner
self.repository = repository
self.id = id
super.init()
title = "Milestone"
}
@available(*, unavailable)
required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(MilestoneCell.self, forCellReuseIdentifier: "milestone")
tableView.register(IssueCell.self, forCellReuseIdentifier: "issue")
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 118
loadContent(refreshing: false)
}
override func loadContent(refreshing: Bool) {
beginLoading(refreshing: refreshing)
loadingTask?.cancel()
loadingTask = Task {
do {
page = try await context.core.milestone(
owner: owner,
repository: repository,
id: id
)
title = page?.milestone.title
tableView.reloadData()
} catch {
if !Task.isCancelled { show(error: error) }
}
endLoading()
}
}
override func numberOfSections(in tableView: UITableView) -> Int { page == nil ? 0 : 2 }
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
section == 0 ? 1 : page?.issues.count ?? 0
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
section == 1 ? "Issues" : nil
}
override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
section == 1 && page?.issues.isEmpty == true ? "No issues are assigned to this milestone." : nil
}
override func tableView(
_ tableView: UITableView,
cellForRowAt indexPath: IndexPath
) -> UITableViewCell {
guard let page else { return UITableViewCell() }
if indexPath.section == 0 {
let cell = tableView.dequeueReusableCell(
withIdentifier: "milestone",
for: indexPath
) as! MilestoneCell
cell.configure(page.milestone, disclosure: false)
return cell
}
let cell = tableView.dequeueReusableCell(withIdentifier: "issue", for: indexPath) as! IssueCell
cell.configure(page.issues[indexPath.row])
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard indexPath.section == 1, let issue = page?.issues[indexPath.row] else { return }
tableView.deselectRow(at: indexPath, animated: true)
navigationController?.pushViewController(
IssueViewController(
context: context,
owner: owner,
repository: repository,
number: issue.number
),
animated: true
)
}
}
final class MilestoneCell: UITableViewCell {
private let titleLabel = UILabel()
private let descriptionLabel = UILabel()
private let metaLabel = UILabel()
private let progress = UIProgressView(progressViewStyle: .bar)
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
titleLabel.font = .preferredFont(forTextStyle: .headline)
titleLabel.numberOfLines = 2
descriptionLabel.font = .preferredFont(forTextStyle: .subheadline)
descriptionLabel.textColor = .secondaryLabel
descriptionLabel.numberOfLines = 2
metaLabel.font = .preferredFont(forTextStyle: .caption1)
metaLabel.textColor = .tertiaryLabel
metaLabel.numberOfLines = 2
progress.progressTintColor = .systemGreen
let stack = UIStackView(arrangedSubviews: [titleLabel, descriptionLabel, progress, metaLabel])
stack.axis = .vertical
stack.spacing = 7
stack.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(stack)
NSLayoutConstraint.activate([
stack.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
stack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
stack.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 12),
stack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -12),
])
}
@available(*, unavailable)
required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
func configure(_ row: MilestoneRow, disclosure: Bool) {
accessoryType = disclosure ? .disclosureIndicator : .none
titleLabel.text = row.title
descriptionLabel.text = row.description
metaLabel.text = row.meta
let total = row.openIssues + row.closedIssues
progress.progress = total == 0 ? 0 : Float(row.closedIssues) / Float(total)
progress.trackTintColor = total == 0 ? .systemGray5 : .systemOrange
progress.accessibilityLabel = "Milestone progress"
progress.accessibilityValue = "\(row.closedIssues) closed, \(row.openIssues) open"
}
}
@MainActor
final class PullsViewController: RefreshingTableViewController {
private let context: AppContext
private var rows: [PullRow] = []
init(context: AppContext) {
self.context = context
super.init()
title = "Pull Requests"
}
@available(*, unavailable)
required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
override func viewDidLoad() {
super.viewDidLoad()
updateFilterMenu()
loadContent(refreshing: false)
}
override func loadContent(refreshing: Bool) {
guard context.core.activeServerIndex() != nil else {
tableView.backgroundView = EmptyBackgroundView(
title: "No server selected",
detail: "Open Issues or Repos to select a server or add your first one."
)
refreshControl?.endRefreshing()
return
}
beginLoading(refreshing: refreshing)
loadingTask?.cancel()
loadingTask = Task {
do {
rows = try await context.core.pulls()
tableView.reloadData()
let status = context.core.settings().pullStatus
tableView.backgroundView = rows.isEmpty
? EmptyBackgroundView(
title: "No \(status) pull requests",
detail: "No pull requests match the selected status."
)
: nil
} catch {
if !Task.isCancelled { show(error: error) }
}
endLoading()
}
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
rows.count
}
override func tableView(
_ tableView: UITableView,
cellForRowAt indexPath: IndexPath
) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "pull")
?? UITableViewCell(style: .subtitle, reuseIdentifier: "pull")
let row = rows[indexPath.row]
configureTextCell(
cell,
title: "\(row.repository) #\(row.number)\n\(row.title)",
detail: "\(row.summary)\n\(row.meta)"
)
cell.accessoryType = .disclosureIndicator
return cell
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
116
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let row = rows[indexPath.row]
navigationController?.pushViewController(
PullViewController(
context: context,
owner: row.owner,
repository: row.repository,
number: row.number
),
animated: true
)
}
private func updateFilterMenu() {
let current = context.core.settings().pullStatus
navigationItem.rightBarButtonItem = UIBarButtonItem(
image: context.symbol("line.3.horizontal.decrease.circle"),
menu: UIMenu(children: ["open", "closed"].map { status in
UIAction(title: status.capitalized, state: current == status ? .on : .off) {
[weak self] _ in
guard let self else { return }
do {
try self.context.core.setPullStatus(status: status)
self.updateFilterMenu()
self.loadContent(refreshing: false)
} catch {
self.show(error: error)
}
}
})
)
}
}
@MainActor
final class CommitsViewController: RefreshingTableViewController {
private let context: AppContext
private let owner: String
private let repository: String
private var page: CommitPage?
private var branch: String?
init(context: AppContext, owner: String, repository: String) {
self.context = context
self.owner = owner
self.repository = repository
super.init()
title = repository
}
@available(*, unavailable)
required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(CommitCell.self, forCellReuseIdentifier: "commit")
updateBranchMenu()
loadContent(refreshing: false)
}
override func loadContent(refreshing: Bool) {
beginLoading(refreshing: refreshing)
loadingTask?.cancel()
loadingTask = Task {
do {
page = try await context.core.commits(
owner: owner,
repository: repository,
branch: branch
)
updateBranchMenu()
tableView.reloadData()
tableView.backgroundView = page?.commits.isEmpty == true
? EmptyBackgroundView(title: "No commits", detail: "This repository has no commit history.")
: nil
} catch {
if !Task.isCancelled { show(error: error) }
}
endLoading()
}
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
page?.commits.count ?? 0
}
override func tableView(
_ tableView: UITableView,
cellForRowAt indexPath: IndexPath
) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "commit", for: indexPath) as! CommitCell
if let page { cell.configure(page.commits[indexPath.row], laneCount: page.laneCount) }
return cell
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
86
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
guard let row = page?.commits[indexPath.row] else { return }
navigationController?.pushViewController(
FilesViewController(
context: context,
owner: owner,
repository: repository,
sha: row.sha
),
animated: true
)
}
private func updateBranchMenu() {
let branches = page?.branches ?? []
let choices: [String?] = [nil] + branches.map(Optional.some)
navigationItem.rightBarButtonItem = UIBarButtonItem(
title: branch ?? "All",
menu: UIMenu(children: choices.map { choice in
UIAction(
title: choice ?? "All",
state: choice == branch ? .on : .off
) { [weak self] _ in
self?.branch = choice
self?.updateBranchMenu()
self?.loadContent(refreshing: false)
}
})
)
}
}
final class CommitCell: UITableViewCell {
private var row: CommitRow?
private var laneCount: UInt32 = 0
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
accessoryType = .disclosureIndicator
backgroundColor = .systemBackground
}
@available(*, unavailable)
required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
func configure(_ row: CommitRow, laneCount: UInt32) {
self.row = row
self.laneCount = laneCount
var content = defaultContentConfiguration()
content.directionalLayoutMargins.leading = laneCount == 0 ? 0 : min(72, CGFloat(laneCount) * 10 + 8)
content.text = row.title
content.secondaryText = row.refs.isEmpty
? "\(row.meta)\n\(row.sha.prefix(8))"
: "\(row.refs)\n\(row.meta) · \(row.sha.prefix(8))"
content.secondaryTextProperties.numberOfLines = 2
contentConfiguration = content
setNeedsDisplay()
}
override func draw(_ rect: CGRect) {
super.draw(rect)
guard let row, laneCount > 0, let context = UIGraphicsGetCurrentContext() else { return }
let spacing: CGFloat = 10
let centerY = bounds.midY
for lane in 0..<laneCount {
let x = 10 + CGFloat(lane) * spacing
let color = laneColor(lane)
context.setStrokeColor(color.cgColor)
context.setLineWidth(3)
if row.topLanes.contains(lane) {
context.move(to: CGPoint(x: x, y: 0))
context.addLine(to: CGPoint(x: x, y: centerY))
context.strokePath()
}
if row.bottomLanes.contains(lane) {
context.move(to: CGPoint(x: x, y: centerY))
context.addLine(to: CGPoint(x: x, y: bounds.height))
context.strokePath()
}
if row.connections.contains(lane), lane > 0 {
context.move(to: CGPoint(x: x - spacing, y: centerY))
context.addLine(to: CGPoint(x: x, y: centerY))
context.strokePath()
}
if row.nodeLane == lane {
context.setFillColor(color.cgColor)
context.fillEllipse(in: CGRect(x: x - 5, y: centerY - 5, width: 10, height: 10))
}
}
}
private func laneColor(_ lane: UInt32) -> UIColor {
UIColor(hue: CGFloat((Double(lane) * 137.508).truncatingRemainder(dividingBy: 360)) / 360,
saturation: 0.78, brightness: 0.78, alpha: 1)
}
}

View File

@@ -0,0 +1,453 @@
import UIKit
@MainActor
class MarkdownPageViewController: UIViewController {
let context: AppContext
let scrollView = UIScrollView()
let stack = UIStackView()
private let spinner = UIActivityIndicatorView(style: .medium)
var loadingTask: Task<Void, Never>?
init(context: AppContext) {
self.context = context
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemGroupedBackground
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.alwaysBounceVertical = true
scrollView.refreshControl = UIRefreshControl()
scrollView.refreshControl?.addTarget(self, action: #selector(refreshRequested), for: .valueChanged)
view.addSubview(scrollView)
stack.axis = .vertical
stack.spacing = 12
stack.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(stack)
NSLayoutConstraint.activate([
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
scrollView.topAnchor.constraint(equalTo: view.topAnchor),
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
stack.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor, constant: 18),
stack.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor, constant: -18),
stack.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor, constant: 18),
stack.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor, constant: -18),
stack.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor, constant: -36),
])
}
deinit { loadingTask?.cancel() }
func loadContent(refreshing: Bool) {}
func beginLoading(refreshing: Bool) {
if !refreshing { beginNavigationLoading(spinner) }
}
func endLoading() {
endNavigationLoading(spinner)
scrollView.refreshControl?.endRefreshing()
}
func replaceContent(_ views: [UIView]) {
stack.arrangedSubviews.forEach { $0.removeFromSuperview() }
views.forEach(stack.addArrangedSubview)
}
@objc private func refreshRequested() {
loadContent(refreshing: true)
}
}
@MainActor
final class IssueViewController: MarkdownPageViewController {
private let owner: String
private let repository: String
private let number: Int64
init(context: AppContext, owner: String, repository: String, number: Int64) {
self.owner = owner
self.repository = repository
self.number = number
super.init(context: context)
title = "Issue"
}
@available(*, unavailable)
required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
override func viewDidLoad() {
super.viewDidLoad()
loadContent(refreshing: false)
}
override func loadContent(refreshing: Bool) {
beginLoading(refreshing: refreshing)
loadingTask?.cancel()
loadingTask = Task {
do {
let page = try await context.core.issue(
owner: owner,
repository: repository,
number: number
)
replaceContent(detailViews(
title: page.title,
meta: page.meta,
body: page.body,
comments: page.comments,
milestone: page.milestone
))
} catch {
if !Task.isCancelled { show(error: error) }
}
endLoading()
}
}
}
@MainActor
final class PullViewController: MarkdownPageViewController {
private let owner: String
private let repository: String
private let number: Int64
init(context: AppContext, owner: String, repository: String, number: Int64) {
self.owner = owner
self.repository = repository
self.number = number
super.init(context: context)
title = "Pull Request"
}
@available(*, unavailable)
required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
override func viewDidLoad() {
super.viewDidLoad()
loadContent(refreshing: false)
}
override func loadContent(refreshing: Bool) {
beginLoading(refreshing: refreshing)
loadingTask?.cancel()
loadingTask = Task {
do {
let page = try await context.core.pull(
owner: owner,
repository: repository,
number: number
)
var views = detailViews(
title: page.title,
meta: page.meta,
body: page.body,
comments: []
)
if !page.files.isEmpty {
views.append(sectionLabel(page.filesRef))
views.append(contentsOf: page.files.map { file in
detailButton(title: file.path, detail: file.status) { [weak self] in
guard let self else { return }
self.navigationController?.pushViewController(
DiffViewController(
context: self.context,
source: .pull(
owner: self.owner,
repository: self.repository,
number: self.number,
path: file.path
)
),
animated: true
)
}
})
}
views.append(contentsOf: commentViews(page.comments))
replaceContent(views)
} catch {
if !Task.isCancelled { show(error: error) }
}
endLoading()
}
}
}
@MainActor
final class FilesViewController: RefreshingTableViewController {
private let context: AppContext
private let owner: String
private let repository: String
private let sha: String
private var rows: [FileRow] = []
init(context: AppContext, owner: String, repository: String, sha: String) {
self.context = context
self.owner = owner
self.repository = repository
self.sha = sha
super.init()
title = "Changed Files"
}
@available(*, unavailable)
required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
override func viewDidLoad() {
super.viewDidLoad()
loadContent(refreshing: false)
}
override func loadContent(refreshing: Bool) {
beginLoading(refreshing: refreshing)
loadingTask?.cancel()
loadingTask = Task {
do {
rows = try await context.core.commitFiles(
owner: owner,
repository: repository,
sha: sha
)
tableView.reloadData()
tableView.backgroundView = rows.isEmpty
? EmptyBackgroundView(title: "No changed files", detail: "This commit does not contain file changes.")
: nil
} catch {
if !Task.isCancelled { show(error: error) }
}
endLoading()
}
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
rows.count
}
override func tableView(
_ tableView: UITableView,
cellForRowAt indexPath: IndexPath
) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "file")
?? UITableViewCell(style: .subtitle, reuseIdentifier: "file")
let row = rows[indexPath.row]
configureTextCell(cell, title: row.path, detail: row.status)
cell.accessoryType = .disclosureIndicator
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let row = rows[indexPath.row]
navigationController?.pushViewController(
DiffViewController(
context: context,
source: .commit(
owner: owner,
repository: repository,
sha: sha,
path: row.path
)
),
animated: true
)
}
}
enum DiffSource {
case commit(owner: String, repository: String, sha: String, path: String)
case pull(owner: String, repository: String, number: Int64, path: String)
}
@MainActor
final class DiffViewController: UIViewController {
private let context: AppContext
private let source: DiffSource
private let textView = UITextView()
private let spinner = UIActivityIndicatorView(style: .medium)
private var loadingTask: Task<Void, Never>?
init(context: AppContext, source: DiffSource) {
self.context = context
self.source = source
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
textView.translatesAutoresizingMaskIntoConstraints = false
textView.isEditable = false
textView.isSelectable = true
textView.alwaysBounceVertical = true
textView.alwaysBounceHorizontal = true
textView.showsHorizontalScrollIndicator = true
textView.textContainer.widthTracksTextView = false
textView.textContainer.lineFragmentPadding = 8
textView.refreshControl = UIRefreshControl()
textView.refreshControl?.addTarget(self, action: #selector(reload), for: .valueChanged)
view.addSubview(textView)
NSLayoutConstraint.activate([
textView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
textView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
textView.topAnchor.constraint(equalTo: view.topAnchor),
textView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
beginNavigationLoading(spinner)
reload()
}
deinit { loadingTask?.cancel() }
@objc private func reload() {
loadingTask?.cancel()
loadingTask = Task {
do {
let page: DiffPage
switch source {
case let .commit(owner, repository, sha, path):
page = try await context.core.commitDiff(
owner: owner,
repository: repository,
sha: sha,
path: path
)
case let .pull(owner, repository, number, path):
page = try await context.core.pullDiff(
owner: owner,
repository: repository,
number: number,
path: path
)
}
title = page.title
textView.attributedText = diffText(page)
} catch {
if !Task.isCancelled { show(error: error) }
}
endNavigationLoading(spinner)
textView.refreshControl?.endRefreshing()
}
}
private func diffText(_ page: DiffPage) -> NSAttributedString {
let output = NSMutableAttributedString()
let font = UIFont.monospacedSystemFont(ofSize: 12, weight: .regular)
for line in page.lines {
let text = String(format: "%4@ %4@ %@\n", line.oldNumber, line.newNumber, line.text)
let color: UIColor
switch line.kind {
case "addition": color = UIColor.systemGreen.withAlphaComponent(0.16)
case "removal": color = UIColor.systemRed.withAlphaComponent(0.16)
case "hunk": color = UIColor.systemBlue.withAlphaComponent(0.14)
case "header": color = UIColor.systemGray.withAlphaComponent(0.14)
default: color = .clear
}
output.append(NSAttributedString(string: text, attributes: [
.font: font,
.foregroundColor: UIColor.label,
.backgroundColor: color,
]))
}
return output
}
}
private func detailViews(
title: String,
meta: String,
body: String,
comments: [CommentRow],
milestone: String = ""
) -> [UIView] {
let titleLabel = UILabel()
titleLabel.text = title
titleLabel.font = .preferredFont(forTextStyle: .title1)
titleLabel.numberOfLines = 0
let metaLabel = UILabel()
metaLabel.text = meta
metaLabel.font = .preferredFont(forTextStyle: .subheadline)
metaLabel.textColor = .secondaryLabel
metaLabel.numberOfLines = 0
let bodyView = markdownView(body)
var views: [UIView] = [titleLabel, metaLabel]
if !milestone.isEmpty {
let milestoneLabel = UILabel()
milestoneLabel.attributedText = symbolText(
"flag.fill",
text: milestone,
font: .preferredFont(forTextStyle: .subheadline),
color: .secondaryLabel
)
milestoneLabel.accessibilityLabel = "Milestone \(milestone)"
views.append(milestoneLabel)
}
return views + [separator(), bodyView] + commentViews(comments)
}
private func commentViews(_ comments: [CommentRow]) -> [UIView] {
guard !comments.isEmpty else { return [] }
var views: [UIView] = [sectionLabel("Comments")]
for comment in comments {
let author = UILabel()
author.text = comment.author
author.font = .preferredFont(forTextStyle: .headline)
let date = UILabel()
date.text = comment.meta
date.font = .preferredFont(forTextStyle: .caption1)
date.textColor = .tertiaryLabel
let stack = UIStackView(arrangedSubviews: [author, markdownView(comment.body), date, separator()])
stack.axis = .vertical
stack.spacing = 6
views.append(stack)
}
return views
}
private func markdownView(_ source: String) -> UITextView {
let view = UITextView()
view.attributedText = markdown(source)
view.isEditable = false
view.isSelectable = true
view.isScrollEnabled = false
view.backgroundColor = .clear
view.textContainerInset = .zero
view.textContainer.lineFragmentPadding = 0
view.adjustsFontForContentSizeCategory = true
return view
}
private func sectionLabel(_ text: String) -> UILabel {
let label = UILabel()
label.text = text
label.font = .preferredFont(forTextStyle: .headline)
return label
}
private func separator() -> UIView {
let line = UIView()
line.backgroundColor = .separator
line.heightAnchor.constraint(equalToConstant: 1 / UIScreen.main.scale).isActive = true
return line
}
private func detailButton(title: String, detail: String, action: @escaping () -> Void) -> UIButton {
var configuration = UIButton.Configuration.plain()
configuration.title = title
configuration.subtitle = detail
configuration.image = UIImage(systemName: "chevron.right")
configuration.imagePlacement = .trailing
configuration.imagePadding = 8
configuration.contentInsets = .init(top: 10, leading: 0, bottom: 10, trailing: 0)
let button = UIButton(configuration: configuration, primaryAction: UIAction { _ in action() })
button.contentHorizontalAlignment = .fill
return button
}

View File

@@ -0,0 +1,454 @@
import UIKit
enum RepositoryMode {
case issues
case commits
case milestones
}
@MainActor
final class ServersViewController: UITableViewController {
private let context: AppContext
private let destination: RepositoryMode
private var servers: [ServerRow] = []
init(context: AppContext, destination: RepositoryMode) {
self.context = context
self.destination = destination
super.init(style: .plain)
title = "Servers"
tableView.backgroundColor = .systemGroupedBackground
tableView.separatorInset = .zero
}
@available(*, unavailable)
required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
servers = context.core.servers()
tableView.reloadData()
tableView.backgroundView = servers.isEmpty
? EmptyBackgroundView(title: "No servers", detail: "Add a Gitea server to get started.")
: nil
navigationItem.rightBarButtonItem = UIBarButtonItem(
systemItem: .add,
primaryAction: UIAction { [weak self] _ in self?.showAddServer() }
)
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
servers.count
}
override func tableView(
_ tableView: UITableView,
cellForRowAt indexPath: IndexPath
) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "server")
?? UITableViewCell(style: .subtitle, reuseIdentifier: "server")
let server = servers[indexPath.row]
configureTextCell(cell, title: server.name, detail: server.url)
cell.accessoryType = .disclosureIndicator
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
do {
try context.selectServer(index: UInt32(indexPath.row))
navigationController?.setViewControllers([
RepositoriesViewController(context: context, mode: destination),
], animated: true)
} catch {
show(error: error)
}
}
private func showAddServer() {
let controller = AddServerViewController(context: context) { [weak self] in
guard let self else { return }
self.servers = self.context.core.servers()
self.tableView.reloadData()
}
present(UINavigationController(rootViewController: controller), animated: true)
}
}
@MainActor
final class AddServerViewController: UITableViewController, UITextFieldDelegate {
private let context: AppContext
private let completion: () -> Void
private let nameField = UITextField()
private let urlField = UITextField()
private let tokenField = UITextField()
private var saveButton: UIBarButtonItem!
init(context: AppContext, completion: @escaping () -> Void) {
self.context = context
self.completion = completion
super.init(style: .insetGrouped)
title = "Add Server"
}
@available(*, unavailable)
required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.leftBarButtonItem = UIBarButtonItem(
systemItem: .cancel,
primaryAction: UIAction { [weak self] _ in self?.dismiss(animated: true) }
)
saveButton = UIBarButtonItem(
title: "Add",
style: .done,
target: self,
action: #selector(save)
)
navigationItem.rightBarButtonItem = saveButton
configure(nameField, placeholder: "Work", contentType: .name)
configure(urlField, placeholder: "https://gitea.example.com", contentType: .URL)
urlField.keyboardType = .URL
urlField.autocapitalizationType = .none
configure(tokenField, placeholder: "Access token", contentType: nil)
tokenField.isSecureTextEntry = true
tokenField.autocapitalizationType = .none
tokenField.returnKeyType = .done
}
override func numberOfSections(in tableView: UITableView) -> Int { 3 }
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 1 }
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
["Name", "Server URL", "Access token"][section]
}
override func tableView(
_ tableView: UITableView,
cellForRowAt indexPath: IndexPath
) -> UITableViewCell {
let cell = UITableViewCell(style: .default, reuseIdentifier: nil)
let field = [nameField, urlField, tokenField][indexPath.section]
field.translatesAutoresizingMaskIntoConstraints = false
cell.contentView.addSubview(field)
NSLayoutConstraint.activate([
field.leadingAnchor.constraint(equalTo: cell.contentView.leadingAnchor, constant: 16),
field.trailingAnchor.constraint(equalTo: cell.contentView.trailingAnchor, constant: -16),
field.topAnchor.constraint(equalTo: cell.contentView.topAnchor),
field.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor),
cell.contentView.heightAnchor.constraint(greaterThanOrEqualToConstant: 48),
])
return cell
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
if textField === nameField { urlField.becomeFirstResponder() }
else if textField === urlField { tokenField.becomeFirstResponder() }
else { save() }
return true
}
@objc private func save() {
view.endEditing(true)
saveButton.isEnabled = false
let spinner = UIActivityIndicatorView(style: .medium)
spinner.startAnimating()
navigationItem.rightBarButtonItem = UIBarButtonItem(customView: spinner)
Task {
do {
let index = try await context.core.addServer(
name: nameField.text ?? "",
url: urlField.text ?? "",
token: tokenField.text ?? ""
)
try context.didAddServer(index: index)
completion()
dismiss(animated: true)
} catch {
navigationItem.rightBarButtonItem = saveButton
saveButton.isEnabled = true
show(error: error)
}
}
}
private func configure(
_ field: UITextField,
placeholder: String,
contentType: UITextContentType?
) {
field.placeholder = placeholder
field.textContentType = contentType
field.clearButtonMode = .whileEditing
field.delegate = self
field.returnKeyType = .next
field.adjustsFontForContentSizeCategory = true
field.font = .preferredFont(forTextStyle: .body)
}
}
@MainActor
final class RepositoriesViewController: RefreshingTableViewController {
private let context: AppContext
private let mode: RepositoryMode
private var rows: [RepositoryRow] = []
init(context: AppContext, mode: RepositoryMode) {
self.context = context
self.mode = mode
super.init()
title = context.core.activeServerName() ?? "Repositories"
}
@available(*, unavailable)
required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.leftBarButtonItem = UIBarButtonItem(
image: context.symbol("server.rack"),
primaryAction: UIAction { [weak self] _ in
guard let self else { return }
self.navigationController?.pushViewController(
ServersViewController(context: self.context, destination: self.mode),
animated: true
)
}
)
loadContent(refreshing: false)
}
override func loadContent(refreshing: Bool) {
beginLoading(refreshing: refreshing)
loadingTask?.cancel()
loadingTask = Task {
do {
rows = try await context.core.repositories()
tableView.reloadData()
tableView.backgroundView = rows.isEmpty
? EmptyBackgroundView(
title: "No repositories",
detail: "This account does not own any repositories on this server."
)
: nil
} catch {
if !Task.isCancelled { show(error: error) }
}
endLoading()
}
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
rows.count
}
override func tableView(
_ tableView: UITableView,
cellForRowAt indexPath: IndexPath
) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "repository")
?? UITableViewCell(style: .subtitle, reuseIdentifier: "repository")
let row = rows[indexPath.row]
configureTextCell(cell, title: row.name, detail: "\(row.description)\n\(row.meta)")
let button = UIButton(type: .system, primaryAction: UIAction { [weak self] _ in
self?.toggleFavorite(row)
})
button.setImage(context.symbol(row.favorite ? "star.fill" : "star"), for: .normal)
button.tintColor = row.favorite ? .systemYellow : .tertiaryLabel
button.frame.size = CGSize(width: 44, height: 44)
cell.accessoryView = button
return cell
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
96
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let row = rows[indexPath.row]
let destination: UIViewController
switch mode {
case .issues:
destination = IssuesViewController(
context: context,
owner: row.owner,
repository: row.name
)
case .commits:
destination = CommitsViewController(
context: context,
owner: row.owner,
repository: row.name
)
case .milestones:
destination = MilestonesViewController(
context: context,
owner: row.owner,
repository: row.name
)
}
navigationController?.pushViewController(destination, animated: true)
}
private func toggleFavorite(_ row: RepositoryRow) {
do {
rows = try context.core.toggleFavorite(owner: row.owner, repository: row.name)
tableView.reloadData()
} catch {
show(error: error)
}
}
}
@MainActor
final class HomeViewController: RefreshingTableViewController {
private let context: AppContext
private var page: HomePage?
init(context: AppContext) {
self.context = context
super.init()
title = "Home"
}
@available(*, unavailable)
required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
override func viewDidLoad() {
super.viewDidLoad()
let settings = UIBarButtonItem(
image: context.symbol("gearshape"),
primaryAction: UIAction { [weak self] _ in
guard let self else { return }
self.navigationController?.pushViewController(
SettingsViewController(context: self.context),
animated: true
)
}
)
settings.accessibilityLabel = "Settings"
navigationItem.rightBarButtonItem = settings
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
guard page == nil else { return }
loadContent(refreshing: false)
}
override func loadContent(refreshing: Bool) {
guard context.core.activeServerIndex() != nil else {
tableView.backgroundView = EmptyBackgroundView(
title: "No server selected",
detail: "Open Issues or Repos to select a server or add your first one."
)
refreshControl?.endRefreshing()
return
}
beginLoading(refreshing: refreshing)
loadingTask?.cancel()
loadingTask = Task {
do {
page = try await context.core.home()
title = page?.serverName
tableView.tableHeaderView = page.map { HeatmapView(page: $0) }
tableView.reloadData()
tableView.backgroundView = nil
} catch {
if !Task.isCancelled { show(error: error) }
}
endLoading()
}
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
page?.activities.count ?? 0
}
override func tableView(
_ tableView: UITableView,
cellForRowAt indexPath: IndexPath
) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "activity")
?? UITableViewCell(style: .subtitle, reuseIdentifier: "activity")
guard let row = page?.activities[indexPath.row] else { return cell }
configureTextCell(
cell,
title: row.title,
detail: "\(row.detail)\n\(row.meta)",
image: context.symbol(symbolName(for: row.icon))
)
cell.accessoryType = row.target.isEmpty ? .none : .disclosureIndicator
return cell
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
92
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
if let row = page?.activities[indexPath.row] { context.route(row) }
}
private func symbolName(for icon: String) -> String {
switch icon {
case let value where value.contains("pull"): return "arrow.triangle.pull"
case let value where value.contains("issue"): return "exclamationmark.circle"
case let value where value.contains("branch"): return "arrow.triangle.branch"
case let value where value.contains("tag"): return "tag"
case "push": return "arrow.up.circle"
case "release": return "shippingbox"
default: return "books.vertical"
}
}
}
final class HeatmapView: UIView {
private let cells: [HeatCell]
init(page: HomePage) {
cells = page.heatCells
super.init(frame: CGRect(x: 0, y: 0, width: 0, height: 132))
backgroundColor = .systemBackground
let title = UILabel(frame: CGRect(x: 16, y: 12, width: 300, height: 22))
title.text = "Activity · last 12 months"
title.font = .preferredFont(forTextStyle: .subheadline)
title.textColor = .secondaryLabel
addSubview(title)
let total = UILabel(frame: CGRect(x: 16, y: 104, width: 300, height: 18))
total.text = "\(page.contributionCount) contributions"
total.font = .preferredFont(forTextStyle: .caption1)
total.textColor = .tertiaryLabel
addSubview(total)
}
@available(*, unavailable)
required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
override func draw(_ rect: CGRect) {
let width = max(4, min(6, (bounds.width - 32) / 55))
let gap = width + 1
for cell in cells {
let colors: [UIColor] = [
.systemGray5,
UIColor(red: 0.72, green: 0.85, blue: 0.96, alpha: 1),
UIColor(red: 0.45, green: 0.71, blue: 0.91, alpha: 1),
UIColor(red: 0.15, green: 0.55, blue: 0.83, alpha: 1),
UIColor(red: 0.04, green: 0.41, blue: 0.72, alpha: 1),
]
colors[Int(min(cell.level, 4))].setFill()
UIBezierPath(
roundedRect: CGRect(
x: 16 + CGFloat(cell.week) * gap,
y: 43 + CGFloat(cell.day) * gap,
width: width,
height: width
),
cornerRadius: 1
).fill()
}
}
}

View File

@@ -0,0 +1,59 @@
import UIKit
@MainActor
final class SettingsViewController: UITableViewController {
private let context: AppContext
private let appearanceControl = UISegmentedControl(items: ["Auto", "Light", "Dark"])
init(context: AppContext) {
self.context = context
super.init(style: .insetGrouped)
title = "Settings"
}
@available(*, unavailable)
required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
override func viewDidLoad() {
super.viewDidLoad()
let settings = context.core.settings()
appearanceControl.selectedSegmentIndex = Int(settings.appearance)
appearanceControl.addTarget(self, action: #selector(appearanceChanged), for: .valueChanged)
}
override func numberOfSections(in tableView: UITableView) -> Int { 1 }
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 1 }
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
"Appearance"
}
override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
"Follow iOS automatically or choose a fixed appearance."
}
override func tableView(
_ tableView: UITableView,
cellForRowAt indexPath: IndexPath
) -> UITableViewCell {
let cell = UITableViewCell(style: .default, reuseIdentifier: nil)
appearanceControl.translatesAutoresizingMaskIntoConstraints = false
cell.contentView.addSubview(appearanceControl)
NSLayoutConstraint.activate([
appearanceControl.leadingAnchor.constraint(equalTo: cell.contentView.leadingAnchor, constant: 16),
appearanceControl.trailingAnchor.constraint(equalTo: cell.contentView.trailingAnchor, constant: -16),
appearanceControl.topAnchor.constraint(equalTo: cell.contentView.topAnchor, constant: 8),
appearanceControl.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor, constant: -8),
])
return cell
}
@objc private func appearanceChanged() {
do {
try context.core.setAppearance(index: UInt32(appearanceControl.selectedSegmentIndex))
context.applyAppearance()
} catch {
show(error: error)
}
}
}

140
ios/Sources/Support.swift Normal file
View File

@@ -0,0 +1,140 @@
import UIKit
func errorAlert(_ message: String) -> UIAlertController {
let alert = UIAlertController(title: "Something went wrong", message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
return alert
}
extension UIViewController {
func show(error: Error) {
present(errorAlert(error.localizedDescription), animated: true)
}
func beginNavigationLoading(_ spinner: UIActivityIndicatorView) {
guard navigationItem.rightBarButtonItem == nil else { return }
spinner.startAnimating()
navigationItem.rightBarButtonItem = UIBarButtonItem(customView: spinner)
}
func endNavigationLoading(_ spinner: UIActivityIndicatorView) {
spinner.stopAnimating()
if navigationItem.rightBarButtonItem?.customView === spinner {
navigationItem.rightBarButtonItem = nil
}
}
}
@MainActor
class RefreshingTableViewController: UITableViewController {
private let spinner = UIActivityIndicatorView(style: .medium)
var loadingTask: Task<Void, Never>?
init() {
super.init(style: .plain)
tableView.backgroundColor = .systemGroupedBackground
tableView.separatorInset = .zero
tableView.refreshControl = UIRefreshControl()
tableView.refreshControl?.addTarget(self, action: #selector(refreshRequested), for: .valueChanged)
navigationItem.largeTitleDisplayMode = .never
}
@available(*, unavailable)
required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
deinit { loadingTask?.cancel() }
func loadContent(refreshing: Bool) {}
func beginLoading(refreshing: Bool) {
if !refreshing { beginNavigationLoading(spinner) }
}
func endLoading() {
endNavigationLoading(spinner)
refreshControl?.endRefreshing()
}
@objc private func refreshRequested() {
loadContent(refreshing: true)
}
}
final class EmptyBackgroundView: UIView {
init(title: String, detail: String) {
super.init(frame: .zero)
let titleLabel = UILabel()
titleLabel.text = title
titleLabel.font = .preferredFont(forTextStyle: .title2)
titleLabel.textAlignment = .center
let detailLabel = UILabel()
detailLabel.text = detail
detailLabel.font = .preferredFont(forTextStyle: .body)
detailLabel.textColor = .secondaryLabel
detailLabel.textAlignment = .center
detailLabel.numberOfLines = 0
let stack = UIStackView(arrangedSubviews: [titleLabel, detailLabel])
stack.axis = .vertical
stack.spacing = 8
stack.translatesAutoresizingMaskIntoConstraints = false
addSubview(stack)
NSLayoutConstraint.activate([
stack.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 28),
stack.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -28),
stack.centerYAnchor.constraint(equalTo: centerYAnchor, constant: -40),
])
}
@available(*, unavailable)
required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
}
extension UIColor {
convenience init?(hex: String) {
let value = hex.trimmingCharacters(in: CharacterSet(charactersIn: "#"))
guard value.count == 6, let rgb = Int(value, radix: 16) else { return nil }
self.init(
red: CGFloat((rgb >> 16) & 0xff) / 255,
green: CGFloat((rgb >> 8) & 0xff) / 255,
blue: CGFloat(rgb & 0xff) / 255,
alpha: 1
)
}
}
func markdown(_ source: String, textStyle: UIFont.TextStyle = .body) -> NSAttributedString {
let attributed = (try? AttributedString(
markdown: source,
options: .init(interpretedSyntax: .full)
)) ?? AttributedString(source)
var mutable = attributed
mutable.font = .preferredFont(forTextStyle: textStyle)
mutable.foregroundColor = UIColor.label
return NSAttributedString(mutable)
}
func configureTextCell(_ cell: UITableViewCell, title: String, detail: String, image: UIImage? = nil) {
var content = cell.defaultContentConfiguration()
content.text = title
content.secondaryText = detail
content.secondaryTextProperties.numberOfLines = 2
content.image = image
content.imageProperties.tintColor = .tintColor
cell.contentConfiguration = content
cell.backgroundColor = .systemBackground
}
func symbolText(_ symbol: String, text: String, font: UIFont, color: UIColor) -> NSAttributedString {
let output = NSMutableAttributedString()
if let image = UIImage(systemName: symbol)?.withTintColor(color, renderingMode: .alwaysOriginal) {
let attachment = NSTextAttachment(image: image)
attachment.bounds = CGRect(x: 0, y: -2, width: font.pointSize, height: font.pointSize)
output.append(NSAttributedString(attachment: attachment))
output.append(NSAttributedString(string: " "))
}
output.append(NSAttributedString(string: text, attributes: [
.font: font,
.foregroundColor: color,
]))
return output
}

View File

@@ -5,7 +5,6 @@ export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:$PAT
export RUSTC="$(rustup which --toolchain stable rustc)"
export RUSTDOC="$(rustup which --toolchain stable rustdoc)"
export CARGO_TARGET_DIR="$DERIVED_FILE_DIR/cargo"
export CARGO_PROFILE_RELEASE_DEBUG="${CARGO_PROFILE_RELEASE_DEBUG:-1}"
if [[ "$CONFIGURATION" == "Debug" ]]; then
profile=debug
@@ -13,14 +12,7 @@ else
profile=release
fi
if [[ "${LLVM_TARGET_TRIPLE_SUFFIX:-}" == "-simulator" ]]; then
separator=$'\x1f'
entitlement_flags="-Clink-arg=-Wl,-sectcreate,__TEXT,__entitlements,$LD_ENTITLEMENTS_SECTION"
entitlement_flags+="$separator-Clink-arg=-Wl,-sectcreate,__TEXT,__ents_der,$LD_ENTITLEMENTS_SECTION_DER"
export CARGO_ENCODED_RUSTFLAGS="${CARGO_ENCODED_RUSTFLAGS:+$CARGO_ENCODED_RUSTFLAGS$separator}$entitlement_flags"
fi
executables=()
libraries=()
for arch in $ARCHS; do
if [[ "$arch" == "arm64" && "${LLVM_TARGET_TRIPLE_SUFFIX:-}" == "-simulator" ]]; then
target=aarch64-apple-ios-sim
@@ -34,15 +26,12 @@ for arch in $ARCHS; do
fi
if [[ "$profile" == "debug" ]]; then
rustup run stable cargo build --locked --target "$target" --bin "$1"
rustup run stable cargo build --locked --target "$target" --package gotcha-app --lib
else
rustup run stable cargo build --locked --release --target "$target" --bin "$1"
rustup run stable cargo build --locked --release --target "$target" --package gotcha-app --lib
fi
executables+=("$CARGO_TARGET_DIR/$target/$profile/$1")
libraries+=("$CARGO_TARGET_DIR/$target/$profile/libgotcha_core.a")
done
lipo -create -output "$TARGET_BUILD_DIR/$EXECUTABLE_PATH" "${executables[@]}"
if [[ -n "${DWARF_DSYM_FOLDER_PATH:-}" && -n "${DWARF_DSYM_FILE_NAME:-}" ]]; then
dsymutil "$TARGET_BUILD_DIR/$EXECUTABLE_PATH" -o "$DWARF_DSYM_FOLDER_PATH/$DWARF_DSYM_FILE_NAME"
fi
mkdir -p "$DERIVED_FILE_DIR/rust"
lipo -create -output "$DERIVED_FILE_DIR/rust/libgotcha_core.a" "${libraries[@]}"

View File

@@ -10,7 +10,14 @@ targets:
deploymentTarget: "17.0"
settings:
PRODUCT_BUNDLE_IDENTIFIER: de.rfc1437.gotcha
DEVELOPMENT_TEAM: MU22FMRGK8
CODE_SIGN_STYLE: Automatic
TARGETED_DEVICE_FAMILY: "1"
CODE_SIGN_ENTITLEMENTS: Gotcha.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"
info:
path: Info.plist
properties:
@@ -20,10 +27,12 @@ targets:
- UIInterfaceOrientationPortrait
sources:
- Assets.xcassets
postCompileScripts:
- name: Build Rust app
- Sources
- Generated/gotcha_core.swift
preBuildScripts:
- name: Build Rust core
basedOnDependencyAnalysis: false
script: |
./build_for_ios_with_cargo.bash gotcha-app
./build_rust_core.bash
outputFiles:
- $(TARGET_BUILD_DIR)/$(EXECUTABLE_PATH)
- $(DERIVED_FILE_DIR)/rust/libgotcha_core.a