Apple Watch support, new widget, and other improvements

This commit is contained in:
Lumaa 2024-03-08 13:50:48 +01:00
parent 3a8176f931
commit 81793f9842
21 changed files with 1075 additions and 24 deletions

View File

@ -48,6 +48,23 @@
B999DE602B76FB3E00509868 /* ContactRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = B999DE5F2B76FB3E00509868 /* ContactRow.swift */; };
B9B469B02B9A275F00AD5585 /* FollowGoalWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9B469AF2B9A275F00AD5585 /* FollowGoalWidget.swift */; };
B9B469B22B9A6E8300AD5585 /* PrivacyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9B469B12B9A6E8300AD5585 /* PrivacyView.swift */; };
B9B469BA2B9A7E6800AD5585 /* ThreadedWatchApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9B469B92B9A7E6800AD5585 /* ThreadedWatchApp.swift */; };
B9B469BC2B9A7E6800AD5585 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9B469BB2B9A7E6800AD5585 /* ContentView.swift */; };
B9B469BE2B9A7E6B00AD5585 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B9B469BD2B9A7E6B00AD5585 /* Assets.xcassets */; };
B9B469C12B9A7E6B00AD5585 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B9B469C02B9A7E6B00AD5585 /* Preview Assets.xcassets */; };
B9B469C42B9A7E6B00AD5585 /* Threaded.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = B9B469B72B9A7E6800AD5585 /* Threaded.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
B9B469C92B9A804800AD5585 /* Redeclarations.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9C20D602B949AD7004DC9B3 /* Redeclarations.swift */; };
B9B469CA2B9A811200AD5585 /* AppInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9FB94A12B2EF24A00D81C07 /* AppInfo.swift */; };
B9B469CB2B9A816D00AD5585 /* LoggedAccounts.swift in Sources */ = {isa = PBXBuildFile; fileRef = B98627302B86F23500844245 /* LoggedAccounts.swift */; };
B9B469CD2B9A823600AD5585 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = B9B469CC2B9A823600AD5585 /* Localizable.xcstrings */; };
B9B469CF2B9A82ED00AD5585 /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = B9B469CE2B9A82ED00AD5585 /* KeychainSwift */; };
B9B469D12B9A82FD00AD5585 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B9B469D02B9A82FD00AD5585 /* SwiftUI.framework */; };
B9B469D42B9A871C00AD5585 /* WatchConnectivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9B469D32B9A871C00AD5585 /* WatchConnectivity.swift */; };
B9B469D52B9A871C00AD5585 /* WatchConnectivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9B469D32B9A871C00AD5585 /* WatchConnectivity.swift */; };
B9B469D72B9A8B0700AD5585 /* GivenAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9B469D62B9A8B0700AD5585 /* GivenAccount.swift */; };
B9B469D82B9A8B1600AD5585 /* GivenAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9B469D62B9A8B0700AD5585 /* GivenAccount.swift */; };
B9B469D92B9AA4DF00AD5585 /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9FB949A2B2EF09A00D81C07 /* Client.swift */; };
B9B469DB2B9B2EDB00AD5585 /* ComingSoonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9B469DA2B9B2EDB00AD5585 /* ComingSoonView.swift */; };
B9B63B212B442D1500BBC82D /* DynamicTextEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9B63B202B442D1500BBC82D /* DynamicTextEditor.swift */; };
B9B63B232B447B8000BBC82D /* PostCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9B63B222B447B8000BBC82D /* PostCardView.swift */; };
B9B63B252B44997400BBC82D /* QuotePostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9B63B242B44997400BBC82D /* QuotePostView.swift */; };
@ -119,6 +136,13 @@
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
B9B469C22B9A7E6B00AD5585 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = B9FB944F2B2DEECE00D81C07 /* Project object */;
proxyType = 1;
remoteGlobalIDString = B9B469B62B9A7E6800AD5585;
remoteInfo = "ThreadedWatch Watch App";
};
B9C20D182B921C7B004DC9B3 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = B9FB944F2B2DEECE00D81C07 /* Project object */;
@ -136,6 +160,17 @@
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
B9B469C82B9A7E6B00AD5585 /* Embed Watch Content */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "$(CONTENTS_FOLDER_PATH)/Watch";
dstSubfolderSpec = 16;
files = (
B9B469C42B9A7E6B00AD5585 /* Threaded.app in Embed Watch Content */,
);
name = "Embed Watch Content";
runOnlyForDeploymentPostprocessing = 0;
};
B9FB94B82B2F009F00D81C07 /* Embed Foundation Extensions */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
@ -185,6 +220,17 @@
B999DE5F2B76FB3E00509868 /* ContactRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactRow.swift; sourceTree = "<group>"; };
B9B469AF2B9A275F00AD5585 /* FollowGoalWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowGoalWidget.swift; sourceTree = "<group>"; };
B9B469B12B9A6E8300AD5585 /* PrivacyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyView.swift; sourceTree = "<group>"; };
B9B469B72B9A7E6800AD5585 /* Threaded.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Threaded.app; sourceTree = BUILT_PRODUCTS_DIR; };
B9B469B92B9A7E6800AD5585 /* ThreadedWatchApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadedWatchApp.swift; sourceTree = "<group>"; };
B9B469BB2B9A7E6800AD5585 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
B9B469BD2B9A7E6B00AD5585 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
B9B469C02B9A7E6B00AD5585 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
B9B469CC2B9A823600AD5585 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
B9B469D02B9A82FD00AD5585 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = Platforms/WatchOS.platform/Developer/SDKs/WatchOS10.4.sdk/System/Library/Frameworks/SwiftUI.framework; sourceTree = DEVELOPER_DIR; };
B9B469D22B9A838500AD5585 /* ThreadedWatch Watch App.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "ThreadedWatch Watch App.entitlements"; sourceTree = "<group>"; };
B9B469D32B9A871C00AD5585 /* WatchConnectivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchConnectivity.swift; sourceTree = "<group>"; };
B9B469D62B9A8B0700AD5585 /* GivenAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GivenAccount.swift; sourceTree = "<group>"; };
B9B469DA2B9B2EDB00AD5585 /* ComingSoonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComingSoonView.swift; sourceTree = "<group>"; };
B9B63B202B442D1500BBC82D /* DynamicTextEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicTextEditor.swift; sourceTree = "<group>"; };
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>"; };
@ -256,6 +302,15 @@
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
B9B469B42B9A7E6800AD5585 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
B9B469D12B9A82FD00AD5585 /* SwiftUI.framework in Frameworks */,
B9B469CF2B9A82ED00AD5585 /* KeychainSwift in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
B9C20D062B921C78004DC9B3 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
@ -310,6 +365,28 @@
path = Content;
sourceTree = "<group>";
};
B9B469B82B9A7E6800AD5585 /* ThreadedWatch */ = {
isa = PBXGroup;
children = (
B9B469B92B9A7E6800AD5585 /* ThreadedWatchApp.swift */,
B9B469BB2B9A7E6800AD5585 /* ContentView.swift */,
B9B469D32B9A871C00AD5585 /* WatchConnectivity.swift */,
B9B469D62B9A8B0700AD5585 /* GivenAccount.swift */,
B9B469BD2B9A7E6B00AD5585 /* Assets.xcassets */,
B9B469BF2B9A7E6B00AD5585 /* Preview Content */,
B9B469CC2B9A823600AD5585 /* Localizable.xcstrings */,
);
path = ThreadedWatch;
sourceTree = "<group>";
};
B9B469BF2B9A7E6B00AD5585 /* Preview Content */ = {
isa = PBXGroup;
children = (
B9B469C02B9A7E6B00AD5585 /* Preview Assets.xcassets */,
);
path = "Preview Content";
sourceTree = "<group>";
};
B9BED5142B5D5CCD00C9B715 /* Post */ = {
isa = PBXGroup;
children = (
@ -361,8 +438,10 @@
B9FB944E2B2DEECE00D81C07 = {
isa = PBXGroup;
children = (
B9B469D22B9A838500AD5585 /* ThreadedWatch Watch App.entitlements */,
B9CC45B92B40AA1E001E4FA5 /* README.md */,
B9FB94592B2DEECE00D81C07 /* Threaded */,
B9B469B82B9A7E6800AD5585 /* ThreadedWatch */,
B9C20D0E2B921C78004DC9B3 /* ThreadedWidgets */,
B9FB94AB2B2F009F00D81C07 /* AuthService */,
B9FB94A82B2F009F00D81C07 /* Frameworks */,
@ -378,6 +457,7 @@
B9FB94572B2DEECE00D81C07 /* Threaded.app */,
B9FB94A72B2F009F00D81C07 /* ThreadedAuthService.appex */,
B9C20D092B921C78004DC9B3 /* ThreadedWidgetsExtension.appex */,
B9B469B72B9A7E6800AD5585 /* Threaded.app */,
);
name = Products;
sourceTree = "<group>";
@ -471,6 +551,7 @@
B9FA6E762B82788A00D63E30 /* AccountRow.swift */,
B97491E22B6E96700098BC48 /* SymbolWidth.swift */,
B9DC69282B78D9A500E625B9 /* SearchResultView.swift */,
B9B469DA2B9B2EDB00AD5585 /* ComingSoonView.swift */,
);
path = Components;
sourceTree = "<group>";
@ -492,6 +573,7 @@
B9FB94A82B2F009F00D81C07 /* Frameworks */ = {
isa = PBXGroup;
children = (
B9B469D02B9A82FD00AD5585 /* SwiftUI.framework */,
B9DC692C2B79362700E625B9 /* StoreKit.framework */,
B9FB94A92B2F009F00D81C07 /* AuthenticationServices.framework */,
B9C20D0A2B921C78004DC9B3 /* WidgetKit.framework */,
@ -525,6 +607,26 @@
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
B9B469B62B9A7E6800AD5585 /* ThreadedWatch Watch App */ = {
isa = PBXNativeTarget;
buildConfigurationList = B9B469C52B9A7E6B00AD5585 /* Build configuration list for PBXNativeTarget "ThreadedWatch Watch App" */;
buildPhases = (
B9B469B32B9A7E6800AD5585 /* Sources */,
B9B469B42B9A7E6800AD5585 /* Frameworks */,
B9B469B52B9A7E6800AD5585 /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = "ThreadedWatch Watch App";
packageProductDependencies = (
B9B469CE2B9A82ED00AD5585 /* KeychainSwift */,
);
productName = "ThreadedWatch Watch App";
productReference = B9B469B72B9A7E6800AD5585 /* Threaded.app */;
productType = "com.apple.product-type.application";
};
B9C20D082B921C78004DC9B3 /* ThreadedWidgetsExtension */ = {
isa = PBXNativeTarget;
buildConfigurationList = B9C20D1D2B921C7B004DC9B3 /* Build configuration list for PBXNativeTarget "ThreadedWidgetsExtension" */;
@ -553,12 +655,14 @@
B9FB94542B2DEECE00D81C07 /* Frameworks */,
B9FB94552B2DEECE00D81C07 /* Resources */,
B9FB94B82B2F009F00D81C07 /* Embed Foundation Extensions */,
B9B469C82B9A7E6B00AD5585 /* Embed Watch Content */,
);
buildRules = (
);
dependencies = (
B9FB94B32B2F009F00D81C07 /* PBXTargetDependency */,
B9C20D192B921C7B004DC9B3 /* PBXTargetDependency */,
B9B469C32B9A7E6B00AD5585 /* PBXTargetDependency */,
);
name = Threaded;
packageProductDependencies = (
@ -600,9 +704,12 @@
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1520;
LastSwiftUpdateCheck = 1530;
LastUpgradeCheck = 1510;
TargetAttributes = {
B9B469B62B9A7E6800AD5585 = {
CreatedOnToolsVersion = 15.3;
};
B9C20D082B921C78004DC9B3 = {
CreatedOnToolsVersion = 15.2;
};
@ -636,13 +743,24 @@
projectRoot = "";
targets = (
B9FB94562B2DEECE00D81C07 /* Threaded */,
B9FB94A62B2F009F00D81C07 /* ThreadedAuthService */,
B9B469B62B9A7E6800AD5585 /* ThreadedWatch Watch App */,
B9C20D082B921C78004DC9B3 /* ThreadedWidgetsExtension */,
B9FB94A62B2F009F00D81C07 /* ThreadedAuthService */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
B9B469B52B9A7E6800AD5585 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
B9B469C12B9A7E6B00AD5585 /* Preview Assets.xcassets in Resources */,
B9B469BE2B9A7E6B00AD5585 /* Assets.xcassets in Resources */,
B9B469CD2B9A823600AD5585 /* Localizable.xcstrings in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
B9C20D072B921C78004DC9B3 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
@ -679,6 +797,21 @@
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
B9B469B32B9A7E6800AD5585 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
B9B469D72B9A8B0700AD5585 /* GivenAccount.swift in Sources */,
B9B469D52B9A871C00AD5585 /* WatchConnectivity.swift in Sources */,
B9B469C92B9A804800AD5585 /* Redeclarations.swift in Sources */,
B9B469CB2B9A816D00AD5585 /* LoggedAccounts.swift in Sources */,
B9B469CA2B9A811200AD5585 /* AppInfo.swift in Sources */,
B9B469BC2B9A7E6800AD5585 /* ContentView.swift in Sources */,
B9B469D92B9AA4DF00AD5585 /* Client.swift in Sources */,
B9B469BA2B9A7E6800AD5585 /* ThreadedWatchApp.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
B9C20D052B921C78004DC9B3 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
@ -722,6 +855,7 @@
B9FB94972B2EDABF00D81C07 /* SupportView.swift in Sources */,
B9F8FA162B5D3AC30044DAB4 /* SafariView.swift in Sources */,
B97798892B853E6600DC869F /* UpdateView.swift in Sources */,
B9B469D82B9A8B1600AD5585 /* GivenAccount.swift in Sources */,
B9842C142B2F310C00D9F3C1 /* FetchTimeline.swift in Sources */,
B9842C162B2F363600D9F3C1 /* TimelineFilter.swift in Sources */,
B9B63B232B447B8000BBC82D /* PostCardView.swift in Sources */,
@ -747,6 +881,7 @@
B9FB94812B2E1FEF00D81C07 /* HTMLString.swift in Sources */,
B9FB947F2B2E1D5F00D81C07 /* Account.swift in Sources */,
B9842C122B2F2A5800D9F3C1 /* TimelineView.swift in Sources */,
B9B469D42B9A871C00AD5585 /* WatchConnectivity.swift in Sources */,
B9FB948C2B2E232300D81C07 /* OnlineImage.swift in Sources */,
B9BED5182B5D649C00C9B715 /* PostMenu.swift in Sources */,
B9FB94742B2DF6A100D81C07 /* ButtonStyles.swift in Sources */,
@ -761,6 +896,7 @@
B9B63B272B449CDC00BBC82D /* SearchResults.swift in Sources */,
B9B63B252B44997400BBC82D /* QuotePostView.swift in Sources */,
B9BCC3182B90B3BC00211976 /* Tenor.swift in Sources */,
B9B469DB2B9B2EDB00AD5585 /* ComingSoonView.swift in Sources */,
B97BCE242B3DD8400044756D /* HapticManager.swift in Sources */,
B9B63B212B442D1500BBC82D /* DynamicTextEditor.swift in Sources */,
B9FB949F2B2EF0F200D81C07 /* MastodonRequest.swift in Sources */,
@ -784,6 +920,11 @@
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
B9B469C32B9A7E6B00AD5585 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = B9B469B62B9A7E6800AD5585 /* ThreadedWatch Watch App */;
targetProxy = B9B469C22B9A7E6B00AD5585 /* PBXContainerItemProxy */;
};
B9C20D192B921C7B004DC9B3 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = B9C20D082B921C78004DC9B3 /* ThreadedWidgetsExtension */;
@ -808,6 +949,66 @@
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
B9B469C62B9A7E6B00AD5585 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = "ThreadedWatch Watch App.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"ThreadedWatch/Preview Content\"";
DEVELOPMENT_TEAM = HB5P3BML86;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = fr.lumaa.Threaded;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = fr.lumaa.Threaded.watchkitapp;
PRODUCT_NAME = Threaded;
SDKROOT = watchos;
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 4;
WATCHOS_DEPLOYMENT_TARGET = 10.0;
};
name = Debug;
};
B9B469C72B9A7E6B00AD5585 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = "ThreadedWatch Watch App.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"ThreadedWatch/Preview Content\"";
DEVELOPMENT_TEAM = HB5P3BML86;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = fr.lumaa.Threaded;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = fr.lumaa.Threaded.watchkitapp;
PRODUCT_NAME = Threaded;
SDKROOT = watchos;
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 4;
WATCHOS_DEPLOYMENT_TARGET = 10.0;
};
name = Release;
};
B9C20D1B2B921C7B004DC9B3 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
@ -831,13 +1032,14 @@
PRODUCT_BUNDLE_IDENTIFIER = fr.lumaa.Threaded.ThreadedWidgets;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator watchos watchsimulator";
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;
TARGETED_DEVICE_FAMILY = "1,4";
WATCHOS_DEPLOYMENT_TARGET = 10.0;
};
name = Debug;
};
@ -864,13 +1066,14 @@
PRODUCT_BUNDLE_IDENTIFIER = fr.lumaa.Threaded.ThreadedWidgets;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator watchos watchsimulator";
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;
TARGETED_DEVICE_FAMILY = "1,4";
WATCHOS_DEPLOYMENT_TARGET = 10.0;
};
name = Release;
};
@ -1001,7 +1204,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = Threaded/Threaded.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 500;
CURRENT_PROJECT_VERSION = 1482;
DEVELOPMENT_ASSET_PATHS = "\"Threaded/Preview Content\"";
DEVELOPMENT_TEAM = HB5P3BML86;
ENABLE_PREVIEWS = YES;
@ -1041,7 +1244,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = Threaded/Threaded.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 500;
CURRENT_PROJECT_VERSION = 1482;
DEVELOPMENT_ASSET_PATHS = "\"Threaded/Preview Content\"";
DEVELOPMENT_TEAM = HB5P3BML86;
ENABLE_PREVIEWS = YES;
@ -1136,6 +1339,15 @@
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
B9B469C52B9A7E6B00AD5585 /* Build configuration list for PBXNativeTarget "ThreadedWatch Watch App" */ = {
isa = XCConfigurationList;
buildConfigurations = (
B9B469C62B9A7E6B00AD5585 /* Debug */,
B9B469C72B9A7E6B00AD5585 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
B9C20D1D2B921C7B004DC9B3 /* Build configuration list for PBXNativeTarget "ThreadedWidgetsExtension" */ = {
isa = XCConfigurationList;
buildConfigurations = (
@ -1253,6 +1465,11 @@
package = B95ED2312B8707D60055F5BD /* XCRemoteSwiftPackageReference "purchases-ios" */;
productName = RevenueCatUI;
};
B9B469CE2B9A82ED00AD5585 /* KeychainSwift */ = {
isa = XCSwiftPackageProductDependency;
package = B9BF54052B6B6823004B24E7 /* XCRemoteSwiftPackageReference "keychain-swift" */;
productName = KeychainSwift;
};
B9BF54062B6B6823004B24E7 /* KeychainSwift */ = {
isa = XCSwiftPackageProductDependency;
package = B9BF54052B6B6823004B24E7 /* XCRemoteSwiftPackageReference "keychain-swift" */;

View File

@ -0,0 +1,96 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1530"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "B9B469B62B9A7E6800AD5585"
BuildableName = "Threaded.app"
BlueprintName = "ThreadedWatch Watch App"
ReferencedContainer = "container:Threaded.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "B9FB94562B2DEECE00D81C07"
BuildableName = "Threaded.app"
BlueprintName = "Threaded"
ReferencedContainer = "container:Threaded.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "NO">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "B9B469B62B9A7E6800AD5585"
BuildableName = "Threaded.app"
BlueprintName = "ThreadedWatch Watch App"
ReferencedContainer = "container:Threaded.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<LocationScenarioReference
identifier = "com.apple.dt.IDEFoundation.CurrentLocationScenarioIdentifier"
referenceType = "1">
</LocationScenarioReference>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "B9B469B62B9A7E6800AD5585"
BuildableName = "Threaded.app"
BlueprintName = "ThreadedWatch Watch App"
ReferencedContainer = "container:Threaded.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -0,0 +1,123 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1530"
wasCreatedForAppExtension = "YES"
version = "2.0">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "B9C20D082B921C78004DC9B3"
BuildableName = "ThreadedWidgetsExtension.appex"
BlueprintName = "ThreadedWidgetsExtension"
ReferencedContainer = "container:Threaded.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "B9FB94562B2DEECE00D81C07"
BuildableName = "Threaded.app"
BlueprintName = "Threaded"
ReferencedContainer = "container:Threaded.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = ""
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
launchStyle = "0"
askForAppToLaunch = "Yes"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES"
launchAutomaticallySubstyle = "2">
<RemoteRunnable
runnableDebuggingMode = "2"
BundleIdentifier = "com.apple.springboard">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "B9C20D082B921C78004DC9B3"
BuildableName = "ThreadedWidgetsExtension.appex"
BlueprintName = "ThreadedWidgetsExtension"
ReferencedContainer = "container:Threaded.xcodeproj">
</BuildableReference>
</RemoteRunnable>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "B9FB94562B2DEECE00D81C07"
BuildableName = "Threaded.app"
BlueprintName = "Threaded"
ReferencedContainer = "container:Threaded.xcodeproj">
</BuildableReference>
</MacroExpansion>
<EnvironmentVariables>
<EnvironmentVariable
key = "_XCWidgetKind"
value = ""
isEnabled = "YES">
</EnvironmentVariable>
<EnvironmentVariable
key = "_XCWidgetDefaultView"
value = "timeline"
isEnabled = "YES">
</EnvironmentVariable>
<EnvironmentVariable
key = "_XCWidgetFamily"
value = "systemMedium"
isEnabled = "YES">
</EnvironmentVariable>
</EnvironmentVariables>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES"
launchAutomaticallySubstyle = "2">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "B9FB94562B2DEECE00D81C07"
BuildableName = "Threaded.app"
BlueprintName = "Threaded"
ReferencedContainer = "container:Threaded.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -0,0 +1,28 @@
//Made by Lumaa
import SwiftUI
struct ComingSoonView: View {
@State private var spin: CGFloat = 0
var body: some View {
VStack(spacing: 5) {
Text(String("👀"))
.font(.system(size: 62))
.rotation3DEffect(.degrees(spin), axis: (x: 0, y: 1, z: 0))
.onTapGesture {
withAnimation(.spring.speed(0.8)) {
spin = 360
}
spin = 0
}
Text("coming-soon")
.font(.title.bold())
}
}
}
#Preview {
ComingSoonView()
}

View File

@ -694,6 +694,9 @@
}
}
}
},
"coming-soon" : {
},
"discovery" : {
"localizations" : {
@ -1559,6 +1562,38 @@
}
}
},
"settings.account-switcher.remove" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Remove"
}
},
"fr" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Supprimer"
}
}
}
},
"settings.account-switcher.send-to-watch" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Send to Apple Watch"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Envoyer vers l'Apple Watch"
}
}
}
},
"settings.cancel" : {
"localizations" : {
"en" : {
@ -2980,4 +3015,4 @@
}
},
"version" : "1.0"
}
}

