Add repository file browser
This commit is contained in:
@@ -222,6 +222,51 @@ pub async fn load_branches(
|
|||||||
Ok(branches)
|
Ok(branches)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn load_repository_contents(
|
||||||
|
server: &Server,
|
||||||
|
owner: &str,
|
||||||
|
repository: &str,
|
||||||
|
path: &str,
|
||||||
|
) -> Result<Vec<models::ContentsResponse>, String> {
|
||||||
|
let client =
|
||||||
|
Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?;
|
||||||
|
let configuration = client.configuration();
|
||||||
|
if path.is_empty() {
|
||||||
|
apis::repository_api::repo_get_contents_list(&configuration, owner, repository, None)
|
||||||
|
.await
|
||||||
|
.map_err(|error| error.to_string())
|
||||||
|
} else {
|
||||||
|
apis::repository_api::repo_get_contents_ext(
|
||||||
|
&configuration,
|
||||||
|
owner,
|
||||||
|
repository,
|
||||||
|
path,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map(|contents| contents.dir_contents.unwrap_or_default())
|
||||||
|
.map_err(|error| error.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn load_repository_file(
|
||||||
|
server: &Server,
|
||||||
|
owner: &str,
|
||||||
|
repository: &str,
|
||||||
|
path: &str,
|
||||||
|
) -> Result<Vec<u8>, String> {
|
||||||
|
let client =
|
||||||
|
Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?;
|
||||||
|
apis::repository_api::repo_get_raw_file(&client.configuration(), owner, repository, path, None)
|
||||||
|
.await
|
||||||
|
.map_err(|error| error.to_string())?
|
||||||
|
.bytes()
|
||||||
|
.await
|
||||||
|
.map(|bytes| bytes.to_vec())
|
||||||
|
.map_err(|error| error.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn load_branch_commits(
|
pub async fn load_branch_commits(
|
||||||
server: &Server,
|
server: &Server,
|
||||||
owner: &str,
|
owner: &str,
|
||||||
|
|||||||
@@ -290,6 +290,26 @@ impl GotchaCore {
|
|||||||
Ok(commit_page(branches, &commits, branch.is_none()))
|
Ok(commit_page(branches, &commits, branch.is_none()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn repository_contents(
|
||||||
|
&self,
|
||||||
|
owner: String,
|
||||||
|
repository: String,
|
||||||
|
path: String,
|
||||||
|
) -> Result<Vec<RepositoryContentRow>, GotchaError> {
|
||||||
|
Ok(repository_content_rows(
|
||||||
|
load_repository_contents(&self.server()?, &owner, &repository, &path).await?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn repository_file(
|
||||||
|
&self,
|
||||||
|
owner: String,
|
||||||
|
repository: String,
|
||||||
|
path: String,
|
||||||
|
) -> Result<Vec<u8>, GotchaError> {
|
||||||
|
Ok(load_repository_file(&self.server()?, &owner, &repository, &path).await?)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn commit_files(
|
pub async fn commit_files(
|
||||||
&self,
|
&self,
|
||||||
owner: String,
|
owner: String,
|
||||||
|
|||||||
@@ -116,6 +116,14 @@ pub struct FileRow {
|
|||||||
pub status: String,
|
pub status: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, uniffi::Record)]
|
||||||
|
pub struct RepositoryContentRow {
|
||||||
|
pub name: String,
|
||||||
|
pub path: String,
|
||||||
|
pub kind: String,
|
||||||
|
pub size: i64,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, uniffi::Record)]
|
#[derive(Clone, uniffi::Record)]
|
||||||
pub struct DiffLine {
|
pub struct DiffLine {
|
||||||
pub old_number: String,
|
pub old_number: String,
|
||||||
@@ -422,6 +430,24 @@ pub fn commit_file_rows(files: Vec<models::CommitAffectedFiles>) -> Vec<FileRow>
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn repository_content_rows(
|
||||||
|
contents: Vec<models::ContentsResponse>,
|
||||||
|
) -> Vec<RepositoryContentRow> {
|
||||||
|
let mut rows: Vec<_> = contents
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|content| {
|
||||||
|
Some(RepositoryContentRow {
|
||||||
|
name: content.name?,
|
||||||
|
path: content.path?,
|
||||||
|
kind: content.r#type.unwrap_or_else(|| "file".into()),
|
||||||
|
size: content.size.unwrap_or_default(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
rows.sort_by_key(|row| (row.kind != "dir", row.name.to_lowercase()));
|
||||||
|
rows
|
||||||
|
}
|
||||||
|
|
||||||
pub fn diff_page(path: &str, diff: crate::diff::Parsed) -> DiffPage {
|
pub fn diff_page(path: &str, diff: crate::diff::Parsed) -> DiffPage {
|
||||||
DiffPage {
|
DiffPage {
|
||||||
title: path.rsplit('/').next().unwrap_or(path).into(),
|
title: path.rsplit('/').next().unwrap_or(path).into(),
|
||||||
@@ -758,6 +784,31 @@ pub fn compact_date(date: Option<&str>) -> String {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn repository_directories_sort_before_files() {
|
||||||
|
let contents = [
|
||||||
|
("z.swift", "file"),
|
||||||
|
("Sources", "dir"),
|
||||||
|
("README.md", "file"),
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.map(|(name, kind)| models::ContentsResponse {
|
||||||
|
name: Some(name.into()),
|
||||||
|
path: Some(name.into()),
|
||||||
|
r#type: Some(kind.into()),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
repository_content_rows(contents)
|
||||||
|
.into_iter()
|
||||||
|
.map(|row| row.name)
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
["Sources", "README.md", "z.swift"]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn maps_heat_levels_and_labels() {
|
fn maps_heat_levels_and_labels() {
|
||||||
let heatmap = vec![
|
let heatmap = vec![
|
||||||
|
|||||||
@@ -564,6 +564,24 @@ fileprivate struct FfiConverterString: FfiConverter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
fileprivate struct FfiConverterData: FfiConverterRustBuffer {
|
||||||
|
typealias SwiftType = Data
|
||||||
|
|
||||||
|
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Data {
|
||||||
|
let len: Int32 = try readInt(&buf)
|
||||||
|
return Data(try readBytes(&buf, count: Int(len)))
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func write(_ value: Data, into buf: inout [UInt8]) {
|
||||||
|
let len = Int32(value.count)
|
||||||
|
writeInt(&buf, len)
|
||||||
|
writeBytes(&buf, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -599,6 +617,10 @@ public protocol GotchaCoreProtocol: AnyObject, Sendable {
|
|||||||
|
|
||||||
func repositories() async throws -> [RepositoryRow]
|
func repositories() async throws -> [RepositoryRow]
|
||||||
|
|
||||||
|
func repositoryContents(owner: String, repository: String, path: String) async throws -> [RepositoryContentRow]
|
||||||
|
|
||||||
|
func repositoryFile(owner: String, repository: String, path: String) async throws -> Data
|
||||||
|
|
||||||
func selectServer(index: UInt32) throws
|
func selectServer(index: UInt32) throws
|
||||||
|
|
||||||
func servers() -> [ServerRow]
|
func servers() -> [ServerRow]
|
||||||
@@ -903,6 +925,38 @@ open func repositories()async throws -> [RepositoryRow] {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
open func repositoryContents(owner: String, repository: String, path: String)async throws -> [RepositoryContentRow] {
|
||||||
|
return
|
||||||
|
try await uniffiRustCallAsync(
|
||||||
|
rustFutureFunc: {
|
||||||
|
uniffi_gotcha_core_fn_method_gotchacore_repository_contents(
|
||||||
|
self.uniffiCloneHandle(),FfiConverterString.lower(owner),FfiConverterString.lower(repository),FfiConverterString.lower(path)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
pollFunc: ffi_gotcha_core_rust_future_poll_rust_buffer,
|
||||||
|
completeFunc: ffi_gotcha_core_rust_future_complete_rust_buffer,
|
||||||
|
freeFunc: ffi_gotcha_core_rust_future_free_rust_buffer,
|
||||||
|
liftFunc: FfiConverterSequenceTypeRepositoryContentRow.lift,
|
||||||
|
errorHandler: FfiConverterTypeGotchaError_lift
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
open func repositoryFile(owner: String, repository: String, path: String)async throws -> Data {
|
||||||
|
return
|
||||||
|
try await uniffiRustCallAsync(
|
||||||
|
rustFutureFunc: {
|
||||||
|
uniffi_gotcha_core_fn_method_gotchacore_repository_file(
|
||||||
|
self.uniffiCloneHandle(),FfiConverterString.lower(owner),FfiConverterString.lower(repository),FfiConverterString.lower(path)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
pollFunc: ffi_gotcha_core_rust_future_poll_rust_buffer,
|
||||||
|
completeFunc: ffi_gotcha_core_rust_future_complete_rust_buffer,
|
||||||
|
freeFunc: ffi_gotcha_core_rust_future_free_rust_buffer,
|
||||||
|
liftFunc: FfiConverterData.lift,
|
||||||
|
errorHandler: FfiConverterTypeGotchaError_lift
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
open func selectServer(index: UInt32)throws {try rustCallWithError(FfiConverterTypeGotchaError_lift) {
|
open func selectServer(index: UInt32)throws {try rustCallWithError(FfiConverterTypeGotchaError_lift) {
|
||||||
uniffiCallStatus in
|
uniffiCallStatus in
|
||||||
uniffi_gotcha_core_fn_method_gotchacore_select_server(
|
uniffi_gotcha_core_fn_method_gotchacore_select_server(
|
||||||
@@ -2061,6 +2115,68 @@ public func FfiConverterTypePullRow_lower(_ value: PullRow) -> RustBuffer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public struct RepositoryContentRow: Equatable, Hashable {
|
||||||
|
public var name: String
|
||||||
|
public var path: String
|
||||||
|
public var kind: String
|
||||||
|
public var size: Int64
|
||||||
|
|
||||||
|
// Default memberwise initializers are never public by default, so we
|
||||||
|
// declare one manually.
|
||||||
|
public init(name: String, path: String, kind: String, size: Int64) {
|
||||||
|
self.name = name
|
||||||
|
self.path = path
|
||||||
|
self.kind = kind
|
||||||
|
self.size = size
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#if compiler(>=6)
|
||||||
|
extension RepositoryContentRow: Sendable {}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public struct FfiConverterTypeRepositoryContentRow: FfiConverterRustBuffer {
|
||||||
|
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> RepositoryContentRow {
|
||||||
|
return
|
||||||
|
try RepositoryContentRow(
|
||||||
|
name: FfiConverterString.read(from: &buf),
|
||||||
|
path: FfiConverterString.read(from: &buf),
|
||||||
|
kind: FfiConverterString.read(from: &buf),
|
||||||
|
size: FfiConverterInt64.read(from: &buf)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func write(_ value: RepositoryContentRow, into buf: inout [UInt8]) {
|
||||||
|
FfiConverterString.write(value.name, into: &buf)
|
||||||
|
FfiConverterString.write(value.path, into: &buf)
|
||||||
|
FfiConverterString.write(value.kind, into: &buf)
|
||||||
|
FfiConverterInt64.write(value.size, into: &buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public func FfiConverterTypeRepositoryContentRow_lift(_ buf: RustBuffer) throws -> RepositoryContentRow {
|
||||||
|
return try FfiConverterTypeRepositoryContentRow.lift(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public func FfiConverterTypeRepositoryContentRow_lower(_ value: RepositoryContentRow) -> RustBuffer {
|
||||||
|
return FfiConverterTypeRepositoryContentRow.lower(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public struct RepositoryRow: Equatable, Hashable {
|
public struct RepositoryRow: Equatable, Hashable {
|
||||||
public var name: String
|
public var name: String
|
||||||
public var owner: String
|
public var owner: String
|
||||||
@@ -2661,6 +2777,31 @@ fileprivate struct FfiConverterSequenceTypePullRow: FfiConverterRustBuffer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
fileprivate struct FfiConverterSequenceTypeRepositoryContentRow: FfiConverterRustBuffer {
|
||||||
|
typealias SwiftType = [RepositoryContentRow]
|
||||||
|
|
||||||
|
public static func write(_ value: [RepositoryContentRow], into buf: inout [UInt8]) {
|
||||||
|
let len = Int32(value.count)
|
||||||
|
writeInt(&buf, len)
|
||||||
|
for item in value {
|
||||||
|
FfiConverterTypeRepositoryContentRow.write(item, into: &buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> [RepositoryContentRow] {
|
||||||
|
let len: Int32 = try readInt(&buf)
|
||||||
|
var seq = [RepositoryContentRow]()
|
||||||
|
seq.reserveCapacity(Int(len))
|
||||||
|
for _ in 0 ..< len {
|
||||||
|
seq.append(try FfiConverterTypeRepositoryContentRow.read(from: &buf))
|
||||||
|
}
|
||||||
|
return seq
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#if swift(>=5.8)
|
#if swift(>=5.8)
|
||||||
@_documentation(visibility: private)
|
@_documentation(visibility: private)
|
||||||
#endif
|
#endif
|
||||||
@@ -2819,6 +2960,12 @@ private let initializationResult: InitializationResult = {
|
|||||||
if (uniffi_gotcha_core_checksum_method_gotchacore_repositories() != 12256) {
|
if (uniffi_gotcha_core_checksum_method_gotchacore_repositories() != 12256) {
|
||||||
return InitializationResult.apiChecksumMismatch
|
return InitializationResult.apiChecksumMismatch
|
||||||
}
|
}
|
||||||
|
if (uniffi_gotcha_core_checksum_method_gotchacore_repository_contents() != 8454) {
|
||||||
|
return InitializationResult.apiChecksumMismatch
|
||||||
|
}
|
||||||
|
if (uniffi_gotcha_core_checksum_method_gotchacore_repository_file() != 27399) {
|
||||||
|
return InitializationResult.apiChecksumMismatch
|
||||||
|
}
|
||||||
if (uniffi_gotcha_core_checksum_method_gotchacore_select_server() != 22721) {
|
if (uniffi_gotcha_core_checksum_method_gotchacore_select_server() != 22721) {
|
||||||
return InitializationResult.apiChecksumMismatch
|
return InitializationResult.apiChecksumMismatch
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -334,6 +334,16 @@ uint64_t uniffi_gotcha_core_fn_method_gotchacore_pulls(uint64_t ptr
|
|||||||
uint64_t uniffi_gotcha_core_fn_method_gotchacore_repositories(uint64_t ptr
|
uint64_t uniffi_gotcha_core_fn_method_gotchacore_repositories(uint64_t ptr
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_REPOSITORY_CONTENTS
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_REPOSITORY_CONTENTS
|
||||||
|
uint64_t uniffi_gotcha_core_fn_method_gotchacore_repository_contents(uint64_t ptr, RustBuffer owner, RustBuffer repository, RustBuffer path
|
||||||
|
);
|
||||||
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_REPOSITORY_FILE
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_REPOSITORY_FILE
|
||||||
|
uint64_t uniffi_gotcha_core_fn_method_gotchacore_repository_file(uint64_t ptr, RustBuffer owner, RustBuffer repository, RustBuffer path
|
||||||
|
);
|
||||||
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_SELECT_SERVER
|
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_SELECT_SERVER
|
||||||
#define 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
|
void uniffi_gotcha_core_fn_method_gotchacore_select_server(uint64_t ptr, uint32_t index, RustCallStatus *_Nonnull out_status
|
||||||
@@ -722,6 +732,18 @@ uint16_t uniffi_gotcha_core_checksum_method_gotchacore_pulls(void
|
|||||||
#define 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
|
uint16_t uniffi_gotcha_core_checksum_method_gotchacore_repositories(void
|
||||||
|
|
||||||
|
);
|
||||||
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_REPOSITORY_CONTENTS
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_REPOSITORY_CONTENTS
|
||||||
|
uint16_t uniffi_gotcha_core_checksum_method_gotchacore_repository_contents(void
|
||||||
|
|
||||||
|
);
|
||||||
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_REPOSITORY_FILE
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_REPOSITORY_FILE
|
||||||
|
uint16_t uniffi_gotcha_core_checksum_method_gotchacore_repository_file(void
|
||||||
|
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_SELECT_SERVER
|
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_SELECT_SERVER
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
1AED04075E363FBC0FF55773 /* ContentScreens.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE5E10787062D1175125BB4C /* ContentScreens.swift */; };
|
1AED04075E363FBC0FF55773 /* ContentScreens.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE5E10787062D1175125BB4C /* ContentScreens.swift */; };
|
||||||
25BDBDED14B3B886E19F511C /* DetailScreens.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13EFB48F40AEB3016D3CF643 /* DetailScreens.swift */; };
|
25BDBDED14B3B886E19F511C /* DetailScreens.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13EFB48F40AEB3016D3CF643 /* DetailScreens.swift */; };
|
||||||
374320EB00185A0C8E40D985 /* ListScreens.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC828923A1E11A58AE348A74 /* ListScreens.swift */; };
|
374320EB00185A0C8E40D985 /* ListScreens.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC828923A1E11A58AE348A74 /* ListScreens.swift */; };
|
||||||
|
7DD332583B169B3CA1CA46E0 /* Highlighter in Frameworks */ = {isa = PBXBuildFile; productRef = EC5F999F50905E3801E8A71A /* Highlighter */; };
|
||||||
950C584D58E80106DF350A22 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C0921C76676A14A024BA417 /* AppDelegate.swift */; };
|
950C584D58E80106DF350A22 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C0921C76676A14A024BA417 /* AppDelegate.swift */; };
|
||||||
96C4AFC206A1DBAE3D3E00CE /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F145F13B8ED83A5AB0009A4 /* SettingsViewController.swift */; };
|
96C4AFC206A1DBAE3D3E00CE /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F145F13B8ED83A5AB0009A4 /* SettingsViewController.swift */; };
|
||||||
D59D3ED36ABCC1D8690A9088 /* gotcha_core.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE12480725C13293FAFDDA52 /* gotcha_core.swift */; };
|
D59D3ED36ABCC1D8690A9088 /* gotcha_core.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE12480725C13293FAFDDA52 /* gotcha_core.swift */; };
|
||||||
@@ -31,6 +32,17 @@
|
|||||||
FE12480725C13293FAFDDA52 /* gotcha_core.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = gotcha_core.swift; sourceTree = "<group>"; };
|
FE12480725C13293FAFDDA52 /* gotcha_core.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = gotcha_core.swift; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
9BC71F568A96324A1942A0AC /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
7DD332583B169B3CA1CA46E0 /* Highlighter in Frameworks */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXGroup section */
|
/* Begin PBXGroup section */
|
||||||
710A50F51478401FC642E6E3 /* Sources */ = {
|
710A50F51478401FC642E6E3 /* Sources */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
@@ -82,6 +94,7 @@
|
|||||||
E01FE2CE8A64C51D25060BE9 /* Build Rust core */,
|
E01FE2CE8A64C51D25060BE9 /* Build Rust core */,
|
||||||
409DBF67E9C2743801B8F8A4 /* Sources */,
|
409DBF67E9C2743801B8F8A4 /* Sources */,
|
||||||
9EDD4380917C29EE67EE344B /* Resources */,
|
9EDD4380917C29EE67EE344B /* Resources */,
|
||||||
|
9BC71F568A96324A1942A0AC /* Frameworks */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
@@ -89,6 +102,7 @@
|
|||||||
);
|
);
|
||||||
name = Gotcha;
|
name = Gotcha;
|
||||||
packageProductDependencies = (
|
packageProductDependencies = (
|
||||||
|
EC5F999F50905E3801E8A71A /* Highlighter */,
|
||||||
);
|
);
|
||||||
productName = Gotcha;
|
productName = Gotcha;
|
||||||
productReference = 5FB3250A93766966A60A685E /* Gotcha.app */;
|
productReference = 5FB3250A93766966A60A685E /* Gotcha.app */;
|
||||||
@@ -118,6 +132,9 @@
|
|||||||
);
|
);
|
||||||
mainGroup = A8558BC8DD12191B80F52573;
|
mainGroup = A8558BC8DD12191B80F52573;
|
||||||
minimizedProjectReferenceProxies = 1;
|
minimizedProjectReferenceProxies = 1;
|
||||||
|
packageReferences = (
|
||||||
|
85EFB86C5B2FDE8DEAD98A24 /* XCRemoteSwiftPackageReference "HighlighterSwift" */,
|
||||||
|
);
|
||||||
preferredProjectObjectVersion = 77;
|
preferredProjectObjectVersion = 77;
|
||||||
productRefGroup = F059299C038F3CAFCE470831 /* Products */;
|
productRefGroup = F059299C038F3CAFCE470831 /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
@@ -370,6 +387,25 @@
|
|||||||
defaultConfigurationName = Debug;
|
defaultConfigurationName = Debug;
|
||||||
};
|
};
|
||||||
/* End XCConfigurationList section */
|
/* End XCConfigurationList section */
|
||||||
|
|
||||||
|
/* Begin XCRemoteSwiftPackageReference section */
|
||||||
|
85EFB86C5B2FDE8DEAD98A24 /* XCRemoteSwiftPackageReference "HighlighterSwift" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/smittytone/HighlighterSwift";
|
||||||
|
requirement = {
|
||||||
|
kind = upToNextMajorVersion;
|
||||||
|
minimumVersion = 3.1.0;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/* End XCRemoteSwiftPackageReference section */
|
||||||
|
|
||||||
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
|
EC5F999F50905E3801E8A71A /* Highlighter */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = 85EFB86C5B2FDE8DEAD98A24 /* XCRemoteSwiftPackageReference "HighlighterSwift" */;
|
||||||
|
productName = Highlighter;
|
||||||
|
};
|
||||||
|
/* End XCSwiftPackageProductDependency section */
|
||||||
};
|
};
|
||||||
rootObject = C30A6F1B592A907D96D6AB40 /* Project object */;
|
rootObject = C30A6F1B592A907D96D6AB40 /* Project object */;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"originHash" : "00a1c96095892a05bfd089593813422ede18735e452666e3e7a642ebd9048a45",
|
||||||
|
"pins" : [
|
||||||
|
{
|
||||||
|
"identity" : "highlighterswift",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/smittytone/HighlighterSwift",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "fe7aae9c9b31d3b296fd3d2dd575e1a207bb29e0",
|
||||||
|
"version" : "3.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"version" : 3
|
||||||
|
}
|
||||||
@@ -502,11 +502,15 @@ final class PullsViewController: RefreshingTableViewController {
|
|||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
final class CommitsViewController: RefreshingTableViewController {
|
final class CommitsViewController: RefreshingTableViewController {
|
||||||
|
private enum Mode: Int { case history, files }
|
||||||
|
|
||||||
private let context: AppContext
|
private let context: AppContext
|
||||||
private let owner: String
|
private let owner: String
|
||||||
private let repository: String
|
private let repository: String
|
||||||
private var page: CommitPage?
|
private var page: CommitPage?
|
||||||
|
private var contents: [RepositoryContentRow] = []
|
||||||
private var branch: String?
|
private var branch: String?
|
||||||
|
private var mode = Mode.history
|
||||||
|
|
||||||
init(context: AppContext, owner: String, repository: String) {
|
init(context: AppContext, owner: String, repository: String) {
|
||||||
self.context = context
|
self.context = context
|
||||||
@@ -522,6 +526,11 @@ final class CommitsViewController: RefreshingTableViewController {
|
|||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
tableView.register(CommitCell.self, forCellReuseIdentifier: "commit")
|
tableView.register(CommitCell.self, forCellReuseIdentifier: "commit")
|
||||||
|
let modeControl = UISegmentedControl(items: ["History", "Files"])
|
||||||
|
modeControl.selectedSegmentIndex = mode.rawValue
|
||||||
|
modeControl.addTarget(self, action: #selector(modeChanged(_:)), for: .valueChanged)
|
||||||
|
modeControl.accessibilityLabel = "Repository view"
|
||||||
|
navigationItem.titleView = modeControl
|
||||||
updateBranchMenu()
|
updateBranchMenu()
|
||||||
loadContent(refreshing: false)
|
loadContent(refreshing: false)
|
||||||
}
|
}
|
||||||
@@ -531,16 +540,23 @@ final class CommitsViewController: RefreshingTableViewController {
|
|||||||
loadingTask?.cancel()
|
loadingTask?.cancel()
|
||||||
loadingTask = Task {
|
loadingTask = Task {
|
||||||
do {
|
do {
|
||||||
page = try await context.core.commits(
|
switch mode {
|
||||||
owner: owner,
|
case .history:
|
||||||
repository: repository,
|
page = try await context.core.commits(
|
||||||
branch: branch
|
owner: owner,
|
||||||
)
|
repository: repository,
|
||||||
updateBranchMenu()
|
branch: branch
|
||||||
|
)
|
||||||
|
updateBranchMenu()
|
||||||
|
case .files:
|
||||||
|
contents = try await context.core.repositoryContents(
|
||||||
|
owner: owner,
|
||||||
|
repository: repository,
|
||||||
|
path: ""
|
||||||
|
)
|
||||||
|
}
|
||||||
tableView.reloadData()
|
tableView.reloadData()
|
||||||
tableView.backgroundView = page?.commits.isEmpty == true
|
updateEmptyView()
|
||||||
? EmptyBackgroundView(title: "No commits", detail: "This repository has no commit history.")
|
|
||||||
: nil
|
|
||||||
} catch {
|
} catch {
|
||||||
if !Task.isCancelled { show(error: error) }
|
if !Task.isCancelled { show(error: error) }
|
||||||
}
|
}
|
||||||
@@ -549,34 +565,75 @@ final class CommitsViewController: RefreshingTableViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||||
page?.commits.count ?? 0
|
mode == .history ? page?.commits.count ?? 0 : contents.count
|
||||||
}
|
}
|
||||||
|
|
||||||
override func tableView(
|
override func tableView(
|
||||||
_ tableView: UITableView,
|
_ tableView: UITableView,
|
||||||
cellForRowAt indexPath: IndexPath
|
cellForRowAt indexPath: IndexPath
|
||||||
) -> UITableViewCell {
|
) -> UITableViewCell {
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: "commit", for: indexPath) as! CommitCell
|
switch mode {
|
||||||
if let page { cell.configure(page.commits[indexPath.row], laneCount: page.laneCount) }
|
case .history:
|
||||||
return cell
|
let cell = tableView.dequeueReusableCell(withIdentifier: "commit", for: indexPath) as! CommitCell
|
||||||
|
if let page { cell.configure(page.commits[indexPath.row], laneCount: page.laneCount) }
|
||||||
|
return cell
|
||||||
|
case .files:
|
||||||
|
let cell = tableView.dequeueReusableCell(withIdentifier: "repository-content")
|
||||||
|
?? UITableViewCell(style: .subtitle, reuseIdentifier: "repository-content")
|
||||||
|
configureRepositoryContentCell(cell, row: contents[indexPath.row])
|
||||||
|
return cell
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
|
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||||
86
|
mode == .history ? 86 : UITableView.automaticDimension
|
||||||
}
|
}
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||||
tableView.deselectRow(at: indexPath, animated: true)
|
tableView.deselectRow(at: indexPath, animated: true)
|
||||||
guard let row = page?.commits[indexPath.row] else { return }
|
switch mode {
|
||||||
navigationController?.pushViewController(
|
case .history:
|
||||||
FilesViewController(
|
guard let row = page?.commits[indexPath.row] else { return }
|
||||||
|
navigationController?.pushViewController(
|
||||||
|
FilesViewController(
|
||||||
|
context: context,
|
||||||
|
owner: owner,
|
||||||
|
repository: repository,
|
||||||
|
sha: row.sha
|
||||||
|
),
|
||||||
|
animated: true
|
||||||
|
)
|
||||||
|
case .files:
|
||||||
|
showRepositoryContent(
|
||||||
|
contents[indexPath.row],
|
||||||
context: context,
|
context: context,
|
||||||
owner: owner,
|
owner: owner,
|
||||||
repository: repository,
|
repository: repository,
|
||||||
sha: row.sha
|
navigationController: navigationController
|
||||||
),
|
)
|
||||||
animated: true
|
}
|
||||||
)
|
}
|
||||||
|
|
||||||
|
@objc private func modeChanged(_ sender: UISegmentedControl) {
|
||||||
|
guard let mode = Mode(rawValue: sender.selectedSegmentIndex), mode != self.mode else { return }
|
||||||
|
self.mode = mode
|
||||||
|
navigationItem.rightBarButtonItem = nil
|
||||||
|
if mode == .history { updateBranchMenu() }
|
||||||
|
tableView.reloadData()
|
||||||
|
loadContent(refreshing: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateEmptyView() {
|
||||||
|
switch mode {
|
||||||
|
case .history:
|
||||||
|
tableView.backgroundView = page?.commits.isEmpty == true
|
||||||
|
? EmptyBackgroundView(title: "No commits", detail: "This repository has no commit history.")
|
||||||
|
: nil
|
||||||
|
case .files:
|
||||||
|
tableView.backgroundView = contents.isEmpty
|
||||||
|
? EmptyBackgroundView(title: "No files", detail: "This repository is empty.")
|
||||||
|
: nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateBranchMenu() {
|
private func updateBranchMenu() {
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
|
import Highlighter
|
||||||
|
import QuickLook
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import UniformTypeIdentifiers
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
class MarkdownPageViewController: UIViewController {
|
class MarkdownPageViewController: UIViewController {
|
||||||
@@ -259,6 +262,192 @@ final class FilesViewController: RefreshingTableViewController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
final class RepositoryDirectoryViewController: RefreshingTableViewController {
|
||||||
|
private let context: AppContext
|
||||||
|
private let owner: String
|
||||||
|
private let repository: String
|
||||||
|
private let path: String
|
||||||
|
private var rows: [RepositoryContentRow] = []
|
||||||
|
|
||||||
|
init(context: AppContext, owner: String, repository: String, path: String) {
|
||||||
|
self.context = context
|
||||||
|
self.owner = owner
|
||||||
|
self.repository = repository
|
||||||
|
self.path = path
|
||||||
|
super.init()
|
||||||
|
title = path.split(separator: "/").last.map(String.init) ?? repository
|
||||||
|
}
|
||||||
|
|
||||||
|
@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.repositoryContents(
|
||||||
|
owner: owner,
|
||||||
|
repository: repository,
|
||||||
|
path: path
|
||||||
|
)
|
||||||
|
tableView.reloadData()
|
||||||
|
tableView.backgroundView = rows.isEmpty
|
||||||
|
? EmptyBackgroundView(title: "Empty folder", detail: "This folder does not contain any files.")
|
||||||
|
: 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-content")
|
||||||
|
?? UITableViewCell(style: .subtitle, reuseIdentifier: "repository-content")
|
||||||
|
configureRepositoryContentCell(cell, row: rows[indexPath.row])
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||||
|
tableView.deselectRow(at: indexPath, animated: true)
|
||||||
|
showRepositoryContent(
|
||||||
|
rows[indexPath.row],
|
||||||
|
context: context,
|
||||||
|
owner: owner,
|
||||||
|
repository: repository,
|
||||||
|
navigationController: navigationController
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
final class RepositoryFileViewController: UIViewController, QLPreviewControllerDataSource {
|
||||||
|
private let context: AppContext
|
||||||
|
private let owner: String
|
||||||
|
private let repository: String
|
||||||
|
private let path: String
|
||||||
|
private let spinner = UIActivityIndicatorView(style: .medium)
|
||||||
|
private var loadingTask: Task<Void, Never>?
|
||||||
|
private var previewURL: URL?
|
||||||
|
|
||||||
|
init(context: AppContext, owner: String, repository: String, path: String) {
|
||||||
|
self.context = context
|
||||||
|
self.owner = owner
|
||||||
|
self.repository = repository
|
||||||
|
self.path = path
|
||||||
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
title = path.split(separator: "/").last.map(String.init)
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, unavailable)
|
||||||
|
required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
view.backgroundColor = .systemBackground
|
||||||
|
beginNavigationLoading(spinner)
|
||||||
|
loadingTask = Task {
|
||||||
|
do {
|
||||||
|
let data = try await context.core.repositoryFile(
|
||||||
|
owner: owner,
|
||||||
|
repository: repository,
|
||||||
|
path: path
|
||||||
|
)
|
||||||
|
if !isMediaFile(path), let source = String(data: data, encoding: .utf8) {
|
||||||
|
showSource(source)
|
||||||
|
} else {
|
||||||
|
try showQuickLook(data)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
if !Task.isCancelled { show(error: error) }
|
||||||
|
}
|
||||||
|
endNavigationLoading(spinner)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func isMediaFile(_ path: String) -> Bool {
|
||||||
|
guard let type = UTType(filenameExtension: (path as NSString).pathExtension) else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return type.conforms(to: .image)
|
||||||
|
|| type.conforms(to: .audio)
|
||||||
|
|| type.conforms(to: .audiovisualContent)
|
||||||
|
|| type.conforms(to: .pdf)
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
loadingTask?.cancel()
|
||||||
|
if let previewURL { try? FileManager.default.removeItem(at: previewURL) }
|
||||||
|
}
|
||||||
|
|
||||||
|
func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
|
||||||
|
previewURL == nil ? 0 : 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
|
||||||
|
previewURL! as NSURL
|
||||||
|
}
|
||||||
|
|
||||||
|
private func showSource(_ source: String) {
|
||||||
|
let textView = UITextView(frame: view.bounds)
|
||||||
|
textView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||||
|
textView.isEditable = false
|
||||||
|
textView.isSelectable = true
|
||||||
|
textView.alwaysBounceHorizontal = true
|
||||||
|
textView.isDirectionalLockEnabled = true
|
||||||
|
textView.showsHorizontalScrollIndicator = true
|
||||||
|
textView.textContainer.widthTracksTextView = false
|
||||||
|
textView.textContainer.size = CGSize(
|
||||||
|
width: CGFloat.greatestFiniteMagnitude,
|
||||||
|
height: CGFloat.greatestFiniteMagnitude
|
||||||
|
)
|
||||||
|
let highlighter = Highlighter()
|
||||||
|
highlighter?.setTheme(
|
||||||
|
traitCollection.userInterfaceStyle == .dark ? "atom-one-dark" : "atom-one-light"
|
||||||
|
)
|
||||||
|
highlighter?.theme.setCodeFont(.monospacedSystemFont(ofSize: 13, weight: .regular))
|
||||||
|
textView.attributedText = highlighter?.highlight(source, as: sourceLanguage(path))
|
||||||
|
?? NSAttributedString(string: source, attributes: [
|
||||||
|
.font: UIFont.monospacedSystemFont(ofSize: 13, weight: .regular),
|
||||||
|
.foregroundColor: UIColor.label,
|
||||||
|
])
|
||||||
|
textView.layoutManager.ensureLayout(for: textView.textContainer)
|
||||||
|
textView.contentSize.width = textView.layoutManager.usedRect(for: textView.textContainer).width
|
||||||
|
+ textView.textContainerInset.left
|
||||||
|
+ textView.textContainerInset.right
|
||||||
|
view.addSubview(textView)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func showQuickLook(_ data: Data) throws {
|
||||||
|
let name = path.split(separator: "/").last.map(String.init) ?? "Preview"
|
||||||
|
let url = FileManager.default.temporaryDirectory
|
||||||
|
.appendingPathComponent("\(UUID().uuidString)-\(name)")
|
||||||
|
try data.write(to: url, options: .atomic)
|
||||||
|
previewURL = url
|
||||||
|
let preview = QLPreviewController()
|
||||||
|
preview.dataSource = self
|
||||||
|
addChild(preview)
|
||||||
|
preview.view.frame = view.bounds
|
||||||
|
preview.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||||
|
view.addSubview(preview.view)
|
||||||
|
preview.didMove(toParent: self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
enum DiffSource {
|
enum DiffSource {
|
||||||
case commit(owner: String, repository: String, sha: String, path: String)
|
case commit(owner: String, repository: String, sha: String, path: String)
|
||||||
case pull(owner: String, repository: String, number: Int64, path: String)
|
case pull(owner: String, repository: String, number: Int64, path: String)
|
||||||
|
|||||||
@@ -124,6 +124,66 @@ func configureTextCell(_ cell: UITableViewCell, title: String, detail: String, i
|
|||||||
cell.backgroundColor = .systemBackground
|
cell.backgroundColor = .systemBackground
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func configureRepositoryContentCell(_ cell: UITableViewCell, row: RepositoryContentRow) {
|
||||||
|
let directory = row.kind == "dir"
|
||||||
|
let detail = directory
|
||||||
|
? "Folder"
|
||||||
|
: ByteCountFormatter.string(fromByteCount: row.size, countStyle: .file)
|
||||||
|
configureTextCell(
|
||||||
|
cell,
|
||||||
|
title: row.name,
|
||||||
|
detail: detail,
|
||||||
|
image: UIImage(systemName: directory ? "folder.fill" : "doc")
|
||||||
|
)
|
||||||
|
cell.accessoryType = .disclosureIndicator
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func showRepositoryContent(
|
||||||
|
_ row: RepositoryContentRow,
|
||||||
|
context: AppContext,
|
||||||
|
owner: String,
|
||||||
|
repository: String,
|
||||||
|
navigationController: UINavigationController?
|
||||||
|
) {
|
||||||
|
let destination: UIViewController = row.kind == "dir"
|
||||||
|
? RepositoryDirectoryViewController(
|
||||||
|
context: context,
|
||||||
|
owner: owner,
|
||||||
|
repository: repository,
|
||||||
|
path: row.path
|
||||||
|
)
|
||||||
|
: RepositoryFileViewController(
|
||||||
|
context: context,
|
||||||
|
owner: owner,
|
||||||
|
repository: repository,
|
||||||
|
path: row.path
|
||||||
|
)
|
||||||
|
navigationController?.pushViewController(destination, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sourceLanguage(_ path: String) -> String? {
|
||||||
|
let filename = (path as NSString).lastPathComponent.lowercased()
|
||||||
|
let filenames = [
|
||||||
|
"cmakelists.txt": "cmake", "dockerfile": "dockerfile", "gemfile": "ruby",
|
||||||
|
"makefile": "makefile", "podfile": "ruby",
|
||||||
|
]
|
||||||
|
if let language = filenames[filename] { return language }
|
||||||
|
let languages = [
|
||||||
|
"asm": "x86asm", "c": "c", "cc": "cpp", "clj": "clojure", "cpp": "cpp",
|
||||||
|
"cs": "csharp", "css": "css", "dart": "dart", "ex": "elixir", "exs": "elixir",
|
||||||
|
"fs": "fsharp", "go": "go", "groovy": "groovy", "h": "cpp", "hpp": "cpp",
|
||||||
|
"hs": "haskell", "html": "xml", "java": "java", "jl": "julia", "js": "javascript",
|
||||||
|
"json": "json", "jsx": "javascript", "kt": "kotlin", "kts": "kotlin", "lua": "lua",
|
||||||
|
"m": "objectivec", "md": "markdown", "mm": "objectivec", "php": "php", "pl": "perl",
|
||||||
|
"plist": "xml", "ps1": "powershell", "py": "python", "r": "r", "rb": "ruby",
|
||||||
|
"rs": "rust", "scala": "scala", "scss": "scss", "sh": "bash", "sql": "sql",
|
||||||
|
"swift": "swift", "toml": "ini", "ts": "typescript", "tsx": "typescript",
|
||||||
|
"txt": "plaintext", "xml": "xml", "yaml": "yaml", "yml": "yaml",
|
||||||
|
]
|
||||||
|
return languages[(path as NSString).pathExtension.lowercased()]
|
||||||
|
}
|
||||||
|
|
||||||
func symbolText(_ symbol: String, text: String, font: UIFont, color: UIColor) -> NSAttributedString {
|
func symbolText(_ symbol: String, text: String, font: UIFont, color: UIColor) -> NSAttributedString {
|
||||||
let output = NSMutableAttributedString()
|
let output = NSMutableAttributedString()
|
||||||
if let image = UIImage(systemName: symbol)?.withTintColor(color, renderingMode: .alwaysOriginal) {
|
if let image = UIImage(systemName: symbol)?.withTintColor(color, renderingMode: .alwaysOriginal) {
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ options:
|
|||||||
bundleIdPrefix: de.rfc1437
|
bundleIdPrefix: de.rfc1437
|
||||||
settings:
|
settings:
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING: NO
|
ENABLE_USER_SCRIPT_SANDBOXING: NO
|
||||||
|
packages:
|
||||||
|
Highlighter:
|
||||||
|
url: https://github.com/smittytone/HighlighterSwift
|
||||||
|
from: 3.1.0
|
||||||
targets:
|
targets:
|
||||||
Gotcha:
|
Gotcha:
|
||||||
type: application
|
type: application
|
||||||
@@ -29,6 +33,8 @@ targets:
|
|||||||
- Assets.xcassets
|
- Assets.xcassets
|
||||||
- Sources
|
- Sources
|
||||||
- Generated/gotcha_core.swift
|
- Generated/gotcha_core.swift
|
||||||
|
dependencies:
|
||||||
|
- package: Highlighter
|
||||||
preBuildScripts:
|
preBuildScripts:
|
||||||
- name: Build Rust core
|
- name: Build Rust core
|
||||||
basedOnDependencyAnalysis: false
|
basedOnDependencyAnalysis: false
|
||||||
|
|||||||
Reference in New Issue
Block a user