Widget, began Tenor integration, other imporvements and fixes

This commit is contained in:
Lumaa 2024-03-07 17:22:11 +01:00
parent 609e3b245b
commit 8abc56227d
26 changed files with 1106 additions and 160 deletions

View File

@ -50,10 +50,25 @@
B9B63B232B447B8000BBC82D /* PostCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9B63B222B447B8000BBC82D /* PostCardView.swift */; };
B9B63B252B44997400BBC82D /* QuotePostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9B63B242B44997400BBC82D /* QuotePostView.swift */; };
B9B63B272B449CDC00BBC82D /* SearchResults.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9B63B262B449CDC00BBC82D /* SearchResults.swift */; };
B9BCC3182B90B3BC00211976 /* Tenor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9BCC3172B90B3BC00211976 /* Tenor.swift */; };
B9BED5162B5D5E6500C9B715 /* PostInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9BED5152B5D5E6500C9B715 /* PostInteractor.swift */; };
B9BED5182B5D649C00C9B715 /* PostMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9BED5172B5D649C00C9B715 /* PostMenu.swift */; };
B9BED51A2B5D662D00C9B715 /* ShareSheetController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9BED5192B5D662D00C9B715 /* ShareSheetController.swift */; };
B9BF54072B6B6823004B24E7 /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = B9BF54062B6B6823004B24E7 /* KeychainSwift */; };
B9C20D0B2B921C78004DC9B3 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B9C20D0A2B921C78004DC9B3 /* WidgetKit.framework */; };
B9C20D0D2B921C78004DC9B3 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B9C20D0C2B921C78004DC9B3 /* SwiftUI.framework */; };
B9C20D102B921C78004DC9B3 /* ThreadedWidgetsBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9C20D0F2B921C78004DC9B3 /* ThreadedWidgetsBundle.swift */; };
B9C20D122B921C78004DC9B3 /* ThreadedWidgets.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9C20D112B921C78004DC9B3 /* ThreadedWidgets.swift */; };
B9C20D142B921C78004DC9B3 /* AppIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9C20D132B921C78004DC9B3 /* AppIntent.swift */; };
B9C20D162B921C7B004DC9B3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B9C20D152B921C7B004DC9B3 /* Assets.xcassets */; };
B9C20D1A2B921C7B004DC9B3 /* ThreadedWidgetsExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = B9C20D092B921C78004DC9B3 /* ThreadedWidgetsExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
B9C20D1F2B921E81004DC9B3 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = B9C20D1E2B921E81004DC9B3 /* Localizable.xcstrings */; };
B9C20D282B9229DF004DC9B3 /* LoggedAccounts.swift in Sources */ = {isa = PBXBuildFile; fileRef = B98627302B86F23500844245 /* LoggedAccounts.swift */; };
B9C20D342B9229EC004DC9B3 /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9FB949A2B2EF09A00D81C07 /* Client.swift */; };
B9C20D372B9229EC004DC9B3 /* AppInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9FB94A12B2EF24A00D81C07 /* AppInfo.swift */; };
B9C20D3D2B9229EC004DC9B3 /* Emoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9FB94872B2E223E00D81C07 /* Emoji.swift */; };
B9C20D562B922DAC004DC9B3 /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = B9C20D552B922DAC004DC9B3 /* KeychainSwift */; };
B9C20D612B949AD7004DC9B3 /* Redeclarations.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9C20D602B949AD7004DC9B3 /* Redeclarations.swift */; };
B9CC45B82B40A2D6001E4FA5 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9CC45B72B40A2D6001E4FA5 /* AboutView.swift */; };
B9CFC43B2B4F08C9004CFCB7 /* LaunchStoryboard.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B9CFC43A2B4F08C9004CFCB7 /* LaunchStoryboard.storyboard */; };
B9D365612B79A1BE004C1255 /* MailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9D365602B79A1BE004C1255 /* MailView.swift */; };
@ -102,6 +117,13 @@
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
B9C20D182B921C7B004DC9B3 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = B9FB944F2B2DEECE00D81C07 /* Project object */;
proxyType = 1;
remoteGlobalIDString = B9C20D082B921C78004DC9B3;
remoteInfo = ThreadedWidgetsExtension;
};
B9FB94B22B2F009F00D81C07 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = B9FB944F2B2DEECE00D81C07 /* Project object */;
@ -119,6 +141,7 @@
dstSubfolderSpec = 13;
files = (
B9FB94B42B2F009F00D81C07 /* ThreadedAuthService.appex in Embed Foundation Extensions */,
B9C20D1A2B921C7B004DC9B3 /* ThreadedWidgetsExtension.appex in Embed Foundation Extensions */,
);
name = "Embed Foundation Extensions";
runOnlyForDeploymentPostprocessing = 0;
@ -162,9 +185,22 @@
B9B63B222B447B8000BBC82D /* PostCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostCardView.swift; sourceTree = "<group>"; };
B9B63B242B44997400BBC82D /* QuotePostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuotePostView.swift; sourceTree = "<group>"; };
B9B63B262B449CDC00BBC82D /* SearchResults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResults.swift; sourceTree = "<group>"; };
B9BCC3172B90B3BC00211976 /* Tenor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tenor.swift; sourceTree = "<group>"; };
B9BED5152B5D5E6500C9B715 /* PostInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostInteractor.swift; sourceTree = "<group>"; };
B9BED5172B5D649C00C9B715 /* PostMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostMenu.swift; sourceTree = "<group>"; };
B9BED5192B5D662D00C9B715 /* ShareSheetController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareSheetController.swift; sourceTree = "<group>"; };
B9C20D092B921C78004DC9B3 /* ThreadedWidgetsExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ThreadedWidgetsExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
B9C20D0A2B921C78004DC9B3 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
B9C20D0C2B921C78004DC9B3 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; };
B9C20D0F2B921C78004DC9B3 /* ThreadedWidgetsBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadedWidgetsBundle.swift; sourceTree = "<group>"; };
B9C20D112B921C78004DC9B3 /* ThreadedWidgets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadedWidgets.swift; sourceTree = "<group>"; };
B9C20D132B921C78004DC9B3 /* AppIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIntent.swift; sourceTree = "<group>"; };
B9C20D152B921C7B004DC9B3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
B9C20D172B921C7B004DC9B3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
B9C20D1E2B921E81004DC9B3 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
B9C20D582B923CDD004DC9B3 /* ThreadedWidgetsExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ThreadedWidgetsExtension.entitlements; sourceTree = "<group>"; };
B9C20D592B923D53004DC9B3 /* Threaded.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Threaded.entitlements; sourceTree = "<group>"; };
B9C20D602B949AD7004DC9B3 /* Redeclarations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Redeclarations.swift; sourceTree = "<group>"; };
B9CC45B72B40A2D6001E4FA5 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = "<group>"; };
B9CC45B92B40AA1E001E4FA5 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
B9CFC43A2B4F08C9004CFCB7 /* LaunchStoryboard.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchStoryboard.storyboard; sourceTree = "<group>"; };
@ -216,6 +252,16 @@
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
B9C20D062B921C78004DC9B3 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
B9C20D0D2B921C78004DC9B3 /* SwiftUI.framework in Frameworks */,
B9C20D0B2B921C78004DC9B3 /* WidgetKit.framework in Frameworks */,
B9C20D562B922DAC004DC9B3 /* KeychainSwift in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
B9FB94542B2DEECE00D81C07 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
@ -255,6 +301,7 @@
B9842C132B2F310C00D9F3C1 /* FetchTimeline.swift */,
B9842C152B2F363600D9F3C1 /* TimelineFilter.swift */,
B98F47992B653CAE0092000F /* Compressor.swift */,
B9BCC3172B90B3BC00211976 /* Tenor.swift */,
);
path = Content;
sourceTree = "<group>";
@ -273,6 +320,21 @@
path = Post;
sourceTree = "<group>";
};
B9C20D0E2B921C78004DC9B3 /* ThreadedWidgets */ = {
isa = PBXGroup;
children = (
B9C20D582B923CDD004DC9B3 /* ThreadedWidgetsExtension.entitlements */,
B9C20D0F2B921C78004DC9B3 /* ThreadedWidgetsBundle.swift */,
B9C20D112B921C78004DC9B3 /* ThreadedWidgets.swift */,
B9C20D132B921C78004DC9B3 /* AppIntent.swift */,
B9C20D602B949AD7004DC9B3 /* Redeclarations.swift */,
B9C20D152B921C7B004DC9B3 /* Assets.xcassets */,
B9C20D1E2B921E81004DC9B3 /* Localizable.xcstrings */,
B9C20D172B921C7B004DC9B3 /* Info.plist */,
);
path = ThreadedWidgets;
sourceTree = "<group>";
};
B9D9C6BF2B6A56D500C26A41 /* Notifications */ = {
isa = PBXGroup;
children = (
@ -296,6 +358,7 @@
children = (
B9CC45B92B40AA1E001E4FA5 /* README.md */,
B9FB94592B2DEECE00D81C07 /* Threaded */,
B9C20D0E2B921C78004DC9B3 /* ThreadedWidgets */,
B9FB94AB2B2F009F00D81C07 /* AuthService */,
B9FB94A82B2F009F00D81C07 /* Frameworks */,
B97BCE292B3ED2C80044756D /* LICENSE */,
@ -309,6 +372,7 @@
children = (
B9FB94572B2DEECE00D81C07 /* Threaded.app */,
B9FB94A72B2F009F00D81C07 /* ThreadedAuthService.appex */,
B9C20D092B921C78004DC9B3 /* ThreadedWidgetsExtension.appex */,
);
name = Products;
sourceTree = "<group>";
@ -316,6 +380,7 @@
B9FB94592B2DEECE00D81C07 /* Threaded */ = {
isa = PBXGroup;
children = (
B9C20D592B923D53004DC9B3 /* Threaded.entitlements */,
B9FB94A02B2EF23100D81C07 /* Info.plist */,
B9029FC12B81259400AA9B68 /* Secret.plist */,
B9FB945A2B2DEECE00D81C07 /* ThreadedApp.swift */,
@ -423,6 +488,8 @@
children = (
B9DC692C2B79362700E625B9 /* StoreKit.framework */,
B9FB94A92B2F009F00D81C07 /* AuthenticationServices.framework */,
B9C20D0A2B921C78004DC9B3 /* WidgetKit.framework */,
B9C20D0C2B921C78004DC9B3 /* SwiftUI.framework */,
);
name = Frameworks;
sourceTree = "<group>";
@ -452,6 +519,26 @@
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
B9C20D082B921C78004DC9B3 /* ThreadedWidgetsExtension */ = {
isa = PBXNativeTarget;
buildConfigurationList = B9C20D1D2B921C7B004DC9B3 /* Build configuration list for PBXNativeTarget "ThreadedWidgetsExtension" */;
buildPhases = (
B9C20D052B921C78004DC9B3 /* Sources */,
B9C20D062B921C78004DC9B3 /* Frameworks */,
B9C20D072B921C78004DC9B3 /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = ThreadedWidgetsExtension;
packageProductDependencies = (
B9C20D552B922DAC004DC9B3 /* KeychainSwift */,
);
productName = ThreadedWidgetsExtension;
productReference = B9C20D092B921C78004DC9B3 /* ThreadedWidgetsExtension.appex */;
productType = "com.apple.product-type.app-extension";
};
B9FB94562B2DEECE00D81C07 /* Threaded */ = {
isa = PBXNativeTarget;
buildConfigurationList = B9FB94672B2DEECF00D81C07 /* Build configuration list for PBXNativeTarget "Threaded" */;
@ -465,6 +552,7 @@
);
dependencies = (
B9FB94B32B2F009F00D81C07 /* PBXTargetDependency */,
B9C20D192B921C7B004DC9B3 /* PBXTargetDependency */,
);
name = Threaded;
packageProductDependencies = (
@ -506,9 +594,12 @@
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1510;
LastSwiftUpdateCheck = 1520;
LastUpgradeCheck = 1510;
TargetAttributes = {
B9C20D082B921C78004DC9B3 = {
CreatedOnToolsVersion = 15.2;
};
B9FB94562B2DEECE00D81C07 = {
CreatedOnToolsVersion = 15.1;
};
@ -540,11 +631,21 @@
targets = (
B9FB94562B2DEECE00D81C07 /* Threaded */,
B9FB94A62B2F009F00D81C07 /* ThreadedAuthService */,
B9C20D082B921C78004DC9B3 /* ThreadedWidgetsExtension */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
B9C20D072B921C78004DC9B3 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
B9C20D1F2B921E81004DC9B3 /* Localizable.xcstrings in Resources */,
B9C20D162B921C7B004DC9B3 /* Assets.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
B9FB94552B2DEECE00D81C07 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
@ -572,6 +673,21 @@
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
B9C20D052B921C78004DC9B3 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
B9C20D282B9229DF004DC9B3 /* LoggedAccounts.swift in Sources */,
B9C20D612B949AD7004DC9B3 /* Redeclarations.swift in Sources */,
B9C20D122B921C78004DC9B3 /* ThreadedWidgets.swift in Sources */,
B9C20D102B921C78004DC9B3 /* ThreadedWidgetsBundle.swift in Sources */,
B9C20D142B921C78004DC9B3 /* AppIntent.swift in Sources */,
B9C20D372B9229EC004DC9B3 /* AppInfo.swift in Sources */,
B9C20D3D2B9229EC004DC9B3 /* Emoji.swift in Sources */,
B9C20D342B9229EC004DC9B3 /* Client.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
B9FB94532B2DEECE00D81C07 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
@ -636,6 +752,7 @@
B98627312B86F23500844245 /* LoggedAccounts.swift in Sources */,
B9B63B272B449CDC00BBC82D /* SearchResults.swift in Sources */,
B9B63B252B44997400BBC82D /* QuotePostView.swift in Sources */,
B9BCC3182B90B3BC00211976 /* Tenor.swift in Sources */,
B97BCE242B3DD8400044756D /* HapticManager.swift in Sources */,
B9B63B212B442D1500BBC82D /* DynamicTextEditor.swift in Sources */,
B9FB949F2B2EF0F200D81C07 /* MastodonRequest.swift in Sources */,
@ -659,6 +776,11 @@
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
B9C20D192B921C7B004DC9B3 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = B9C20D082B921C78004DC9B3 /* ThreadedWidgetsExtension */;
targetProxy = B9C20D182B921C7B004DC9B3 /* PBXContainerItemProxy */;
};
B9FB94B32B2F009F00D81C07 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = B9FB94A62B2F009F00D81C07 /* ThreadedAuthService */;
@ -678,6 +800,72 @@
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
B9C20D1B2B921C7B004DC9B3 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CODE_SIGN_ENTITLEMENTS = ThreadedWidgets/ThreadedWidgetsExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = HB5P3BML86;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = ThreadedWidgets/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = ThreadedWidgets;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = fr.lumaa.Threaded.ThreadedWidgets;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
};
name = Debug;
};
B9C20D1C2B921C7B004DC9B3 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CODE_SIGN_ENTITLEMENTS = ThreadedWidgets/ThreadedWidgetsExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = HB5P3BML86;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = ThreadedWidgets/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = ThreadedWidgets;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = fr.lumaa.Threaded.ThreadedWidgets;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
};
name = Release;
};
B9FB94652B2DEECF00D81C07 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
@ -803,8 +991,9 @@
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = Threaded/Threaded.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1425;
CURRENT_PROJECT_VERSION = 500;
DEVELOPMENT_ASSET_PATHS = "\"Threaded/Preview Content\"";
DEVELOPMENT_TEAM = HB5P3BML86;
ENABLE_PREVIEWS = YES;
@ -823,7 +1012,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 0.2.0;
MARKETING_VERSION = 0.4.0;
PRODUCT_BUNDLE_IDENTIFIER = fr.lumaa.Threaded;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
@ -842,8 +1031,9 @@
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = Threaded/Threaded.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1425;
CURRENT_PROJECT_VERSION = 500;
DEVELOPMENT_ASSET_PATHS = "\"Threaded/Preview Content\"";
DEVELOPMENT_TEAM = HB5P3BML86;
ENABLE_PREVIEWS = YES;
@ -862,7 +1052,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 0.2.0;
MARKETING_VERSION = 0.4.0;
PRODUCT_BUNDLE_IDENTIFIER = fr.lumaa.Threaded;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
@ -938,6 +1128,15 @@
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
B9C20D1D2B921C7B004DC9B3 /* Build configuration list for PBXNativeTarget "ThreadedWidgetsExtension" */ = {
isa = XCConfigurationList;
buildConfigurations = (
B9C20D1B2B921C7B004DC9B3 /* Debug */,
B9C20D1C2B921C7B004DC9B3 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
B9FB94522B2DEECE00D81C07 /* Build configuration list for PBXProject "Threaded" */ = {
isa = XCConfigurationList;
buildConfigurations = (
@ -1051,6 +1250,11 @@
package = B9BF54052B6B6823004B24E7 /* XCRemoteSwiftPackageReference "keychain-swift" */;
productName = KeychainSwift;
};
B9C20D552B922DAC004DC9B3 /* KeychainSwift */ = {
isa = XCSwiftPackageProductDependency;
package = B9BF54052B6B6823004B24E7 /* XCRemoteSwiftPackageReference "keychain-swift" */;
productName = KeychainSwift;
};
B9FB94832B2E20AF00D81C07 /* SwiftSoup */ = {
isa = XCSwiftPackageProductDependency;
package = B9FB94822B2E20AF00D81C07 /* XCRemoteSwiftPackageReference "SwiftSoup" */;

View File

@ -0,0 +1,5 @@
<?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/>
</plist>

View File

@ -8,7 +8,6 @@ struct QuotePostView: View {
var body: some View {
CompactPostView(status: status, quoted: true)
.environmentObject(navigator)
.frame(maxWidth: 250, maxHeight: 200)
.padding(15)
.padding([.horizontal], 20)

View File

@ -1,7 +1,6 @@
//Made by Lumaa
import Foundation
import KeychainSwift
@Observable
public class AccountManager: ObservableObject {
@ -52,121 +51,3 @@ public class AccountManager: ObservableObject {
return account
}
}
public struct AppAccount: Codable, Identifiable, Hashable {
public let server: String
public var accountName: String?
public let oauthToken: OauthToken?
private static let saveKey: String = "threaded-appaccount.current"
private static var keychain: KeychainSwift {
let kc = KeychainSwift()
// synchronise later
return kc
}
public var key: String {
if let oauthToken {
"\(server):\(oauthToken.createdAt)"
} else {
"\(server):anonymous"
}
}
public var id: String {
key
}
public init(server: String, accountName: String?, oauthToken: OauthToken? = nil) {
self.server = server
self.accountName = accountName
self.oauthToken = oauthToken
}
public static func clear() {
Self.keychain.delete(Self.saveKey)
}
public func clear() {
Self.clear()
}
/// This function only works with the given `AppAccount`
public func saveAsCurrent(_ appAccount: AppAccount? = nil) {
let encoder = JSONEncoder()
if let data = try? encoder.encode(appAccount ?? self) {
Self.keychain.set(data, forKey: Self.saveKey, withAccess: .accessibleWhenUnlocked)
} else {
fatalError("Couldn't encode AppAccount correctly to save")
}
}
/// This function only works with the last saved `AppAccount` or with the given `Data`
public static func loadAsCurrent(_ data: Data? = nil) -> Self? {
let decoder = JSONDecoder()
if let newData = data ?? keychain.getData(Self.saveKey) {
if let decoded = try? decoder.decode(Self.self, from: newData) {
return decoded
} else {
return nil
}
} else {
return nil
}
}
}
extension AppAccount: Sendable {}
public enum Oauth: Endpoint {
case authorize(clientId: String)
case token(code: String, clientId: String, clientSecret: String)
public func path() -> String {
switch self {
case .authorize:
"oauth/authorize"
case .token:
"oauth/token"
}
}
public var jsonValue: Encodable? {
switch self {
case let .token(code, clientId, clientSecret):
TokenData(clientId: clientId, clientSecret: clientSecret, code: code)
default:
nil
}
}
public struct TokenData: Encodable {
public let grantType = "authorization_code"
public let clientId: String
public let clientSecret: String
public let redirectUri = AppInfo.scheme
public let code: String
public let scope = AppInfo.scopes
}
public func queryItems() -> [URLQueryItem]? {
switch self {
case let .authorize(clientId):
return [
.init(name: "response_type", value: "code"),
.init(name: "client_id", value: clientId),
.init(name: "redirect_uri", value: AppInfo.scheme),
.init(name: "scope", value: AppInfo.scopes),
]
default:
return nil
}
}
}
public struct OauthToken: Codable, Hashable, Sendable {
public let accessToken: String
public let tokenType: String
public let scope: String
public let createdAt: Double
}

View File

@ -3,14 +3,15 @@
import Foundation
import SwiftUI
import SwiftData
import KeychainSwift
@Model
class LoggedAccount {
let token: OauthToken
let token: OauthToken = OauthToken(accessToken: "ABC", tokenType: "ABC", scope: "ABC", createdAt: 0.0)
let acct: String
let app: AppAccount?
init(token: OauthToken, acct: String) {
init(token: OauthToken = OauthToken(accessToken: "ABC", tokenType: "ABC", scope: "ABC", createdAt: 0.0), acct: String) {
self.token = token
self.acct = acct
self.app = nil
@ -31,3 +32,75 @@ public extension View {
.modelContainer(for: LoggedAccount.self)
}
}
public struct AppAccount: Codable, Identifiable, Hashable {
public let server: String
public var accountName: String?
public let oauthToken: OauthToken?
private static let saveKey: String = "threaded-appaccount.current"
private static var keychain: KeychainSwift {
let kc = KeychainSwift()
// synchronise later
return kc
}
public var key: String {
if let oauthToken {
"\(server):\(oauthToken.createdAt)"
} else {
"\(server):anonymous"
}
}
public var id: String {
key
}
public init(server: String, accountName: String?, oauthToken: OauthToken? = nil) {
self.server = server
self.accountName = accountName
self.oauthToken = oauthToken
}
public static func clear() {
Self.keychain.delete(Self.saveKey)
}
public func clear() {
Self.clear()
}
/// This function only works with the given `AppAccount`
public func saveAsCurrent(_ appAccount: AppAccount? = nil) {
let encoder = JSONEncoder()
if let data = try? encoder.encode(appAccount ?? self) {
Self.keychain.set(data, forKey: Self.saveKey, withAccess: .accessibleWhenUnlocked)
} else {
fatalError("Couldn't encode AppAccount correctly to save")
}
}
/// This function only works with the last saved `AppAccount` or with the given `Data`
public static func loadAsCurrent(_ data: Data? = nil) -> Self? {
let decoder = JSONDecoder()
if let newData = data ?? keychain.getData(Self.saveKey) {
if let decoded = try? decoder.decode(Self.self, from: newData) {
return decoded
} else {
return nil
}
} else {
return nil
}
}
}
extension AppAccount: Sendable {}
public struct OauthToken: Codable, Hashable, Sendable {
public let accessToken: String
public let tokenType: String
public let scope: String
public let createdAt: Double
}

View File

@ -139,6 +139,10 @@ public final class Client: Equatable, Identifiable, Hashable {
try await makeEntityRequest(endpoint: endpoint, method: "GET", forceVersion: forceVersion)
}
public func getString(endpoint: Endpoint, forceVersion: Version? = nil) async throws -> String {
try await makeEntityRequestForString(endpoint: endpoint, method: "GET", forceVersion: forceVersion)
}
public func getWithLink<Entity: Decodable>(endpoint: Endpoint) async throws -> (Entity, LinkHandler?) {
let (data, httpResponse) = try await urlSession.data(for: makeGet(endpoint: endpoint))
var linkHandler: LinkHandler?
@ -180,10 +184,7 @@ public final class Client: Equatable, Identifiable, Hashable {
return httpResponse as? HTTPURLResponse
}
private func makeEntityRequest<Entity: Decodable>(endpoint: Endpoint,
method: String,
forceVersion: Version? = nil) async throws -> Entity
{
private func makeEntityRequest<Entity: Decodable>(endpoint: Endpoint, method: String, forceVersion: Version? = nil) async throws -> Entity {
let url = try makeURL(endpoint: endpoint, forceVersion: forceVersion)
let request = makeURLRequest(url: url, endpoint: endpoint, httpMethod: method)
let (data, httpResponse) = try await urlSession.data(for: request)
@ -202,6 +203,14 @@ public final class Client: Equatable, Identifiable, Hashable {
}
}
private func makeEntityRequestForString(endpoint: Endpoint, method: String, forceVersion: Version? = nil) async throws -> String {
let url = try makeURL(endpoint: endpoint, forceVersion: forceVersion)
let request = makeURLRequest(url: url, endpoint: endpoint, httpMethod: method)
let (data, httpResponse) = try await urlSession.data(for: request)
logResponseOnError(httpResponse: httpResponse, data: data)
return String(data: data, encoding: .utf8) ?? ""
}
public func oauthURL() async throws -> URL {
let app: InstanceApp = try await post(endpoint: Apps.registerApp)
critical.withLock { $0.oauthApp = app }

View File

@ -0,0 +1,50 @@
//Made by Lumaa
import Foundation
class Tenor {
private static let url: String = "https://tenor.googleapis.com/v2"
private static var token: String = ""
private static let limit: Int = 20
private static let contentFilter: String = "off" // all content other than nudity
private static let mediaFilter: String = "preview,tinygif,gif,tinywebm,webm"
init(token: String) {
_ = Self.getToken()
}
static func getToken() -> String? {
guard let plist = AppDelegate.readSecret() else { return nil }
Self.token = plist["Tenor_Token"] ?? ""
return Self.token
}
func search(query: String) {
let params: [URLQueryItem] = [
.init(name: "q", value: query),
.init(name: "key", value: Self.token),
.init(name: "client_key", value: "\(AppInfo.clientName)-\(AppInfo.appVersion)"),
.init(name: "contentfilter", value: Self.contentFilter),
.init(name: "media_filter", value: Self.mediaFilter)
]
if var comp = URLComponents(string: "\(Self.url)/search") {
comp.queryItems = params
if let url = comp.url {
var req = URLRequest(url: url)
req.httpMethod = "GET"
let semaphore = DispatchSemaphore(value: 0)
var jsonResponse: [String: Any]?
URLSession.shared.dataTask(with: req) { (data, response, error) in
defer { semaphore.signal() }
if let data = data {
jsonResponse = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any]
}
}.resume()
}
}
}
}

View File

@ -28,6 +28,52 @@ extension Endpoint {
}
}
public enum Oauth: Endpoint {
case authorize(clientId: String)
case token(code: String, clientId: String, clientSecret: String)
public func path() -> String {
switch self {
case .authorize:
"oauth/authorize"
case .token:
"oauth/token"
}
}
public var jsonValue: Encodable? {
switch self {
case let .token(code, clientId, clientSecret):
TokenData(clientId: clientId, clientSecret: clientSecret, code: code)
default:
nil
}
}
public struct TokenData: Encodable {
public let grantType = "authorization_code"
public let clientId: String
public let clientSecret: String
public let redirectUri = AppInfo.scheme
public let code: String
public let scope = AppInfo.scopes
}
public func queryItems() -> [URLQueryItem]? {
switch self {
case let .authorize(clientId):
return [
.init(name: "response_type", value: "code"),
.init(name: "client_id", value: clientId),
.init(name: "redirect_uri", value: AppInfo.scheme),
.init(name: "scope", value: AppInfo.scopes),
]
default:
return nil
}
}
}
public struct LinkHandler {
public let rawLink: String

View File

@ -49,6 +49,13 @@ extension [Notification] {
}
}
public extension Array where Element: Hashable {
func uniqued() -> [Element] {
var seen = Set<Element>()
return filter { seen.insert($0).inserted }
}
}
public struct GroupedNotification: Identifiable, Hashable {
public var id: String? { notifications.first?.id }
public var notifications: [Notification]

View File

@ -2916,4 +2916,4 @@
}
},
"version" : "1.0"
}
}

View File

@ -0,0 +1,12 @@
<?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>aps-environment</key>
<string>development</string>
<key>com.apple.security.application-groups</key>
<array>
<string>group.lumaa.ThreadedApp</string>
</array>
</dict>
</plist>

View File

@ -318,6 +318,6 @@ private extension View {
}
#Preview {
AttachmentView(attachments: [.init(id: "ABC", type: "image", url: URL(string: "https://i.stack.imgur.com/HX3Aj.png"), previewUrl: URL(string: "https://cdn.pixabay.com/photo/2023/08/28/20/32/flower-8220018_1280.jpg"), description: String("This displays the TabView with a page indicator at the bottom"), meta: nil), .init(id: "DEF", type: "image", url: URL(string: "https://cdn.pixabay.com/photo/2023/08/28/20/32/flower-8220018_1280.jpg"), previewUrl: URL(string: "https://cdn.pixabay.com/photo/2023/08/28/20/32/flower-8220018_1280.jpg"), description: nil, meta: nil)])
AttachmentView(attachments: [.init(id: "ABC", type: "image", url: URL(string: "https://i.stack.imgur.com/HX3Aj.png"), previewUrl: URL.placeholder, description: String("This displays the TabView with a page indicator at the bottom"), meta: nil), .init(id: "DEF", type: "image", url: URL(string: "https://cdn.pixabay.com/photo/2023/08/28/20/32/flower-8220018_1280.jpg"), previewUrl: URL(string: "https://cdn.pixabay.com/photo/2023/08/28/20/32/flower-8220018_1280.jpg"), description: nil, meta: nil)])
.environment(AppDelegate())
}

View File

@ -147,10 +147,3 @@ struct NotificationsView: View {
var image: Image? = Image(systemName: "paperplane")
}
}
public extension Array where Element: Hashable {
func uniqued() -> [Element] {
var seen = Set<Element>()
return filter { seen.insert($0).inserted }
}
}

View File

@ -5,6 +5,7 @@ import SwiftUI
struct PostDetailsView: View {
@EnvironmentObject private var navigator: Navigator
@Environment(AccountManager.self) private var accountManager: AccountManager
@Environment(AppDelegate.self) private var delegate: AppDelegate
var detailedStatus: Status
@ -128,6 +129,8 @@ struct PostDetailsView: View {
statusesContext.append(contentsOf: data.context.descendants)
statuses = statusesContext
// await loadEmbeddedStatus()
} catch {
if let error = error as? ServerError, error.httpCode == 404 {
_ = navigator.path.popLast()

View File

@ -304,26 +304,6 @@ extension ShopView {
// .environment(\.locale, Locale(identifier: "en-us"))
}
private func hasActuallyPlus(customerInfo: CustomerInfo?) -> Bool {
return customerInfo?.entitlements[PlusEntitlements.lifetime.getEntitlementId()]?.isActive == true || customerInfo?.entitlements[PlusEntitlements.monthly.getEntitlementId()]?.isActive == true || customerInfo?.entitlements[PlusEntitlements.yearly.getEntitlementId()]?.isActive == true
}
private func purchase(entitlement: PlusEntitlements) {
Purchases.shared.getOfferings { (offerings, error) in
if let product = entitlement.toPackage(offerings: offerings) {
Purchases.shared.purchase(package: product) { (transaction, customerInfo, error, userCancelled) in
if hasActuallyPlus(customerInfo: customerInfo) {
print("BOUGHT PLUS")
AppDelegate.premium = true
}
}
}
if let e = error {
print(e)
}
}
}
enum PlusEntitlements: String {
case monthly
case yearly
@ -355,3 +335,23 @@ enum PlusEntitlements: String {
}
}
}
private func hasActuallyPlus(customerInfo: CustomerInfo?) -> Bool {
return customerInfo?.entitlements[PlusEntitlements.lifetime.getEntitlementId()]?.isActive == true || customerInfo?.entitlements[PlusEntitlements.monthly.getEntitlementId()]?.isActive == true || customerInfo?.entitlements[PlusEntitlements.yearly.getEntitlementId()]?.isActive == true
}
private func purchase(entitlement: PlusEntitlements) {
Purchases.shared.getOfferings { (offerings, error) in
if let product = entitlement.toPackage(offerings: offerings) {
Purchases.shared.purchase(package: product) { (transaction, customerInfo, error, userCancelled) in
if hasActuallyPlus(customerInfo: customerInfo) {
print("BOUGHT PLUS")
AppDelegate.premium = true
}
}
}
if let e = error {
print(e)
}
}
}

View File

@ -0,0 +1,92 @@
//Made by Lumaa
import SwiftUI
import SwiftData
import WidgetKit
import AppIntents
/// Widgets that require to select an account will use this `ConfigurationIntent`
struct AccountAppIntent: WidgetConfigurationIntent {
static var title: LocalizedStringResource = "widget.follow-count"
static var description = IntentDescription("widget.follow-count.description")
@Parameter(title: "widget.select-account")
var account: AccountEntity?
}
struct AccountEntity: AppEntity {
let client: Client
let id: String
let username: String
/// Bearer token
let token: OauthToken
static var typeDisplayRepresentation: TypeDisplayRepresentation = "account"
static var defaultQuery = AccountQuery()
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(stringLiteral: "@\(username)")
}
init(acct: String, username: String, token: OauthToken) {
self.client = Client(server: String(acct.split(separator: "@")[1]), version: .v2, oauthToken: token)
self.id = acct
self.username = username
self.token = token
}
init(loggedAccount: LoggedAccount) {
self.client = Client(server: String(loggedAccount.acct.split(separator: "@")[1]), version: .v2, oauthToken: loggedAccount.token)
self.id = loggedAccount.acct
self.username = String(loggedAccount.acct.split(separator: "@")[0])
self.token = loggedAccount.token
}
func toUIImage() -> URL? {
Task { [self] in
if let account: String = try? await client.getString(endpoint: Accounts.verifyCredentials) {
do {
if let serialized: [String : Any] = try JSONSerialization.jsonObject(with: account.data(using: String.Encoding.utf8) ?? Data()) as? [String : Any] {
let avatar: String = serialized["avatar"] as! String
return URL(string: avatar) ?? URL.placeholder
}
} catch {
print("Error fetching image data: \(error)")
}
}
return URL.placeholder
}
return URL.placeholder
}
}
struct AccountQuery: EntityQuery {
private static func getAccountsQuery() -> [LoggedAccount] {
let query = Query<LoggedAccount, [LoggedAccount]>()
let loggedAccounts: [LoggedAccount] = query.wrappedValue
return loggedAccounts
}
private static func getAccounts() -> [LoggedAccount] {
guard let modelContainer: ModelContainer = try? ModelContainer(for: LoggedAccount.self, configurations: ModelConfiguration(isStoredInMemoryOnly: false)) else { return [] }
let modelContext = ModelContext(modelContainer)
let loggedAccounts = try? modelContext.fetch(FetchDescriptor<LoggedAccount>())
return loggedAccounts ?? []
}
func entities(for identifiers: [AccountEntity.ID]) async throws -> [AccountEntity] {
let accountEntities: [AccountEntity] = Self.getAccounts().map({ return AccountEntity(loggedAccount: $0) })
return accountEntities.filter({ identifiers.contains($0.id) })
}
func suggestedEntities() async throws -> [AccountEntity] {
let accountEntities: [AccountEntity] = Self.getAccounts().map({ return AccountEntity(loggedAccount: $0) })
return accountEntities
}
func defaultResult() async -> AccountEntity? {
try? await suggestedEntities().first
}
}

View File

@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,13 @@
{
"images" : [
{
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "display-p3",
"components" : {
"alpha" : "1.000",
"blue" : "0xFF",
"green" : "0xFF",
"red" : "0xFF"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "display-p3",
"components" : {
"alpha" : "1.000",
"blue" : "0x10",
"green" : "0x10",
"red" : "0x10"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,11 @@
<?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>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.widgetkit-extension</string>
</dict>
</dict>
</plist>

View File

@ -0,0 +1,59 @@
{
"sourceLanguage" : "en",
"strings" : {
"@%@" : {
},
"account" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Account"
}
}
}
},
"widget.follow-count" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Follower Count"
}
}
}
},
"widget.follow-count.description" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Get the current amount of followers of the accounts you have logged in Threaded"
}
}
}
},
"widget.followers" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Followers"
}
}
}
},
"widget.select-account" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Select an account"
}
}
}
}
},
"version" : "1.0"
}