View File

@ -25,6 +25,7 @@ struct PrivacyView: View {
}
} label: {
Text(clearedCache ? "settings.privacy.cleared" : "settings.privacy.clear")
.foregroundStyle(clearedCache ? Color(uiColor: UIColor.label) : Color(uiColor: UIColor.systemBackground))
}
.buttonStyle(LargeButton(filled: true, filledColor: clearedCache ? Color.green : Color(uiColor: UIColor.label), height: 7.5))
.disabled(clearedCache)

View File

@ -2,6 +2,7 @@
import SwiftUI
import SwiftData
import WatchConnectivity
//TODO: Bring back "Privacy" with mutelist, blocklist and default visibility
@ -20,7 +21,7 @@ struct SettingsView: View {
Section {
ForEach(loggedAccounts) { logged in
if let app = logged.app {
SwitcherRow(app: app)
SwitcherRow(app: app, loggedAccount: logged)
.listRowThreaded()
}
}
@ -122,12 +123,16 @@ struct SettingsView: View {
extension SettingsView {
struct SwitcherRow: View {
@Environment(\.modelContext) private var modelContext
@Environment(AccountManager.self) private var accountManager: AccountManager
@Environment(UniversalNavigator.self) private var uniNav: UniversalNavigator
@EnvironmentObject private var navigator: Navigator
var logged: LoggedAccount
var app: AppAccount
private let connectivity: SessionDelegator = .init()
@State private var account: Account? = nil
@State private var error: Bool = false
@ -139,8 +144,9 @@ extension SettingsView {
return currentAcct == app.accountName ?? ""
}
init(app: AppAccount) {
init(app: AppAccount, loggedAccount: LoggedAccount) {
self.app = app
self.logged = loggedAccount
}
var body: some View {
@ -174,8 +180,9 @@ extension SettingsView {
} else {
AccountManager.shared.setAccount(fetched!)
AccountManager.shared.setClient(c)
uniNav.selectedTab = .timeline
navigator.path = []
uniNav.selectedTab = .timeline
}
}
} label: {
@ -191,6 +198,33 @@ extension SettingsView {
.lineLimit(1)
}
}
.contextMenu {
Button(role: .destructive) {
modelContext.delete(self.logged)
} label: {
Label("settings.account-switcher.remove", systemImage: "trash")
}
Divider()
if connectivity.isWorking {
Button {
// double check in case states change in between
if connectivity.isWorking {
let message = GivenAccount(acct: app.accountName!, bearerToken: app.oauthToken?.accessToken ?? "")
connectivity.session.sendMessageData(message.turnToMessage(), replyHandler: { data in
let str = String(data: data, encoding: .utf8)
print(str ?? "No data?")
HapticManager.playHaptics(haptics: Haptic.success)
})
} else {
print("No Watch?")
}
} label: {
Label("settings.account-switcher.send-to-watch", systemImage: "applewatch.and.arrow.forward")
}
}
}
} else {
Circle()
.fill(error ? Color.red.opacity(0.45) : Color.gray.opacity(0.45))
@ -221,6 +255,8 @@ extension SettingsView {
}
.task {
account = await findAccount(acct: app.accountName!)
connectivity.initialize()
}
}

View File

@ -0,0 +1,10 @@
<?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.lumaa.ThreadedApp</string>
</array>
</dict>
</plist>

View File

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

View File

@ -0,0 +1,14 @@
{
"images" : [
{
"filename" : "ThreadedApp.png",
"idiom" : "universal",
"platform" : "watchos",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

View File

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

View File

@ -0,0 +1,152 @@
//Made by Lumaa
import SwiftUI
import SwiftData
struct ContentView: View {
@State private var givenAccounts: [GivenAccount] = []
private let connectivity: SessionDelegator = .init()
@State private var fetching: Bool = false
@State private var currentAccount: (UIImage, Int)? = nil
var body: some View {
NavigationStack {
TabView {
if givenAccounts.isEmpty || givenAccounts.count < 1 {
ContentUnavailableView("iphone.login", systemImage: "iphone", description: Text("iphone.login.description"))
.containerBackground(Color.yellow.gradient, for: .tabView)
.scrollDisabled(true)
} else {
ForEach(givenAccounts, id: \.bearerToken) { acc in
ZStack {
if currentAccount == nil {
ProgressView()
.task {
self.currentAccount = await getData(givenAccount: acc)
}
} else {
let username: String = String(acc.acct.split(separator: "@")[0])
let server: String = String(acc.acct.split(separator: "@")[1])
VStack {
Image(uiImage: currentAccount!.0)
.resizable()
.scaledToFit()
.frame(width: 60, height: 60)
.clipShape(Circle())
.padding(.top, 7.5)
.onDisappear() {
guard !fetching else { print("Fetching..."); return }
self.currentAccount = nil
}
Text(String("@\(username)"))
.font(.title2.bold())
Text(server)
.font(.caption)
.foregroundStyle(Color.gray)
ScrollView(.horizontal) {
HStack {
VStack {
Text(currentAccount!.1, format: .number.notation(.compactName))
.font(.headline)
Text("account.followers")
.font(.caption)
.foregroundStyle(Color.gray)
}
.safeAreaPadding()
}
}
.padding(.top, 7.5)
}
}
}
}
}
}
.tabViewStyle(.verticalPage(transitionStyle: .blur))
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItemGroup(placement: .topBarLeading) {
Button {
refresh()
} label: {
Image(systemName: "arrow.clockwise")
}
}
}
}
.onAppear {
connectivity.initialize()
self.refresh()
}
}
private func refresh() {
self.givenAccounts = self.getAccounts()
if connectivity.isWorking {
withAnimation {
self.givenAccounts.append(contentsOf: connectivity.allMessage.filter({ !self.givenAccounts.contains($0) }))
if let _givenAccounts = connectivity.lastMessage, self.givenAccounts.isEmpty {
self.givenAccounts = [_givenAccounts] // fallback
}
}
}
self.givenAccounts = self.givenAccounts.uniqued()
self.saveCurrentModels()
}
private func getAccounts() -> [GivenAccount] {
guard let modelContainer: ModelContainer = try? ModelContainer(for: LoggedAccount.self, configurations: ModelConfiguration(isStoredInMemoryOnly: false)) else { return [] }
let modelContext = ModelContext(modelContainer)
let loggedAccounts: [LoggedAccount]? = try? modelContext.fetch(FetchDescriptor<LoggedAccount>())
let given: [GivenAccount] = loggedAccounts?.map({ $0.toGiven() }) ?? []
return given
}
private func getData(givenAccount: GivenAccount) async -> (UIImage, Int) {
let server = givenAccount.acct.split(separator: "@")[1]
let client: Client = Client(server: String(server), oauthToken: OauthToken(accessToken: givenAccount.bearerToken, tokenType: "Bearer", scope: "", createdAt: .nan))
var pfp: UIImage
do {
fetching = true
let acc = try await client.getString(endpoint: Accounts.verifyCredentials, forceVersion: .v1)
fetching = false
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 (UIImage(), 0)
}
private func saveModel(model: LoggedAccount) {
guard let modelContainer: ModelContainer = try? ModelContainer(for: LoggedAccount.self, configurations: ModelConfiguration(isStoredInMemoryOnly: false)) else { return }
let modelContext = ModelContext(modelContainer)
modelContext.insert(model)
}
private func saveCurrentModels() {
for given in self.givenAccounts {
saveModel(model: given.toLogged())
}
}
}
#Preview {
ContentView()
}

View File

@ -0,0 +1,59 @@
//Made by Lumaa
import Foundation
struct GivenAccount: Hashable {
static let messageSeparator: String = "||"
let acct: String
let bearerToken: String
init(acct: String, bearerToken: String) {
self.acct = acct
self.bearerToken = bearerToken
}
private func messageString() -> String {
return "\(self.acct)\(Self.messageSeparator)\(self.bearerToken)"
}
func turnToMessage() -> Data {
let str = self.messageString()
return str.data(using: .utf8) ?? Data()
}
func turnToDictionary() -> [String : Any] {
let str = self.messageString()
return ["givenAccount" : str]
}
func toLogged() -> LoggedAccount {
let oauth = OauthToken(accessToken: self.bearerToken, tokenType: "Bearer", scope: "", createdAt: .nan)
return LoggedAccount(token: oauth, acct: self.acct)
}
static func makeFromMessage(_ message: Data) -> GivenAccount {
if let string = String(data: message, encoding: .utf8) {
let decomposed = string.split(separator: Self.messageSeparator)
let acct = decomposed[0]
let bearerToken = decomposed[1]
return GivenAccount(acct: String(acct), bearerToken: String(bearerToken))
}
fatalError("Message couldn't be stringified")
}
static func makeFromMessage(_ message: String) -> GivenAccount {
let decomposed = message.split(separator: Self.messageSeparator)
let acct = decomposed[0]
let bearerToken = decomposed[1]
return GivenAccount(acct: String(acct), bearerToken: String(bearerToken))
}
}
extension LoggedAccount {
func toGiven() -> GivenAccount {
return GivenAccount(acct: self.acct, bearerToken: self.token.accessToken)
}
}

View File

@ -0,0 +1,54 @@
{
"sourceLanguage" : "en",
"strings" : {
"account.followers" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "followers"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "followers"
}
}
}
},
"iphone.login" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Login with your iPhone"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Connexion via l'iPhone"
}
}
}
},
"iphone.login.description" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Use your iPhone to see your account on your Apple Watch"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Utilisez votre iPhone pour voir vos comptes sur votre Apple Watch"
}
}
}
}
},
"version" : "1.0"
}

