Initial commit — iOS share extension for filing Gitea issues
Two-target Xcode project (xcodegen spec). The GiteaIssue container app holds the base URL + personal access token in a shared keychain group; the GiteaIssueShare extension reads them, surfaces a repo picker (with recents) and a title/notes form, then creates the issue, uploads the screenshot as an asset, and patches the body to embed it inline. Min iOS 26.0, signing team V3PF3M6B6U. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+25
@@ -0,0 +1,25 @@
|
||||
## macOS
|
||||
.DS_Store
|
||||
|
||||
## Xcode
|
||||
build/
|
||||
*.xcuserstate
|
||||
xcuserdata/
|
||||
*.xcworkspace
|
||||
DerivedData/
|
||||
*.moved-aside
|
||||
*.hmap
|
||||
*.ipa
|
||||
*.dSYM.zip
|
||||
*.dSYM
|
||||
|
||||
## Generated by xcodegen — keep tracked so the builder doesn't need xcodegen installed
|
||||
# (intentionally NOT ignoring *.xcodeproj)
|
||||
|
||||
## Swift Package Manager
|
||||
.swiftpm/
|
||||
.build/
|
||||
Packages/
|
||||
Package.pins
|
||||
Package.resolved
|
||||
*.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/
|
||||
@@ -0,0 +1,487 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 77;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
01EE3AAB26736728CA93C686 /* ShareSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 943C9E92238296254E79717B /* ShareSheetView.swift */; };
|
||||
07EED00FE7DDDA0CAFBC35E3 /* GiteaIssueShare.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = C835A0DE1B14653FF82534D6 /* GiteaIssueShare.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
186BF400248B98EFD6B73FE4 /* Keychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8783ACB69D59F2D49C512D5E /* Keychain.swift */; };
|
||||
1BD5244ACCEAC64099EEBB4D /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88DA04623CDDD48C183CD674 /* Settings.swift */; };
|
||||
289A0520E023B3DE765CAE54 /* GiteaClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FBDAD2C584C35B08A12E6C5 /* GiteaClient.swift */; };
|
||||
35FC7E228E0B41F5D0D692B3 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96E3E2D5BC7DE7A285F7AFB6 /* SettingsView.swift */; };
|
||||
3764DB595E046C831517776B /* HelpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E60AB1F455BE4290BBB0A653 /* HelpView.swift */; };
|
||||
5014022146E8A565C0FE4742 /* Keychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8783ACB69D59F2D49C512D5E /* Keychain.swift */; };
|
||||
5259B0BCE30064A20F5750C9 /* Models.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F9B72394E791840385F7CA /* Models.swift */; };
|
||||
58CB61A5A917AD7174F17E45 /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88DA04623CDDD48C183CD674 /* Settings.swift */; };
|
||||
6CDAA55A2740CF46137C04F3 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6A36396FE48CA333CA1E4B7 /* ShareViewController.swift */; };
|
||||
A956C3B78247D43E0BA4C6D5 /* RepoCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50238EB026C45E93757D6034 /* RepoCache.swift */; };
|
||||
B3EE4A41233945C5FDDF138D /* GiteaIssueApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5302974F9B6C6614445906E /* GiteaIssueApp.swift */; };
|
||||
BE4BF5491648BEFAB36609C3 /* Models.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F9B72394E791840385F7CA /* Models.swift */; };
|
||||
CB763FC7C88126BDC8C70675 /* GiteaClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FBDAD2C584C35B08A12E6C5 /* GiteaClient.swift */; };
|
||||
DB1BA9B0DAFEE5F278C26869 /* RepoCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50238EB026C45E93757D6034 /* RepoCache.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
F5C239800D6DD31B3979E2A3 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 27251B64FF480AB8EC0A6E41 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 8530533F8ED42AE5A8DC73EB;
|
||||
remoteInfo = GiteaIssueShare;
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
437F35B7AC1E4C86A2B4B3AB /* Embed Foundation Extensions */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 13;
|
||||
files = (
|
||||
07EED00FE7DDDA0CAFBC35E3 /* GiteaIssueShare.appex in Embed Foundation Extensions */,
|
||||
);
|
||||
name = "Embed Foundation Extensions";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
282BE61DAAEDDE1523B95A12 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
||||
50238EB026C45E93757D6034 /* RepoCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepoCache.swift; sourceTree = "<group>"; };
|
||||
5B7B8092213B20CA751D33FE /* GiteaIssue.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = GiteaIssue.entitlements; sourceTree = "<group>"; };
|
||||
7FBDAD2C584C35B08A12E6C5 /* GiteaClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GiteaClient.swift; sourceTree = "<group>"; };
|
||||
8783ACB69D59F2D49C512D5E /* Keychain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Keychain.swift; sourceTree = "<group>"; };
|
||||
88DA04623CDDD48C183CD674 /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = "<group>"; };
|
||||
943C9E92238296254E79717B /* ShareSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareSheetView.swift; sourceTree = "<group>"; };
|
||||
96E3E2D5BC7DE7A285F7AFB6 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
|
||||
B5302974F9B6C6614445906E /* GiteaIssueApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GiteaIssueApp.swift; sourceTree = "<group>"; };
|
||||
C4F9B72394E791840385F7CA /* Models.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Models.swift; sourceTree = "<group>"; };
|
||||
C8019B6DFDB9FFE978C1A8A1 /* GiteaIssueShare.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = GiteaIssueShare.entitlements; sourceTree = "<group>"; };
|
||||
C835A0DE1B14653FF82534D6 /* GiteaIssueShare.appex */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = "wrapper.app-extension"; path = GiteaIssueShare.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
E60AB1F455BE4290BBB0A653 /* HelpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelpView.swift; sourceTree = "<group>"; };
|
||||
E6A36396FE48CA333CA1E4B7 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = "<group>"; };
|
||||
F817F3CE31E9F23573E92E96 /* GiteaIssue.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = GiteaIssue.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
260438FC4CFC248774904FD2 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
99014873FB3E05D905AC0F72 /* GiteaIssue */,
|
||||
943233C666B3E7A65EBA983B /* GiteaIssueShare */,
|
||||
282DF4582F6764AE4B98418F /* Shared */,
|
||||
5F0DE786E5E3BF399FAB1AA1 /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
282DF4582F6764AE4B98418F /* Shared */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
7FBDAD2C584C35B08A12E6C5 /* GiteaClient.swift */,
|
||||
8783ACB69D59F2D49C512D5E /* Keychain.swift */,
|
||||
C4F9B72394E791840385F7CA /* Models.swift */,
|
||||
50238EB026C45E93757D6034 /* RepoCache.swift */,
|
||||
88DA04623CDDD48C183CD674 /* Settings.swift */,
|
||||
);
|
||||
path = Shared;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
5F0DE786E5E3BF399FAB1AA1 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
F817F3CE31E9F23573E92E96 /* GiteaIssue.app */,
|
||||
C835A0DE1B14653FF82534D6 /* GiteaIssueShare.appex */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
943233C666B3E7A65EBA983B /* GiteaIssueShare */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C8019B6DFDB9FFE978C1A8A1 /* GiteaIssueShare.entitlements */,
|
||||
282BE61DAAEDDE1523B95A12 /* Info.plist */,
|
||||
943C9E92238296254E79717B /* ShareSheetView.swift */,
|
||||
E6A36396FE48CA333CA1E4B7 /* ShareViewController.swift */,
|
||||
);
|
||||
path = GiteaIssueShare;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
99014873FB3E05D905AC0F72 /* GiteaIssue */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5B7B8092213B20CA751D33FE /* GiteaIssue.entitlements */,
|
||||
B5302974F9B6C6614445906E /* GiteaIssueApp.swift */,
|
||||
E60AB1F455BE4290BBB0A653 /* HelpView.swift */,
|
||||
96E3E2D5BC7DE7A285F7AFB6 /* SettingsView.swift */,
|
||||
);
|
||||
path = GiteaIssue;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
8530533F8ED42AE5A8DC73EB /* GiteaIssueShare */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 5442EF69B0C92817306C8BB0 /* Build configuration list for PBXNativeTarget "GiteaIssueShare" */;
|
||||
buildPhases = (
|
||||
9B221782680ADACF44216436 /* Sources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = GiteaIssueShare;
|
||||
packageProductDependencies = (
|
||||
);
|
||||
productName = GiteaIssueShare;
|
||||
productReference = C835A0DE1B14653FF82534D6 /* GiteaIssueShare.appex */;
|
||||
productType = "com.apple.product-type.app-extension";
|
||||
};
|
||||
C0A6998D8057F3D0D81418CC /* GiteaIssue */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = EBCA52B3DCF7CF96695F838F /* Build configuration list for PBXNativeTarget "GiteaIssue" */;
|
||||
buildPhases = (
|
||||
9E8A23BF1FC9CD9F5A31EF77 /* Sources */,
|
||||
437F35B7AC1E4C86A2B4B3AB /* Embed Foundation Extensions */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
BF1D933F53035433A27EFE31 /* PBXTargetDependency */,
|
||||
);
|
||||
name = GiteaIssue;
|
||||
packageProductDependencies = (
|
||||
);
|
||||
productName = GiteaIssue;
|
||||
productReference = F817F3CE31E9F23573E92E96 /* GiteaIssue.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
27251B64FF480AB8EC0A6E41 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = YES;
|
||||
LastUpgradeCheck = 1430;
|
||||
TargetAttributes = {
|
||||
8530533F8ED42AE5A8DC73EB = {
|
||||
DevelopmentTeam = V3PF3M6B6U;
|
||||
ProvisioningStyle = Automatic;
|
||||
};
|
||||
C0A6998D8057F3D0D81418CC = {
|
||||
DevelopmentTeam = V3PF3M6B6U;
|
||||
ProvisioningStyle = Automatic;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 7B049227F0861B8B31732B4B /* Build configuration list for PBXProject "GiteaIssue" */;
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
Base,
|
||||
en,
|
||||
);
|
||||
mainGroup = 260438FC4CFC248774904FD2;
|
||||
minimizedProjectReferenceProxies = 1;
|
||||
preferredProjectObjectVersion = 77;
|
||||
productRefGroup = 5F0DE786E5E3BF399FAB1AA1 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
C0A6998D8057F3D0D81418CC /* GiteaIssue */,
|
||||
8530533F8ED42AE5A8DC73EB /* GiteaIssueShare */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
9B221782680ADACF44216436 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
289A0520E023B3DE765CAE54 /* GiteaClient.swift in Sources */,
|
||||
186BF400248B98EFD6B73FE4 /* Keychain.swift in Sources */,
|
||||
BE4BF5491648BEFAB36609C3 /* Models.swift in Sources */,
|
||||
DB1BA9B0DAFEE5F278C26869 /* RepoCache.swift in Sources */,
|
||||
58CB61A5A917AD7174F17E45 /* Settings.swift in Sources */,
|
||||
01EE3AAB26736728CA93C686 /* ShareSheetView.swift in Sources */,
|
||||
6CDAA55A2740CF46137C04F3 /* ShareViewController.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
9E8A23BF1FC9CD9F5A31EF77 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
CB763FC7C88126BDC8C70675 /* GiteaClient.swift in Sources */,
|
||||
B3EE4A41233945C5FDDF138D /* GiteaIssueApp.swift in Sources */,
|
||||
3764DB595E046C831517776B /* HelpView.swift in Sources */,
|
||||
5014022146E8A565C0FE4742 /* Keychain.swift in Sources */,
|
||||
5259B0BCE30064A20F5750C9 /* Models.swift in Sources */,
|
||||
A956C3B78247D43E0BA4C6D5 /* RepoCache.swift in Sources */,
|
||||
1BD5244ACCEAC64099EEBB4D /* Settings.swift in Sources */,
|
||||
35FC7E228E0B41F5D0D692B3 /* SettingsView.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
BF1D933F53035433A27EFE31 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 8530533F8ED42AE5A8DC73EB /* GiteaIssueShare */;
|
||||
targetProxy = F5C239800D6DD31B3979E2A3 /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
1758188C5D1C2C0169FB4797 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = GiteaIssueShare/GiteaIssueShare.entitlements;
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
INFOPLIST_FILE = GiteaIssueShare/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.treytartt.GiteaIssue.Share;
|
||||
SDKROOT = iphoneos;
|
||||
SKIP_INSTALL = YES;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
451ECB132FE921DA75BBA36D /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = V3PF3M6B6U;
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
|
||||
MARKETING_VERSION = 1.0.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
SWIFT_VERSION = 5.10;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
6F09C48D2C8D1468759D6FD8 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = GiteaIssueShare/GiteaIssueShare.entitlements;
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
INFOPLIST_FILE = GiteaIssueShare/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.treytartt.GiteaIssue.Share;
|
||||
SDKROOT = iphoneos;
|
||||
SKIP_INSTALL = YES;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
732A9163E55043084DB2FB4A /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = GiteaIssue/GiteaIssue.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Gitea Issue";
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.treytartt.GiteaIssue;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTS_MACCATALYST = NO;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
976B108502C6E173A5101066 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = V3PF3M6B6U;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"$(inherited)",
|
||||
"DEBUG=1",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
|
||||
MARKETING_VERSION = 1.0.0;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.10;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
D1D47330074D8F620569EEFB /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = GiteaIssue/GiteaIssue.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Gitea Issue";
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.treytartt.GiteaIssue;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTS_MACCATALYST = NO;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
5442EF69B0C92817306C8BB0 /* Build configuration list for PBXNativeTarget "GiteaIssueShare" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
1758188C5D1C2C0169FB4797 /* Debug */,
|
||||
6F09C48D2C8D1468759D6FD8 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Debug;
|
||||
};
|
||||
7B049227F0861B8B31732B4B /* Build configuration list for PBXProject "GiteaIssue" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
976B108502C6E173A5101066 /* Debug */,
|
||||
451ECB132FE921DA75BBA36D /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Debug;
|
||||
};
|
||||
EBCA52B3DCF7CF96695F838F /* Build configuration list for PBXNativeTarget "GiteaIssue" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
732A9163E55043084DB2FB4A /* Debug */,
|
||||
D1D47330074D8F620569EEFB /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Debug;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 27251B64FF480AB8EC0A6E41 /* Project object */;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.com.treytartt.GiteaIssue</string>
|
||||
</array>
|
||||
<key>keychain-access-groups</key>
|
||||
<array>
|
||||
<string>$(AppIdentifierPrefix)com.treytartt.GiteaIssue</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,21 @@
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
struct GiteaIssueApp: App {
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
RootView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct RootView: View {
|
||||
var body: some View {
|
||||
TabView {
|
||||
SettingsView()
|
||||
.tabItem { Label("Settings", systemImage: "gearshape") }
|
||||
HelpView()
|
||||
.tabItem { Label("How to use", systemImage: "questionmark.circle") }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import SwiftUI
|
||||
|
||||
struct HelpView: View {
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
Text("How to file an issue")
|
||||
.font(.title2.bold())
|
||||
|
||||
step(number: 1, title: "Take a screenshot", body: "Side button + volume up.")
|
||||
step(number: 2, title: "Open the screenshot", body: "Either tap the thumbnail right after capture, or open Photos.")
|
||||
step(number: 3, title: "Tap Share", body: "The square-with-arrow icon at the bottom-left.")
|
||||
step(number: 4, title: "Pick \"Gitea Issue\"", body: "Scroll the icon row and tap the Gitea Issue extension.")
|
||||
step(number: 5, title: "Choose a repo, write a title", body: "Notes are optional. Tap Submit. The screenshot gets attached and embedded inline.")
|
||||
|
||||
Divider()
|
||||
|
||||
Text("First time?")
|
||||
.font(.headline)
|
||||
Text("Open the Settings tab and add your Gitea base URL and a personal access token (with `repo` scope). Tap Save & test connection — when it shows your login, you're set.")
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.navigationTitle("Help")
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func step(number: Int, title: String, body: String) -> some View {
|
||||
HStack(alignment: .top, spacing: 12) {
|
||||
ZStack {
|
||||
Circle().fill(.tint).frame(width: 28, height: 28)
|
||||
Text("\(number)").font(.callout.bold()).foregroundStyle(.white)
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(title).font(.headline)
|
||||
Text(body).foregroundStyle(.secondary).font(.subheadline)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
HelpView()
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import SwiftUI
|
||||
|
||||
struct SettingsView: View {
|
||||
@State private var baseURL: String = AppSettings.baseURL
|
||||
@State private var token: String = AppSettings.token
|
||||
@State private var testResult: TestResult?
|
||||
@State private var testing = false
|
||||
|
||||
enum TestResult: Equatable {
|
||||
case success(String)
|
||||
case failure(String)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
Form {
|
||||
Section("Gitea") {
|
||||
TextField("Base URL", text: $baseURL)
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled()
|
||||
.keyboardType(.URL)
|
||||
SecureField("Personal access token", text: $token)
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled()
|
||||
}
|
||||
|
||||
Section {
|
||||
Button {
|
||||
save()
|
||||
runTest()
|
||||
} label: {
|
||||
HStack {
|
||||
Text(testing ? "Testing…" : "Save & test connection")
|
||||
Spacer()
|
||||
if testing { ProgressView() }
|
||||
}
|
||||
}
|
||||
.disabled(testing || baseURL.isEmpty || token.isEmpty)
|
||||
|
||||
if let result = testResult {
|
||||
switch result {
|
||||
case .success(let login):
|
||||
Label("Connected as \(login)", systemImage: "checkmark.circle.fill")
|
||||
.foregroundStyle(.green)
|
||||
case .failure(let msg):
|
||||
Label(msg, systemImage: "xmark.octagon.fill")
|
||||
.foregroundStyle(.red)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section("Token scope") {
|
||||
Text("The token needs `repo` scope so the share extension can create issues and upload attachments.")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
Link("Open Gitea token settings", destination: tokenSettingsURL)
|
||||
.font(.footnote)
|
||||
}
|
||||
}
|
||||
.navigationTitle("Gitea Issue")
|
||||
}
|
||||
}
|
||||
|
||||
private var tokenSettingsURL: URL {
|
||||
URL(string: baseURL)?.appendingPathComponent("/user/settings/applications") ?? URL(string: "https://gitea.treytartt.com/user/settings/applications")!
|
||||
}
|
||||
|
||||
private func save() {
|
||||
AppSettings.baseURL = baseURL.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
AppSettings.token = token.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
}
|
||||
|
||||
private func runTest() {
|
||||
testing = true
|
||||
testResult = nil
|
||||
Task {
|
||||
do {
|
||||
let client = try GiteaClient()
|
||||
let user = try await client.currentUser()
|
||||
let repos = try await client.searchRepos()
|
||||
RepoCache.repos = repos
|
||||
await MainActor.run {
|
||||
testing = false
|
||||
testResult = .success("\(user.login) — cached \(repos.count) repos")
|
||||
}
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
testing = false
|
||||
testResult = .failure(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
SettingsView()
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.com.treytartt.GiteaIssue</string>
|
||||
</array>
|
||||
<key>keychain-access-groups</key>
|
||||
<array>
|
||||
<string>$(AppIdentifierPrefix)com.treytartt.GiteaIssue</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Gitea Issue</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionAttributes</key>
|
||||
<dict>
|
||||
<key>NSExtensionActivationRule</key>
|
||||
<dict>
|
||||
<key>NSExtensionActivationSupportsImageWithMaxCount</key>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.share-services</string>
|
||||
<key>NSExtensionPrincipalClass</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).ShareViewController</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,227 @@
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
struct ShareSheetView: View {
|
||||
let image: UIImage?
|
||||
var imageData: Data? = nil
|
||||
var errorMessage: String? = nil
|
||||
let onClose: () -> Void
|
||||
|
||||
@State private var repos: [GiteaRepo] = RepoCache.repos
|
||||
@State private var loadingRepos = false
|
||||
@State private var loadError: String?
|
||||
@State private var search = ""
|
||||
@State private var selectedRepo: GiteaRepo?
|
||||
@State private var title = ""
|
||||
@State private var notes = ""
|
||||
@State private var submitting = false
|
||||
@State private var submitError: String?
|
||||
@State private var submitted: GiteaIssue?
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
Form {
|
||||
if let error = errorMessage {
|
||||
Section {
|
||||
Label(error, systemImage: "exclamationmark.triangle.fill")
|
||||
.foregroundStyle(.red)
|
||||
}
|
||||
}
|
||||
|
||||
if let image {
|
||||
Section {
|
||||
Image(uiImage: image)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(maxHeight: 160)
|
||||
.frame(maxWidth: .infinity)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 12))
|
||||
}
|
||||
}
|
||||
|
||||
Section("Repository") {
|
||||
if loadingRepos && repos.isEmpty {
|
||||
HStack { ProgressView(); Text("Loading repos…") }
|
||||
} else if let loadError {
|
||||
Label(loadError, systemImage: "xmark.octagon")
|
||||
.foregroundStyle(.red)
|
||||
} else {
|
||||
repoPicker
|
||||
}
|
||||
}
|
||||
|
||||
Section("Issue") {
|
||||
TextField("Title", text: $title)
|
||||
.textInputAutocapitalization(.sentences)
|
||||
TextField("Notes (optional)", text: $notes, axis: .vertical)
|
||||
.lineLimit(3...8)
|
||||
}
|
||||
|
||||
if let submitError {
|
||||
Section {
|
||||
Label(submitError, systemImage: "exclamationmark.triangle")
|
||||
.foregroundStyle(.red)
|
||||
}
|
||||
}
|
||||
|
||||
if let issue = submitted {
|
||||
Section {
|
||||
Label("Created issue #\(issue.number)", systemImage: "checkmark.circle.fill")
|
||||
.foregroundStyle(.green)
|
||||
Link("Open in Gitea", destination: URL(string: issue.htmlUrl)!)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("New issue")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("Cancel") { onClose() }
|
||||
}
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
Button(submitting ? "Sending…" : "Submit") { submit() }
|
||||
.disabled(!canSubmit)
|
||||
}
|
||||
}
|
||||
.task { await loadReposIfNeeded() }
|
||||
}
|
||||
}
|
||||
|
||||
private var canSubmit: Bool {
|
||||
selectedRepo != nil && !title.trimmingCharacters(in: .whitespaces).isEmpty && !submitting && submitted == nil
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var repoPicker: some View {
|
||||
TextField("Search repos", text: $search)
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled()
|
||||
|
||||
let filtered = filteredRepos
|
||||
let recents = recentRepos.filter { search.isEmpty || $0.fullName.localizedCaseInsensitiveContains(search) }
|
||||
|
||||
if !recents.isEmpty {
|
||||
ForEach(recents) { repo in
|
||||
row(repo, badge: "Recent")
|
||||
}
|
||||
}
|
||||
|
||||
ForEach(filtered.filter { r in !recents.contains(where: { $0.id == r.id }) }) { repo in
|
||||
row(repo, badge: nil)
|
||||
}
|
||||
|
||||
Button {
|
||||
Task { await refreshRepos(force: true) }
|
||||
} label: {
|
||||
Label("Refresh repo list", systemImage: "arrow.clockwise")
|
||||
}
|
||||
}
|
||||
|
||||
private func row(_ repo: GiteaRepo, badge: String?) -> some View {
|
||||
Button {
|
||||
selectedRepo = repo
|
||||
} label: {
|
||||
HStack {
|
||||
VStack(alignment: .leading) {
|
||||
Text(repo.fullName)
|
||||
.foregroundStyle(.primary)
|
||||
if let badge {
|
||||
Text(badge).font(.caption2).foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
if selectedRepo?.id == repo.id {
|
||||
Image(systemName: "checkmark").foregroundStyle(.tint)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var filteredRepos: [GiteaRepo] {
|
||||
let sorted = repos.sorted { $0.fullName.localizedCaseInsensitiveCompare($1.fullName) == .orderedAscending }
|
||||
guard !search.isEmpty else { return sorted }
|
||||
return sorted.filter { $0.fullName.localizedCaseInsensitiveContains(search) }
|
||||
}
|
||||
|
||||
private var recentRepos: [GiteaRepo] {
|
||||
let names = RepoCache.recentFullNames
|
||||
return names.compactMap { name in repos.first(where: { $0.fullName == name }) }
|
||||
}
|
||||
|
||||
private func loadReposIfNeeded() async {
|
||||
if repos.isEmpty || RepoCache.isStale {
|
||||
await refreshRepos(force: false)
|
||||
}
|
||||
}
|
||||
|
||||
private func refreshRepos(force: Bool) async {
|
||||
loadingRepos = true
|
||||
loadError = nil
|
||||
do {
|
||||
let client = try GiteaClient()
|
||||
let fresh = try await client.searchRepos()
|
||||
await MainActor.run {
|
||||
self.repos = fresh
|
||||
RepoCache.repos = fresh
|
||||
self.loadingRepos = false
|
||||
}
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
self.loadError = error.localizedDescription
|
||||
self.loadingRepos = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func submit() {
|
||||
guard let repo = selectedRepo, let imageData else {
|
||||
submitError = "Pick a repo and make sure the screenshot loaded."
|
||||
return
|
||||
}
|
||||
submitting = true
|
||||
submitError = nil
|
||||
|
||||
Task {
|
||||
do {
|
||||
let client = try GiteaClient()
|
||||
let bodyText = notes.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let issue = try await client.createIssue(
|
||||
owner: repo.owner.login,
|
||||
repo: repo.name,
|
||||
title: title.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
body: bodyText
|
||||
)
|
||||
let asset = try await client.uploadAsset(
|
||||
owner: repo.owner.login,
|
||||
repo: repo.name,
|
||||
issueNumber: issue.number,
|
||||
image: imageData,
|
||||
filename: "screenshot-\(Int(Date().timeIntervalSince1970)).png"
|
||||
)
|
||||
let updatedBody = (bodyText.isEmpty ? "" : bodyText + "\n\n") +
|
||||
")"
|
||||
try await client.updateIssueBody(
|
||||
owner: repo.owner.login,
|
||||
repo: repo.name,
|
||||
issueNumber: issue.number,
|
||||
body: updatedBody
|
||||
)
|
||||
RepoCache.touch(repo.fullName)
|
||||
|
||||
await MainActor.run {
|
||||
submitting = false
|
||||
submitted = issue
|
||||
UINotificationFeedbackGenerator().notificationOccurred(.success)
|
||||
}
|
||||
try? await Task.sleep(nanoseconds: 800_000_000)
|
||||
await MainActor.run { onClose() }
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
submitting = false
|
||||
submitError = error.localizedDescription
|
||||
UINotificationFeedbackGenerator().notificationOccurred(.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
final class ShareViewController: UIViewController {
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
view.backgroundColor = .clear
|
||||
Task { await loadAttachment() }
|
||||
}
|
||||
|
||||
private func loadAttachment() async {
|
||||
guard let item = (extensionContext?.inputItems as? [NSExtensionItem])?.first,
|
||||
let provider = item.attachments?.first(where: { $0.hasItemConformingToTypeIdentifier(UTType.image.identifier) })
|
||||
else {
|
||||
present(host: ShareSheetView(image: nil, onClose: { [weak self] in self?.complete() }))
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
let data = try await loadImageData(from: provider)
|
||||
await MainActor.run {
|
||||
let image = data.flatMap { UIImage(data: $0) }
|
||||
let host = ShareSheetView(
|
||||
image: image,
|
||||
imageData: data,
|
||||
onClose: { [weak self] in self?.complete() }
|
||||
)
|
||||
self.present(host: host)
|
||||
}
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
present(host: ShareSheetView(image: nil, errorMessage: error.localizedDescription, onClose: { [weak self] in self?.complete() }))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func loadImageData(from provider: NSItemProvider) async throws -> Data? {
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
provider.loadItem(forTypeIdentifier: UTType.image.identifier, options: nil) { item, error in
|
||||
if let error { continuation.resume(throwing: error); return }
|
||||
if let data = item as? Data { continuation.resume(returning: data); return }
|
||||
if let url = item as? URL, let data = try? Data(contentsOf: url) { continuation.resume(returning: data); return }
|
||||
if let image = item as? UIImage, let data = image.pngData() { continuation.resume(returning: data); return }
|
||||
continuation.resume(returning: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func present(host: ShareSheetView) {
|
||||
let controller = UIHostingController(rootView: host)
|
||||
controller.modalPresentationStyle = .formSheet
|
||||
addChild(controller)
|
||||
controller.view.frame = view.bounds
|
||||
controller.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
view.addSubview(controller.view)
|
||||
controller.didMove(toParent: self)
|
||||
}
|
||||
|
||||
fileprivate func complete() {
|
||||
extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
|
||||
}
|
||||
|
||||
fileprivate func cancel() {
|
||||
extensionContext?.cancelRequest(withError: NSError(domain: "GiteaIssueShare", code: 0))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
# Gitea Issue
|
||||
|
||||
iOS share-extension for filing issues to a self-hosted Gitea instance straight from a screenshot.
|
||||
|
||||
Take screenshot → Share → **Gitea Issue** → pick repo, title, notes → submit. The screenshot is uploaded as an attachment and embedded inline in the issue body.
|
||||
|
||||
## Architecture
|
||||
|
||||
Two targets in one Xcode project, sharing an App Group + Keychain:
|
||||
|
||||
- **GiteaIssue** (`com.treytartt.GiteaIssue`) — container app, settings + help only
|
||||
- **GiteaIssueShare** (`com.treytartt.GiteaIssue.Share`) — share extension that does the real work
|
||||
- **App Group**: `group.com.treytartt.GiteaIssue` (cached repo list, recents)
|
||||
- **Keychain group**: `$(AppIdentifierPrefix)com.treytartt.GiteaIssue` (base URL, token)
|
||||
|
||||
iOS 26.0 minimum. Signing team `V3PF3M6B6U`.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
GiteaIssue/ Container app (settings + help)
|
||||
GiteaIssueShare/ Share extension UI
|
||||
Shared/ Models, Keychain, Settings, GiteaClient, RepoCache
|
||||
project.yml xcodegen spec — regenerate with `xcodegen generate`
|
||||
```
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
xcodegen generate
|
||||
xcodebuild -project GiteaIssue.xcodeproj -scheme GiteaIssue \
|
||||
-destination 'generic/platform=iOS' build
|
||||
```
|
||||
|
||||
## Deploy (ad-hoc)
|
||||
|
||||
Push to `admin/GiteaIssue` on Gitea, then trigger the Mac mini builder at
|
||||
`http://Treys-Mac-mini.local:3090` against the git URL. Builder ships the IPA
|
||||
to `https://appstore.treytartt.com` for OTA install.
|
||||
|
||||
## First-time setup on device
|
||||
|
||||
1. Install via Safari from `appstore.treytartt.com`
|
||||
2. Open the Gitea Issue app, set base URL + personal access token (`repo` scope)
|
||||
3. Tap **Save & test connection** — confirms login and pre-fetches the repo list
|
||||
4. From now on: Photos → screenshot → Share → Gitea Issue
|
||||
@@ -0,0 +1,99 @@
|
||||
import Foundation
|
||||
|
||||
struct GiteaClient {
|
||||
let baseURL: URL
|
||||
let token: String
|
||||
|
||||
init() throws {
|
||||
guard AppSettings.isConfigured else { throw GiteaError.notConfigured }
|
||||
guard let url = URL(string: AppSettings.baseURL) else { throw GiteaError.invalidBaseURL }
|
||||
self.baseURL = url
|
||||
self.token = AppSettings.token
|
||||
}
|
||||
|
||||
func currentUser() async throws -> GiteaUser {
|
||||
let req = makeRequest(path: "/api/v1/user")
|
||||
let (data, resp) = try await URLSession.shared.data(for: req)
|
||||
try ensureOK(resp, data: data)
|
||||
return try decode(data)
|
||||
}
|
||||
|
||||
func searchRepos() async throws -> [GiteaRepo] {
|
||||
var components = URLComponents(url: baseURL.appendingPathComponent("/api/v1/repos/search"), resolvingAgainstBaseURL: false)!
|
||||
components.queryItems = [
|
||||
URLQueryItem(name: "limit", value: "50"),
|
||||
URLQueryItem(name: "sort", value: "updated"),
|
||||
URLQueryItem(name: "order", value: "desc"),
|
||||
]
|
||||
var req = URLRequest(url: components.url!)
|
||||
req.setValue("token \(token)", forHTTPHeaderField: "Authorization")
|
||||
let (data, resp) = try await URLSession.shared.data(for: req)
|
||||
try ensureOK(resp, data: data)
|
||||
let decoder = JSONDecoder()
|
||||
decoder.dateDecodingStrategy = .iso8601
|
||||
let result = try decoder.decode(GiteaRepoSearch.self, from: data)
|
||||
return result.data
|
||||
}
|
||||
|
||||
func createIssue(owner: String, repo: String, title: String, body: String) async throws -> GiteaIssue {
|
||||
let payload = ["title": title, "body": body]
|
||||
var req = makeRequest(path: "/api/v1/repos/\(owner)/\(repo)/issues", method: "POST")
|
||||
req.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
req.httpBody = try JSONSerialization.data(withJSONObject: payload)
|
||||
let (data, resp) = try await URLSession.shared.data(for: req)
|
||||
try ensureOK(resp, data: data)
|
||||
return try decode(data)
|
||||
}
|
||||
|
||||
func uploadAsset(owner: String, repo: String, issueNumber: Int, image: Data, filename: String) async throws -> GiteaIssueAsset {
|
||||
let boundary = "Boundary-\(UUID().uuidString)"
|
||||
var req = makeRequest(path: "/api/v1/repos/\(owner)/\(repo)/issues/\(issueNumber)/assets", method: "POST")
|
||||
req.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
|
||||
|
||||
var body = Data()
|
||||
body.append("--\(boundary)\r\n".data(using: .utf8)!)
|
||||
body.append("Content-Disposition: form-data; name=\"attachment\"; filename=\"\(filename)\"\r\n".data(using: .utf8)!)
|
||||
body.append("Content-Type: image/png\r\n\r\n".data(using: .utf8)!)
|
||||
body.append(image)
|
||||
body.append("\r\n--\(boundary)--\r\n".data(using: .utf8)!)
|
||||
req.httpBody = body
|
||||
|
||||
let (data, resp) = try await URLSession.shared.data(for: req)
|
||||
try ensureOK(resp, data: data)
|
||||
return try decode(data)
|
||||
}
|
||||
|
||||
func updateIssueBody(owner: String, repo: String, issueNumber: Int, body: String) async throws {
|
||||
let payload = ["body": body]
|
||||
var req = makeRequest(path: "/api/v1/repos/\(owner)/\(repo)/issues/\(issueNumber)", method: "PATCH")
|
||||
req.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
req.httpBody = try JSONSerialization.data(withJSONObject: payload)
|
||||
let (data, resp) = try await URLSession.shared.data(for: req)
|
||||
try ensureOK(resp, data: data)
|
||||
}
|
||||
|
||||
private func makeRequest(path: String, method: String = "GET") -> URLRequest {
|
||||
var req = URLRequest(url: baseURL.appendingPathComponent(path))
|
||||
req.httpMethod = method
|
||||
req.setValue("token \(token)", forHTTPHeaderField: "Authorization")
|
||||
req.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||
return req
|
||||
}
|
||||
|
||||
private func ensureOK(_ resp: URLResponse, data: Data) throws {
|
||||
guard let http = resp as? HTTPURLResponse else { return }
|
||||
if (200..<300).contains(http.statusCode) { return }
|
||||
let body = String(data: data, encoding: .utf8) ?? ""
|
||||
throw GiteaError.http(http.statusCode, body)
|
||||
}
|
||||
|
||||
private func decode<T: Decodable>(_ data: Data) throws -> T {
|
||||
do {
|
||||
let decoder = JSONDecoder()
|
||||
decoder.dateDecodingStrategy = .iso8601
|
||||
return try decoder.decode(T.self, from: data)
|
||||
} catch {
|
||||
throw GiteaError.decoding(String(describing: error))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import Foundation
|
||||
import Security
|
||||
|
||||
enum Keychain {
|
||||
static let accessGroup = "$(AppIdentifierPrefix)com.treytartt.GiteaIssue"
|
||||
static let service = "com.treytartt.GiteaIssue"
|
||||
|
||||
static func read(_ key: String) -> String? {
|
||||
var query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: service,
|
||||
kSecAttrAccount as String: key,
|
||||
kSecMatchLimit as String: kSecMatchLimitOne,
|
||||
kSecReturnData as String: true,
|
||||
]
|
||||
if let group = resolvedAccessGroup() { query[kSecAttrAccessGroup as String] = group }
|
||||
|
||||
var item: CFTypeRef?
|
||||
let status = SecItemCopyMatching(query as CFDictionary, &item)
|
||||
guard status == errSecSuccess, let data = item as? Data else { return nil }
|
||||
return String(data: data, encoding: .utf8)
|
||||
}
|
||||
|
||||
static func write(_ key: String, value: String) {
|
||||
let data = Data(value.utf8)
|
||||
var query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: service,
|
||||
kSecAttrAccount as String: key,
|
||||
]
|
||||
if let group = resolvedAccessGroup() { query[kSecAttrAccessGroup as String] = group }
|
||||
|
||||
SecItemDelete(query as CFDictionary)
|
||||
|
||||
var add = query
|
||||
add[kSecValueData as String] = data
|
||||
add[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
|
||||
SecItemAdd(add as CFDictionary, nil)
|
||||
}
|
||||
|
||||
static func delete(_ key: String) {
|
||||
var query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: service,
|
||||
kSecAttrAccount as String: key,
|
||||
]
|
||||
if let group = resolvedAccessGroup() { query[kSecAttrAccessGroup as String] = group }
|
||||
SecItemDelete(query as CFDictionary)
|
||||
}
|
||||
|
||||
private static func resolvedAccessGroup() -> String? {
|
||||
guard let prefix = appIdentifierPrefix() else { return nil }
|
||||
return "\(prefix)com.treytartt.GiteaIssue"
|
||||
}
|
||||
|
||||
private static func appIdentifierPrefix() -> String? {
|
||||
let query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrAccount as String: "__appIdentifierPrefixProbe",
|
||||
kSecAttrService as String: service,
|
||||
kSecReturnAttributes as String: true,
|
||||
kSecMatchLimit as String: kSecMatchLimitOne,
|
||||
]
|
||||
var result: CFTypeRef?
|
||||
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
||||
if status == errSecSuccess,
|
||||
let attrs = result as? [String: Any],
|
||||
let group = attrs[kSecAttrAccessGroup as String] as? String,
|
||||
let dot = group.firstIndex(of: ".") {
|
||||
return String(group[..<group.index(after: dot)])
|
||||
}
|
||||
if let bundleSeed = Bundle.main.object(forInfoDictionaryKey: "AppIdentifierPrefix") as? String {
|
||||
return bundleSeed
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import Foundation
|
||||
|
||||
struct GiteaUser: Decodable, Sendable {
|
||||
let login: String
|
||||
let id: Int
|
||||
}
|
||||
|
||||
struct GiteaRepo: Codable, Identifiable, Hashable, Sendable {
|
||||
let id: Int
|
||||
let fullName: String
|
||||
let name: String
|
||||
let owner: Owner
|
||||
let updatedAt: Date
|
||||
|
||||
struct Owner: Codable, Hashable, Sendable {
|
||||
let login: String
|
||||
}
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id, name, owner
|
||||
case fullName = "full_name"
|
||||
case updatedAt = "updated_at"
|
||||
}
|
||||
}
|
||||
|
||||
struct GiteaRepoSearch: Decodable, Sendable {
|
||||
let data: [GiteaRepo]
|
||||
}
|
||||
|
||||
struct GiteaIssue: Decodable, Sendable {
|
||||
let id: Int
|
||||
let number: Int
|
||||
let htmlUrl: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id, number
|
||||
case htmlUrl = "html_url"
|
||||
}
|
||||
}
|
||||
|
||||
struct GiteaIssueAsset: Decodable, Sendable {
|
||||
let id: Int
|
||||
let name: String
|
||||
let browserDownloadUrl: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id, name
|
||||
case browserDownloadUrl = "browser_download_url"
|
||||
}
|
||||
}
|
||||
|
||||
enum GiteaError: LocalizedError {
|
||||
case notConfigured
|
||||
case invalidBaseURL
|
||||
case http(Int, String)
|
||||
case decoding(String)
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .notConfigured: "Open the Gitea Issue app and set your base URL + token."
|
||||
case .invalidBaseURL: "Base URL is not a valid URL."
|
||||
case .http(let code, let body): "Gitea returned \(code). \(body)"
|
||||
case .decoding(let msg): "Couldn't read response: \(msg)"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import Foundation
|
||||
|
||||
enum RepoCache {
|
||||
private static let listKey = "repo.list"
|
||||
private static let listFetchedKey = "repo.list.fetchedAt"
|
||||
private static let recentsKey = "repo.recents"
|
||||
private static let recentsCap = 5
|
||||
static let staleAfter: TimeInterval = 24 * 60 * 60
|
||||
|
||||
static var repos: [GiteaRepo] {
|
||||
get {
|
||||
guard let data = AppSettings.sharedDefaults.data(forKey: listKey) else { return [] }
|
||||
let decoder = JSONDecoder()
|
||||
decoder.dateDecodingStrategy = .iso8601
|
||||
return (try? decoder.decode([GiteaRepo].self, from: data)) ?? []
|
||||
}
|
||||
set {
|
||||
let encoder = JSONEncoder()
|
||||
encoder.dateEncodingStrategy = .iso8601
|
||||
if let data = try? encoder.encode(newValue) {
|
||||
AppSettings.sharedDefaults.set(data, forKey: listKey)
|
||||
AppSettings.sharedDefaults.set(Date(), forKey: listFetchedKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static var fetchedAt: Date? {
|
||||
AppSettings.sharedDefaults.object(forKey: listFetchedKey) as? Date
|
||||
}
|
||||
|
||||
static var isStale: Bool {
|
||||
guard let fetched = fetchedAt else { return true }
|
||||
return Date().timeIntervalSince(fetched) > staleAfter
|
||||
}
|
||||
|
||||
static var recentFullNames: [String] {
|
||||
AppSettings.sharedDefaults.stringArray(forKey: recentsKey) ?? []
|
||||
}
|
||||
|
||||
static func touch(_ fullName: String) {
|
||||
var current = recentFullNames.filter { $0 != fullName }
|
||||
current.insert(fullName, at: 0)
|
||||
if current.count > recentsCap { current = Array(current.prefix(recentsCap)) }
|
||||
AppSettings.sharedDefaults.set(current, forKey: recentsKey)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import Foundation
|
||||
|
||||
enum AppSettings {
|
||||
static let appGroup = "group.com.treytartt.GiteaIssue"
|
||||
|
||||
private static let baseURLKey = "gitea.baseURL"
|
||||
private static let tokenKey = "gitea.token"
|
||||
|
||||
static var baseURL: String {
|
||||
get { Keychain.read(baseURLKey) ?? "https://gitea.treytartt.com" }
|
||||
set { Keychain.write(baseURLKey, value: newValue) }
|
||||
}
|
||||
|
||||
static var token: String {
|
||||
get { Keychain.read(tokenKey) ?? "" }
|
||||
set { Keychain.write(tokenKey, value: newValue) }
|
||||
}
|
||||
|
||||
static var isConfigured: Bool {
|
||||
!token.isEmpty && URL(string: baseURL) != nil
|
||||
}
|
||||
|
||||
static var sharedDefaults: UserDefaults {
|
||||
UserDefaults(suiteName: appGroup) ?? .standard
|
||||
}
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
name: GiteaIssue
|
||||
options:
|
||||
bundleIdPrefix: com.treytartt
|
||||
deploymentTarget:
|
||||
iOS: "26.0"
|
||||
developmentLanguage: en
|
||||
createIntermediateGroups: true
|
||||
settings:
|
||||
base:
|
||||
DEVELOPMENT_TEAM: V3PF3M6B6U
|
||||
SWIFT_VERSION: "5.10"
|
||||
MARKETING_VERSION: "1.0.0"
|
||||
CURRENT_PROJECT_VERSION: "1"
|
||||
ENABLE_USER_SCRIPT_SANDBOXING: NO
|
||||
SWIFT_EMIT_LOC_STRINGS: YES
|
||||
GENERATE_INFOPLIST_FILE: YES
|
||||
CODE_SIGN_STYLE: Automatic
|
||||
|
||||
targets:
|
||||
GiteaIssue:
|
||||
type: application
|
||||
platform: iOS
|
||||
sources:
|
||||
- path: GiteaIssue
|
||||
- path: Shared
|
||||
settings:
|
||||
base:
|
||||
PRODUCT_BUNDLE_IDENTIFIER: com.treytartt.GiteaIssue
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation: YES
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation: YES
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone: "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"
|
||||
INFOPLIST_KEY_CFBundleDisplayName: "Gitea Issue"
|
||||
TARGETED_DEVICE_FAMILY: "1,2"
|
||||
SUPPORTS_MACCATALYST: NO
|
||||
CODE_SIGN_ENTITLEMENTS: GiteaIssue/GiteaIssue.entitlements
|
||||
dependencies:
|
||||
- target: GiteaIssueShare
|
||||
|
||||
GiteaIssueShare:
|
||||
type: app-extension
|
||||
platform: iOS
|
||||
sources:
|
||||
- path: GiteaIssueShare
|
||||
- path: Shared
|
||||
settings:
|
||||
base:
|
||||
PRODUCT_BUNDLE_IDENTIFIER: com.treytartt.GiteaIssue.Share
|
||||
INFOPLIST_FILE: GiteaIssueShare/Info.plist
|
||||
GENERATE_INFOPLIST_FILE: NO
|
||||
CODE_SIGN_ENTITLEMENTS: GiteaIssueShare/GiteaIssueShare.entitlements
|
||||
TARGETED_DEVICE_FAMILY: "1,2"
|
||||
SKIP_INSTALL: YES
|
||||
Reference in New Issue
Block a user