View File

@ -0,0 +1,224 @@
//Made by Lumaa
import Foundation
import SwiftUI
import UIKit
// this is messy but it's alr
public class AppDelegate: NSObject, UIWindowSceneDelegate, Sendable, UIApplicationDelegate {
public var window: UIWindow?
public private(set) var windowWidth: CGFloat = UIScreen.main.bounds.size.width
public private(set) var windowHeight: CGFloat = UIScreen.main.bounds.size.height
public private(set) var secret: [String: String] = [:]
public func scene(_ scene: UIScene, willConnectTo _: UISceneSession, options _: UIScene.ConnectionOptions) {
guard let windowScene = scene as? UIWindowScene else { return }
window = windowScene.keyWindow
}
override public init() {
super.init()
if let path = Bundle.main.path(forResource: "Secret", ofType: "plist") {
let url = URL(fileURLWithPath: path)
let data = try! Data(contentsOf: url)
if let plist = try! PropertyListSerialization.propertyList(from: data, options: .mutableContainers, format: nil) as? [String: String] {
secret = plist
}
}
windowWidth = window?.bounds.size.width ?? UIScreen.main.bounds.size.width
windowHeight = window?.bounds.size.height ?? UIScreen.main.bounds.size.height
Self.observedSceneDelegate.insert(self)
_ = Self.observer // just for activating the lazy static property
}
static func readSecret() -> [String: String]? {
if let path = Bundle.main.path(forResource: "Secret", ofType: "plist") {
let url = URL(fileURLWithPath: path)
let data = try! Data(contentsOf: url)
if let plist = try! PropertyListSerialization.propertyList(from: data, options: .mutableContainers, format: nil) as? [String: String] {
return plist
}
}
return nil
}
deinit {
Task { @MainActor in
Self.observedSceneDelegate.remove(self)
}
}
private static var observedSceneDelegate: Set<AppDelegate> = []
private static let observer = Task {
while true {
try? await Task.sleep(for: .seconds(0.1))
for delegate in observedSceneDelegate {
let newWidth = delegate.window?.bounds.size.width ?? UIScreen.main.bounds.size.width
if delegate.windowWidth != newWidth {
delegate.windowWidth = newWidth
}
let newHeight = delegate.window?.bounds.size.height ?? UIScreen.main.bounds.size.height
if delegate.windowHeight != newHeight {
delegate.windowHeight = newHeight
}
}
}
}
}
public extension URL {
static let placeholder: URL = URL(string: "https://cdn.pixabay.com/photo/2023/08/28/20/32/flower-8220018_1280.jpg")!
}
extension AppInfo {
static var appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown"
}
public protocol Endpoint: Sendable {
func path() -> String
func queryItems() -> [URLQueryItem]?
var jsonValue: Encodable? { get }
}
public extension Endpoint {
var jsonValue: Encodable? {
nil
}
}
extension Endpoint {
func makePaginationParam(sinceId: String?, maxId: String?, mindId: String?) -> [URLQueryItem]? {
if let sinceId {
return [.init(name: "since_id", value: sinceId)]
} else if let maxId {
return [.init(name: "max_id", value: maxId)]
} else if let mindId {
return [.init(name: "min_id", value: mindId)]
}
return nil
}
}
public enum Oauth: Endpoint {
case authorize(clientId: String)
case token(code: String, clientId: String, clientSecret: String)
public func path() -> String {
switch self {
case .authorize:
"oauth/authorize"
case .token:
"oauth/token"
}
}
public var jsonValue: Encodable? {
switch self {
case let .token(code, clientId, clientSecret):
TokenData(clientId: clientId, clientSecret: clientSecret, code: code)
default:
nil
}
}
public struct TokenData: Encodable {
public let grantType = "authorization_code"
public let clientId: String
public let clientSecret: String
public let redirectUri = AppInfo.scheme
public let code: String
public let scope = AppInfo.scopes
}
public func queryItems() -> [URLQueryItem]? {
switch self {
case let .authorize(clientId):
return [
.init(name: "response_type", value: "code"),
.init(name: "client_id", value: clientId),
.init(name: "redirect_uri", value: AppInfo.scheme),
.init(name: "scope", value: AppInfo.scopes),
]
default:
return nil
}
}
}
public enum Accounts: Endpoint {
case verifyCredentials
public func path() -> String {
switch self {
case .verifyCredentials:
"accounts/verify_credentials"
}
}
public func queryItems() -> [URLQueryItem]? {
nil
}
}
public struct InstanceApp: Codable, Identifiable {
public let id: String
public let name: String
public let website: URL?
public let redirectUri: String
public let clientId: String
public let clientSecret: String
public let vapidKey: String?
}
extension InstanceApp: Sendable {}
public struct ServerError: Decodable, Error {
public let error: String?
public var httpCode: Int?
}
public enum Apps: Endpoint {
case registerApp
public func path() -> String {
switch self {
case .registerApp:
"apps"
}
}
public func queryItems() -> [URLQueryItem]? {
switch self {
case .registerApp:
return [
.init(name: "client_name", value: AppInfo.clientName),
.init(name: "redirect_uris", value: AppInfo.scheme),
.init(name: "scopes", value: AppInfo.scopes),
.init(name: "website", value: AppInfo.website),
]
}
}
}
public struct LinkHandler {
public let rawLink: String
public var maxId: String? {
do {
let regex = try Regex("max_id=[0-9]+")
if let match = rawLink.firstMatch(of: regex) {
return match.output.first?.substring?.replacingOccurrences(of: "max_id=", with: "")
}
} catch {
return nil
}
return nil
}
}
extension LinkHandler: Sendable {}