View File

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

View File

@ -0,0 +1,13 @@
//Made by Lumaa
import SwiftUI
@main
struct ThreadedWatch_Watch_AppApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.modelData()
}
}
}

View File

@ -0,0 +1,122 @@
//Made by Lumaa
import Foundation
import WatchConnectivity
#if os(watchOS)
import ClockKit
#endif
// Implement WCSessionDelegate methods to receive Watch Connectivity data and notify clients.
// Handle WCSession status changes.
//
class SessionDelegator: NSObject, WCSessionDelegate {
public var session: WCSession = .default
public var lastMessage: GivenAccount? = nil
public var allMessage: [GivenAccount] = []
public var isWorking: Bool {
self.session.isReachable
}
func initialize() {
guard WCSession.isSupported() else { fatalError("Doesn't support WCSession") }
session.delegate = self
session.activate()
}
// Monitor WCSession activation state changes.
//
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
return
}
// Monitor WCSession reachability state changes.
//
func sessionReachabilityDidChange(_ session: WCSession) {
self.session = session
}
// Did receive an app context.
//
func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String: Any]) {
if let givenAccount = applicationContext["givenAccount"] as? String {
self.lastMessage = GivenAccount.makeFromMessage(givenAccount)
guard !self.allMessage.contains(where: { $0.bearerToken == self.lastMessage!.bearerToken }) else { return }
self.allMessage.append(self.lastMessage!)
}
}
// Did receive a message, and the peer doesn't need a response.
//
func session(_ session: WCSession, didReceiveMessage message: [String: Any]) {
if let givenAccount = message["givenAccount"] as? String {
self.lastMessage = GivenAccount.makeFromMessage(givenAccount)
guard !self.allMessage.contains(where: { $0.bearerToken == self.lastMessage!.bearerToken }) else { return }
self.allMessage.append(self.lastMessage!)
}
}
// Did receive a message, and the peer needs a response.
//
func session(_ session: WCSession, didReceiveMessage message: [String: Any], replyHandler: @escaping ([String: Any]) -> Void) {
self.session(session, didReceiveMessage: message)
let response = ["success" : true, "timestamp": Int(Date.now.timeIntervalSince1970)] as [String : Any]
replyHandler(response)
}
// Did receive a piece of message data, and the peer doesn't need a response.
//
func session(_ session: WCSession, didReceiveMessageData messageData: Data) {
self.lastMessage = GivenAccount.makeFromMessage(messageData)
guard !self.allMessage.contains(where: { $0.bearerToken == self.lastMessage!.bearerToken }) else { return }
self.allMessage.append(self.lastMessage!)
}
// Did receive a piece of message data, and the peer needs a response.
//
func session(_ session: WCSession, didReceiveMessageData messageData: Data, replyHandler: @escaping (Data) -> Void) {
self.session(session, didReceiveMessageData: messageData)
replyHandler(messageData) // Echo back the data.
}
// Did receive a piece of userInfo.
//
func session(_ session: WCSession, didReceiveUserInfo userInfo: [String: Any] = [:]) {
return
}
// Did finish sending a piece of userInfo.
//
func session(_ session: WCSession, didFinish userInfoTransfer: WCSessionUserInfoTransfer, error: Error?) {
return
}
// Did receive a file.
//
func session(_ session: WCSession, didReceive file: WCSessionFile) {
return
}
// Did finish a file transfer.
//
func session(_ session: WCSession, didFinish fileTransfer: WCSessionFileTransfer, error: Error?) {
return
}
// WCSessionDelegate methods for iOS only.
//
#if os(iOS)
func sessionDidBecomeInactive(_ session: WCSession) {
print("\(#function): activationState = \(session.activationState.rawValue)")
}
func sessionDidDeactivate(_ session: WCSession) {
// Activate the new session after having switched to a new watch.
session.activate()
}
func sessionWatchStateDidChange(_ session: WCSession) {
print("\(#function): activationState = \(session.activationState.rawValue)")
}
#endif
}

View File

@ -11,15 +11,16 @@ struct FollowCountWidgetView: View {
var body: some View {
if let account = entry.configuration.account {
ZStack {
#if os(iOS)
if family == WidgetFamily.systemSmall {
small
} else if family == WidgetFamily.systemMedium {
medium
} else if family == WidgetFamily.accessoryRectangular {
}
#endif
if family == WidgetFamily.accessoryRectangular {
rectangular
} else {
Text(String("Unsupported WidgetFamily"))
.font(.caption)
}
}
.modelContainer(for: [LoggedAccount.self])
@ -124,13 +125,22 @@ struct FollowCountWidget: Widget {
}
.configurationDisplayName("widget.follow-count")
.description("widget.follow-count.description")
#if os(iOS)
.supportedFamilies([.systemSmall, .systemMedium, .accessoryRectangular])
#else
.supportedFamilies([.accessoryRectangular])
#endif
}
struct Provider: AppIntentTimelineProvider {
func recommendations() -> [AppIntentRecommendation<AccountAppIntent>] {
let intent = AccountAppIntent()
return [.init(intent: intent, description: intent.account?.username ?? String("Mastodon"))]
}
func placeholder(in context: Context) -> SimpleEntry {
let placeholder: UIImage = UIImage(systemName: "person.crop.circle") ?? UIImage()
placeholder.withTintColor(UIColor.label)
placeholder.withTintColor(UIColor.actualLabel)
return SimpleEntry(date: Date(), pfp: placeholder, followers: 38, configuration: AccountAppIntent())
}
@ -157,7 +167,7 @@ struct FollowCountWidget: Widget {
private func getData(configuration: AccountAppIntent) async -> (UIImage, Int) {
var pfp: UIImage = UIImage(systemName: "person.crop.circle") ?? UIImage()
pfp.withTintColor(UIColor.label)
pfp.withTintColor(UIColor.actualLabel)
if let account = configuration.account {
do {
let acc = try await account.client.getString(endpoint: Accounts.verifyCredentials, forceVersion: .v1)
@ -185,3 +195,23 @@ struct FollowCountWidget: Widget {
let configuration: AccountAppIntent
}
}
private extension Color {
private static var label: Color {
#if os(iOS)
return Color(uiColor: UIColor.label)
#else
return Color.white
#endif
}
}
private extension UIColor {
static var actualLabel: UIColor {
#if os(iOS)
return UIColor.label
#else
return UIColor.white
#endif
}
}

View File

@ -15,15 +15,16 @@ struct FollowGoalWidgetView: View {
var body: some View {
if let account = entry.configuration.account {
ZStack {
#if os(iOS)
if family == WidgetFamily.systemMedium {
medium
} else if family == WidgetFamily.accessoryRectangular {
}
#endif
if family == WidgetFamily.accessoryRectangular {
rectangular
} else if family == WidgetFamily.accessoryCircular {
circular
} else {
Text(String("Unsupported WidgetFamily"))
.font(.caption)
}
}
.modelContainer(for: [LoggedAccount.self])
@ -135,13 +136,21 @@ struct FollowGoalWidget: Widget {
}
.configurationDisplayName("widget.follow-goal")
.description("widget.follow-goal.description")
#if os(iOS)
.supportedFamilies([.systemMedium, .accessoryRectangular, .accessoryCircular])
#else
.supportedFamilies([.accessoryRectangular, .accessoryCircular])
#endif
}
struct Provider: AppIntentTimelineProvider {
func recommendations() -> [AppIntentRecommendation<AccountGoalAppIntent>] {
return []
}
func placeholder(in context: Context) -> SimpleEntry {
let placeholder: UIImage = UIImage(systemName: "person.crop.circle") ?? UIImage()
placeholder.withTintColor(UIColor.label)
placeholder.withTintColor(UIColor.actualLabel)
return SimpleEntry(date: Date(), pfp: placeholder, followers: 38, configuration: AccountGoalAppIntent())
}
@ -168,7 +177,7 @@ struct FollowGoalWidget: Widget {
private func getData(configuration: AccountGoalAppIntent) async -> (UIImage, Int) {
var pfp: UIImage = UIImage(systemName: "person.crop.circle") ?? UIImage()
pfp.withTintColor(UIColor.label)
pfp.withTintColor(UIColor.actualLabel)
if let account = configuration.account {
do {
let acc = try await account.client.getString(endpoint: Accounts.verifyCredentials, forceVersion: .v1)
@ -196,3 +205,23 @@ struct FollowGoalWidget: Widget {
let configuration: AccountGoalAppIntent
}
}
private extension Color {
private static var label: Color {
#if os(iOS)
return Color(uiColor: UIColor.label)
#else
return Color.white
#endif
}
}
private extension UIColor {
static var actualLabel: UIColor {
#if os(iOS)
return UIColor.label
#else
return UIColor.white
#endif
}
}

View File

@ -6,6 +6,7 @@ import UIKit
// this is messy but it's alr
#if os(iOS)
public class AppDelegate: NSObject, UIWindowSceneDelegate, Sendable, UIApplicationDelegate {
public var window: UIWindow?
public private(set) var windowWidth: CGFloat = UIScreen.main.bounds.size.width
@ -70,6 +71,7 @@ public class AppDelegate: NSObject, UIWindowSceneDelegate, Sendable, UIApplicati
}
}
}
#endif
public extension URL {
static let placeholder: URL = URL(string: "https://cdn.pixabay.com/photo/2023/08/28/20/32/flower-8220018_1280.jpg")!
@ -222,3 +224,10 @@ public struct LinkHandler {
}
extension LinkHandler: Sendable {}
public extension Array where Element: Hashable {
func uniqued() -> [Element] {
var seen = Set<Element>()
return filter { seen.insert($0).inserted }
}
}