View File

@ -0,0 +1,186 @@
//Made by Lumaa
import WidgetKit
import SwiftUI
import SwiftData
struct Provider: AppIntentTimelineProvider {
func placeholder(in context: Context) -> SimpleEntry {
let placeholder: UIImage = UIImage(systemName: "person.crop.circle") ?? UIImage()
placeholder.withTintColor(UIColor.label)
return SimpleEntry(date: Date(), pfp: placeholder, followers: 38, configuration: AccountAppIntent())
}
func snapshot(for configuration: AccountAppIntent, in context: Context) async -> SimpleEntry {
let data = await getData(configuration: configuration)
return SimpleEntry(date: Date(), pfp: data.0, followers: data.1, configuration: configuration)
}
func timeline(for configuration: AccountAppIntent, in context: Context) async -> Timeline<SimpleEntry> {
var entries: [SimpleEntry] = []
let data = await getData(configuration: configuration)
// Generate a timeline consisting of two entries an hour apart, starting from the current date.
let currentDate = Date()
for hourOffset in 0 ..< 2 {
let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
let entry = SimpleEntry(date: entryDate, pfp: data.0, followers: data.1, configuration: configuration)
entries.append(entry)
}
return Timeline(entries: entries, policy: .atEnd)
}
private func getData(configuration: AccountAppIntent) async -> (UIImage, Int) {
var pfp: UIImage = UIImage(systemName: "person.crop.circle") ?? UIImage()
pfp.withTintColor(UIColor.label)
if let account = configuration.account {
do {
let acc = try await account.client.getString(endpoint: Accounts.verifyCredentials, forceVersion: .v1)
if let serialized: [String : Any] = try JSONSerialization.jsonObject(with: acc.data(using: String.Encoding.utf8) ?? Data()) as? [String : Any] {
let avatar: String = serialized["avatar"] as! String
let task = try await URLSession.shared.data(from: URL(string: avatar)!)
pfp = UIImage(data: task.0) ?? UIImage()
let followers: Int = serialized["followers_count"] as! Int
return (pfp, followers)
}
} catch {
print(error)
}
}
return (pfp, 0)
}
}
struct SimpleEntry: TimelineEntry {
let date: Date
let pfp: UIImage
let followers: Int
let configuration: AccountAppIntent
}
struct FollowCountWidgetView: View {
@Environment(\.widgetFamily) private var family: WidgetFamily
var entry: Provider.Entry
var body: some View {
if let account = entry.configuration.account {
ZStack {
if family == WidgetFamily.systemSmall {
small
} else if family == WidgetFamily.systemMedium {
medium
} else if family == WidgetFamily.accessoryRectangular {
rectangular
} else {
Text(String("Unsupported WidgetFamily"))
.font(.caption)
}
}
.modelContainer(for: [LoggedAccount.self])
} else {
Text("widget.select-account")
.font(.caption)
}
}
var small: some View {
VStack(alignment: .center) {
Image(uiImage: entry.pfp)
.resizable()
.scaledToFit()
.frame(width: 60, height: 60)
.foregroundStyle(Color.white)
.clipShape(Circle())
Spacer()
Text(entry.followers, format: .number.notation(.compactName))
.font(.title.monospacedDigit().bold())
.contentTransition(.numericText())
Text("widget.followers")
.font(.caption)
.foregroundStyle(Color.gray)
Spacer()
Text("@\(entry.configuration.account!.username)")
.redacted(reason: .privacy)
.font(.caption.bold())
}
}
var medium: some View {
HStack {
Spacer()
VStack(alignment: .center, spacing: 10) {
Image(uiImage: entry.pfp)
.resizable()
.scaledToFit()
.frame(width: 60, height: 60)
.foregroundStyle(Color.white)
.clipShape(Circle())
Text("@\(entry.configuration.account!.username)")
.redacted(reason: .privacy)
.font(.caption.bold())
}
Spacer()
VStack {
Text(entry.followers, format: .number.notation(.compactName))
.font(.title.monospacedDigit().bold())
.contentTransition(.numericText())
Text("widget.followers")
.font(.caption)
.foregroundStyle(Color.gray)
}
Spacer()
}
}
var rectangular: some View {
HStack {
VStack(alignment: .leading) {
Text("@\(entry.configuration.account!.username)")
.multilineTextAlignment(.leading)
.font(.caption)
.opacity(0.7)
Text(entry.followers, format: .number.notation(.compactName))
.multilineTextAlignment(.leading)
.font(.system(size: 32, weight: .bold).monospacedDigit())
.contentTransition(.numericText())
}
.padding(.horizontal, 7.5)
Spacer()
}
}
}
struct FollowCountWidget: Widget {
let kind: String = "FollowCountWidget"
let modelContainer: ModelContainer
init() {
guard let modelContainer: ModelContainer = try? .init(for: LoggedAccount.self, configurations: ModelConfiguration(isStoredInMemoryOnly: true)) else { fatalError("Couldn't get LoggedAccounts") }
self.modelContainer = modelContainer
}
var body: some WidgetConfiguration {
AppIntentConfiguration(kind: kind, intent: AccountAppIntent.self, provider: Provider()) { entry in
FollowCountWidgetView(entry: entry)
.containerBackground(Color("WidgetBackground"), for: .widget)
}
.configurationDisplayName("widget.follow-count")
.description("widget.follow-count.description")
.supportedFamilies([.systemSmall, .systemMedium, .accessoryRectangular])
}
}

View File

@ -0,0 +1,12 @@
//Made by Lumaa
import WidgetKit
import SwiftUI
import UIKit
@main
struct ThreadedWidgetsBundle: WidgetBundle {
var body: some Widget {
FollowCountWidget()
}
}

View File

@ -0,0 +1,12 @@
<?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>aps-environment</key>
<string>development</string>
<key>com.apple.security.application-groups</key>
<array>
<string>group.lumaa.ThreadedApp</string>
</array>
</dict>
</plist>