From 5a3636cb2266e7ad688109c7381c82fff3b3d34f Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Tue, 24 Jan 2023 11:14:50 +0100 Subject: [PATCH 01/24] feat(Widget): Implement FollowersCountIntent --- Mastodon.xcodeproj/project.pbxproj | 265 ++++++++++++++++++ Mastodon/Info.plist | 1 + .../Handler/FollowersCountIntentHandler.swift | 42 +++ MastodonIntent/Info.plist | 1 + MastodonIntent/IntentHandler.swift | 2 + .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 13 + WidgetExtension/Assets.xcassets/Contents.json | 6 + .../WidgetBackground.colorset/Contents.json | 11 + .../FollowersWidgetExtension.swift | 61 ++++ WidgetExtension/Info.plist | 11 + WidgetExtension/WidgetExtension.entitlements | 10 + .../WidgetExtension.intentdefinition | 153 ++++++++++ WidgetExtension/WidgetExtension.swift | 9 + WidgetExtension/WidgetExtensionBundle.swift | 11 + 15 files changed, 607 insertions(+) create mode 100644 MastodonIntent/Handler/FollowersCountIntentHandler.swift create mode 100644 WidgetExtension/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 WidgetExtension/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 WidgetExtension/Assets.xcassets/Contents.json create mode 100644 WidgetExtension/Assets.xcassets/WidgetBackground.colorset/Contents.json create mode 100644 WidgetExtension/FollowersWidgetExtension.swift create mode 100644 WidgetExtension/Info.plist create mode 100644 WidgetExtension/WidgetExtension.entitlements create mode 100644 WidgetExtension/WidgetExtension.intentdefinition create mode 100644 WidgetExtension/WidgetExtension.swift create mode 100644 WidgetExtension/WidgetExtensionBundle.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 4caf3ebe5..31a895c05 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -33,10 +33,23 @@ 2A71F541296DBDA80049F54A /* Media.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2A71F53D296DBDA80049F54A /* Media.xcassets */; }; 2A71F542296DBDA80049F54A /* Action.js in Resources */ = {isa = PBXBuildFile; fileRef = 2A71F53E296DBDA80049F54A /* Action.js */; }; 2A71F543296DBDA80049F54A /* ActionRequestHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A71F53F296DBDA80049F54A /* ActionRequestHandler.swift */; }; + 2A728122297EA9D7004138C5 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2A728121297EA9D7004138C5 /* WidgetKit.framework */; }; + 2A728124297EA9D7004138C5 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2A728123297EA9D7004138C5 /* SwiftUI.framework */; }; + 2A728127297EA9D7004138C5 /* WidgetExtensionBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A728126297EA9D7004138C5 /* WidgetExtensionBundle.swift */; }; + 2A72812B297EA9D7004138C5 /* FollowersWidgetExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A72812A297EA9D7004138C5 /* FollowersWidgetExtension.swift */; }; + 2A72812E297EA9D8004138C5 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2A72812D297EA9D8004138C5 /* Assets.xcassets */; }; + 2A728130297EA9D8004138C5 /* WidgetExtension.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 2A72812C297EA9D7004138C5 /* WidgetExtension.intentdefinition */; }; + 2A728131297EA9D8004138C5 /* WidgetExtension.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 2A72812C297EA9D7004138C5 /* WidgetExtension.intentdefinition */; }; + 2A728134297EA9D8004138C5 /* WidgetExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 2A728120297EA9D7004138C5 /* WidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 2A72813B297EC6F7004138C5 /* MastodonSDKDynamic in Frameworks */ = {isa = PBXBuildFile; productRef = 2A72813A297EC6F7004138C5 /* MastodonSDKDynamic */; }; + 2A72813F297EC762004138C5 /* WidgetExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A72813E297EC762004138C5 /* WidgetExtension.swift */; }; 2A76F75C2930D94700B3388D /* HashtagTimelineHeaderViewActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A76F75B2930D94700B3388D /* HashtagTimelineHeaderViewActionButton.swift */; }; 2A82294F29262EE000D2A1F7 /* AppContext+NextAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A82294E29262EE000D2A1F7 /* AppContext+NextAccount.swift */; }; 2A90A157296EEE500026C155 /* MastodonSDKDynamic in Frameworks */ = {isa = PBXBuildFile; productRef = 2A90A156296EEE500026C155 /* MastodonSDKDynamic */; }; 2AB12E4629362F27006BC925 /* DataSourceFacade+Translate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AB12E4529362F27006BC925 /* DataSourceFacade+Translate.swift */; }; + 2AE202AA297FE10B00F66E55 /* WidgetExtension.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 2A72812C297EA9D7004138C5 /* WidgetExtension.intentdefinition */; }; + 2AE202AC297FE19100F66E55 /* FollowersCountIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AE202AB297FE19100F66E55 /* FollowersCountIntentHandler.swift */; }; + 2AE202AD297FE1CD00F66E55 /* WidgetExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A72813E297EC762004138C5 /* WidgetExtension.swift */; }; 2AE244482927831100BDBF7C /* UIImage+SFSymbols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AE244472927831100BDBF7C /* UIImage+SFSymbols.swift */; }; 2D198643261BF09500F0B013 /* SearchResultItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D198642261BF09500F0B013 /* SearchResultItem.swift */; }; 2D198649261C0B8500F0B013 /* SearchResultSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D198648261C0B8500F0B013 /* SearchResultSection.swift */; }; @@ -467,6 +480,13 @@ remoteGlobalIDString = 2A64515C29642A8A00CD8B8A; remoteInfo = FollowActionExtension; }; + 2A728132297EA9D8004138C5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = DB427DCA25BAA00100D1B89D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 2A72811F297EA9D7004138C5; + remoteInfo = WidgetExtensionExtension; + }; DB427DE925BAA00100D1B89D /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = DB427DCA25BAA00100D1B89D /* Project object */; @@ -505,6 +525,16 @@ /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ + 2A72813D297EC6F7004138C5 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; 2A90A159296EEE500026C155 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; @@ -544,6 +574,7 @@ dstSubfolderSpec = 13; files = ( DB8FABCE26AEC7B2008E5AF4 /* MastodonIntent.appex in Embed Foundation Extensions */, + 2A728134297EA9D8004138C5 /* WidgetExtension.appex in Embed Foundation Extensions */, 2A64516929642A8B00CD8B8A /* OpenInActionExtension.appex in Embed Foundation Extensions */, DBC6461C26A170AB00B0E31B /* ShareActionExtension.appex in Embed Foundation Extensions */, DBF8AE1A263293E400C9C23C /* NotificationService.appex in Embed Foundation Extensions */, @@ -584,9 +615,20 @@ 2A71F53E296DBDA80049F54A /* Action.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = Action.js; sourceTree = ""; }; 2A71F53F296DBDA80049F54A /* ActionRequestHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActionRequestHandler.swift; sourceTree = ""; }; 2A71F540296DBDA80049F54A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 2A728120297EA9D7004138C5 /* WidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 2A728121297EA9D7004138C5 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; }; + 2A728123297EA9D7004138C5 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; + 2A728126297EA9D7004138C5 /* WidgetExtensionBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetExtensionBundle.swift; sourceTree = ""; }; + 2A72812A297EA9D7004138C5 /* FollowersWidgetExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowersWidgetExtension.swift; sourceTree = ""; }; + 2A72812C297EA9D7004138C5 /* WidgetExtension.intentdefinition */ = {isa = PBXFileReference; lastKnownFileType = file.intentdefinition; path = WidgetExtension.intentdefinition; sourceTree = ""; }; + 2A72812D297EA9D8004138C5 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 2A72812F297EA9D8004138C5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 2A72813E297EC762004138C5 /* WidgetExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetExtension.swift; sourceTree = ""; }; 2A76F75B2930D94700B3388D /* HashtagTimelineHeaderViewActionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagTimelineHeaderViewActionButton.swift; sourceTree = ""; }; 2A82294E29262EE000D2A1F7 /* AppContext+NextAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppContext+NextAccount.swift"; sourceTree = ""; }; 2AB12E4529362F27006BC925 /* DataSourceFacade+Translate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Translate.swift"; sourceTree = ""; }; + 2AE202A9297FDDF500F66E55 /* WidgetExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WidgetExtension.entitlements; sourceTree = ""; }; + 2AE202AB297FE19100F66E55 /* FollowersCountIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowersCountIntentHandler.swift; sourceTree = ""; }; 2AE244472927831100BDBF7C /* UIImage+SFSymbols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+SFSymbols.swift"; sourceTree = ""; }; 2D198642261BF09500F0B013 /* SearchResultItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultItem.swift; sourceTree = ""; }; 2D198648261C0B8500F0B013 /* SearchResultSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultSection.swift; sourceTree = ""; }; @@ -1148,6 +1190,16 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 2A72811D297EA9D7004138C5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 2A72813B297EC6F7004138C5 /* MastodonSDKDynamic in Frameworks */, + 2A728124297EA9D7004138C5 /* SwiftUI.framework in Frameworks */, + 2A728122297EA9D7004138C5 /* WidgetKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; DB427DCF25BAA00100D1B89D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -1343,6 +1395,20 @@ path = OpenInActionExtension; sourceTree = ""; }; + 2A728125297EA9D7004138C5 /* WidgetExtension */ = { + isa = PBXGroup; + children = ( + 2AE202A9297FDDF500F66E55 /* WidgetExtension.entitlements */, + 2A72813E297EC762004138C5 /* WidgetExtension.swift */, + 2A728126297EA9D7004138C5 /* WidgetExtensionBundle.swift */, + 2A72812A297EA9D7004138C5 /* FollowersWidgetExtension.swift */, + 2A72812C297EA9D7004138C5 /* WidgetExtension.intentdefinition */, + 2A72812D297EA9D8004138C5 /* Assets.xcassets */, + 2A72812F297EA9D8004138C5 /* Info.plist */, + ); + path = WidgetExtension; + sourceTree = ""; + }; 2D152A8A25C295B8009AA50C /* Content */ = { isa = PBXGroup; children = ( @@ -1562,6 +1628,8 @@ DB8FAB9E26AEC3A2008E5AF4 /* Intents.framework */, DB8FABA926AEC3A2008E5AF4 /* IntentsUI.framework */, 2A6451022964223800CD8B8A /* UniformTypeIdentifiers.framework */, + 2A728121297EA9D7004138C5 /* WidgetKit.framework */, + 2A728123297EA9D7004138C5 /* SwiftUI.framework */, ); name = Frameworks; sourceTree = ""; @@ -1880,6 +1948,7 @@ DBC6461326A170AB00B0E31B /* ShareActionExtension */, DB8FABC826AEC7B2008E5AF4 /* MastodonIntent */, 2A71F53C296DBDA80049F54A /* OpenInActionExtension */, + 2A728125297EA9D7004138C5 /* WidgetExtension */, DB427DD325BAA00100D1B89D /* Products */, 1EBA4F56E920856A3FC84ACB /* Pods */, 3FE14AD363ED19AE7FF210A6 /* Frameworks */, @@ -1898,6 +1967,7 @@ DBC6461226A170AB00B0E31B /* ShareActionExtension.appex */, DB8FABC626AEC7B2008E5AF4 /* MastodonIntent.appex */, 2A64515D29642A8A00CD8B8A /* OpenInActionExtension.appex */, + 2A728120297EA9D7004138C5 /* WidgetExtension.appex */, ); name = Products; sourceTree = ""; @@ -2164,6 +2234,7 @@ isa = PBXGroup; children = ( DBB8AB4526AECDE200F6D281 /* SendPostIntentHandler.swift */, + 2AE202AB297FE19100F66E55 /* FollowersCountIntentHandler.swift */, ); path = Handler; sourceTree = ""; @@ -2860,6 +2931,27 @@ productReference = 2A64515D29642A8A00CD8B8A /* OpenInActionExtension.appex */; productType = "com.apple.product-type.app-extension"; }; + 2A72811F297EA9D7004138C5 /* WidgetExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = 2A728139297EA9D8004138C5 /* Build configuration list for PBXNativeTarget "WidgetExtension" */; + buildPhases = ( + 2A72811C297EA9D7004138C5 /* Sources */, + 2A72811D297EA9D7004138C5 /* Frameworks */, + 2A72811E297EA9D7004138C5 /* Resources */, + 2A72813D297EC6F7004138C5 /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = WidgetExtension; + packageProductDependencies = ( + 2A72813A297EC6F7004138C5 /* MastodonSDKDynamic */, + ); + productName = WidgetExtensionExtension; + productReference = 2A728120297EA9D7004138C5 /* WidgetExtension.appex */; + productType = "com.apple.product-type.app-extension"; + }; DB427DD125BAA00100D1B89D /* Mastodon */ = { isa = PBXNativeTarget; buildConfigurationList = DB427DFC25BAA00100D1B89D /* Build configuration list for PBXNativeTarget "Mastodon" */; @@ -2882,6 +2974,7 @@ DBC6461B26A170AB00B0E31B /* PBXTargetDependency */, DB8FABCD26AEC7B2008E5AF4 /* PBXTargetDependency */, 2A64516829642A8B00CD8B8A /* PBXTargetDependency */, + 2A728133297EA9D8004138C5 /* PBXTargetDependency */, ); name = Mastodon; packageProductDependencies = ( @@ -3006,6 +3099,9 @@ 2A64515C29642A8A00CD8B8A = { CreatedOnToolsVersion = 14.2; }; + 2A72811F297EA9D7004138C5 = { + CreatedOnToolsVersion = 14.2; + }; DB427DD125BAA00100D1B89D = { CreatedOnToolsVersion = 12.4; LastSwiftMigration = 1300; @@ -3076,6 +3172,7 @@ DBC6461126A170AB00B0E31B /* ShareActionExtension */, DB8FABC526AEC7B2008E5AF4 /* MastodonIntent */, 2A64515C29642A8A00CD8B8A /* OpenInActionExtension */, + 2A72811F297EA9D7004138C5 /* WidgetExtension */, ); }; /* End PBXProject section */ @@ -3090,6 +3187,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 2A72811E297EA9D7004138C5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2A72812E297EA9D8004138C5 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; DB427DD025BAA00100D1B89D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -3316,6 +3421,17 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 2A72811C297EA9D7004138C5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2A728130297EA9D8004138C5 /* WidgetExtension.intentdefinition in Sources */, + 2A72813F297EC762004138C5 /* WidgetExtension.swift in Sources */, + 2A728127297EA9D7004138C5 /* WidgetExtensionBundle.swift in Sources */, + 2A72812B297EA9D7004138C5 /* FollowersWidgetExtension.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; DB427DCE25BAA00100D1B89D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -3610,6 +3726,7 @@ DB98EB5627B0FF1B0082E365 /* ReportViewControllerAppearance.swift in Sources */, DB3EA8E6281B79E200598866 /* DiscoveryCommunityViewController.swift in Sources */, 2D206B8625F5FB0900143C56 /* Double.swift in Sources */, + 2A728131297EA9D8004138C5 /* WidgetExtension.intentdefinition in Sources */, DB9F58F126EF512300E7BBE9 /* AccountListTableViewCell.swift in Sources */, 2D76319F25C1521200929FB9 /* StatusSection.swift in Sources */, DB938F0326240EA300E5B6C1 /* CachedThreadViewModel.swift in Sources */, @@ -3745,7 +3862,10 @@ buildActionMask = 2147483647; files = ( DB0009A726AEE5DC009B9D2D /* Intents.intentdefinition in Sources */, + 2AE202AD297FE1CD00F66E55 /* WidgetExtension.swift in Sources */, DBB8AB4626AECDE200F6D281 /* SendPostIntentHandler.swift in Sources */, + 2AE202AA297FE10B00F66E55 /* WidgetExtension.intentdefinition in Sources */, + 2AE202AC297FE19100F66E55 /* FollowersCountIntentHandler.swift in Sources */, DB64BA452851F23000ADF1B7 /* MastodonAuthentication+Fetch.swift in Sources */, DB64BA482851F29300ADF1B7 /* Account+Fetch.swift in Sources */, DB8FABCA26AEC7B2008E5AF4 /* IntentHandler.swift in Sources */, @@ -3781,6 +3901,11 @@ target = 2A64515C29642A8A00CD8B8A /* OpenInActionExtension */; targetProxy = 2A64516729642A8B00CD8B8A /* PBXContainerItemProxy */; }; + 2A728133297EA9D8004138C5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 2A72811F297EA9D7004138C5 /* WidgetExtension */; + targetProxy = 2A728132297EA9D8004138C5 /* PBXContainerItemProxy */; + }; DB427DEA25BAA00100D1B89D /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = DB427DD125BAA00100D1B89D /* Mastodon */; @@ -4057,6 +4182,131 @@ }; name = "Release Snapshot"; }; + 2A728135297EA9D8004138C5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = 5Z4GVSS33P; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = WidgetExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = WidgetExtension; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.WidgetExtension; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 2A728136297EA9D8004138C5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 5Z4GVSS33P; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = WidgetExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = WidgetExtension; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.WidgetExtension; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Profile; + }; + 2A728137297EA9D8004138C5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 5Z4GVSS33P; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = WidgetExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = WidgetExtension; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.WidgetExtension; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 2A728138297EA9D8004138C5 /* Release Snapshot */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 5Z4GVSS33P; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = WidgetExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = WidgetExtension; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.WidgetExtension; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = "Release Snapshot"; + }; DB427DFA25BAA00100D1B89D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -4890,6 +5140,17 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 2A728139297EA9D8004138C5 /* Build configuration list for PBXNativeTarget "WidgetExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2A728135297EA9D8004138C5 /* Debug */, + 2A728136297EA9D8004138C5 /* Profile */, + 2A728137297EA9D8004138C5 /* Release */, + 2A728138297EA9D8004138C5 /* Release Snapshot */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; DB427DCD25BAA00100D1B89D /* Build configuration list for PBXProject "Mastodon" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -4970,6 +5231,10 @@ /* End XCConfigurationList section */ /* Begin XCSwiftPackageProductDependency section */ + 2A72813A297EC6F7004138C5 /* MastodonSDKDynamic */ = { + isa = XCSwiftPackageProductDependency; + productName = MastodonSDKDynamic; + }; 2A90A156296EEE500026C155 /* MastodonSDKDynamic */ = { isa = XCSwiftPackageProductDependency; productName = MastodonSDKDynamic; diff --git a/Mastodon/Info.plist b/Mastodon/Info.plist index df221e66d..fbe3fd94a 100644 --- a/Mastodon/Info.plist +++ b/Mastodon/Info.plist @@ -61,6 +61,7 @@ NSUserActivityTypes + FollowersCountIntent SendPostIntent UIApplicationSceneManifest diff --git a/MastodonIntent/Handler/FollowersCountIntentHandler.swift b/MastodonIntent/Handler/FollowersCountIntentHandler.swift new file mode 100644 index 000000000..7cf852de1 --- /dev/null +++ b/MastodonIntent/Handler/FollowersCountIntentHandler.swift @@ -0,0 +1,42 @@ +// Copyright © 2023 Mastodon gGmbH. All rights reserved. + +import Foundation +import Intents +import MastodonCore +import MastodonSDK +import MastodonLocalization + +class FollowersCountIntentHandler: INExtension, FollowersCountIntentHandling { + func resolveAccount(for intent: FollowersCountIntent) async -> INStringResolutionResult { + .confirmationRequired(with: intent.account) + } + + func provideAccountOptionsCollection(for intent: FollowersCountIntent, searchTerm: String?) async throws -> INObjectCollection { + guard + let searchTerm = searchTerm, + let authenticationBox = WidgetExtension.appContext + .authenticationService + .mastodonAuthenticationBoxes + .first + else { + return INObjectCollection(items: []) + } + + let results = try await WidgetExtension.appContext + .apiService + .search(query: .init(q: searchTerm), authenticationBox: authenticationBox) + + debugPrint(results.value.statuses) + + return INObjectCollection(items: results.value.accounts.map { $0.acctWithDomain(localDomain: authenticationBox.domain) as NSString }) + } +} + +private extension Mastodon.Entity.Account { + func acctWithDomain(localDomain: String) -> String { + guard acct.contains("@") else { + return "\(acct)@\(localDomain)" + } + return acct + } +} diff --git a/MastodonIntent/Info.plist b/MastodonIntent/Info.plist index 3c4a6e453..7dbb09f9e 100644 --- a/MastodonIntent/Info.plist +++ b/MastodonIntent/Info.plist @@ -30,6 +30,7 @@ IntentsSupported + FollowersCountIntent SendPostIntent diff --git a/MastodonIntent/IntentHandler.swift b/MastodonIntent/IntentHandler.swift index cb8588d67..b8f26d8ca 100644 --- a/MastodonIntent/IntentHandler.swift +++ b/MastodonIntent/IntentHandler.swift @@ -15,6 +15,8 @@ class IntentHandler: INExtension { switch intent { case is SendPostIntent: return SendPostIntentHandler() + case is FollowersCountIntent: + return FollowersCountIntentHandler() default: return self } diff --git a/WidgetExtension/Assets.xcassets/AccentColor.colorset/Contents.json b/WidgetExtension/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 000000000..eb8789700 --- /dev/null +++ b/WidgetExtension/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/WidgetExtension/Assets.xcassets/AppIcon.appiconset/Contents.json b/WidgetExtension/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..13613e3ee --- /dev/null +++ b/WidgetExtension/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/WidgetExtension/Assets.xcassets/Contents.json b/WidgetExtension/Assets.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/WidgetExtension/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/WidgetExtension/Assets.xcassets/WidgetBackground.colorset/Contents.json b/WidgetExtension/Assets.xcassets/WidgetBackground.colorset/Contents.json new file mode 100644 index 000000000..eb8789700 --- /dev/null +++ b/WidgetExtension/Assets.xcassets/WidgetBackground.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/WidgetExtension/FollowersWidgetExtension.swift b/WidgetExtension/FollowersWidgetExtension.swift new file mode 100644 index 000000000..e279831d0 --- /dev/null +++ b/WidgetExtension/FollowersWidgetExtension.swift @@ -0,0 +1,61 @@ +// Copyright © 2023 Mastodon gGmbH. All rights reserved. + +import WidgetKit +import SwiftUI +import Intents + +struct FollowersProvider: IntentTimelineProvider { + func placeholder(in context: Context) -> FollowersEntry { + FollowersEntry(date: Date(), configuration: FollowersCountIntent()) + } + + func getSnapshot(for configuration: FollowersCountIntent, in context: Context, completion: @escaping (FollowersEntry) -> ()) { + let entry = FollowersEntry(date: Date(), configuration: configuration) + completion(entry) + } + + func getTimeline(for configuration: FollowersCountIntent, in context: Context, completion: @escaping (Timeline) -> ()) { + var entries: [FollowersEntry] = [] + + // Generate a timeline consisting of five entries an hour apart, starting from the current date. + let currentDate = Date() + for hourOffset in 0 ..< 5 { + let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)! + let entry = FollowersEntry(date: entryDate, configuration: configuration) + entries.append(entry) + } + + let timeline = Timeline(entries: entries, policy: .atEnd) + completion(timeline) + } +} + +struct FollowersEntry: TimelineEntry { + let date: Date + let configuration: FollowersCountIntent +} + +struct FollowersWidgetExtensionEntryView : View { + var entry: FollowersProvider.Entry + + var body: some View { + Text(entry.date, style: .time) + } +} + +struct FollowersWidgetExtension: Widget { + var body: some WidgetConfiguration { + IntentConfiguration(kind: "Followers", intent: FollowersCountIntent.self, provider: FollowersProvider()) { entry in + FollowersWidgetExtensionEntryView(entry: entry) + } + .configurationDisplayName("Followers") + .description("Show number of followers.") + } +} + +struct WidgetExtension_Previews: PreviewProvider { + static var previews: some View { + FollowersWidgetExtensionEntryView(entry: FollowersEntry(date: Date(), configuration: FollowersCountIntent())) + .previewContext(WidgetPreviewContext(family: .systemSmall)) + } +} diff --git a/WidgetExtension/Info.plist b/WidgetExtension/Info.plist new file mode 100644 index 000000000..0f118fb75 --- /dev/null +++ b/WidgetExtension/Info.plist @@ -0,0 +1,11 @@ + + + + + NSExtension + + NSExtensionPointIdentifier + com.apple.widgetkit-extension + + + diff --git a/WidgetExtension/WidgetExtension.entitlements b/WidgetExtension/WidgetExtension.entitlements new file mode 100644 index 000000000..c3bc3f816 --- /dev/null +++ b/WidgetExtension/WidgetExtension.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.org.joinmastodon.app + + + diff --git a/WidgetExtension/WidgetExtension.intentdefinition b/WidgetExtension/WidgetExtension.intentdefinition new file mode 100644 index 000000000..4c32b0533 --- /dev/null +++ b/WidgetExtension/WidgetExtension.intentdefinition @@ -0,0 +1,153 @@ + + + + + INEnums + + INIntentDefinitionModelVersion + 1.2 + INIntentDefinitionNamespace + 88xZPY + INIntentDefinitionSystemVersion + 22C65 + INIntentDefinitionToolsBuildVersion + 14C18 + INIntentDefinitionToolsVersion + 14.2 + INIntents + + + INIntentCategory + information + INIntentDescription + Followers + INIntentDescriptionID + tVvJ9c + INIntentEligibleForWidgets + + INIntentIneligibleForSuggestions + + INIntentLastParameterTag + 5 + INIntentName + FollowersCount + INIntentParameters + + + INIntentParameterConfigurable + + INIntentParameterDisplayName + Account + INIntentParameterDisplayNameID + OL6lkx + INIntentParameterDisplayPriority + 1 + INIntentParameterMetadata + + INIntentParameterMetadataCapitalization + Sentences + INIntentParameterMetadataDefaultValueID + 2V4PKr + + INIntentParameterName + account + INIntentParameterPromptDialogs + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + Enter follower Username + INIntentParameterPromptDialogFormatStringID + sOLUtG + INIntentParameterPromptDialogType + Configuration + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogType + Primary + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + There are ${count} options matching ‘${account}’. + INIntentParameterPromptDialogFormatStringID + ceymMR + INIntentParameterPromptDialogType + DisambiguationIntroduction + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + Just to confirm, you wanted ‘${account}’? + INIntentParameterPromptDialogFormatStringID + z8tfVG + INIntentParameterPromptDialogType + Confirmation + + + INIntentParameterSupportsDynamicEnumeration + + INIntentParameterSupportsSearch + + INIntentParameterTag + 5 + INIntentParameterType + String + + + INIntentResponse + + INIntentResponseCodes + + + INIntentResponseCodeName + success + INIntentResponseCodeSuccess + + + + INIntentResponseCodeName + failure + + + INIntentResponseLastParameterTag + 4 + INIntentResponseOutput + username + INIntentResponseParameters + + + INIntentResponseParameterDisplayName + Username + INIntentResponseParameterDisplayNameID + BFppgH + INIntentResponseParameterDisplayPriority + 1 + INIntentResponseParameterName + username + INIntentResponseParameterTag + 4 + INIntentResponseParameterType + String + + + + INIntentTitle + Followers Count + INIntentTitleID + gpCwrM + INIntentType + Custom + INIntentVerb + View + + + INTypes + + + diff --git a/WidgetExtension/WidgetExtension.swift b/WidgetExtension/WidgetExtension.swift new file mode 100644 index 000000000..43dd95b6d --- /dev/null +++ b/WidgetExtension/WidgetExtension.swift @@ -0,0 +1,9 @@ +// Copyright © 2023 Mastodon gGmbH. All rights reserved. + +import MastodonCore +import MastodonSDK +import MastodonLocalization + +enum WidgetExtension { + static let appContext = AppContext() +} diff --git a/WidgetExtension/WidgetExtensionBundle.swift b/WidgetExtension/WidgetExtensionBundle.swift new file mode 100644 index 000000000..d94557d88 --- /dev/null +++ b/WidgetExtension/WidgetExtensionBundle.swift @@ -0,0 +1,11 @@ +// Copyright © 2023 Mastodon gGmbH. All rights reserved. + +import WidgetKit +import SwiftUI + +@main +struct WidgetExtensionBundle: WidgetBundle { + var body: some Widget { + FollowersWidgetExtension() + } +} From 6a0bd94bf448f735897022c3bb60819428efd408 Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Tue, 24 Jan 2023 12:47:48 +0100 Subject: [PATCH 02/24] feat(Widget): Implement basic version of FollowersWidgetExtensionEntryView --- .../FollowersWidgetExtension.swift | 111 +++++++++++++++--- 1 file changed, 94 insertions(+), 17 deletions(-) diff --git a/WidgetExtension/FollowersWidgetExtension.swift b/WidgetExtension/FollowersWidgetExtension.swift index e279831d0..5c9a546e6 100644 --- a/WidgetExtension/FollowersWidgetExtension.swift +++ b/WidgetExtension/FollowersWidgetExtension.swift @@ -3,43 +3,73 @@ import WidgetKit import SwiftUI import Intents +import MastodonSDK struct FollowersProvider: IntentTimelineProvider { func placeholder(in context: Context) -> FollowersEntry { - FollowersEntry(date: Date(), configuration: FollowersCountIntent()) + .empty(with: FollowersCountIntent()) } func getSnapshot(for configuration: FollowersCountIntent, in context: Context, completion: @escaping (FollowersEntry) -> ()) { - let entry = FollowersEntry(date: Date(), configuration: configuration) - completion(entry) + loadCurrentEntry(for: configuration, in: context, completion: completion) } func getTimeline(for configuration: FollowersCountIntent, in context: Context, completion: @escaping (Timeline) -> ()) { - var entries: [FollowersEntry] = [] - - // Generate a timeline consisting of five entries an hour apart, starting from the current date. - let currentDate = Date() - for hourOffset in 0 ..< 5 { - let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)! - let entry = FollowersEntry(date: entryDate, configuration: configuration) - entries.append(entry) + loadCurrentEntry(for: configuration, in: context) { entry in + completion(Timeline(entries: [entry], policy: .after(.now))) } - - let timeline = Timeline(entries: entries, policy: .atEnd) - completion(timeline) } } struct FollowersEntry: TimelineEntry { let date: Date + let account: Mastodon.Entity.Account? + let avatarImage: UIImage? let configuration: FollowersCountIntent + + static func empty(with configuration: FollowersCountIntent) -> Self { + FollowersEntry(date: .now, account: nil, avatarImage: nil, configuration: configuration) + } } struct FollowersWidgetExtensionEntryView : View { var entry: FollowersProvider.Entry var body: some View { - Text(entry.date, style: .time) + if let account = entry.account { + HStack { + VStack(alignment: .leading, spacing: 0) { + if let avatarImage = entry.avatarImage { + Image(uiImage: avatarImage) + .resizable() + .frame(width: 50, height: 50) + .cornerRadius(12) + .padding(.bottom, 8) + } + + Text(account.followersCount.asAbbreviatedCountString()) + .font(.largeTitle) + .lineLimit(1) + .truncationMode(.tail) + + Text("\(account.displayNameWithFallback)") + .font(.system(size: 13)) + .lineLimit(1) + .truncationMode(.tail) + + Text("@\(account.acct)") + .font(.caption2) + .foregroundColor(.secondary) + .lineLimit(1) + .truncationMode(.tail) + } + .padding(.leading, 20) + .padding([.top, .bottom], 16) + Spacer() + } + } else { + Text("Please use the Widget settings to select an Account.") + } } } @@ -55,7 +85,54 @@ struct FollowersWidgetExtension: Widget { struct WidgetExtension_Previews: PreviewProvider { static var previews: some View { - FollowersWidgetExtensionEntryView(entry: FollowersEntry(date: Date(), configuration: FollowersCountIntent())) - .previewContext(WidgetPreviewContext(family: .systemSmall)) + FollowersWidgetExtensionEntryView(entry: FollowersEntry( + date: Date(), + account: nil, + avatarImage: nil, + configuration: FollowersCountIntent()) + ) + .previewContext(WidgetPreviewContext(family: .systemSmall)) + } +} + +private extension FollowersProvider { + func loadCurrentEntry(for configuration: FollowersCountIntent, in context: Context, completion: @escaping (FollowersEntry) -> Void) { + Task { + guard + let authBox = WidgetExtension.appContext + .authenticationService + .mastodonAuthenticationBoxes + .first, + let account = configuration.account + else { + return completion(.empty(with: configuration)) + } + let resultingAccount = try await WidgetExtension.appContext + .apiService + .search(query: .init(q: account, type: .accounts), authenticationBox: authBox) + .value + .accounts + .first + + let image: UIImage? = try await { + guard + let account = resultingAccount + else { + return nil + } + + let imageData = try await URLSession.shared.data(from: account.avatarImageURLWithFallback(domain: authBox.domain)).0 + + return UIImage(data: imageData) + }() + + let entry = FollowersEntry( + date: Date(), + account: resultingAccount, + avatarImage: image, + configuration: configuration + ) + completion(entry) + } } } From 0d69e5ea4d67b44102ddcd567ae94a543dc0a19c Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Tue, 24 Jan 2023 15:23:25 +0100 Subject: [PATCH 03/24] feat(Widget): Improve FollowerCount Widget placeholder/preview --- .../missingAvatar.imageset/Contents.json | 12 +++ .../missingAvatar.imageset/missing.png | Bin 0 -> 2897 bytes .../FollowersWidgetExtension.swift | 83 +++++++++++++----- 3 files changed, 72 insertions(+), 23 deletions(-) create mode 100644 WidgetExtension/Assets.xcassets/missingAvatar.imageset/Contents.json create mode 100644 WidgetExtension/Assets.xcassets/missingAvatar.imageset/missing.png diff --git a/WidgetExtension/Assets.xcassets/missingAvatar.imageset/Contents.json b/WidgetExtension/Assets.xcassets/missingAvatar.imageset/Contents.json new file mode 100644 index 000000000..c48e46bbf --- /dev/null +++ b/WidgetExtension/Assets.xcassets/missingAvatar.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "missing.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/WidgetExtension/Assets.xcassets/missingAvatar.imageset/missing.png b/WidgetExtension/Assets.xcassets/missingAvatar.imageset/missing.png new file mode 100644 index 0000000000000000000000000000000000000000..781370782ecf61f63f882639a74c3d1fc7474772 GIT binary patch literal 2897 zcmaKu`9BkmAIBY&nK?r4`zUJ0hh&7d*&I#eoHLP(h^P^{Z$*)njA=1M$mBj&Ls}tc zhn!mzLxqnapYZMT{R2MF$K(BYzaH<`3FB zpJ@YAV^a&g!!z`usfJe{syoKYTZW%?jaRmh*3l-&O+$AnJ(-WYlZ)Ep9<-D+3_N)D z=9`;J6b}!m(H4bpxyidq|BIK02f(9GPQ0vDGR60&|Gy{=;I96Nz^VXS%HUEN7{?y9 zYCQhnG%2R&{neyrJ6FlQdU(p}KlV*$ozYFZ+yTtw!R|uw?j>OG8WDftb_Uczh(mfE zVg*8f0O|CfhJwy{ER94(t~mzA#e1SMhaXu?c=R$uqQrOIv!NXktU_gds%S(B=5=Q54P2G-H* z`~AfjJ>2^ciSXE)UCswh+wgq#+=bs=m6X3;?cD~l3H4|HnU}D7&}yx|B*Ax{V#7ur zrIuND57a|$ORam`KW?`6=O@`K1S}c_4``ux>8hv8IiZIIb`E(z9F2wZZn&kE+IbTT zYV*WrH$bjjvW!v`8kffiV^Y4!aX4Vn$p|NI&ZA@hDz9!qIg-R%Ks#{68Eu4}@}21dC1ADNS8vlvX}z{{4Hh_0XR*8ZN9AFj z7%{_-!O`nK{NBrRh1msK29@D|TboEE?9W6OhO32lO70ysAPWP|t6T0u<$P3=Mvd=m zZw78h)*)foU(6=bVQ+Hfsx4tGYAFIQx0}+Y?Adrf-y6 zD2Nx>_^y=One+O|E$@1lAPB&Wj-5#a7gO8Pev-gPOagen&6=*1@)@jL`!R!muB&Wi z*#Ixm0r>e0${&Yj@ceOskDF+wQtV?h42hHm=<7gtC_u9p65rA)my3yH<1@4XYTnuS zNMMRS1Sf_7W-Ji20KeWi-PZ{A%=k($g5W@i-$MZU3BbCI&Bc1PtTP)DvXRUc~>cf@k~?EE52S8kAWwIi31wR@{;+D7WhkT z57Pn?mhIy)Hv5-6Uy8H!jBxo&W?+85Gm!MRaR-<7-Q8-j*0Y1HVjdrI2**Fh4#E>! zltTy87SBgPF$ybP+wbMg-wE~aCgSc)Pp+MSINthvT{vh@M8QgK^p}EN64eWd#6ncR z>{x4hL<;|qlX(M}|Jl%|$0|6n^xH7(7OL#nXYPxk4Z35f??JEtsd-C6q1CkoY~p30 zZrD*SStG$KBvS;!ot8$kBR3}p%P$**B`Jo???6Us1h;=Hh(>i@L9-C+ar^lO^zEgz zv(q&p+ld0$gl|y+7hmW`V%w;%jscm@({f*?K=-ry{wh2OQ7QEglf%6|Rz*tr zs_-85?ig^naLkbC?-~xubI>1Jz9V8hf8ZeCb_ow6@H@Srk>j3dkgJnI_wf9|Qjkti z-$z48_ZOXuv!dG24WE}!vEd-%-0F;QY>mFSz`P{TmYtsiZaAozrG?cIk6ZD(EY2nQ zn%LR%HM<%nyeGA|ccP<`>H??DNj)ly$aqIdi-+u2D~*pUWQZ>}iqAxk45bUSJNXt& zJU3JIEurZj%{{blK@M7J^T`fR#-8+4B={(OlD;W%eBvLv1eEP#1 z&r1=5YgZ1XOMX1*V4^H&-^xWiQ+mG|qbtAY-F$l4)JzFGf;&oD&(gW6WyI?GHAoTB zDkU_|vam+a{Wo_=TsJ1#L_S7Dw=AtH1FN5h%o#!P>xHd8MdbidGYm{fHW-ywVJxCo zTWJhpl~hKLj|ptlr@_AR#mmY-^P&&2a;h9U(}mfo>97h8ru$klgLp({>1DP|W7Pq; zFFS>XH`ntoneG-jw&Rh}TQnP4BP!Y_npN573K zv3_~i?#PEqcLH=HZKyQURtFu}1DDz76PGUla!tTVoY`pAK8<3 zr)L#aTZ@%(1*zN^=EQb6T1m8%$sSF;GE;N^Xz5_{Pu`kd4RM|)cOKkYT3GFzf4xKC zcFhMc`^8R4KS#L+8($f&-u(W!jxyu`xF;!6wE@!(XvGhvPk0evqgknwow^$Q8>#Cs zY($y)-58C`t>UfDCvZD@b`;F@kWj!Df5CZEl!v_wGiO{g+?wWst?XsW+?I?%Jw4%E zwtl>XHmelDAt_@YVPY8VBLwL}ZTsTTtCJrNLUr4dIY+@SoDbRxn`tN%^1sc zz}6~#phQ>&w+}v$_hWJdO77PeDM>SdKXeH;&?JL1%WUtts&++=Lqy^n0OKE zi(sw->u{gpZ6Wb{6KnCNS(!DDIS2E$s&%P1Yj;s5_P83)aWU0nYY0ZNhf^(pyX*?$AJXnRyW@?y&W0G3~6ApigX literal 0 HcmV?d00001 diff --git a/WidgetExtension/FollowersWidgetExtension.swift b/WidgetExtension/FollowersWidgetExtension.swift index 5c9a546e6..7c3bb526d 100644 --- a/WidgetExtension/FollowersWidgetExtension.swift +++ b/WidgetExtension/FollowersWidgetExtension.swift @@ -7,10 +7,13 @@ import MastodonSDK struct FollowersProvider: IntentTimelineProvider { func placeholder(in context: Context) -> FollowersEntry { - .empty(with: FollowersCountIntent()) + .placeholder } func getSnapshot(for configuration: FollowersCountIntent, in context: Context, completion: @escaping (FollowersEntry) -> ()) { + guard !context.isPreview else { + return completion(.placeholder) + } loadCurrentEntry(for: configuration, in: context, completion: completion) } @@ -23,12 +26,28 @@ struct FollowersProvider: IntentTimelineProvider { struct FollowersEntry: TimelineEntry { let date: Date - let account: Mastodon.Entity.Account? - let avatarImage: UIImage? + let account: FollowersEntryAccountable? let configuration: FollowersCountIntent - static func empty(with configuration: FollowersCountIntent) -> Self { - FollowersEntry(date: .now, account: nil, avatarImage: nil, configuration: configuration) + static var placeholder: Self { + FollowersEntry( + date: .now, + account: FollowersEntryAccount( + followersCount: 99_900, + displayNameWithFallback: "Mastodon", + acct: "mastodon", + avatarImage: UIImage(named: "missingAvatar")! + ), + configuration: FollowersCountIntent() + ) + } + + static var unconfigured: Self { + FollowersEntry( + date: .now, + account: nil, + configuration: FollowersCountIntent() + ) } } @@ -39,7 +58,7 @@ struct FollowersWidgetExtensionEntryView : View { if let account = entry.account { HStack { VStack(alignment: .leading, spacing: 0) { - if let avatarImage = entry.avatarImage { + if let avatarImage = account.avatarImage { Image(uiImage: avatarImage) .resizable() .frame(width: 50, height: 50) @@ -52,7 +71,7 @@ struct FollowersWidgetExtensionEntryView : View { .lineLimit(1) .truncationMode(.tail) - Text("\(account.displayNameWithFallback)") + Text(account.displayNameWithFallback) .font(.system(size: 13)) .lineLimit(1) .truncationMode(.tail) @@ -69,6 +88,9 @@ struct FollowersWidgetExtensionEntryView : View { } } else { Text("Please use the Widget settings to select an Account.") + .multilineTextAlignment(.center) + .font(.caption) + .padding(.all, 20) } } } @@ -80,6 +102,7 @@ struct FollowersWidgetExtension: Widget { } .configurationDisplayName("Followers") .description("Show number of followers.") + .supportedFamilies([.systemSmall]) } } @@ -88,7 +111,6 @@ struct WidgetExtension_Previews: PreviewProvider { FollowersWidgetExtensionEntryView(entry: FollowersEntry( date: Date(), account: nil, - avatarImage: nil, configuration: FollowersCountIntent()) ) .previewContext(WidgetPreviewContext(family: .systemSmall)) @@ -105,34 +127,49 @@ private extension FollowersProvider { .first, let account = configuration.account else { - return completion(.empty(with: configuration)) + return completion(.unconfigured) } let resultingAccount = try await WidgetExtension.appContext .apiService .search(query: .init(q: account, type: .accounts), authenticationBox: authBox) .value .accounts - .first + .first! - let image: UIImage? = try await { - guard - let account = resultingAccount - else { - return nil - } - - let imageData = try await URLSession.shared.data(from: account.avatarImageURLWithFallback(domain: authBox.domain)).0 - - return UIImage(data: imageData) - }() + let imageData = try await URLSession.shared.data(from: resultingAccount.avatarImageURLWithFallback(domain: authBox.domain)).0 let entry = FollowersEntry( date: Date(), - account: resultingAccount, - avatarImage: image, + account: FollowersEntryAccount.from( + mastodonAccount: resultingAccount, + avatarImage: UIImage(data: imageData) ?? UIImage(named: "missingAvatar")! + ), configuration: configuration ) completion(entry) } } } + +protocol FollowersEntryAccountable { + var followersCount: Int { get } + var displayNameWithFallback: String { get } + var acct: String { get } + var avatarImage: UIImage { get } +} + +struct FollowersEntryAccount: FollowersEntryAccountable { + let followersCount: Int + let displayNameWithFallback: String + let acct: String + let avatarImage: UIImage + + static func from(mastodonAccount: Mastodon.Entity.Account, avatarImage: UIImage) -> Self { + FollowersEntryAccount( + followersCount: mastodonAccount.followersCount, + displayNameWithFallback: mastodonAccount.displayNameWithFallback, + acct: mastodonAccount.acct, + avatarImage: avatarImage + ) + } +} From 4e591bcd1cd1ecb8f0a6cce96cc9439cfb8fdbd9 Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Thu, 26 Jan 2023 15:15:59 +0100 Subject: [PATCH 04/24] feat(Widget): Default to currently active user's account in Widget --- WidgetExtension/FollowersWidgetExtension.swift | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/WidgetExtension/FollowersWidgetExtension.swift b/WidgetExtension/FollowersWidgetExtension.swift index 7c3bb526d..2f01e74ea 100644 --- a/WidgetExtension/FollowersWidgetExtension.swift +++ b/WidgetExtension/FollowersWidgetExtension.swift @@ -124,14 +124,23 @@ private extension FollowersProvider { let authBox = WidgetExtension.appContext .authenticationService .mastodonAuthenticationBoxes - .first, - let account = configuration.account + .first else { return completion(.unconfigured) } + + guard let desiredAccount: String = { + guard let account = configuration.account else { + return authBox.authenticationRecord.object(in: WidgetExtension.appContext.managedObjectContext)?.user.acct + } + return account + }() else { + return completion(.unconfigured) + } + let resultingAccount = try await WidgetExtension.appContext .apiService - .search(query: .init(q: account, type: .accounts), authenticationBox: authBox) + .search(query: .init(q: desiredAccount, type: .accounts), authenticationBox: authBox) .value .accounts .first! From aeaa3ea3ab3ca885b6076e2b60e5259944fddddb Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Thu, 26 Jan 2023 16:22:18 +0100 Subject: [PATCH 05/24] feat(Widget): Implement lockscreen widgets --- Mastodon.xcodeproj/project.pbxproj | 12 +++ .../BrandIcon.imageset/Contents.json | 56 ++++++++++ .../BrandIcon.imageset/Logo 1.png | Bin 0 -> 284 bytes .../BrandIcon.imageset/Logo.png | Bin 0 -> 286 bytes .../BrandIcon.imageset/Logo@2x 1.png | Bin 0 -> 484 bytes .../BrandIcon.imageset/Logo@2x.png | Bin 0 -> 444 bytes .../BrandIcon.imageset/Logo@3x 1.png | Bin 0 -> 694 bytes .../BrandIcon.imageset/Logo@3x.png | Bin 0 -> 636 bytes .../FollowersWidgetExtension.swift | 68 ++---------- .../WidgetViews/FollowCountWidgetView.swift | 98 ++++++++++++++++++ 10 files changed, 176 insertions(+), 58 deletions(-) create mode 100644 WidgetExtension/Assets.xcassets/BrandIcon.imageset/Contents.json create mode 100644 WidgetExtension/Assets.xcassets/BrandIcon.imageset/Logo 1.png create mode 100644 WidgetExtension/Assets.xcassets/BrandIcon.imageset/Logo.png create mode 100644 WidgetExtension/Assets.xcassets/BrandIcon.imageset/Logo@2x 1.png create mode 100644 WidgetExtension/Assets.xcassets/BrandIcon.imageset/Logo@2x.png create mode 100644 WidgetExtension/Assets.xcassets/BrandIcon.imageset/Logo@3x 1.png create mode 100644 WidgetExtension/Assets.xcassets/BrandIcon.imageset/Logo@3x.png create mode 100644 WidgetExtension/WidgetViews/FollowCountWidgetView.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 31a895c05..d1fb8b234 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -24,6 +24,7 @@ 27D701F5292FC2D60031BCBB /* DataSourceFacade+URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27D701F4292FC2D60031BCBB /* DataSourceFacade+URL.swift */; }; 2A1FE47C2938BB2600784BF1 /* FollowedTagsViewModel+DiffableDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A1FE47B2938BB2600784BF1 /* FollowedTagsViewModel+DiffableDataSource.swift */; }; 2A1FE47E2938C11200784BF1 /* Collection+IsNotEmpty.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A1FE47D2938C11200784BF1 /* Collection+IsNotEmpty.swift */; }; + 2A33AB662982C4AF008A7FB1 /* FollowCountWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A33AB652982C4AF008A7FB1 /* FollowCountWidgetView.swift */; }; 2A3F6FE3292ECB5E002E6DA7 /* FollowedTagsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A3F6FE2292ECB5E002E6DA7 /* FollowedTagsViewModel.swift */; }; 2A3F6FE5292F6E44002E6DA7 /* FollowedTagsTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A3F6FE4292F6E44002E6DA7 /* FollowedTagsTableViewCell.swift */; }; 2A506CF4292CD85800059C37 /* FollowedTagsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A506CF3292CD85800059C37 /* FollowedTagsViewController.swift */; }; @@ -605,6 +606,7 @@ 2A1FE47B2938BB2600784BF1 /* FollowedTagsViewModel+DiffableDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FollowedTagsViewModel+DiffableDataSource.swift"; sourceTree = ""; }; 2A1FE47D2938C11200784BF1 /* Collection+IsNotEmpty.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection+IsNotEmpty.swift"; sourceTree = ""; }; 2A33625329759B4200481A90 /* OpenInActionExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = OpenInActionExtension.entitlements; sourceTree = ""; }; + 2A33AB652982C4AF008A7FB1 /* FollowCountWidgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowCountWidgetView.swift; sourceTree = ""; }; 2A3F6FE2292ECB5E002E6DA7 /* FollowedTagsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowedTagsViewModel.swift; sourceTree = ""; }; 2A3F6FE4292F6E44002E6DA7 /* FollowedTagsTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowedTagsTableViewCell.swift; sourceTree = ""; }; 2A506CF3292CD85800059C37 /* FollowedTagsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowedTagsViewController.swift; sourceTree = ""; }; @@ -1372,6 +1374,14 @@ path = Pods; sourceTree = ""; }; + 2A33AB642982C4A3008A7FB1 /* WidgetViews */ = { + isa = PBXGroup; + children = ( + 2A33AB652982C4AF008A7FB1 /* FollowCountWidgetView.swift */, + ); + path = WidgetViews; + sourceTree = ""; + }; 2A506CF2292CD83B00059C37 /* FollowedTags */ = { isa = PBXGroup; children = ( @@ -1398,6 +1408,7 @@ 2A728125297EA9D7004138C5 /* WidgetExtension */ = { isa = PBXGroup; children = ( + 2A33AB642982C4A3008A7FB1 /* WidgetViews */, 2AE202A9297FDDF500F66E55 /* WidgetExtension.entitlements */, 2A72813E297EC762004138C5 /* WidgetExtension.swift */, 2A728126297EA9D7004138C5 /* WidgetExtensionBundle.swift */, @@ -3427,6 +3438,7 @@ files = ( 2A728130297EA9D8004138C5 /* WidgetExtension.intentdefinition in Sources */, 2A72813F297EC762004138C5 /* WidgetExtension.swift in Sources */, + 2A33AB662982C4AF008A7FB1 /* FollowCountWidgetView.swift in Sources */, 2A728127297EA9D7004138C5 /* WidgetExtensionBundle.swift in Sources */, 2A72812B297EA9D7004138C5 /* FollowersWidgetExtension.swift in Sources */, ); diff --git a/WidgetExtension/Assets.xcassets/BrandIcon.imageset/Contents.json b/WidgetExtension/Assets.xcassets/BrandIcon.imageset/Contents.json new file mode 100644 index 000000000..e3acfa8de --- /dev/null +++ b/WidgetExtension/Assets.xcassets/BrandIcon.imageset/Contents.json @@ -0,0 +1,56 @@ +{ + "images" : [ + { + "filename" : "Logo.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "Logo 1.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Logo@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "Logo@2x 1.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Logo@3x.png", + "idiom" : "universal", + "scale" : "3x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "Logo@3x 1.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/WidgetExtension/Assets.xcassets/BrandIcon.imageset/Logo 1.png b/WidgetExtension/Assets.xcassets/BrandIcon.imageset/Logo 1.png new file mode 100644 index 0000000000000000000000000000000000000000..0cd6d0b8b956ff9e1f425e44d4124104ce7980ba GIT binary patch literal 284 zcmeAS@N?(olHy`uVBq!ia0vp^JRmj)8<3o<+3y6TI14-?iy0WWg+Z8+Vb&Z8pde#$ zkh>GZx^prwfgF}}M_)$E)e-c@N{EeP2jv*C{Qv(n39x~u55%_Q9Wp#}@UuYhvCB9TzlQ@#Di3EUrBuwaJlM4zV({9;DJ-j7f3SoNiM<6rRtXJ*?;%NKq) zd*9$ii3|T6&x)p~OwX5(w{5YUVpn6d|DM2x$_<;E%S`XB{GZJ6(9kRCTW8d%W5KnT ZrL|9_8vdE`ni1%D22WQ%mvv4FO#p`2XQ2Q9 literal 0 HcmV?d00001 diff --git a/WidgetExtension/Assets.xcassets/BrandIcon.imageset/Logo.png b/WidgetExtension/Assets.xcassets/BrandIcon.imageset/Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..12f3c23d34370c29fe24be9f1c5c5d5946453bd8 GIT binary patch literal 286 zcmeAS@N?(olHy`uVBq!ia0vp^JRmj)8<3o<+3y6TI14-?iy0WWg+Z8+Vb&Z8pde#$ zkh>GZx^prwfgF}}M_)$E)e-c@N{LP*&jv*C{Qzz~gJmkRRV(O50f>@+ zgU~t_uLZ&{8iX_swg~KGdGX-J=5RSh9_CqSwNbT`bYAdQhBfQ(Lwq@x1Ydq=Mit=2_?Fg!@J<_}csA{EPv`TCAFb~zZQR-y evaRaF+1gKw literal 0 HcmV?d00001 diff --git a/WidgetExtension/Assets.xcassets/BrandIcon.imageset/Logo@2x 1.png b/WidgetExtension/Assets.xcassets/BrandIcon.imageset/Logo@2x 1.png new file mode 100644 index 0000000000000000000000000000000000000000..496d567732aee87f2b2b2b3ac4a9a34d0d2cfa4a GIT binary patch literal 484 zcmV06OwPn4f zC{_y!V6CnAamUom(oJiU(R)wBJQPLIik9qHGmrNF=$N(SytB(<2j7Jg=n`r8E0D6= zEj!;woVUz_GsL^N_v|qD$~?Ocl0fZk`1a=Cy!ppqkMe{k*a(ptZ>+qrj*LUQ2{yDc zAkfo#Fl$F(wlj=NuY}9oO%O1dws?^h%|kB!Td%1`WA%VP=1Q$;)$Ej zHAQDy7REV$!4C+0CXB~5REH}&300000HV*BT$_x!n&gXdmyJ``}y`OF1MJ10&daI}iSN0@5k z*(E{H#|JiU6(8>;a1;hQEMbDk06FP<$oar2>FlAS2{YDU1(A!4mW}> m9*h4x@Q6wL7#y2xPJRKKoA^XREjLC00000<3V9i0_IhdksL|B#DCU(0sTX;?wRBy1`CCL z0A^-v%-k}a>{rT;_&OGjS)claL3d{vYs-qC289HRy-14~=qmIVV1ab@rtxgf9DxI* zx5}T+P1mnIK*Jyp_5PF6jyjb(V?ExvdDc$UekcdwYgA7~J%Y-=cIEY2KRYU#OIt-ml!MSr1frujbIpd-+8k?IjNXYx&P--qJbT&ky_!DNt3pt^=lr&sBXrFsS( z7^K6u)LX#@WiTLyW>DJ2E9;cO8T17A6XNZ+?k=ZhgHo`6g-z_CJF=m@vfQ8x42QGH zhbGcDXaax7m--4ddWSM}7y=x1OHs}QXv!MY?=pxb_wpe0Z1+9;RP4{au^>C;)4@f0`5;O5|*la;wXOZ{YREXs)6Qj@5}pg6-i|%Lf@jn=w6CMXd65)rqXU`LTbU cbeo@vA7dL)HB<7XWdHyG07*qoM6N<$g4tL;*8l(j literal 0 HcmV?d00001 diff --git a/WidgetExtension/Assets.xcassets/BrandIcon.imageset/Logo@3x.png b/WidgetExtension/Assets.xcassets/BrandIcon.imageset/Logo@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..02abd6055f1512d0798b57dc63e1482894f26d6f GIT binary patch literal 636 zcmV-?0)zdDP)K~#7F%~;`i zf-n$$_Ur$$fpLPeLFoo;pc~XqP&$EbP&d#GXeY2v08Aho*qnxvrpXZmmG^xQl5iw1 z6*}yWi*M{xS+j1)A}SdOJh(nUzaLYNJ3M`dSzDYF|bn6++g~5jUrymy2{~r9*C*;H zYY*xksb>>(fIz=TGZ}h90zXIh8RFg`aN71}X9%IrEYZ=2I7EuJ6IpZe+o*Kw9nBB2 W-0yoRL)mx$0000) -> ()) { + func getTimeline(for configuration: FollowersCountIntent, in context: Context, completion: @escaping (Timeline) -> ()) { loadCurrentEntry(for: configuration, in: context) { entry in completion(Timeline(entries: [entry], policy: .after(.now))) } @@ -51,69 +51,21 @@ struct FollowersEntry: TimelineEntry { } } -struct FollowersWidgetExtensionEntryView : View { - var entry: FollowersProvider.Entry - - var body: some View { - if let account = entry.account { - HStack { - VStack(alignment: .leading, spacing: 0) { - if let avatarImage = account.avatarImage { - Image(uiImage: avatarImage) - .resizable() - .frame(width: 50, height: 50) - .cornerRadius(12) - .padding(.bottom, 8) - } - - Text(account.followersCount.asAbbreviatedCountString()) - .font(.largeTitle) - .lineLimit(1) - .truncationMode(.tail) - - Text(account.displayNameWithFallback) - .font(.system(size: 13)) - .lineLimit(1) - .truncationMode(.tail) - - Text("@\(account.acct)") - .font(.caption2) - .foregroundColor(.secondary) - .lineLimit(1) - .truncationMode(.tail) - } - .padding(.leading, 20) - .padding([.top, .bottom], 16) - Spacer() - } - } else { - Text("Please use the Widget settings to select an Account.") - .multilineTextAlignment(.center) - .font(.caption) - .padding(.all, 20) - } - } -} - struct FollowersWidgetExtension: Widget { + private var availableFamilies: [WidgetFamily] { + if #available(iOS 16, *) { + return [.systemSmall, .accessoryRectangular, .accessoryCircular] + } + return [.systemSmall] + } + var body: some WidgetConfiguration { IntentConfiguration(kind: "Followers", intent: FollowersCountIntent.self, provider: FollowersProvider()) { entry in - FollowersWidgetExtensionEntryView(entry: entry) + FollowCountWidgetView(entry: entry) } .configurationDisplayName("Followers") .description("Show number of followers.") - .supportedFamilies([.systemSmall]) - } -} - -struct WidgetExtension_Previews: PreviewProvider { - static var previews: some View { - FollowersWidgetExtensionEntryView(entry: FollowersEntry( - date: Date(), - account: nil, - configuration: FollowersCountIntent()) - ) - .previewContext(WidgetPreviewContext(family: .systemSmall)) + .supportedFamilies(availableFamilies) } } diff --git a/WidgetExtension/WidgetViews/FollowCountWidgetView.swift b/WidgetExtension/WidgetViews/FollowCountWidgetView.swift new file mode 100644 index 000000000..c040f8d75 --- /dev/null +++ b/WidgetExtension/WidgetViews/FollowCountWidgetView.swift @@ -0,0 +1,98 @@ +// Copyright © 2023 Mastodon gGmbH. All rights reserved. + +import SwiftUI +import WidgetKit + +struct FollowCountWidgetView: View { + @Environment(\.widgetFamily) var family + + var entry: FollowersProvider.Entry + + var body: some View { + if let account = entry.account { + switch family { + case .systemSmall: + viewForSmallWidget(account) + case .accessoryRectangular: + viewForAccessoryRectangular(account) + case .accessoryCircular: + viewForAccessoryCircular(account) + default: + Text("Sorry but this Widget family is unsupported.") + } + } else { + Text("Please open Mastodon to log in to an Account.") + .multilineTextAlignment(.center) + .font(.caption) + .padding(.all, 20) + } + } + + private func viewForSmallWidget(_ account: FollowersEntryAccountable) -> some View { + HStack { + VStack(alignment: .leading, spacing: 0) { + if let avatarImage = account.avatarImage { + Image(uiImage: avatarImage) + .resizable() + .frame(width: 50, height: 50) + .cornerRadius(12) + .padding(.bottom, 8) + } + + Text(account.followersCount.asAbbreviatedCountString()) + .font(.largeTitle) + .lineLimit(1) + .truncationMode(.tail) + + Text(account.displayNameWithFallback) + .font(.system(size: 13)) + .lineLimit(1) + .truncationMode(.tail) + + Text("@\(account.acct)") + .font(.caption2) + .foregroundColor(.secondary) + .lineLimit(1) + .truncationMode(.tail) + } + .padding(.leading, 20) + .padding([.top, .bottom], 16) + Spacer() + } + } + + private func viewForAccessoryRectangular(_ account :FollowersEntryAccountable) -> some View { + HStack(spacing: 0) { + VStack(alignment: .leading, spacing: 0) { + HStack(alignment: .center) { + Image("BrandIcon") + Text("FOLLOWERS") + .font(.system(size: 15, weight: .semibold)) + } + .padding(.top, 6) + + Text(account.followersCount.asAbbreviatedCountString()) + .font(.system(size: 43)) + .lineLimit(1) + .truncationMode(.tail) + } + Spacer() + } + } + + private func viewForAccessoryCircular(_ account :FollowersEntryAccountable) -> some View { + ZStack { + if #available(iOS 16, *) { + AccessoryWidgetBackground() + } + VStack { + Image("BrandIcon") + + Text(account.followersCount.asAbbreviatedCountString()) + .font(.system(size: 15)) + .lineLimit(1) + .truncationMode(.tail) + } + } + } +} From 1558579a861bbfcad191532c655b0db9e522677c Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Mon, 30 Jan 2023 11:21:04 +0100 Subject: [PATCH 06/24] feat(Widget): Implement LightChartView for Small FollowerCount --- Mastodon.xcodeproj/project.pbxproj | 20 ++++++ .../xcshareddata/swiftpm/Package.resolved | 9 +++ .../Handler/FollowersCountIntentHandler.swift | 4 ++ .../WidgetExtension.intentdefinition | 72 +++++++++++++------ .../WidgetViews/FollowCountWidgetView.swift | 69 +++++++++++++++++- 5 files changed, 149 insertions(+), 25 deletions(-) diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index d1fb8b234..a6699f7ed 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -25,6 +25,7 @@ 2A1FE47C2938BB2600784BF1 /* FollowedTagsViewModel+DiffableDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A1FE47B2938BB2600784BF1 /* FollowedTagsViewModel+DiffableDataSource.swift */; }; 2A1FE47E2938C11200784BF1 /* Collection+IsNotEmpty.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A1FE47D2938C11200784BF1 /* Collection+IsNotEmpty.swift */; }; 2A33AB662982C4AF008A7FB1 /* FollowCountWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A33AB652982C4AF008A7FB1 /* FollowCountWidgetView.swift */; }; + 2A33AB6D2987C2B3008A7FB1 /* LightChart in Frameworks */ = {isa = PBXBuildFile; productRef = 2A33AB6C2987C2B3008A7FB1 /* LightChart */; }; 2A3F6FE3292ECB5E002E6DA7 /* FollowedTagsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A3F6FE2292ECB5E002E6DA7 /* FollowedTagsViewModel.swift */; }; 2A3F6FE5292F6E44002E6DA7 /* FollowedTagsTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A3F6FE4292F6E44002E6DA7 /* FollowedTagsTableViewCell.swift */; }; 2A506CF4292CD85800059C37 /* FollowedTagsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A506CF3292CD85800059C37 /* FollowedTagsViewController.swift */; }; @@ -1196,6 +1197,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 2A33AB6D2987C2B3008A7FB1 /* LightChart in Frameworks */, 2A72813B297EC6F7004138C5 /* MastodonSDKDynamic in Frameworks */, 2A728124297EA9D7004138C5 /* SwiftUI.framework in Frameworks */, 2A728122297EA9D7004138C5 /* WidgetKit.framework in Frameworks */, @@ -2958,6 +2960,7 @@ name = WidgetExtension; packageProductDependencies = ( 2A72813A297EC6F7004138C5 /* MastodonSDKDynamic */, + 2A33AB6C2987C2B3008A7FB1 /* LightChart */, ); productName = WidgetExtensionExtension; productReference = 2A728120297EA9D7004138C5 /* WidgetExtension.appex */; @@ -3171,6 +3174,7 @@ ); mainGroup = DB427DC925BAA00100D1B89D; packageReferences = ( + 2A33AB6B2987C2B3008A7FB1 /* XCRemoteSwiftPackageReference "LightChart" */, ); productRefGroup = DB427DD325BAA00100D1B89D /* Products */; projectDirPath = ""; @@ -5242,7 +5246,23 @@ }; /* End XCConfigurationList section */ +/* Begin XCRemoteSwiftPackageReference section */ + 2A33AB6B2987C2B3008A7FB1 /* XCRemoteSwiftPackageReference "LightChart" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/pichukov/LightChart.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.0.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + /* Begin XCSwiftPackageProductDependency section */ + 2A33AB6C2987C2B3008A7FB1 /* LightChart */ = { + isa = XCSwiftPackageProductDependency; + package = 2A33AB6B2987C2B3008A7FB1 /* XCRemoteSwiftPackageReference "LightChart" */; + productName = LightChart; + }; 2A72813A297EC6F7004138C5 /* MastodonSDKDynamic */ = { isa = XCSwiftPackageProductDependency; productName = MastodonSDKDynamic; diff --git a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved index cd7395c7e..37232c2ef 100644 --- a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -73,6 +73,15 @@ "version": "4.2.2" } }, + { + "package": "LightChart", + "repositoryURL": "https://github.com/pichukov/LightChart.git", + "state": { + "branch": null, + "revision": "206fe7ab50620891c89531e2598e36e965678a1a", + "version": "1.0.3" + } + }, { "package": "MetaTextKit", "repositoryURL": "https://github.com/TwidereProject/MetaTextKit.git", diff --git a/MastodonIntent/Handler/FollowersCountIntentHandler.swift b/MastodonIntent/Handler/FollowersCountIntentHandler.swift index 7cf852de1..31c306127 100644 --- a/MastodonIntent/Handler/FollowersCountIntentHandler.swift +++ b/MastodonIntent/Handler/FollowersCountIntentHandler.swift @@ -7,6 +7,10 @@ import MastodonSDK import MastodonLocalization class FollowersCountIntentHandler: INExtension, FollowersCountIntentHandling { + func resolveShowChart(for intent: FollowersCountIntent) async -> INBooleanResolutionResult { + return .success(with: intent.showChart?.boolValue ?? false) + } + func resolveAccount(for intent: FollowersCountIntent) async -> INStringResolutionResult { .confirmationRequired(with: intent.account) } diff --git a/WidgetExtension/WidgetExtension.intentdefinition b/WidgetExtension/WidgetExtension.intentdefinition index 4c32b0533..bf1619332 100644 --- a/WidgetExtension/WidgetExtension.intentdefinition +++ b/WidgetExtension/WidgetExtension.intentdefinition @@ -9,7 +9,7 @@ INIntentDefinitionNamespace 88xZPY INIntentDefinitionSystemVersion - 22C65 + 22D49 INIntentDefinitionToolsBuildVersion 14C18 INIntentDefinitionToolsVersion @@ -28,7 +28,7 @@ INIntentIneligibleForSuggestions INIntentLastParameterTag - 5 + 7 INIntentName FollowersCount INIntentParameters @@ -69,26 +69,6 @@ INIntentParameterPromptDialogType Primary - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogFormatString - There are ${count} options matching ‘${account}’. - INIntentParameterPromptDialogFormatStringID - ceymMR - INIntentParameterPromptDialogType - DisambiguationIntroduction - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogFormatString - Just to confirm, you wanted ‘${account}’? - INIntentParameterPromptDialogFormatStringID - z8tfVG - INIntentParameterPromptDialogType - Confirmation - INIntentParameterSupportsDynamicEnumeration @@ -99,6 +79,54 @@ INIntentParameterType String + + INIntentParameterConfigurable + + INIntentParameterDisplayName + Show chart + INIntentParameterDisplayNameID + xVtyec + INIntentParameterDisplayPriority + 2 + INIntentParameterMetadata + + INIntentParameterMetadataFalseDisplayName + No + INIntentParameterMetadataFalseDisplayNameID + jg9D5P + INIntentParameterMetadataTrueDisplayName + Yes + INIntentParameterMetadataTrueDisplayNameID + 82L4Nj + + INIntentParameterName + showChart + INIntentParameterPromptDialogs + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogType + Configuration + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + Should the Widget show a chart? + INIntentParameterPromptDialogFormatStringID + zeJo4f + INIntentParameterPromptDialogType + Primary + + + INIntentParameterSupportsResolution + + INIntentParameterTag + 7 + INIntentParameterType + Boolean + INIntentResponse diff --git a/WidgetExtension/WidgetViews/FollowCountWidgetView.swift b/WidgetExtension/WidgetViews/FollowCountWidgetView.swift index c040f8d75..9b075f67a 100644 --- a/WidgetExtension/WidgetViews/FollowCountWidgetView.swift +++ b/WidgetExtension/WidgetViews/FollowCountWidgetView.swift @@ -2,6 +2,8 @@ import SwiftUI import WidgetKit +import LightChart +import MastodonAsset struct FollowCountWidgetView: View { @Environment(\.widgetFamily) var family @@ -12,7 +14,11 @@ struct FollowCountWidgetView: View { if let account = entry.account { switch family { case .systemSmall: - viewForSmallWidget(account) + if let showChart = entry.configuration.showChart?.boolValue, showChart { + viewForSmallWidgetYesChart(account) + } else { + viewForSmallWidgetNoChart(account) + } case .accessoryRectangular: viewForAccessoryRectangular(account) case .accessoryCircular: @@ -28,7 +34,7 @@ struct FollowCountWidgetView: View { } } - private func viewForSmallWidget(_ account: FollowersEntryAccountable) -> some View { + private func viewForSmallWidgetNoChart(_ account: FollowersEntryAccountable) -> some View { HStack { VStack(alignment: .leading, spacing: 0) { if let avatarImage = account.avatarImage { @@ -56,11 +62,68 @@ struct FollowCountWidgetView: View { .truncationMode(.tail) } .padding(.leading, 20) - .padding([.top, .bottom], 16) + .padding(.vertical, 16) Spacer() } } + private func viewForSmallWidgetYesChart(_ account: FollowersEntryAccountable) -> some View { + VStack(alignment: .leading, spacing: 0) { + HStack { + if let avatarImage = account.avatarImage { + Image(uiImage: avatarImage) + .resizable() + .frame(width: 23, height: 23) + .cornerRadius(5) + } + VStack(alignment: .leading) { + Text(account.displayNameWithFallback) + .font(.caption) + .lineLimit(1) + .truncationMode(.tail) + + Text("@\(account.acct)") + .font(.caption2) + .foregroundColor(.secondary) + .lineLimit(1) + .truncationMode(.tail) + } + Spacer() + } + .padding(.leading, 20) + + ZStack { + LightChartView( + data: [200, 205, 208, 213, 210, 211, 212], + type: .line, + visualType: .filled(color: Asset.Colors.Brand.blurple.swiftUIColor, lineWidth: 2), + offset: 0.8 /// this is the positive offset from the bottom edge of the graph (~80% above bottom level) + ) + + HStack { + VStack(alignment: .leading, spacing: 0) { + Spacer() + Text("+4 followers today") + .font(.system(size: 12)) + .foregroundColor(.secondary) + .lineLimit(1) + .truncationMode(.tail) + + Text(account.followersCount.asAbbreviatedCountString()) + .font(.largeTitle) + .lineLimit(1) + .truncationMode(.tail) + + } + Spacer() + } + .padding(.bottom, 16) + .padding(.leading, 20) + } + } + .padding(.top, 16) + } + private func viewForAccessoryRectangular(_ account :FollowersEntryAccountable) -> some View { HStack(spacing: 0) { VStack(alignment: .leading, spacing: 0) { From e2fe1263a4be9752d0c55c40733252fb71e18ea6 Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Mon, 30 Jan 2023 15:28:16 +0100 Subject: [PATCH 07/24] feat(Widget): Implement Followers Widget chart --- Mastodon.xcodeproj/project.pbxproj | 64 ++++--- .../xcshareddata/swiftpm/Package.resolved | 9 - WidgetExtension/FollowersCountHistory.swift | 112 ++++++++++++ .../FollowersWidgetExtension.swift | 19 +- WidgetExtension/LightChart/ChartType.swift | 25 +++ .../LightChart/Charts/CurvedChart.swift | 170 ++++++++++++++++++ .../LightChart/Charts/LineChart.swift | 138 ++++++++++++++ .../LightChart/DataRepresentable.swift | 42 +++++ WidgetExtension/LightChart/LightChart.swift | 52 ++++++ WidgetExtension/LightChart/Math.swift | 34 ++++ .../WidgetViews/FollowCountWidgetView.swift | 29 +-- 11 files changed, 650 insertions(+), 44 deletions(-) create mode 100644 WidgetExtension/FollowersCountHistory.swift create mode 100644 WidgetExtension/LightChart/ChartType.swift create mode 100644 WidgetExtension/LightChart/Charts/CurvedChart.swift create mode 100644 WidgetExtension/LightChart/Charts/LineChart.swift create mode 100644 WidgetExtension/LightChart/DataRepresentable.swift create mode 100644 WidgetExtension/LightChart/LightChart.swift create mode 100644 WidgetExtension/LightChart/Math.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index a6699f7ed..d1e0fc798 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -24,8 +24,14 @@ 27D701F5292FC2D60031BCBB /* DataSourceFacade+URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27D701F4292FC2D60031BCBB /* DataSourceFacade+URL.swift */; }; 2A1FE47C2938BB2600784BF1 /* FollowedTagsViewModel+DiffableDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A1FE47B2938BB2600784BF1 /* FollowedTagsViewModel+DiffableDataSource.swift */; }; 2A1FE47E2938C11200784BF1 /* Collection+IsNotEmpty.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A1FE47D2938C11200784BF1 /* Collection+IsNotEmpty.swift */; }; + 2A33062D2987DBFA001D4C51 /* FollowersCountHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A33062C2987DBFA001D4C51 /* FollowersCountHistory.swift */; }; + 2A33063629880835001D4C51 /* Math.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A33062F29880834001D4C51 /* Math.swift */; }; + 2A33063729880835001D4C51 /* DataRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A33063029880834001D4C51 /* DataRepresentable.swift */; }; + 2A33063829880835001D4C51 /* LineChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A33063229880834001D4C51 /* LineChart.swift */; }; + 2A33063929880835001D4C51 /* CurvedChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A33063329880834001D4C51 /* CurvedChart.swift */; }; + 2A33063A29880835001D4C51 /* LightChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A33063429880834001D4C51 /* LightChart.swift */; }; + 2A33063B29880835001D4C51 /* ChartType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A33063529880834001D4C51 /* ChartType.swift */; }; 2A33AB662982C4AF008A7FB1 /* FollowCountWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A33AB652982C4AF008A7FB1 /* FollowCountWidgetView.swift */; }; - 2A33AB6D2987C2B3008A7FB1 /* LightChart in Frameworks */ = {isa = PBXBuildFile; productRef = 2A33AB6C2987C2B3008A7FB1 /* LightChart */; }; 2A3F6FE3292ECB5E002E6DA7 /* FollowedTagsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A3F6FE2292ECB5E002E6DA7 /* FollowedTagsViewModel.swift */; }; 2A3F6FE5292F6E44002E6DA7 /* FollowedTagsTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A3F6FE4292F6E44002E6DA7 /* FollowedTagsTableViewCell.swift */; }; 2A506CF4292CD85800059C37 /* FollowedTagsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A506CF3292CD85800059C37 /* FollowedTagsViewController.swift */; }; @@ -606,6 +612,13 @@ 27D701F4292FC2D60031BCBB /* DataSourceFacade+URL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+URL.swift"; sourceTree = ""; }; 2A1FE47B2938BB2600784BF1 /* FollowedTagsViewModel+DiffableDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FollowedTagsViewModel+DiffableDataSource.swift"; sourceTree = ""; }; 2A1FE47D2938C11200784BF1 /* Collection+IsNotEmpty.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection+IsNotEmpty.swift"; sourceTree = ""; }; + 2A33062C2987DBFA001D4C51 /* FollowersCountHistory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowersCountHistory.swift; sourceTree = ""; }; + 2A33062F29880834001D4C51 /* Math.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Math.swift; sourceTree = ""; }; + 2A33063029880834001D4C51 /* DataRepresentable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataRepresentable.swift; sourceTree = ""; }; + 2A33063229880834001D4C51 /* LineChart.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LineChart.swift; sourceTree = ""; }; + 2A33063329880834001D4C51 /* CurvedChart.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CurvedChart.swift; sourceTree = ""; }; + 2A33063429880834001D4C51 /* LightChart.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LightChart.swift; sourceTree = ""; }; + 2A33063529880834001D4C51 /* ChartType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChartType.swift; sourceTree = ""; }; 2A33625329759B4200481A90 /* OpenInActionExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = OpenInActionExtension.entitlements; sourceTree = ""; }; 2A33AB652982C4AF008A7FB1 /* FollowCountWidgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowCountWidgetView.swift; sourceTree = ""; }; 2A3F6FE2292ECB5E002E6DA7 /* FollowedTagsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowedTagsViewModel.swift; sourceTree = ""; }; @@ -1197,7 +1210,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 2A33AB6D2987C2B3008A7FB1 /* LightChart in Frameworks */, 2A72813B297EC6F7004138C5 /* MastodonSDKDynamic in Frameworks */, 2A728124297EA9D7004138C5 /* SwiftUI.framework in Frameworks */, 2A728122297EA9D7004138C5 /* WidgetKit.framework in Frameworks */, @@ -1376,6 +1388,27 @@ path = Pods; sourceTree = ""; }; + 2A33062E29880834001D4C51 /* LightChart */ = { + isa = PBXGroup; + children = ( + 2A33063129880834001D4C51 /* Charts */, + 2A33062F29880834001D4C51 /* Math.swift */, + 2A33063029880834001D4C51 /* DataRepresentable.swift */, + 2A33063429880834001D4C51 /* LightChart.swift */, + 2A33063529880834001D4C51 /* ChartType.swift */, + ); + path = LightChart; + sourceTree = ""; + }; + 2A33063129880834001D4C51 /* Charts */ = { + isa = PBXGroup; + children = ( + 2A33063229880834001D4C51 /* LineChart.swift */, + 2A33063329880834001D4C51 /* CurvedChart.swift */, + ); + path = Charts; + sourceTree = ""; + }; 2A33AB642982C4A3008A7FB1 /* WidgetViews */ = { isa = PBXGroup; children = ( @@ -1410,11 +1443,13 @@ 2A728125297EA9D7004138C5 /* WidgetExtension */ = { isa = PBXGroup; children = ( + 2A33062E29880834001D4C51 /* LightChart */, 2A33AB642982C4A3008A7FB1 /* WidgetViews */, 2AE202A9297FDDF500F66E55 /* WidgetExtension.entitlements */, 2A72813E297EC762004138C5 /* WidgetExtension.swift */, 2A728126297EA9D7004138C5 /* WidgetExtensionBundle.swift */, 2A72812A297EA9D7004138C5 /* FollowersWidgetExtension.swift */, + 2A33062C2987DBFA001D4C51 /* FollowersCountHistory.swift */, 2A72812C297EA9D7004138C5 /* WidgetExtension.intentdefinition */, 2A72812D297EA9D8004138C5 /* Assets.xcassets */, 2A72812F297EA9D8004138C5 /* Info.plist */, @@ -2960,7 +2995,6 @@ name = WidgetExtension; packageProductDependencies = ( 2A72813A297EC6F7004138C5 /* MastodonSDKDynamic */, - 2A33AB6C2987C2B3008A7FB1 /* LightChart */, ); productName = WidgetExtensionExtension; productReference = 2A728120297EA9D7004138C5 /* WidgetExtension.appex */; @@ -3174,7 +3208,6 @@ ); mainGroup = DB427DC925BAA00100D1B89D; packageReferences = ( - 2A33AB6B2987C2B3008A7FB1 /* XCRemoteSwiftPackageReference "LightChart" */, ); productRefGroup = DB427DD325BAA00100D1B89D /* Products */; projectDirPath = ""; @@ -3440,11 +3473,18 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 2A33063729880835001D4C51 /* DataRepresentable.swift in Sources */, + 2A33062D2987DBFA001D4C51 /* FollowersCountHistory.swift in Sources */, + 2A33063829880835001D4C51 /* LineChart.swift in Sources */, 2A728130297EA9D8004138C5 /* WidgetExtension.intentdefinition in Sources */, 2A72813F297EC762004138C5 /* WidgetExtension.swift in Sources */, + 2A33063A29880835001D4C51 /* LightChart.swift in Sources */, + 2A33063B29880835001D4C51 /* ChartType.swift in Sources */, + 2A33063629880835001D4C51 /* Math.swift in Sources */, 2A33AB662982C4AF008A7FB1 /* FollowCountWidgetView.swift in Sources */, 2A728127297EA9D7004138C5 /* WidgetExtensionBundle.swift in Sources */, 2A72812B297EA9D7004138C5 /* FollowersWidgetExtension.swift in Sources */, + 2A33063929880835001D4C51 /* CurvedChart.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -5246,23 +5286,7 @@ }; /* End XCConfigurationList section */ -/* Begin XCRemoteSwiftPackageReference section */ - 2A33AB6B2987C2B3008A7FB1 /* XCRemoteSwiftPackageReference "LightChart" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/pichukov/LightChart.git"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 1.0.0; - }; - }; -/* End XCRemoteSwiftPackageReference section */ - /* Begin XCSwiftPackageProductDependency section */ - 2A33AB6C2987C2B3008A7FB1 /* LightChart */ = { - isa = XCSwiftPackageProductDependency; - package = 2A33AB6B2987C2B3008A7FB1 /* XCRemoteSwiftPackageReference "LightChart" */; - productName = LightChart; - }; 2A72813A297EC6F7004138C5 /* MastodonSDKDynamic */ = { isa = XCSwiftPackageProductDependency; productName = MastodonSDKDynamic; diff --git a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved index 37232c2ef..cd7395c7e 100644 --- a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -73,15 +73,6 @@ "version": "4.2.2" } }, - { - "package": "LightChart", - "repositoryURL": "https://github.com/pichukov/LightChart.git", - "state": { - "branch": null, - "revision": "206fe7ab50620891c89531e2598e36e965678a1a", - "version": "1.0.3" - } - }, { "package": "MetaTextKit", "repositoryURL": "https://github.com/TwidereProject/MetaTextKit.git", diff --git a/WidgetExtension/FollowersCountHistory.swift b/WidgetExtension/FollowersCountHistory.swift new file mode 100644 index 000000000..d6218660a --- /dev/null +++ b/WidgetExtension/FollowersCountHistory.swift @@ -0,0 +1,112 @@ +// Copyright © 2023 Mastodon gGmbH. All rights reserved. + +import Foundation +import MastodonSDK + +struct FollowersCountHistoryDay: Codable { + let dstring: String + let day: Int + let count: Int + + func copy(count: Int) -> Self { + FollowersCountHistoryDay(dstring: dstring, day: day, count: count) + } +} + +class FollowersCountHistory { + + static let shared = FollowersCountHistory() + + private let userDefaults = UserDefaults.standard + private let calendar = Calendar.current + private let followersCountCacheDateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateFormat = "yyyyMMdd" + return formatter + }() + + private func elapsedFollowersCountDateStrings() -> [String] { + (-7...0).map { elapsedDay in + let date = calendar.date(byAdding: .day, value: elapsedDay, to: .now)! + return followersCountCacheDateFormatter.string(from: date) + } + } + + private func userDefaultsKey(for account: FollowersEntryAccountable) -> String { + if account.acct.contains("@") { + return account.acct + } + return "\(account.acct)@\(account.domain)" + } + + private func emptyHistoricDataForToday(for account: FollowersEntryAccountable) -> [FollowersCountHistoryDay] { + elapsedFollowersCountDateStrings().enumerated().map { FollowersCountHistoryDay(dstring: $0.element, day: $0.offset, count: account.followersCount) } + } + + private func followersHistorySorted(for account: FollowersEntryAccountable) -> [FollowersCountHistoryDay] { + guard + let jsonData = userDefaults.string(forKey: userDefaultsKey(for: account))?.data(using: .utf8), + let jsonObject = try? JSONDecoder().decode([FollowersCountHistoryDay].self, from: jsonData) + else { + return emptyHistoricDataForToday(for: account) + } + return jsonObject + } + + func updateFollowersTodayCount(account: FollowersEntryAccountable, count: Int) { + let relevantDays = elapsedFollowersCountDateStrings() + let existingHistory = followersHistorySorted(for: account) + var newHistory = existingHistory + + /// first we're going to update the existing day and remove legacy days (older than 7) + existingHistory.forEach { existingDay in + if !relevantDays.contains(where: { $0 == existingDay.dstring }) { + /// remove legacy data/ + newHistory.removeAll(where: { $0.dstring == existingDay.dstring }) + } + } + + relevantDays.enumerated().forEach { index, day in + if !newHistory.contains(where: { $0.dstring == day }) { + newHistory.insert( + FollowersCountHistoryDay(dstring: day, day: index, count: account.followersCount), + at: index + ) + } + } + + /// then we're going to update the history dataset with new value, if this is the first encounter + if let last = newHistory.popLast()?.copy(count: count) { + newHistory.append(last) + } + + if let jsonData = try? JSONEncoder().encode(newHistory), let jsonString = String(data: jsonData, encoding: .utf8) { + userDefaults.set(jsonString, forKey: userDefaultsKey(for: account)) + } + } + + func chartValues(for account: FollowersEntryAccountable) -> [Double] { + followersHistorySorted(for: account).map { Double($0.count) } + } + + func increaseCountString(for account: FollowersEntryAccountable) -> String? { + let history = followersHistorySorted(for: account) + let relevantDays = elapsedFollowersCountDateStrings() + let today = relevantDays.last! + let yesterday = relevantDays[relevantDays.count - 2] + + let followersToday = history.first(where: { $0.dstring == today })?.count ?? account.followersCount + let followersYesterday = history.first(where: { $0.dstring == yesterday })?.count ?? account.followersCount + + let followersChange = followersToday - followersYesterday + + switch followersChange { + case ..<0: + return "\(followersChange)" + case 0: + return nil + default: + return "+\(followersChange)" + } + } +} diff --git a/WidgetExtension/FollowersWidgetExtension.swift b/WidgetExtension/FollowersWidgetExtension.swift index e354a4ce9..4b00106a9 100644 --- a/WidgetExtension/FollowersWidgetExtension.swift +++ b/WidgetExtension/FollowersWidgetExtension.swift @@ -6,6 +6,8 @@ import Intents import MastodonSDK struct FollowersProvider: IntentTimelineProvider { + private let followersHistory = FollowersCountHistory.shared + func placeholder(in context: Context) -> FollowersEntry { .placeholder } @@ -36,7 +38,8 @@ struct FollowersEntry: TimelineEntry { followersCount: 99_900, displayNameWithFallback: "Mastodon", acct: "mastodon", - avatarImage: UIImage(named: "missingAvatar")! + avatarImage: UIImage(named: "missingAvatar")!, + domain: "mastodon" ), configuration: FollowersCountIntent() ) @@ -103,10 +106,17 @@ private extension FollowersProvider { date: Date(), account: FollowersEntryAccount.from( mastodonAccount: resultingAccount, + domain: authBox.domain, avatarImage: UIImage(data: imageData) ?? UIImage(named: "missingAvatar")! ), configuration: configuration ) + + followersHistory.updateFollowersTodayCount( + account: entry.account!, + count: resultingAccount.followersCount + ) + completion(entry) } } @@ -117,6 +127,7 @@ protocol FollowersEntryAccountable { var displayNameWithFallback: String { get } var acct: String { get } var avatarImage: UIImage { get } + var domain: String { get } } struct FollowersEntryAccount: FollowersEntryAccountable { @@ -124,13 +135,15 @@ struct FollowersEntryAccount: FollowersEntryAccountable { let displayNameWithFallback: String let acct: String let avatarImage: UIImage + let domain: String - static func from(mastodonAccount: Mastodon.Entity.Account, avatarImage: UIImage) -> Self { + static func from(mastodonAccount: Mastodon.Entity.Account, domain: String, avatarImage: UIImage) -> Self { FollowersEntryAccount( followersCount: mastodonAccount.followersCount, displayNameWithFallback: mastodonAccount.displayNameWithFallback, acct: mastodonAccount.acct, - avatarImage: avatarImage + avatarImage: avatarImage, + domain: domain ) } } diff --git a/WidgetExtension/LightChart/ChartType.swift b/WidgetExtension/LightChart/ChartType.swift new file mode 100644 index 000000000..1a83b0b4c --- /dev/null +++ b/WidgetExtension/LightChart/ChartType.swift @@ -0,0 +1,25 @@ +// +// ChartType.swift +// +// +// Created by Alexey Pichukov on 19.08.2020. +// + +import SwiftUI + +public enum ChartType { + case line + case curved +} + +public enum ChartVisualType { + case outline(color: Color, lineWidth: CGFloat) + case filled(color: Color, lineWidth: CGFloat) + case customFilled(color: Color, lineWidth: CGFloat, fillGradient: LinearGradient) +} + +public enum CurrentValueLineType { + case none + case line(color: Color, lineWidth: CGFloat) + case dash(color: Color, lineWidth: CGFloat, dash: [CGFloat]) +} diff --git a/WidgetExtension/LightChart/Charts/CurvedChart.swift b/WidgetExtension/LightChart/Charts/CurvedChart.swift new file mode 100644 index 000000000..c6df1d934 --- /dev/null +++ b/WidgetExtension/LightChart/Charts/CurvedChart.swift @@ -0,0 +1,170 @@ +// +// File.swift +// +// +// Created by Alexey Pichukov on 20.08.2020. +// + +import SwiftUI + +public struct CurvedChart: View { + + private let data: [Double] + private let frame: CGRect + private let offset: Double + private let type: ChartVisualType + private let currentValueLineType: CurrentValueLineType + private var points: [CGPoint] = [] + + /// Creates a new `CurvedChart` + /// + /// - Parameters: + /// - data: A data set that should be presented on the chart + /// - frame: A frame from the parent view + /// - visualType: A type of chart, `.outline` by default + /// - offset: An offset for the chart, a space below the chart in percentage (0 - 1) + /// For example `offset: 0.2` means that the chart will occupy 80% of the upper + /// part of the view + /// - currentValueLineType: A type of current value line (`none` for no line on chart) + public init(data: [Double], + frame: CGRect, + visualType: ChartVisualType = .outline(color: .red, lineWidth: 2), + offset: Double = 0, + currentValueLineType: CurrentValueLineType = .none) { + self.data = data + self.frame = frame + self.type = visualType + self.offset = offset + self.currentValueLineType = currentValueLineType + self.points = points(forData: data, + frame: frame, + offset: offset, + lineWidth: lineWidth(visualType: visualType)) + } + + public var body: some View { + ZStack { + chart + .rotationEffect(.degrees(180), anchor: .center) + .rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0)) + .drawingGroup() + line + .rotationEffect(.degrees(180), anchor: .center) + .rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0)) + .drawingGroup() + } + } + + private var chart: some View { + switch type { + case .outline(let color, let lineWidth): + return AnyView(curvedPath(points: points) + .stroke(color, style: StrokeStyle(lineWidth: lineWidth, lineJoin: .round))) + case .filled(let color, let lineWidth): + return AnyView(ZStack { + curvedPathGradient(points: points) + .fill(LinearGradient( + gradient: .init(colors: [color.opacity(0.2), color.opacity(0.02)]), + startPoint: .init(x: 0.5, y: 1), + endPoint: .init(x: 0.5, y: 0) + )) + curvedPath(points: points) + .stroke(color, style: StrokeStyle(lineWidth: lineWidth, lineJoin: .round)) + }) + case .customFilled(let color, let lineWidth, let fillGradient): + return AnyView(ZStack { + curvedPathGradient(points: points) + .fill(fillGradient) + curvedPath(points: points) + .stroke(color, style: StrokeStyle(lineWidth: lineWidth, lineJoin: .round)) + }) + } + } + + private var line: some View { + switch currentValueLineType { + case .none: + return AnyView(EmptyView()) + case .line(let color, let lineWidth): + return AnyView( + currentValueLinePath(points: points) + .stroke(color, style: StrokeStyle(lineWidth: lineWidth)) + ) + case .dash(let color, let lineWidth, let dash): + return AnyView( + currentValueLinePath(points: points) + .stroke(color, style: StrokeStyle(lineWidth: lineWidth, dash: dash)) + ) + } + } + + // MARK: private functions + + private func curvedPath(points: [CGPoint]) -> Path { + func mid(_ point1: CGPoint, _ point2: CGPoint) -> CGPoint { + return CGPoint(x: (point1.x + point2.x) / 2, y:(point1.y + point2.y) / 2) + } + + func control(_ point1: CGPoint, _ point2: CGPoint) -> CGPoint { + var controlPoint = mid(point1, point2) + let delta = abs(point2.y - controlPoint.y) + + if point1.y < point2.y { + controlPoint.y += delta + } else if point1.y > point2.y { + controlPoint.y -= delta + } + + return controlPoint + } + + var path = Path() + guard points.count > 1 else { + return path + } + + var startPoint = points[0] + path.move(to: startPoint) + + guard points.count > 2 else { + path.addLine(to: points[1]) + return path + } + + for i in 1.. Path { + var path = curvedPath(points: points) + guard let lastPoint = points.last else { + return path + } + path.addLine(to: CGPoint(x: lastPoint.x, y: 0)) + path.addLine(to: CGPoint(x: 0, y: 0)) + path.addLine(to: CGPoint(x: 0, y: points[0].y)) + + return path + } + + private func currentValueLinePath(points: [CGPoint]) -> Path { + var path = Path() + guard let lastPoint = points.last else { + return path + } + path.move(to: CGPoint(x: 0, y: lastPoint.y)) + path.addLine(to: lastPoint) + return path + } +} + +extension CurvedChart: DataRepresentable { } diff --git a/WidgetExtension/LightChart/Charts/LineChart.swift b/WidgetExtension/LightChart/Charts/LineChart.swift new file mode 100644 index 000000000..b2a0a3ce9 --- /dev/null +++ b/WidgetExtension/LightChart/Charts/LineChart.swift @@ -0,0 +1,138 @@ +// +// LineChart.swift +// +// +// Created by Alexey Pichukov on 19.08.2020. +// + +import SwiftUI + +public struct LineChart: View { + + private let data: [Double] + private let frame: CGRect + private let offset: Double + private let type: ChartVisualType + private let currentValueLineType: CurrentValueLineType + private var points: [CGPoint] = [] + + /// Creates a new `LineChart` + /// + /// - Parameters: + /// - data: A data set that should be presented on the chart + /// - frame: A frame from the parent view + /// - visualType: A type of chart, `.outline` by default + /// - offset: An offset for the chart, a space below the chart in percentage (0 - 1) + /// For example `offset: 0.2` means that the chart will occupy 80% of the upper + /// part of the view + /// - currentValueLineType: A type of current value line (`none` for no line on chart) + public init(data: [Double], + frame: CGRect, + visualType: ChartVisualType = .outline(color: .red, lineWidth: 2), + offset: Double = 0, + currentValueLineType: CurrentValueLineType = .none) { + self.data = data + self.frame = frame + self.type = visualType + self.offset = offset + self.currentValueLineType = currentValueLineType + self.points = points(forData: data, + frame: frame, + offset: offset, + lineWidth: lineWidth(visualType: visualType)) + } + + public var body: some View { + ZStack { + chart + .rotationEffect(.degrees(180), anchor: .center) + .rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0)) + .drawingGroup() + line + .rotationEffect(.degrees(180), anchor: .center) + .rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0)) + .drawingGroup() + } + } + + private var chart: some View { + switch type { + case .outline(let color, let lineWidth): + return AnyView(linePath(points: points) + .stroke(color, style: StrokeStyle(lineWidth: lineWidth, lineJoin: .round))) + case .filled(let color, let lineWidth): + return AnyView(ZStack { + linePathGradient(points: points) + .fill(LinearGradient( + gradient: .init(colors: [color.opacity(0.2), color.opacity(0.02)]), + startPoint: .init(x: 0.5, y: 1), + endPoint: .init(x: 0.5, y: 0) + )) + linePath(points: points) + .stroke(color, style: StrokeStyle(lineWidth: lineWidth, lineJoin: .round)) + }) + case .customFilled(let color, let lineWidth, let fillGradient): + return AnyView(ZStack { + linePathGradient(points: points) + .fill(fillGradient) + linePath(points: points) + .stroke(color, style: StrokeStyle(lineWidth: lineWidth, lineJoin: .round)) + }) + } + } + + private var line: some View { + switch currentValueLineType { + case .none: + return AnyView(EmptyView()) + case .line(let color, let lineWidth): + return AnyView( + currentValueLinePath(points: points) + .stroke(color, style: StrokeStyle(lineWidth: lineWidth)) + ) + case .dash(let color, let lineWidth, let dash): + return AnyView( + currentValueLinePath(points: points) + .stroke(color, style: StrokeStyle(lineWidth: lineWidth, dash: dash)) + ) + } + } + + // MARK: private functions + + private func linePath(points: [CGPoint]) -> Path { + var path = Path() + guard points.count > 1 else { + return path + } + path.move(to: points[0]) + for i in 1.. Path { + var path = linePath(points: points) + guard let lastPoint = points.last else { + return path + } + path.addLine(to: CGPoint(x: lastPoint.x, y: 0)) + path.addLine(to: CGPoint(x: 0, y: 0)) + path.addLine(to: CGPoint(x: 0, y: points[0].y)) + + return path + } + + private func currentValueLinePath(points: [CGPoint]) -> Path { + var path = Path() + guard let lastPoint = points.last else { + return path + } + path.move(to: CGPoint(x: 0, y: lastPoint.y)) + path.addLine(to: lastPoint) + return path + } +} + +extension LineChart: DataRepresentable { } diff --git a/WidgetExtension/LightChart/DataRepresentable.swift b/WidgetExtension/LightChart/DataRepresentable.swift new file mode 100644 index 000000000..cb96037ce --- /dev/null +++ b/WidgetExtension/LightChart/DataRepresentable.swift @@ -0,0 +1,42 @@ +// +// DataRepresentable.swift +// +// +// Created by Alexey Pichukov on 19.08.2020. +// + +import Foundation +import CoreGraphics + +protocol DataRepresentable { + func points(forData data: [Double], frame: CGRect, offset: Double, lineWidth: CGFloat) -> [CGPoint] + func lineWidth(visualType: ChartVisualType) -> CGFloat +} + +extension DataRepresentable { + + func points(forData data: [Double], frame: CGRect, offset: Double, lineWidth: CGFloat) -> [CGPoint] { + var vector = Math.stretchOut(Math.norm(data)) + if offset != 0 { + vector = Math.stretchIn(vector, offset: offset) + } + var points: [CGPoint] = [] + for i in 0.. CGFloat { + switch visualType { + case .outline(_, let lineWidth): + return lineWidth + case .filled(_, let lineWidth): + return lineWidth + case .customFilled(_, let lineWidth, _): + return lineWidth + } + } +} diff --git a/WidgetExtension/LightChart/LightChart.swift b/WidgetExtension/LightChart/LightChart.swift new file mode 100644 index 000000000..e7b868730 --- /dev/null +++ b/WidgetExtension/LightChart/LightChart.swift @@ -0,0 +1,52 @@ +import SwiftUI + +public struct LightChartView: View { + + private let data: [Double] + private let type: ChartType + private let visualType: ChartVisualType + private let offset: Double + private let currentValueLineType: CurrentValueLineType + + public init(data: [Double], + type: ChartType = .line, + visualType: ChartVisualType = .outline(color: .red, lineWidth: 2), + offset: Double = 0, + currentValueLineType: CurrentValueLineType = .none) { + self.data = data + self.type = type + self.visualType = visualType + self.offset = offset + self.currentValueLineType = currentValueLineType + } + + public var body: some View { + GeometryReader { reader in + chart(withFrame: CGRect(x: 0, + y: 0, + width: reader.frame(in: .local).width , + height: reader.frame(in: .local).height)) + } + } + + private func chart(withFrame frame: CGRect) -> AnyView { + switch type { + case .line: + return AnyView( + LineChart(data: data, + frame: frame, + visualType: visualType, + offset: offset, + currentValueLineType: currentValueLineType) + ) + case .curved: + return AnyView( + CurvedChart(data: data, + frame: frame, + visualType: visualType, + offset: offset, + currentValueLineType: currentValueLineType) + ) + } + } +} diff --git a/WidgetExtension/LightChart/Math.swift b/WidgetExtension/LightChart/Math.swift new file mode 100644 index 000000000..837af4ceb --- /dev/null +++ b/WidgetExtension/LightChart/Math.swift @@ -0,0 +1,34 @@ +// +// Math.swift +// +// +// Created by Alexey Pichukov on 19.08.2020. +// + +import Foundation +import CoreGraphics + +struct Math { + + static func norm(_ vector: [Double]) -> [Double] { + let norm = sqrt(Double(vector.reduce(0) { $0 + $1 * $1 })) + return norm == 0 ? vector : vector.map { $0 / norm } + } + + static func stretchOut(_ vector: [Double]) -> [Double] { + guard let min = vector.min(), + let rawMax = vector.max() else { + return vector + } + let max = rawMax - min + return vector.map { ($0 - min) / (max != 0 ? max : 1) } + } + + static func stretchIn(_ vector: [Double], offset: Double) -> [Double] { + guard let max = vector.max() else { + return vector + } + let newMax = max - offset + return vector.map { $0 * newMax + offset } + } +} diff --git a/WidgetExtension/WidgetViews/FollowCountWidgetView.swift b/WidgetExtension/WidgetViews/FollowCountWidgetView.swift index 9b075f67a..d4bc2a86a 100644 --- a/WidgetExtension/WidgetViews/FollowCountWidgetView.swift +++ b/WidgetExtension/WidgetViews/FollowCountWidgetView.swift @@ -2,10 +2,11 @@ import SwiftUI import WidgetKit -import LightChart import MastodonAsset struct FollowCountWidgetView: View { + private let followersHistory = FollowersCountHistory.shared + @Environment(\.widgetFamily) var family var entry: FollowersProvider.Entry @@ -93,21 +94,25 @@ struct FollowCountWidgetView: View { .padding(.leading, 20) ZStack { - LightChartView( - data: [200, 205, 208, 213, 210, 211, 212], - type: .line, - visualType: .filled(color: Asset.Colors.Brand.blurple.swiftUIColor, lineWidth: 2), - offset: 0.8 /// this is the positive offset from the bottom edge of the graph (~80% above bottom level) - ) + if let account = entry.account { + LightChartView( + data: followersHistory.chartValues(for: account), + type: .line, + visualType: .filled(color: Asset.Colors.Brand.blurple.swiftUIColor, lineWidth: 2), + offset: 0.8 /// this is the positive offset from the bottom edge of the graph (~80% above bottom level) + ) + } HStack { VStack(alignment: .leading, spacing: 0) { Spacer() - Text("+4 followers today") - .font(.system(size: 12)) - .foregroundColor(.secondary) - .lineLimit(1) - .truncationMode(.tail) + if let increaseCount = followersHistory.increaseCountString(for: account) { + Text("\(increaseCount) followers today") + .font(.system(size: 12)) + .foregroundColor(.secondary) + .lineLimit(1) + .truncationMode(.tail) + } Text(account.followersCount.asAbbreviatedCountString()) .font(.largeTitle) From e05a8602d57684c66a2d0a113b2687158bb87a31 Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Tue, 31 Jan 2023 11:43:45 +0100 Subject: [PATCH 08/24] feat(Widget): Reorganize and rename FollowersCountWidget code --- Mastodon.xcodeproj/project.pbxproj | 40 ++-- .../FollowersCountWidget.swift} | 26 +-- .../FollowersCountWidgetView.swift} | 4 +- .../WidgetExtension.intentdefinition | 213 +++++++++++++++++- WidgetExtension/WidgetExtensionBundle.swift | 2 +- 5 files changed, 252 insertions(+), 33 deletions(-) rename WidgetExtension/{FollowersWidgetExtension.swift => Variants/FollowersCount/FollowersCountWidget.swift} (86%) rename WidgetExtension/{WidgetViews/FollowCountWidgetView.swift => Variants/FollowersCount/FollowersCountWidgetView.swift} (98%) diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index d1e0fc798..5e2370938 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -31,7 +31,7 @@ 2A33063929880835001D4C51 /* CurvedChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A33063329880834001D4C51 /* CurvedChart.swift */; }; 2A33063A29880835001D4C51 /* LightChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A33063429880834001D4C51 /* LightChart.swift */; }; 2A33063B29880835001D4C51 /* ChartType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A33063529880834001D4C51 /* ChartType.swift */; }; - 2A33AB662982C4AF008A7FB1 /* FollowCountWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A33AB652982C4AF008A7FB1 /* FollowCountWidgetView.swift */; }; + 2A33AB662982C4AF008A7FB1 /* FollowersCountWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A33AB652982C4AF008A7FB1 /* FollowersCountWidgetView.swift */; }; 2A3F6FE3292ECB5E002E6DA7 /* FollowedTagsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A3F6FE2292ECB5E002E6DA7 /* FollowedTagsViewModel.swift */; }; 2A3F6FE5292F6E44002E6DA7 /* FollowedTagsTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A3F6FE4292F6E44002E6DA7 /* FollowedTagsTableViewCell.swift */; }; 2A506CF4292CD85800059C37 /* FollowedTagsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A506CF3292CD85800059C37 /* FollowedTagsViewController.swift */; }; @@ -44,7 +44,7 @@ 2A728122297EA9D7004138C5 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2A728121297EA9D7004138C5 /* WidgetKit.framework */; }; 2A728124297EA9D7004138C5 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2A728123297EA9D7004138C5 /* SwiftUI.framework */; }; 2A728127297EA9D7004138C5 /* WidgetExtensionBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A728126297EA9D7004138C5 /* WidgetExtensionBundle.swift */; }; - 2A72812B297EA9D7004138C5 /* FollowersWidgetExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A72812A297EA9D7004138C5 /* FollowersWidgetExtension.swift */; }; + 2A72812B297EA9D7004138C5 /* FollowersCountWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A72812A297EA9D7004138C5 /* FollowersCountWidget.swift */; }; 2A72812E297EA9D8004138C5 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2A72812D297EA9D8004138C5 /* Assets.xcassets */; }; 2A728130297EA9D8004138C5 /* WidgetExtension.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 2A72812C297EA9D7004138C5 /* WidgetExtension.intentdefinition */; }; 2A728131297EA9D8004138C5 /* WidgetExtension.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 2A72812C297EA9D7004138C5 /* WidgetExtension.intentdefinition */; }; @@ -620,7 +620,7 @@ 2A33063429880834001D4C51 /* LightChart.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LightChart.swift; sourceTree = ""; }; 2A33063529880834001D4C51 /* ChartType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChartType.swift; sourceTree = ""; }; 2A33625329759B4200481A90 /* OpenInActionExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = OpenInActionExtension.entitlements; sourceTree = ""; }; - 2A33AB652982C4AF008A7FB1 /* FollowCountWidgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowCountWidgetView.swift; sourceTree = ""; }; + 2A33AB652982C4AF008A7FB1 /* FollowersCountWidgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowersCountWidgetView.swift; sourceTree = ""; }; 2A3F6FE2292ECB5E002E6DA7 /* FollowedTagsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowedTagsViewModel.swift; sourceTree = ""; }; 2A3F6FE4292F6E44002E6DA7 /* FollowedTagsTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowedTagsTableViewCell.swift; sourceTree = ""; }; 2A506CF3292CD85800059C37 /* FollowedTagsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowedTagsViewController.swift; sourceTree = ""; }; @@ -635,7 +635,7 @@ 2A728121297EA9D7004138C5 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; }; 2A728123297EA9D7004138C5 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; 2A728126297EA9D7004138C5 /* WidgetExtensionBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetExtensionBundle.swift; sourceTree = ""; }; - 2A72812A297EA9D7004138C5 /* FollowersWidgetExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowersWidgetExtension.swift; sourceTree = ""; }; + 2A72812A297EA9D7004138C5 /* FollowersCountWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowersCountWidget.swift; sourceTree = ""; }; 2A72812C297EA9D7004138C5 /* WidgetExtension.intentdefinition */ = {isa = PBXFileReference; lastKnownFileType = file.intentdefinition; path = WidgetExtension.intentdefinition; sourceTree = ""; }; 2A72812D297EA9D8004138C5 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 2A72812F297EA9D8004138C5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -1409,14 +1409,6 @@ path = Charts; sourceTree = ""; }; - 2A33AB642982C4A3008A7FB1 /* WidgetViews */ = { - isa = PBXGroup; - children = ( - 2A33AB652982C4AF008A7FB1 /* FollowCountWidgetView.swift */, - ); - path = WidgetViews; - sourceTree = ""; - }; 2A506CF2292CD83B00059C37 /* FollowedTags */ = { isa = PBXGroup; children = ( @@ -1444,11 +1436,10 @@ isa = PBXGroup; children = ( 2A33062E29880834001D4C51 /* LightChart */, - 2A33AB642982C4A3008A7FB1 /* WidgetViews */, + 2A86A14329892700007F1062 /* Variants */, 2AE202A9297FDDF500F66E55 /* WidgetExtension.entitlements */, 2A72813E297EC762004138C5 /* WidgetExtension.swift */, 2A728126297EA9D7004138C5 /* WidgetExtensionBundle.swift */, - 2A72812A297EA9D7004138C5 /* FollowersWidgetExtension.swift */, 2A33062C2987DBFA001D4C51 /* FollowersCountHistory.swift */, 2A72812C297EA9D7004138C5 /* WidgetExtension.intentdefinition */, 2A72812D297EA9D8004138C5 /* Assets.xcassets */, @@ -1457,6 +1448,23 @@ path = WidgetExtension; sourceTree = ""; }; + 2A86A14329892700007F1062 /* Variants */ = { + isa = PBXGroup; + children = ( + 2A86A14429892709007F1062 /* FollowersCount */, + ); + path = Variants; + sourceTree = ""; + }; + 2A86A14429892709007F1062 /* FollowersCount */ = { + isa = PBXGroup; + children = ( + 2A72812A297EA9D7004138C5 /* FollowersCountWidget.swift */, + 2A33AB652982C4AF008A7FB1 /* FollowersCountWidgetView.swift */, + ); + path = FollowersCount; + sourceTree = ""; + }; 2D152A8A25C295B8009AA50C /* Content */ = { isa = PBXGroup; children = ( @@ -3481,9 +3489,9 @@ 2A33063A29880835001D4C51 /* LightChart.swift in Sources */, 2A33063B29880835001D4C51 /* ChartType.swift in Sources */, 2A33063629880835001D4C51 /* Math.swift in Sources */, - 2A33AB662982C4AF008A7FB1 /* FollowCountWidgetView.swift in Sources */, + 2A33AB662982C4AF008A7FB1 /* FollowersCountWidgetView.swift in Sources */, 2A728127297EA9D7004138C5 /* WidgetExtensionBundle.swift in Sources */, - 2A72812B297EA9D7004138C5 /* FollowersWidgetExtension.swift in Sources */, + 2A72812B297EA9D7004138C5 /* FollowersCountWidget.swift in Sources */, 2A33063929880835001D4C51 /* CurvedChart.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/WidgetExtension/FollowersWidgetExtension.swift b/WidgetExtension/Variants/FollowersCount/FollowersCountWidget.swift similarity index 86% rename from WidgetExtension/FollowersWidgetExtension.swift rename to WidgetExtension/Variants/FollowersCount/FollowersCountWidget.swift index 4b00106a9..3cdff147a 100644 --- a/WidgetExtension/FollowersWidgetExtension.swift +++ b/WidgetExtension/Variants/FollowersCount/FollowersCountWidget.swift @@ -5,34 +5,34 @@ import SwiftUI import Intents import MastodonSDK -struct FollowersProvider: IntentTimelineProvider { +struct FollowersCountWidgetProvider: IntentTimelineProvider { private let followersHistory = FollowersCountHistory.shared - func placeholder(in context: Context) -> FollowersEntry { + func placeholder(in context: Context) -> FollowersCountEntry { .placeholder } - func getSnapshot(for configuration: FollowersCountIntent, in context: Context, completion: @escaping (FollowersEntry) -> ()) { + func getSnapshot(for configuration: FollowersCountIntent, in context: Context, completion: @escaping (FollowersCountEntry) -> ()) { guard !context.isPreview else { return completion(.placeholder) } loadCurrentEntry(for: configuration, in: context, completion: completion) } - func getTimeline(for configuration: FollowersCountIntent, in context: Context, completion: @escaping (Timeline) -> ()) { + func getTimeline(for configuration: FollowersCountIntent, in context: Context, completion: @escaping (Timeline) -> ()) { loadCurrentEntry(for: configuration, in: context) { entry in completion(Timeline(entries: [entry], policy: .after(.now))) } } } -struct FollowersEntry: TimelineEntry { +struct FollowersCountEntry: TimelineEntry { let date: Date let account: FollowersEntryAccountable? let configuration: FollowersCountIntent static var placeholder: Self { - FollowersEntry( + FollowersCountEntry( date: .now, account: FollowersEntryAccount( followersCount: 99_900, @@ -46,7 +46,7 @@ struct FollowersEntry: TimelineEntry { } static var unconfigured: Self { - FollowersEntry( + FollowersCountEntry( date: .now, account: nil, configuration: FollowersCountIntent() @@ -54,7 +54,7 @@ struct FollowersEntry: TimelineEntry { } } -struct FollowersWidgetExtension: Widget { +struct FollowersCountWidget: Widget { private var availableFamilies: [WidgetFamily] { if #available(iOS 16, *) { return [.systemSmall, .accessoryRectangular, .accessoryCircular] @@ -63,8 +63,8 @@ struct FollowersWidgetExtension: Widget { } var body: some WidgetConfiguration { - IntentConfiguration(kind: "Followers", intent: FollowersCountIntent.self, provider: FollowersProvider()) { entry in - FollowCountWidgetView(entry: entry) + IntentConfiguration(kind: "Followers", intent: FollowersCountIntent.self, provider: FollowersCountWidgetProvider()) { entry in + FollowersCountWidgetView(entry: entry) } .configurationDisplayName("Followers") .description("Show number of followers.") @@ -72,8 +72,8 @@ struct FollowersWidgetExtension: Widget { } } -private extension FollowersProvider { - func loadCurrentEntry(for configuration: FollowersCountIntent, in context: Context, completion: @escaping (FollowersEntry) -> Void) { +private extension FollowersCountWidgetProvider { + func loadCurrentEntry(for configuration: FollowersCountIntent, in context: Context, completion: @escaping (FollowersCountEntry) -> Void) { Task { guard let authBox = WidgetExtension.appContext @@ -102,7 +102,7 @@ private extension FollowersProvider { let imageData = try await URLSession.shared.data(from: resultingAccount.avatarImageURLWithFallback(domain: authBox.domain)).0 - let entry = FollowersEntry( + let entry = FollowersCountEntry( date: Date(), account: FollowersEntryAccount.from( mastodonAccount: resultingAccount, diff --git a/WidgetExtension/WidgetViews/FollowCountWidgetView.swift b/WidgetExtension/Variants/FollowersCount/FollowersCountWidgetView.swift similarity index 98% rename from WidgetExtension/WidgetViews/FollowCountWidgetView.swift rename to WidgetExtension/Variants/FollowersCount/FollowersCountWidgetView.swift index d4bc2a86a..41648805d 100644 --- a/WidgetExtension/WidgetViews/FollowCountWidgetView.swift +++ b/WidgetExtension/Variants/FollowersCount/FollowersCountWidgetView.swift @@ -4,12 +4,12 @@ import SwiftUI import WidgetKit import MastodonAsset -struct FollowCountWidgetView: View { +struct FollowersCountWidgetView: View { private let followersHistory = FollowersCountHistory.shared @Environment(\.widgetFamily) var family - var entry: FollowersProvider.Entry + var entry: FollowersCountWidgetProvider.Entry var body: some View { if let account = entry.account { diff --git a/WidgetExtension/WidgetExtension.intentdefinition b/WidgetExtension/WidgetExtension.intentdefinition index bf1619332..a010349a7 100644 --- a/WidgetExtension/WidgetExtension.intentdefinition +++ b/WidgetExtension/WidgetExtension.intentdefinition @@ -174,8 +174,219 @@ INIntentVerb View + + INIntentCategory + information + INIntentDescriptionID + B9KyhZ + INIntentEligibleForWidgets + + INIntentIneligibleForSuggestions + + INIntentLastParameterTag + 5 + INIntentName + MultiFollowCountSmall + INIntentParameters + + + INIntentParameterArraySizes + + + INIntentParameterArraySizeSize + 3 + INIntentParameterArraySizeSizeClass + Small + + + INIntentParameterArraySizeSize + 6 + INIntentParameterArraySizeSizeClass + Medium + + + INIntentParameterArraySizeSize + 6 + INIntentParameterArraySizeSizeClass + Large + + + INIntentParameterArraySizeSize + 6 + INIntentParameterArraySizeSizeClass + ExtraLarge + + + INIntentParameterArraySizeSize + 1 + INIntentParameterArraySizeSizeClass + AccessoryInline + + + INIntentParameterArraySizeSize + 1 + INIntentParameterArraySizeSizeClass + AccessoryCorner + + + INIntentParameterArraySizeSize + 1 + INIntentParameterArraySizeSizeClass + AccessoryCircular + + + INIntentParameterArraySizeSize + 1 + INIntentParameterArraySizeSizeClass + AccessoryRectangular + + + INIntentParameterConfigurable + + INIntentParameterDisplayName + Accounts + INIntentParameterDisplayNameID + fovmPX + INIntentParameterDisplayPriority + 1 + INIntentParameterFixedSizeArray + 1 + INIntentParameterName + accounts + INIntentParameterObjectType + MultiFollowAccountsSmall + INIntentParameterObjectTypeNamespace + 88xZPY + INIntentParameterPromptDialogs + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogType + Configuration + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogType + Primary + + + INIntentParameterSupportsMultipleValues + + INIntentParameterTag + 5 + INIntentParameterType + Object + + + INIntentResponse + + INIntentResponseCodes + + + INIntentResponseCodeName + success + INIntentResponseCodeSuccess + + + + INIntentResponseCodeName + failure + + + + INIntentTitle + Multi Follow Count Small + INIntentTitleID + e0W2wo + INIntentType + Custom + INIntentVerb + View + INTypes - + + + INTypeDisplayName + Account + INTypeDisplayNameID + LUrJ3D + INTypeLastPropertyTag + 101 + INTypeName + MultiFollowAccountsSmall + INTypeProperties + + + INTypePropertyDefault + + INTypePropertyDisplayPriority + 1 + INTypePropertyName + identifier + INTypePropertyTag + 1 + INTypePropertyType + String + + + INTypePropertyDefault + + INTypePropertyDisplayPriority + 2 + INTypePropertyName + displayString + INTypePropertyTag + 2 + INTypePropertyType + String + + + INTypePropertyDefault + + INTypePropertyDisplayPriority + 3 + INTypePropertyName + pronunciationHint + INTypePropertyTag + 3 + INTypePropertyType + String + + + INTypePropertyDefault + + INTypePropertyDisplayPriority + 4 + INTypePropertyName + alternativeSpeakableMatches + INTypePropertySupportsMultipleValues + + INTypePropertyTag + 4 + INTypePropertyType + SpeakableString + + + INTypePropertyDisplayName + username + INTypePropertyDisplayNameID + TQNJZz + INTypePropertyDisplayPriority + 5 + INTypePropertyName + property + INTypePropertySupportsMultipleValues + + INTypePropertyTag + 101 + INTypePropertyType + String + + + + diff --git a/WidgetExtension/WidgetExtensionBundle.swift b/WidgetExtension/WidgetExtensionBundle.swift index d94557d88..5c5ea71ba 100644 --- a/WidgetExtension/WidgetExtensionBundle.swift +++ b/WidgetExtension/WidgetExtensionBundle.swift @@ -6,6 +6,6 @@ import SwiftUI @main struct WidgetExtensionBundle: WidgetBundle { var body: some Widget { - FollowersWidgetExtension() + FollowersCountWidget() } } From 9eb26d4ed8f5590cd30d79e8b3faaff4e14205ad Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Tue, 31 Jan 2023 14:37:49 +0100 Subject: [PATCH 09/24] feat(Widget): Implement MultiFollowersCountWidget for single Column --- Mastodon.xcodeproj/project.pbxproj | 22 ++- Mastodon/Info.plist | 1 + .../Handler/FollowersCountIntentHandler.swift | 4 +- .../MultiFollowersCountIntentHandler.swift | 27 +++ MastodonIntent/Info.plist | 1 + MastodonIntent/IntentHandler.swift | 2 + .../FollowersCountHistory.swift | 0 .../MultiFollowersCountWidget.swift | 150 +++++++++++++++++ .../MultiFollowersCountWidgetView.swift | 58 +++++++ .../WidgetExtension.intentdefinition | 154 ++++++++---------- WidgetExtension/WidgetExtensionBundle.swift | 1 + 11 files changed, 326 insertions(+), 94 deletions(-) create mode 100644 MastodonIntent/Handler/MultiFollowersCountIntentHandler.swift rename WidgetExtension/{ => Variants/FollowersCount}/FollowersCountHistory.swift (100%) create mode 100644 WidgetExtension/Variants/MultiFollowersCount/MultiFollowersCountWidget.swift create mode 100644 WidgetExtension/Variants/MultiFollowersCount/MultiFollowersCountWidgetView.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 5e2370938..85cc5e0a6 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -53,6 +53,9 @@ 2A72813F297EC762004138C5 /* WidgetExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A72813E297EC762004138C5 /* WidgetExtension.swift */; }; 2A76F75C2930D94700B3388D /* HashtagTimelineHeaderViewActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A76F75B2930D94700B3388D /* HashtagTimelineHeaderViewActionButton.swift */; }; 2A82294F29262EE000D2A1F7 /* AppContext+NextAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A82294E29262EE000D2A1F7 /* AppContext+NextAccount.swift */; }; + 2A86A14629892944007F1062 /* MultiFollowersCountIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A86A14529892944007F1062 /* MultiFollowersCountIntentHandler.swift */; }; + 2A86A14929892B3A007F1062 /* MultiFollowersCountWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A86A14829892B3A007F1062 /* MultiFollowersCountWidget.swift */; }; + 2A86A14B2989326E007F1062 /* MultiFollowersCountWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A86A14A2989326E007F1062 /* MultiFollowersCountWidgetView.swift */; }; 2A90A157296EEE500026C155 /* MastodonSDKDynamic in Frameworks */ = {isa = PBXBuildFile; productRef = 2A90A156296EEE500026C155 /* MastodonSDKDynamic */; }; 2AB12E4629362F27006BC925 /* DataSourceFacade+Translate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AB12E4529362F27006BC925 /* DataSourceFacade+Translate.swift */; }; 2AE202AA297FE10B00F66E55 /* WidgetExtension.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 2A72812C297EA9D7004138C5 /* WidgetExtension.intentdefinition */; }; @@ -642,6 +645,9 @@ 2A72813E297EC762004138C5 /* WidgetExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetExtension.swift; sourceTree = ""; }; 2A76F75B2930D94700B3388D /* HashtagTimelineHeaderViewActionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagTimelineHeaderViewActionButton.swift; sourceTree = ""; }; 2A82294E29262EE000D2A1F7 /* AppContext+NextAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppContext+NextAccount.swift"; sourceTree = ""; }; + 2A86A14529892944007F1062 /* MultiFollowersCountIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiFollowersCountIntentHandler.swift; sourceTree = ""; }; + 2A86A14829892B3A007F1062 /* MultiFollowersCountWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiFollowersCountWidget.swift; sourceTree = ""; }; + 2A86A14A2989326E007F1062 /* MultiFollowersCountWidgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiFollowersCountWidgetView.swift; sourceTree = ""; }; 2AB12E4529362F27006BC925 /* DataSourceFacade+Translate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Translate.swift"; sourceTree = ""; }; 2AE202A9297FDDF500F66E55 /* WidgetExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WidgetExtension.entitlements; sourceTree = ""; }; 2AE202AB297FE19100F66E55 /* FollowersCountIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowersCountIntentHandler.swift; sourceTree = ""; }; @@ -1440,7 +1446,6 @@ 2AE202A9297FDDF500F66E55 /* WidgetExtension.entitlements */, 2A72813E297EC762004138C5 /* WidgetExtension.swift */, 2A728126297EA9D7004138C5 /* WidgetExtensionBundle.swift */, - 2A33062C2987DBFA001D4C51 /* FollowersCountHistory.swift */, 2A72812C297EA9D7004138C5 /* WidgetExtension.intentdefinition */, 2A72812D297EA9D8004138C5 /* Assets.xcassets */, 2A72812F297EA9D8004138C5 /* Info.plist */, @@ -1452,6 +1457,7 @@ isa = PBXGroup; children = ( 2A86A14429892709007F1062 /* FollowersCount */, + 2A86A14729892B1B007F1062 /* MultiFollowersCount */, ); path = Variants; sourceTree = ""; @@ -1459,12 +1465,22 @@ 2A86A14429892709007F1062 /* FollowersCount */ = { isa = PBXGroup; children = ( + 2A33062C2987DBFA001D4C51 /* FollowersCountHistory.swift */, 2A72812A297EA9D7004138C5 /* FollowersCountWidget.swift */, 2A33AB652982C4AF008A7FB1 /* FollowersCountWidgetView.swift */, ); path = FollowersCount; sourceTree = ""; }; + 2A86A14729892B1B007F1062 /* MultiFollowersCount */ = { + isa = PBXGroup; + children = ( + 2A86A14829892B3A007F1062 /* MultiFollowersCountWidget.swift */, + 2A86A14A2989326E007F1062 /* MultiFollowersCountWidgetView.swift */, + ); + path = MultiFollowersCount; + sourceTree = ""; + }; 2D152A8A25C295B8009AA50C /* Content */ = { isa = PBXGroup; children = ( @@ -2291,6 +2307,7 @@ children = ( DBB8AB4526AECDE200F6D281 /* SendPostIntentHandler.swift */, 2AE202AB297FE19100F66E55 /* FollowersCountIntentHandler.swift */, + 2A86A14529892944007F1062 /* MultiFollowersCountIntentHandler.swift */, ); path = Handler; sourceTree = ""; @@ -3485,10 +3502,12 @@ 2A33062D2987DBFA001D4C51 /* FollowersCountHistory.swift in Sources */, 2A33063829880835001D4C51 /* LineChart.swift in Sources */, 2A728130297EA9D8004138C5 /* WidgetExtension.intentdefinition in Sources */, + 2A86A14B2989326E007F1062 /* MultiFollowersCountWidgetView.swift in Sources */, 2A72813F297EC762004138C5 /* WidgetExtension.swift in Sources */, 2A33063A29880835001D4C51 /* LightChart.swift in Sources */, 2A33063B29880835001D4C51 /* ChartType.swift in Sources */, 2A33063629880835001D4C51 /* Math.swift in Sources */, + 2A86A14929892B3A007F1062 /* MultiFollowersCountWidget.swift in Sources */, 2A33AB662982C4AF008A7FB1 /* FollowersCountWidgetView.swift in Sources */, 2A728127297EA9D7004138C5 /* WidgetExtensionBundle.swift in Sources */, 2A72812B297EA9D7004138C5 /* FollowersCountWidget.swift in Sources */, @@ -3932,6 +3951,7 @@ 2AE202AC297FE19100F66E55 /* FollowersCountIntentHandler.swift in Sources */, DB64BA452851F23000ADF1B7 /* MastodonAuthentication+Fetch.swift in Sources */, DB64BA482851F29300ADF1B7 /* Account+Fetch.swift in Sources */, + 2A86A14629892944007F1062 /* MultiFollowersCountIntentHandler.swift in Sources */, DB8FABCA26AEC7B2008E5AF4 /* IntentHandler.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Mastodon/Info.plist b/Mastodon/Info.plist index fbe3fd94a..99b66b264 100644 --- a/Mastodon/Info.plist +++ b/Mastodon/Info.plist @@ -62,6 +62,7 @@ NSUserActivityTypes FollowersCountIntent + MultiFollowersCountSmallIntent SendPostIntent UIApplicationSceneManifest diff --git a/MastodonIntent/Handler/FollowersCountIntentHandler.swift b/MastodonIntent/Handler/FollowersCountIntentHandler.swift index 31c306127..170508325 100644 --- a/MastodonIntent/Handler/FollowersCountIntentHandler.swift +++ b/MastodonIntent/Handler/FollowersCountIntentHandler.swift @@ -30,13 +30,11 @@ class FollowersCountIntentHandler: INExtension, FollowersCountIntentHandling { .apiService .search(query: .init(q: searchTerm), authenticationBox: authenticationBox) - debugPrint(results.value.statuses) - return INObjectCollection(items: results.value.accounts.map { $0.acctWithDomain(localDomain: authenticationBox.domain) as NSString }) } } -private extension Mastodon.Entity.Account { +extension Mastodon.Entity.Account { func acctWithDomain(localDomain: String) -> String { guard acct.contains("@") else { return "\(acct)@\(localDomain)" diff --git a/MastodonIntent/Handler/MultiFollowersCountIntentHandler.swift b/MastodonIntent/Handler/MultiFollowersCountIntentHandler.swift new file mode 100644 index 000000000..984a4e39d --- /dev/null +++ b/MastodonIntent/Handler/MultiFollowersCountIntentHandler.swift @@ -0,0 +1,27 @@ +// Copyright © 2023 Mastodon gGmbH. All rights reserved. + +import Foundation +import Intents +import MastodonCore +import MastodonSDK +import MastodonLocalization + +class MultiFollowersCountIntentHandler: INExtension, MultiFollowersCountSmallIntentHandling { + func provideAccountsOptionsCollection(for intent: MultiFollowersCountSmallIntent, searchTerm: String?) async throws -> INObjectCollection { + guard + let searchTerm = searchTerm, + let authenticationBox = WidgetExtension.appContext + .authenticationService + .mastodonAuthenticationBoxes + .first + else { + return INObjectCollection(items: []) + } + + let results = try await WidgetExtension.appContext + .apiService + .search(query: .init(q: searchTerm), authenticationBox: authenticationBox) + + return INObjectCollection(items: results.value.accounts.map { $0.acctWithDomain(localDomain: authenticationBox.domain) as NSString }) + } +} diff --git a/MastodonIntent/Info.plist b/MastodonIntent/Info.plist index 7dbb09f9e..457385c05 100644 --- a/MastodonIntent/Info.plist +++ b/MastodonIntent/Info.plist @@ -31,6 +31,7 @@ IntentsSupported FollowersCountIntent + MultiFollowersCountSmallIntent SendPostIntent diff --git a/MastodonIntent/IntentHandler.swift b/MastodonIntent/IntentHandler.swift index b8f26d8ca..4b13e5839 100644 --- a/MastodonIntent/IntentHandler.swift +++ b/MastodonIntent/IntentHandler.swift @@ -17,6 +17,8 @@ class IntentHandler: INExtension { return SendPostIntentHandler() case is FollowersCountIntent: return FollowersCountIntentHandler() + case is MultiFollowersCountSmallIntent: + return MultiFollowersCountIntentHandler() default: return self } diff --git a/WidgetExtension/FollowersCountHistory.swift b/WidgetExtension/Variants/FollowersCount/FollowersCountHistory.swift similarity index 100% rename from WidgetExtension/FollowersCountHistory.swift rename to WidgetExtension/Variants/FollowersCount/FollowersCountHistory.swift diff --git a/WidgetExtension/Variants/MultiFollowersCount/MultiFollowersCountWidget.swift b/WidgetExtension/Variants/MultiFollowersCount/MultiFollowersCountWidget.swift new file mode 100644 index 000000000..1dc4ec7e7 --- /dev/null +++ b/WidgetExtension/Variants/MultiFollowersCount/MultiFollowersCountWidget.swift @@ -0,0 +1,150 @@ +// Copyright © 2023 Mastodon gGmbH. All rights reserved. + +import WidgetKit +import SwiftUI +import Intents +import MastodonSDK + +struct MultiFollowersCountWidgetProvider: IntentTimelineProvider { + func placeholder(in context: Context) -> MultiFollowersCountEntry { + .placeholder + } + + func getSnapshot(for configuration: MultiFollowersCountSmallIntent, in context: Context, completion: @escaping (MultiFollowersCountEntry) -> ()) { + guard !context.isPreview else { + return completion(.placeholder) + } + loadCurrentEntry(for: configuration, in: context, completion: completion) + } + + func getTimeline(for configuration: MultiFollowersCountSmallIntent, in context: Context, completion: @escaping (Timeline) -> ()) { + loadCurrentEntry(for: configuration, in: context) { entry in + completion(Timeline(entries: [entry], policy: .after(.now))) + } + } +} + +struct MultiFollowersCountEntry: TimelineEntry { + let date: Date + let accounts: [FollowersEntryAccountable]? + let configuration: MultiFollowersCountSmallIntent + + static var placeholder: Self { + MultiFollowersCountEntry( + date: .now, + accounts: [ + FollowersEntryAccount( + followersCount: 99_900, + displayNameWithFallback: "Mastodon", + acct: "mastodon", + avatarImage: UIImage(named: "missingAvatar")!, + domain: "mastodon" + ) + ], + configuration: MultiFollowersCountSmallIntent() + ) + } + + static var unconfigured: Self { + MultiFollowersCountEntry( + date: .now, + accounts: [], + configuration: MultiFollowersCountSmallIntent() + ) + } +} + +struct MultiFollowersCountWidget: Widget { + private var availableFamilies: [WidgetFamily] { + return [.systemSmall] + } + + var body: some WidgetConfiguration { + IntentConfiguration(kind: "Multiple followers", intent: MultiFollowersCountSmallIntent.self, provider: MultiFollowersCountWidgetProvider()) { entry in + MultiFollowersCountWidgetView(entry: entry) + } + .configurationDisplayName("Multiple followers") + .description("Show number of followers for multiple accounts.") + .supportedFamilies(availableFamilies) + } +} + +private extension MultiFollowersCountWidgetProvider { + func loadCurrentEntry(for configuration: MultiFollowersCountSmallIntent, in context: Context, completion: @escaping (MultiFollowersCountEntry) -> Void) { + Task { + guard + let authBox = WidgetExtension.appContext + .authenticationService + .mastodonAuthenticationBoxes + .first + else { + return completion(.unconfigured) + } + + guard let desiredAccounts: [String] = { + guard let account = configuration.accounts?.compactMap({ $0 }) else { + if let acct = authBox.authenticationRecord.object(in: WidgetExtension.appContext.managedObjectContext)?.user.acct { + return [acct] + } + return nil + } + return account + }() else { + return completion(.unconfigured) + } + + var accounts = [FollowersEntryAccountable]() + + for desiredAccount in desiredAccounts { + let resultingAccount = try await WidgetExtension.appContext + .apiService + .search(query: .init(q: desiredAccount, type: .accounts), authenticationBox: authBox) + .value + .accounts + .first! + + let imageData = try await URLSession.shared.data(from: resultingAccount.avatarImageURLWithFallback(domain: authBox.domain)).0 + + accounts.append(FollowersEntryAccount.from( + mastodonAccount: resultingAccount, + domain: authBox.domain, + avatarImage: UIImage(data: imageData) ?? UIImage(named: "missingAvatar")! + )) + } + + let entry = MultiFollowersCountEntry( + date: Date(), + accounts: accounts, + configuration: configuration + ) + + completion(entry) + } + } +} + +protocol MultiFollowersEntryAccountable { + var followersCount: Int { get } + var displayNameWithFallback: String { get } + var acct: String { get } + var avatarImage: UIImage { get } + var domain: String { get } +} + +struct MultiFollowersEntryAccount: MultiFollowersEntryAccountable { + let followersCount: Int + let displayNameWithFallback: String + let acct: String + let avatarImage: UIImage + let domain: String + + static func from(mastodonAccount: Mastodon.Entity.Account, domain: String, avatarImage: UIImage) -> Self { + MultiFollowersEntryAccount( + followersCount: mastodonAccount.followersCount, + displayNameWithFallback: mastodonAccount.displayNameWithFallback, + acct: mastodonAccount.acct, + avatarImage: avatarImage, + domain: domain + ) + } +} diff --git a/WidgetExtension/Variants/MultiFollowersCount/MultiFollowersCountWidgetView.swift b/WidgetExtension/Variants/MultiFollowersCount/MultiFollowersCountWidgetView.swift new file mode 100644 index 000000000..4fb374ea4 --- /dev/null +++ b/WidgetExtension/Variants/MultiFollowersCount/MultiFollowersCountWidgetView.swift @@ -0,0 +1,58 @@ +// Copyright © 2023 Mastodon gGmbH. All rights reserved. + +import SwiftUI +import WidgetKit +import MastodonAsset + +struct MultiFollowersCountWidgetView: View { + @Environment(\.widgetFamily) var family + + var entry: MultiFollowersCountWidgetProvider.Entry + + var body: some View { + if let accounts = entry.accounts { + switch family { + case .systemSmall: + viewForSmallWidgetNoChart(accounts) + default: + Text("Sorry but this Widget family is unsupported.") + } + } else { + Text("Please open Mastodon to log in to an Account.") + .multilineTextAlignment(.center) + .font(.caption) + .padding(.all, 20) + } + } + + private func viewForSmallWidgetNoChart(_ accounts: [FollowersEntryAccountable]) -> some View { + VStack(alignment: .leading, spacing: 0) { + ForEach(accounts, id: \.acct) { account in + HStack { + if let avatarImage = account.avatarImage { + Image(uiImage: avatarImage) + .resizable() + .frame(width: 32, height: 32) + .cornerRadius(5) + } + VStack(alignment: .leading) { + Text(account.followersCount.asAbbreviatedCountString()) + .font(.title2) + .lineLimit(1) + .truncationMode(.tail) + + Text("@\(account.acct)") + .font(.caption2) + .foregroundColor(.secondary) + .lineLimit(1) + .truncationMode(.tail) + } + Spacer() + } + .padding(.leading, 20) + } + Spacer() + } + .padding(.vertical, 16) + } +} diff --git a/WidgetExtension/WidgetExtension.intentdefinition b/WidgetExtension/WidgetExtension.intentdefinition index a010349a7..a2b148563 100644 --- a/WidgetExtension/WidgetExtension.intentdefinition +++ b/WidgetExtension/WidgetExtension.intentdefinition @@ -184,9 +184,9 @@ INIntentIneligibleForSuggestions INIntentLastParameterTag - 5 + 6 INIntentName - MultiFollowCountSmall + MultiFollowersCountSmall INIntentParameters @@ -251,17 +251,24 @@ 1 INIntentParameterFixedSizeArray 1 + INIntentParameterMetadata + + INIntentParameterMetadataCapitalization + Sentences + INIntentParameterMetadataDefaultValueID + SNXOJo + INIntentParameterName accounts - INIntentParameterObjectType - MultiFollowAccountsSmall - INIntentParameterObjectTypeNamespace - 88xZPY INIntentParameterPromptDialogs INIntentParameterPromptDialogCustom + INIntentParameterPromptDialogFormatString + Enter username + INIntentParameterPromptDialogFormatStringID + 3d6HSO INIntentParameterPromptDialogType Configuration @@ -271,13 +278,37 @@ INIntentParameterPromptDialogType Primary + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + There are ${count} options matching ‘${accounts}’. + INIntentParameterPromptDialogFormatStringID + 3nWfxd + INIntentParameterPromptDialogType + DisambiguationIntroduction + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogFormatString + Just to confirm, you wanted ‘${accounts}’? + INIntentParameterPromptDialogFormatStringID + IP6ujX + INIntentParameterPromptDialogType + Confirmation + + INIntentParameterSupportsDynamicEnumeration + INIntentParameterSupportsMultipleValues + INIntentParameterSupportsSearch + INIntentParameterTag - 5 + 6 INIntentParameterType - Object + String INIntentResponse @@ -295,9 +326,32 @@ failure + INIntentResponseLastParameterTag + 4 + INIntentResponseOutput + username + INIntentResponseParameters + + + INIntentResponseParameterDisplayName + Username + INIntentResponseParameterDisplayNameID + 7DZrRA + INIntentResponseParameterDisplayPriority + 1 + INIntentResponseParameterName + username + INIntentResponseParameterSupportsMultipleValues + + INIntentResponseParameterTag + 4 + INIntentResponseParameterType + Object + + INIntentTitle - Multi Follow Count Small + Multi Followers Count Small INIntentTitleID e0W2wo INIntentType @@ -307,86 +361,6 @@ INTypes - - - INTypeDisplayName - Account - INTypeDisplayNameID - LUrJ3D - INTypeLastPropertyTag - 101 - INTypeName - MultiFollowAccountsSmall - INTypeProperties - - - INTypePropertyDefault - - INTypePropertyDisplayPriority - 1 - INTypePropertyName - identifier - INTypePropertyTag - 1 - INTypePropertyType - String - - - INTypePropertyDefault - - INTypePropertyDisplayPriority - 2 - INTypePropertyName - displayString - INTypePropertyTag - 2 - INTypePropertyType - String - - - INTypePropertyDefault - - INTypePropertyDisplayPriority - 3 - INTypePropertyName - pronunciationHint - INTypePropertyTag - 3 - INTypePropertyType - String - - - INTypePropertyDefault - - INTypePropertyDisplayPriority - 4 - INTypePropertyName - alternativeSpeakableMatches - INTypePropertySupportsMultipleValues - - INTypePropertyTag - 4 - INTypePropertyType - SpeakableString - - - INTypePropertyDisplayName - username - INTypePropertyDisplayNameID - TQNJZz - INTypePropertyDisplayPriority - 5 - INTypePropertyName - property - INTypePropertySupportsMultipleValues - - INTypePropertyTag - 101 - INTypePropertyType - String - - - - + diff --git a/WidgetExtension/WidgetExtensionBundle.swift b/WidgetExtension/WidgetExtensionBundle.swift index 5c5ea71ba..b8f519328 100644 --- a/WidgetExtension/WidgetExtensionBundle.swift +++ b/WidgetExtension/WidgetExtensionBundle.swift @@ -7,5 +7,6 @@ import SwiftUI struct WidgetExtensionBundle: WidgetBundle { var body: some Widget { FollowersCountWidget() + MultiFollowersCountWidget() } } From 15b6b9fb9333a331e803f9c3c84a52a2f4dcaf53 Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Tue, 31 Jan 2023 14:54:05 +0100 Subject: [PATCH 10/24] feat(Widget): Implement MultiFollowersCountWidget for medium size --- .../MultiFollowersCountWidget.swift | 2 +- .../MultiFollowersCountWidgetView.swift | 42 ++++++++++++++++++- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/WidgetExtension/Variants/MultiFollowersCount/MultiFollowersCountWidget.swift b/WidgetExtension/Variants/MultiFollowersCount/MultiFollowersCountWidget.swift index 1dc4ec7e7..050c8bb47 100644 --- a/WidgetExtension/Variants/MultiFollowersCount/MultiFollowersCountWidget.swift +++ b/WidgetExtension/Variants/MultiFollowersCount/MultiFollowersCountWidget.swift @@ -56,7 +56,7 @@ struct MultiFollowersCountEntry: TimelineEntry { struct MultiFollowersCountWidget: Widget { private var availableFamilies: [WidgetFamily] { - return [.systemSmall] + return [.systemSmall, .systemMedium] } var body: some WidgetConfiguration { diff --git a/WidgetExtension/Variants/MultiFollowersCount/MultiFollowersCountWidgetView.swift b/WidgetExtension/Variants/MultiFollowersCount/MultiFollowersCountWidgetView.swift index 4fb374ea4..a73500d27 100644 --- a/WidgetExtension/Variants/MultiFollowersCount/MultiFollowersCountWidgetView.swift +++ b/WidgetExtension/Variants/MultiFollowersCount/MultiFollowersCountWidgetView.swift @@ -13,7 +13,9 @@ struct MultiFollowersCountWidgetView: View { if let accounts = entry.accounts { switch family { case .systemSmall: - viewForSmallWidgetNoChart(accounts) + viewForSmallWidget(accounts) + case .systemMedium: + viewForMediumWidget(accounts) default: Text("Sorry but this Widget family is unsupported.") } @@ -25,7 +27,7 @@ struct MultiFollowersCountWidgetView: View { } } - private func viewForSmallWidgetNoChart(_ accounts: [FollowersEntryAccountable]) -> some View { + private func viewForSmallWidget(_ accounts: [FollowersEntryAccountable]) -> some View { VStack(alignment: .leading, spacing: 0) { ForEach(accounts, id: \.acct) { account in HStack { @@ -55,4 +57,40 @@ struct MultiFollowersCountWidgetView: View { } .padding(.vertical, 16) } + + private func viewForMediumWidget(_ accounts: [FollowersEntryAccountable]) -> some View { + VStack(alignment: .leading, spacing: 0) { + LazyVGrid(columns: [ + GridItem(.flexible()), + GridItem(.flexible()) + ]) { + ForEach(accounts, id: \.acct) { account in + HStack { + if let avatarImage = account.avatarImage { + Image(uiImage: avatarImage) + .resizable() + .frame(width: 32, height: 32) + .cornerRadius(5) + } + VStack(alignment: .leading) { + Text(account.followersCount.asAbbreviatedCountString()) + .font(.title2) + .lineLimit(1) + .truncationMode(.tail) + + Text("@\(account.acct)") + .font(.caption2) + .foregroundColor(.secondary) + .lineLimit(1) + .truncationMode(.tail) + } + Spacer() + } + .padding(.leading, 20) + } + } + Spacer() + } + .padding(.vertical, 16) + } } From 8438bbc03203c9b5522ae7145c241689b5bfb4ec Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Tue, 31 Jan 2023 14:55:59 +0100 Subject: [PATCH 11/24] chore(Widget): Rename MultiFollowersCountSmallIntent -> MultiFollowersCountIntent --- Mastodon/Info.plist | 2 +- .../Handler/MultiFollowersCountIntentHandler.swift | 4 ++-- MastodonIntent/Info.plist | 2 +- MastodonIntent/IntentHandler.swift | 2 +- .../MultiFollowersCountWidget.swift | 14 +++++++------- WidgetExtension/WidgetExtension.intentdefinition | 4 ++-- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Mastodon/Info.plist b/Mastodon/Info.plist index 99b66b264..7cd0704b8 100644 --- a/Mastodon/Info.plist +++ b/Mastodon/Info.plist @@ -62,7 +62,7 @@ NSUserActivityTypes FollowersCountIntent - MultiFollowersCountSmallIntent + MultiFollowersCountIntent SendPostIntent UIApplicationSceneManifest diff --git a/MastodonIntent/Handler/MultiFollowersCountIntentHandler.swift b/MastodonIntent/Handler/MultiFollowersCountIntentHandler.swift index 984a4e39d..785949e2d 100644 --- a/MastodonIntent/Handler/MultiFollowersCountIntentHandler.swift +++ b/MastodonIntent/Handler/MultiFollowersCountIntentHandler.swift @@ -6,8 +6,8 @@ import MastodonCore import MastodonSDK import MastodonLocalization -class MultiFollowersCountIntentHandler: INExtension, MultiFollowersCountSmallIntentHandling { - func provideAccountsOptionsCollection(for intent: MultiFollowersCountSmallIntent, searchTerm: String?) async throws -> INObjectCollection { +class MultiFollowersCountIntentHandler: INExtension, MultiFollowersCountIntentHandling { + func provideAccountsOptionsCollection(for intent: MultiFollowersCountIntent, searchTerm: String?) async throws -> INObjectCollection { guard let searchTerm = searchTerm, let authenticationBox = WidgetExtension.appContext diff --git a/MastodonIntent/Info.plist b/MastodonIntent/Info.plist index 457385c05..06804cb31 100644 --- a/MastodonIntent/Info.plist +++ b/MastodonIntent/Info.plist @@ -31,7 +31,7 @@ IntentsSupported FollowersCountIntent - MultiFollowersCountSmallIntent + MultiFollowersCountIntent SendPostIntent diff --git a/MastodonIntent/IntentHandler.swift b/MastodonIntent/IntentHandler.swift index 4b13e5839..c8fb67d4c 100644 --- a/MastodonIntent/IntentHandler.swift +++ b/MastodonIntent/IntentHandler.swift @@ -17,7 +17,7 @@ class IntentHandler: INExtension { return SendPostIntentHandler() case is FollowersCountIntent: return FollowersCountIntentHandler() - case is MultiFollowersCountSmallIntent: + case is MultiFollowersCountIntent: return MultiFollowersCountIntentHandler() default: return self diff --git a/WidgetExtension/Variants/MultiFollowersCount/MultiFollowersCountWidget.swift b/WidgetExtension/Variants/MultiFollowersCount/MultiFollowersCountWidget.swift index 050c8bb47..ca3d3efc0 100644 --- a/WidgetExtension/Variants/MultiFollowersCount/MultiFollowersCountWidget.swift +++ b/WidgetExtension/Variants/MultiFollowersCount/MultiFollowersCountWidget.swift @@ -10,14 +10,14 @@ struct MultiFollowersCountWidgetProvider: IntentTimelineProvider { .placeholder } - func getSnapshot(for configuration: MultiFollowersCountSmallIntent, in context: Context, completion: @escaping (MultiFollowersCountEntry) -> ()) { + func getSnapshot(for configuration: MultiFollowersCountIntent, in context: Context, completion: @escaping (MultiFollowersCountEntry) -> ()) { guard !context.isPreview else { return completion(.placeholder) } loadCurrentEntry(for: configuration, in: context, completion: completion) } - func getTimeline(for configuration: MultiFollowersCountSmallIntent, in context: Context, completion: @escaping (Timeline) -> ()) { + func getTimeline(for configuration: MultiFollowersCountIntent, in context: Context, completion: @escaping (Timeline) -> ()) { loadCurrentEntry(for: configuration, in: context) { entry in completion(Timeline(entries: [entry], policy: .after(.now))) } @@ -27,7 +27,7 @@ struct MultiFollowersCountWidgetProvider: IntentTimelineProvider { struct MultiFollowersCountEntry: TimelineEntry { let date: Date let accounts: [FollowersEntryAccountable]? - let configuration: MultiFollowersCountSmallIntent + let configuration: MultiFollowersCountIntent static var placeholder: Self { MultiFollowersCountEntry( @@ -41,7 +41,7 @@ struct MultiFollowersCountEntry: TimelineEntry { domain: "mastodon" ) ], - configuration: MultiFollowersCountSmallIntent() + configuration: MultiFollowersCountIntent() ) } @@ -49,7 +49,7 @@ struct MultiFollowersCountEntry: TimelineEntry { MultiFollowersCountEntry( date: .now, accounts: [], - configuration: MultiFollowersCountSmallIntent() + configuration: MultiFollowersCountIntent() ) } } @@ -60,7 +60,7 @@ struct MultiFollowersCountWidget: Widget { } var body: some WidgetConfiguration { - IntentConfiguration(kind: "Multiple followers", intent: MultiFollowersCountSmallIntent.self, provider: MultiFollowersCountWidgetProvider()) { entry in + IntentConfiguration(kind: "Multiple followers", intent: MultiFollowersCountIntent.self, provider: MultiFollowersCountWidgetProvider()) { entry in MultiFollowersCountWidgetView(entry: entry) } .configurationDisplayName("Multiple followers") @@ -70,7 +70,7 @@ struct MultiFollowersCountWidget: Widget { } private extension MultiFollowersCountWidgetProvider { - func loadCurrentEntry(for configuration: MultiFollowersCountSmallIntent, in context: Context, completion: @escaping (MultiFollowersCountEntry) -> Void) { + func loadCurrentEntry(for configuration: MultiFollowersCountIntent, in context: Context, completion: @escaping (MultiFollowersCountEntry) -> Void) { Task { guard let authBox = WidgetExtension.appContext diff --git a/WidgetExtension/WidgetExtension.intentdefinition b/WidgetExtension/WidgetExtension.intentdefinition index a2b148563..394b736b9 100644 --- a/WidgetExtension/WidgetExtension.intentdefinition +++ b/WidgetExtension/WidgetExtension.intentdefinition @@ -186,7 +186,7 @@ INIntentLastParameterTag 6 INIntentName - MultiFollowersCountSmall + MultiFollowersCount INIntentParameters @@ -351,7 +351,7 @@ INIntentTitle - Multi Followers Count Small + Multi Followers Count INIntentTitleID e0W2wo INIntentType From d685b9e3654d930a9fe1c3c12b7145430ce5dae8 Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Mon, 6 Feb 2023 11:39:40 +0100 Subject: [PATCH 12/24] Add LatestFollowersWidget --- Mastodon.xcodeproj/project.pbxproj | 26 ++- Mastodon/Info.plist | 1 + .../CoreDataStack/Extension/Collection.swift | 6 + .../BrandIconColored.imageset/Contents.json | 23 +++ .../BrandIconColored.imageset/Logo.png | Bin 0 -> 576 bytes .../BrandIconColored.imageset/Logo@2x.png | Bin 0 -> 1036 bytes .../BrandIconColored.imageset/Logo@3x.png | Bin 0 -> 1444 bytes .../FollowersCountHistory.swift | 4 +- .../LatestFollowersWidget.swift | 175 ++++++++++++++++++ .../LatestFollowersWidgetView.swift | 132 +++++++++++++ .../WidgetExtension.intentdefinition | 56 ++++-- WidgetExtension/WidgetExtensionBundle.swift | 1 + 12 files changed, 397 insertions(+), 27 deletions(-) create mode 100644 WidgetExtension/Assets.xcassets/BrandIconColored.imageset/Contents.json create mode 100644 WidgetExtension/Assets.xcassets/BrandIconColored.imageset/Logo.png create mode 100644 WidgetExtension/Assets.xcassets/BrandIconColored.imageset/Logo@2x.png create mode 100644 WidgetExtension/Assets.xcassets/BrandIconColored.imageset/Logo@3x.png create mode 100644 WidgetExtension/Variants/LatestFollowers/LatestFollowersWidget.swift create mode 100644 WidgetExtension/Variants/LatestFollowers/LatestFollowersWidgetView.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 85cc5e0a6..fa1ec86e3 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -49,7 +49,6 @@ 2A728130297EA9D8004138C5 /* WidgetExtension.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 2A72812C297EA9D7004138C5 /* WidgetExtension.intentdefinition */; }; 2A728131297EA9D8004138C5 /* WidgetExtension.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 2A72812C297EA9D7004138C5 /* WidgetExtension.intentdefinition */; }; 2A728134297EA9D8004138C5 /* WidgetExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 2A728120297EA9D7004138C5 /* WidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - 2A72813B297EC6F7004138C5 /* MastodonSDKDynamic in Frameworks */ = {isa = PBXBuildFile; productRef = 2A72813A297EC6F7004138C5 /* MastodonSDKDynamic */; }; 2A72813F297EC762004138C5 /* WidgetExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A72813E297EC762004138C5 /* WidgetExtension.swift */; }; 2A76F75C2930D94700B3388D /* HashtagTimelineHeaderViewActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A76F75B2930D94700B3388D /* HashtagTimelineHeaderViewActionButton.swift */; }; 2A82294F29262EE000D2A1F7 /* AppContext+NextAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A82294E29262EE000D2A1F7 /* AppContext+NextAccount.swift */; }; @@ -57,6 +56,9 @@ 2A86A14929892B3A007F1062 /* MultiFollowersCountWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A86A14829892B3A007F1062 /* MultiFollowersCountWidget.swift */; }; 2A86A14B2989326E007F1062 /* MultiFollowersCountWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A86A14A2989326E007F1062 /* MultiFollowersCountWidgetView.swift */; }; 2A90A157296EEE500026C155 /* MastodonSDKDynamic in Frameworks */ = {isa = PBXBuildFile; productRef = 2A90A156296EEE500026C155 /* MastodonSDKDynamic */; }; + 2A9D0664298C048800BF38CB /* LatestFollowersWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A9D0663298C048800BF38CB /* LatestFollowersWidget.swift */; }; + 2A9D0666298C05A800BF38CB /* LatestFollowersWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A9D0665298C05A800BF38CB /* LatestFollowersWidgetView.swift */; }; + 2A9D066F298D0FD100BF38CB /* MastodonSDKDynamic in Frameworks */ = {isa = PBXBuildFile; productRef = 2A9D066E298D0FD100BF38CB /* MastodonSDKDynamic */; }; 2AB12E4629362F27006BC925 /* DataSourceFacade+Translate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AB12E4529362F27006BC925 /* DataSourceFacade+Translate.swift */; }; 2AE202AA297FE10B00F66E55 /* WidgetExtension.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 2A72812C297EA9D7004138C5 /* WidgetExtension.intentdefinition */; }; 2AE202AC297FE19100F66E55 /* FollowersCountIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AE202AB297FE19100F66E55 /* FollowersCountIntentHandler.swift */; }; @@ -648,6 +650,8 @@ 2A86A14529892944007F1062 /* MultiFollowersCountIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiFollowersCountIntentHandler.swift; sourceTree = ""; }; 2A86A14829892B3A007F1062 /* MultiFollowersCountWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiFollowersCountWidget.swift; sourceTree = ""; }; 2A86A14A2989326E007F1062 /* MultiFollowersCountWidgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiFollowersCountWidgetView.swift; sourceTree = ""; }; + 2A9D0663298C048800BF38CB /* LatestFollowersWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LatestFollowersWidget.swift; sourceTree = ""; }; + 2A9D0665298C05A800BF38CB /* LatestFollowersWidgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LatestFollowersWidgetView.swift; sourceTree = ""; }; 2AB12E4529362F27006BC925 /* DataSourceFacade+Translate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Translate.swift"; sourceTree = ""; }; 2AE202A9297FDDF500F66E55 /* WidgetExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WidgetExtension.entitlements; sourceTree = ""; }; 2AE202AB297FE19100F66E55 /* FollowersCountIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowersCountIntentHandler.swift; sourceTree = ""; }; @@ -1216,7 +1220,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 2A72813B297EC6F7004138C5 /* MastodonSDKDynamic in Frameworks */, + 2A9D066F298D0FD100BF38CB /* MastodonSDKDynamic in Frameworks */, 2A728124297EA9D7004138C5 /* SwiftUI.framework in Frameworks */, 2A728122297EA9D7004138C5 /* WidgetKit.framework in Frameworks */, ); @@ -1458,6 +1462,7 @@ children = ( 2A86A14429892709007F1062 /* FollowersCount */, 2A86A14729892B1B007F1062 /* MultiFollowersCount */, + 2A9D0662298C045000BF38CB /* LatestFollowers */, ); path = Variants; sourceTree = ""; @@ -1481,6 +1486,15 @@ path = MultiFollowersCount; sourceTree = ""; }; + 2A9D0662298C045000BF38CB /* LatestFollowers */ = { + isa = PBXGroup; + children = ( + 2A9D0663298C048800BF38CB /* LatestFollowersWidget.swift */, + 2A9D0665298C05A800BF38CB /* LatestFollowersWidgetView.swift */, + ); + path = LatestFollowers; + sourceTree = ""; + }; 2D152A8A25C295B8009AA50C /* Content */ = { isa = PBXGroup; children = ( @@ -3019,7 +3033,7 @@ ); name = WidgetExtension; packageProductDependencies = ( - 2A72813A297EC6F7004138C5 /* MastodonSDKDynamic */, + 2A9D066E298D0FD100BF38CB /* MastodonSDKDynamic */, ); productName = WidgetExtensionExtension; productReference = 2A728120297EA9D7004138C5 /* WidgetExtension.appex */; @@ -3501,6 +3515,7 @@ 2A33063729880835001D4C51 /* DataRepresentable.swift in Sources */, 2A33062D2987DBFA001D4C51 /* FollowersCountHistory.swift in Sources */, 2A33063829880835001D4C51 /* LineChart.swift in Sources */, + 2A9D0666298C05A800BF38CB /* LatestFollowersWidgetView.swift in Sources */, 2A728130297EA9D8004138C5 /* WidgetExtension.intentdefinition in Sources */, 2A86A14B2989326E007F1062 /* MultiFollowersCountWidgetView.swift in Sources */, 2A72813F297EC762004138C5 /* WidgetExtension.swift in Sources */, @@ -3509,6 +3524,7 @@ 2A33063629880835001D4C51 /* Math.swift in Sources */, 2A86A14929892B3A007F1062 /* MultiFollowersCountWidget.swift in Sources */, 2A33AB662982C4AF008A7FB1 /* FollowersCountWidgetView.swift in Sources */, + 2A9D0664298C048800BF38CB /* LatestFollowersWidget.swift in Sources */, 2A728127297EA9D7004138C5 /* WidgetExtensionBundle.swift in Sources */, 2A72812B297EA9D7004138C5 /* FollowersCountWidget.swift in Sources */, 2A33063929880835001D4C51 /* CurvedChart.swift in Sources */, @@ -5315,11 +5331,11 @@ /* End XCConfigurationList section */ /* Begin XCSwiftPackageProductDependency section */ - 2A72813A297EC6F7004138C5 /* MastodonSDKDynamic */ = { + 2A90A156296EEE500026C155 /* MastodonSDKDynamic */ = { isa = XCSwiftPackageProductDependency; productName = MastodonSDKDynamic; }; - 2A90A156296EEE500026C155 /* MastodonSDKDynamic */ = { + 2A9D066E298D0FD100BF38CB /* MastodonSDKDynamic */ = { isa = XCSwiftPackageProductDependency; productName = MastodonSDKDynamic; }; diff --git a/Mastodon/Info.plist b/Mastodon/Info.plist index 7cd0704b8..0e0deb2cc 100644 --- a/Mastodon/Info.plist +++ b/Mastodon/Info.plist @@ -63,6 +63,7 @@ FollowersCountIntent MultiFollowersCountIntent + LatestFollowersIntent SendPostIntent UIApplicationSceneManifest diff --git a/MastodonSDK/Sources/CoreDataStack/Extension/Collection.swift b/MastodonSDK/Sources/CoreDataStack/Extension/Collection.swift index a57737d1a..b97e6cce8 100644 --- a/MastodonSDK/Sources/CoreDataStack/Extension/Collection.swift +++ b/MastodonSDK/Sources/CoreDataStack/Extension/Collection.swift @@ -27,3 +27,9 @@ extension Collection where Iterator.Element: NSManagedObject { } } } + +extension Collection { + public subscript (safe index: Index) -> Element? { + return indices.contains(index) ? self[index] : nil + } +} diff --git a/WidgetExtension/Assets.xcassets/BrandIconColored.imageset/Contents.json b/WidgetExtension/Assets.xcassets/BrandIconColored.imageset/Contents.json new file mode 100644 index 000000000..c75069e2a --- /dev/null +++ b/WidgetExtension/Assets.xcassets/BrandIconColored.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "Logo.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Logo@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Logo@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/WidgetExtension/Assets.xcassets/BrandIconColored.imageset/Logo.png b/WidgetExtension/Assets.xcassets/BrandIconColored.imageset/Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..31db32f7a3a7b39256dd4b6798ddf104226bdb0f GIT binary patch literal 576 zcmV-G0>Axqs51IaQqrJaWsPNCtTS16RXHfV>wMA8OF^WS2Ug(S(q05Dtv`ul0$g5ph%~TZ@#@BgJUh?M0DXN%9~?Bc<_$1D)^RKpfaR4GnQQUl#M;P+A!#&# zqhk|6iKDx}@7Vqn%@0aLDI!}TaWayQV0X`?wCx$*LPjP2mCU4maoLE#-pBrzJ- zM&2~L?|ql37btG90N(&BYs1_;vca96Nz`m26U153#H2pu_kUuL6Ou=YoGkQ9$SEd& zq6uRX(N(-5i=bSU0b*sq4aU7P&CDAivPd zW%L3>?2(GZ)|MzXU$v5%mpSUv;EOAZUm7@ zM64MVK!ypVQozVad)fXz@coDL-Q+7^YRV?nic%R^U-xeWgLq6U{6GMi)m3jYR^^kx z(z|AU>NPMs+fJtbueA@<%fs-6P%y4gKaKhsa7%}V5Qumj7#p)IWcwg6(b+ft<+S!` z5sEdhLau--a)b47abdT#-V;ou+%gngUM}0QyLUOy;phlnjG{~^>QQ6~*p1KY%^xx0 zr6rpfr#Ud(TCs*|%{h$mm=7V?)84a(cviLI@X+}auGlST#@?P&xNOJbZ^hsse4!vD zc&Oa)YUSzQ_IFIG6<(t6n2;ab)A;f~x zr#tgOn`apCO5GJ9zZLg3ASa;6n`>8u?h>@Qo+_&)?;pGe+7Mx4I*VYjh z$26=vxn}&Pg|0)`tqOfn>>kXFXP@Z(VQhkEBo)L}_vigRE{SyfG z%NXSH#;HXy!9iNifo9!?xNkz5Hy=jr_QR-3{N{zP`V@J6L-^{}A|y@Z$pv|f5ZK8G z9rp-pbETQKix68#kPmmKge)Dwo_2wRfcN!FfqNyy*vX0ws`1+m_FAQi19ra=0(=9P zPRkfNfMe50CZ$G8zyT%0{G>p-%hPb5z_iv9O<|D>ykO#_%b0cTN21=&h?fY7wq2}7 zYJ-xYL>{7sjtI4#7+Fu-S&^icrXadspG1e}E#D(7PjA{^67<8BTN2|_#O9TXnNB_u zVGUWxCTU9*80Y&WX)mN&qjEaNC<+Q>b3zN?ZqU_l5Uv4my+oU8eyw}}0000yWIRa`@((!BdnUN^3^s_rAegjdq7c;&Ci-g7o{*`aH}TkG~M!YY_=<-A>VBqr%I#2-)ZdZs5%m$Obb>jxafQ=O{v-V0eZ3NP(c(#MH1~`w+Qx=VOY%7{|F|BojFe1Gc0Rv; zgH-h69s;KUv0BX$=2$Bsw`YN+C183QxOMCLwUZOz_haDrIQ||B3qY}${vH98%4J<} zoX1*)flUIw`VzQzFTR{KZm&uUyuNvD>5KM*i9@XS=uz-J!l0<{b8eq3n^sy~0Tvg{ z%1%wuDz||bFW4(CI_FJA31Z3OJWyw(cz$kHhsypE=TR~&?lkDm9V9}!s?_t4`lg~h zWc;m^&CAnbMT>#$oJnE1dC~@XNVgS%f?s43#PX&OmE)>lL7Rn5CSxiJXIy^X?w)B` zNYGpIbQ?3uVDTbPEDZ5n;=#_&fXT^D_w$R8`kvMN9RA#{FXKn*N;!E5VQr9m#yS$2 ztSKtiVlse(8s&-F-Rpdp28zH}^oS21UU%c&E?w&R-MLry4L#wv-vs^~W@pVjq&SaQ zVbh<6Uy~;ugt+OWqwsn%6@<-EFJHC=2H~H`qp;8e^5aoYFKTj?#z!H-gD@SVM#3gT zZWcGMh}FW%oc$SD)qKthp(l%(5Z1u+cNi!G7Z%39?=es;y88yQM{{Tv8z?HP!Z5@! zHfSvO)pF(#i)B_==8%nyJO+%!i`~Jm&}(E7hX^~x06F>vLm`u0kt^tbWCfi$=fQPz zJ(KQUTNH*2y89M|<+;z>R52q>7L<+UK(5 zfP!jK1+TUvxU2a$(j=@XJcf-LZC~OK&hhya(T~Iz-A)waU%kbllH7MI! z!b;sIs@qNy$xtW$t0?jE(n&K)K3Q`w%1M41F|LHkL`|zbMhY%_$PpP7*W!9Ma2qLH zza)*Uuj?X4h-<N}eXl4% zB`w!XZMil@iU=HgCYcJo5cyJImvI=M-aN)ImW+L`C}E6bZt}^enhMfj_uHQq`9+03>4PVJj}cHN01HM4;6|$++)#E8E8_J#z!dDmSt y0$SI0000 LatestFollowersEntry { + .placeholder + } + + func getSnapshot(for configuration: LatestFollowersIntent, in context: Context, completion: @escaping (LatestFollowersEntry) -> ()) { + guard !context.isPreview else { + return completion(.placeholder) + } + loadCurrentEntry(for: configuration, in: context, completion: completion) + } + + func getTimeline(for configuration: LatestFollowersIntent, in context: Context, completion: @escaping (Timeline) -> ()) { + loadCurrentEntry(for: configuration, in: context) { entry in + completion(Timeline(entries: [entry], policy: .after(.now))) + } + } +} + +struct LatestFollowersEntry: TimelineEntry { + let date: Date + let accounts: [LatestFollowersEntryAccountable]? + let configuration: LatestFollowersIntent + + static var placeholder: Self { + LatestFollowersEntry( + date: .now, + accounts: [ + LatestFollowersEntryAccount( + note: "Just another Mastodon user", + displayNameWithFallback: "Mastodon", + acct: "mastodon", + avatarImage: UIImage(named: "missingAvatar")!, + domain: "mastodon" + ), + LatestFollowersEntryAccount( + note: "Yet another Mastodon user", + displayNameWithFallback: "Mastodon", + acct: "mastodon", + avatarImage: UIImage(named: "missingAvatar")!, + domain: "mastodon" + ) + ], + configuration: LatestFollowersIntent() + ) + } + + static var unconfigured: Self { + LatestFollowersEntry( + date: .now, + accounts: [], + configuration: LatestFollowersIntent() + ) + } +} + +struct LatestFollowersWidget: Widget { + private var availableFamilies: [WidgetFamily] { + return [.systemSmall, .systemMedium] + } + + var body: some WidgetConfiguration { + IntentConfiguration(kind: "Latest followers", intent: LatestFollowersIntent.self, provider: LatestFollowersWidgetProvider()) { entry in + LatestFollowersWidgetView(entry: entry) + } + .configurationDisplayName("Latest followers") + .description("Show latest followers.") + .supportedFamilies(availableFamilies) + } +} + +private extension LatestFollowersWidgetProvider { + func loadCurrentEntry(for configuration: LatestFollowersIntent, in context: Context, completion: @escaping (LatestFollowersEntry) -> Void) { + Task { @MainActor in + guard + let authBox = WidgetExtension.appContext + .authenticationService + .mastodonAuthenticationBoxes + .first + else { + return completion(.unconfigured) + } + +// guard let desiredAccount: String = { +// guard let account = authBox.authenticationRecord.object(in: WidgetExtension.appContext.managedObjectContext)?.user.acct else { +// return nil +// } +// return account +// }() else { +// return completion(.unconfigured) +// } + + + var accounts = [LatestFollowersEntryAccountable]() + + let followers = try await WidgetExtension.appContext + .apiService + .followers(userID: authBox.userID, maxID: nil, authenticationBox: authBox) + .value + .prefix(2) // X most recent followers + + for follower in followers { + let imageData = try await URLSession.shared.data(from: follower.avatarImageURLWithFallback(domain: authBox.domain)).0 + + accounts.append( + LatestFollowersEntryAccount( + note: follower.note, + displayNameWithFallback: follower.displayNameWithFallback, + acct: follower.acct, + avatarImage: UIImage(data: imageData) ?? UIImage(named: "missingAvatar")!, + domain: authBox.domain + ) + ) + } + + let entry = LatestFollowersEntry( + date: Date(), + accounts: accounts, + configuration: configuration + ) + + completion(entry) + +// for desiredAccount in desiredAccounts { +// let resultingAccount = try await WidgetExtension.appContext +// .apiService +// .search(query: .init(q: desiredAccount, type: .accounts), authenticationBox: authBox) +// .value +// .accounts +// .first! +// +// let imageData = try await URLSession.shared.data(from: resultingAccount.avatarImageURLWithFallback(domain: authBox.domain)).0 +// +// accounts.append(FollowersEntryAccount.from( +// mastodonAccount: resultingAccount, +// domain: authBox.domain, +// avatarImage: UIImage(data: imageData) ?? UIImage(named: "missingAvatar")! +// )) +// } + } + } +} + +protocol LatestFollowersEntryAccountable { + var note: String { get } + var displayNameWithFallback: String { get } + var acct: String { get } + var avatarImage: UIImage { get } + var domain: String { get } +} + +struct LatestFollowersEntryAccount: LatestFollowersEntryAccountable { + let note: String + let displayNameWithFallback: String + let acct: String + let avatarImage: UIImage + let domain: String + + static func from(mastodonAccount: Mastodon.Entity.Account, domain: String, avatarImage: UIImage) -> Self { + LatestFollowersEntryAccount( + note: mastodonAccount.header, + displayNameWithFallback: mastodonAccount.displayNameWithFallback, + acct: mastodonAccount.acct, + avatarImage: avatarImage, + domain: domain + ) + } +} diff --git a/WidgetExtension/Variants/LatestFollowers/LatestFollowersWidgetView.swift b/WidgetExtension/Variants/LatestFollowers/LatestFollowersWidgetView.swift new file mode 100644 index 000000000..9a8e36c32 --- /dev/null +++ b/WidgetExtension/Variants/LatestFollowers/LatestFollowersWidgetView.swift @@ -0,0 +1,132 @@ +// Copyright © 2023 Mastodon gGmbH. All rights reserved. + +import SwiftUI +import WidgetKit +import MastodonSDK +import MastodonAsset +import MastodonUI + +struct LatestFollowersWidgetView: View { + private let dateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateStyle = .none + formatter.timeStyle = .short + return formatter + }() + + @Environment(\.widgetFamily) var family + + var entry: LatestFollowersWidgetProvider.Entry + + var body: some View { + if let accounts = entry.accounts { + switch family { + case .systemSmall: + viewForSmallWidget(accounts, lastUpdate: entry.date) + case .systemMedium: + viewForMediumWidget(accounts, lastUpdate: entry.date) + default: + Text("Sorry but this Widget family is unsupported.") + } + } else { + Text("Please open Mastodon to log in to an Account.") + .multilineTextAlignment(.center) + .font(.caption) + .padding(.all, 20) + } + } + + private func viewForSmallWidget(_ accounts: [LatestFollowersEntryAccountable], lastUpdate: Date) -> some View { + VStack(alignment: .leading, spacing: 0) { + ForEach(accounts, id: \.acct) { account in + HStack { + if let avatarImage = account.avatarImage { + Image(uiImage: avatarImage) + .resizable() + .frame(width: 32, height: 32) + .cornerRadius(5) + } + VStack(alignment: .leading) { + Text(account.note) + .font(.caption) + .lineLimit(1) + .truncationMode(.tail) + + Text("@\(account.acct)") + .font(.caption2) + .foregroundColor(.secondary) + .lineLimit(1) + .truncationMode(.tail) + } + Spacer() + } + .padding(.leading, 20) + } + Spacer() + } + .padding(.vertical, 16) + } + + private func viewForMediumWidget(_ accounts: [LatestFollowersEntryAccountable], lastUpdate: Date) -> some View { + VStack(alignment: .leading) { + HStack { + Text("Latest followers") + .font(.system(size: UIFontMetrics.default.scaledValue(for: 16))) + Spacer() + Image("BrandIconColored") + } + + ForEach(accounts, id: \.acct) { account in + HStack { + if let avatarImage = account.avatarImage { + Image(uiImage: avatarImage) + .resizable() + .frame(width: 32, height: 32) + .cornerRadius(5) + } + VStack(alignment: .leading) { + + HStack { + Text(account.displayNameWithFallback) + .font(.footnote.bold()) + .foregroundColor(.secondary) + .lineLimit(1) + .truncationMode(.tail) + + Text("@\(account.acct)") + .font(.caption2) + .foregroundColor(.secondary) + .lineLimit(1) + .truncationMode(.tail) + } + + Text(account.noteWithoutHtmlTags!) + .font(.caption) + .lineLimit(1) + .truncationMode(.tail) + } + Spacer() + } + } + Spacer() + Text("Last update: \(dateFormatter.string(from: lastUpdate))") + .font(.caption2) + .foregroundColor(.secondary) + } + .padding(.horizontal, 20) + .padding(.vertical, 16) + } +} + +private extension LatestFollowersEntryAccountable { + var noteWithoutHtmlTags: String? { + do { + let regex = "<[^>]+>" + let expr = try NSRegularExpression(pattern: regex, options: NSRegularExpression.Options.caseInsensitive) + let result = expr.stringByReplacingMatches(in: note, options: [], range: NSMakeRange(0, note.count), withTemplate: "") + return result + } catch { + return nil + } + } +} diff --git a/WidgetExtension/WidgetExtension.intentdefinition b/WidgetExtension/WidgetExtension.intentdefinition index 394b736b9..b4f48b63f 100644 --- a/WidgetExtension/WidgetExtension.intentdefinition +++ b/WidgetExtension/WidgetExtension.intentdefinition @@ -278,26 +278,6 @@ INIntentParameterPromptDialogType Primary - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogFormatString - There are ${count} options matching ‘${accounts}’. - INIntentParameterPromptDialogFormatStringID - 3nWfxd - INIntentParameterPromptDialogType - DisambiguationIntroduction - - - INIntentParameterPromptDialogCustom - - INIntentParameterPromptDialogFormatString - Just to confirm, you wanted ‘${accounts}’? - INIntentParameterPromptDialogFormatStringID - IP6ujX - INIntentParameterPromptDialogType - Confirmation - INIntentParameterSupportsDynamicEnumeration @@ -359,6 +339,42 @@ INIntentVerb View + + INIntentCategory + information + INIntentDescriptionID + 5KZ2fm + INIntentEligibleForWidgets + + INIntentIneligibleForSuggestions + + INIntentName + LatestFollowers + INIntentResponse + + INIntentResponseCodes + + + INIntentResponseCodeName + success + INIntentResponseCodeSuccess + + + + INIntentResponseCodeName + failure + + + + INIntentTitle + Latest Followers + INIntentTitleID + ZLZ6sg + INIntentType + Custom + INIntentVerb + View + INTypes diff --git a/WidgetExtension/WidgetExtensionBundle.swift b/WidgetExtension/WidgetExtensionBundle.swift index b8f519328..61e383660 100644 --- a/WidgetExtension/WidgetExtensionBundle.swift +++ b/WidgetExtension/WidgetExtensionBundle.swift @@ -8,5 +8,6 @@ struct WidgetExtensionBundle: WidgetBundle { var body: some Widget { FollowersCountWidget() MultiFollowersCountWidget() + LatestFollowersWidget() } } From 3240a36013e471fc9e85edb50ebe107bc93e7e61 Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Mon, 6 Feb 2023 11:46:46 +0100 Subject: [PATCH 13/24] feat(Widget): Implement small latest followers widget --- .../FollowersCount/FollowersCountWidget.swift | 6 ++-- .../LatestFollowersWidget.swift | 35 +++---------------- .../LatestFollowersWidgetView.swift | 31 ++++++++++------ .../MultiFollowersCountWidget.swift | 6 ++-- 4 files changed, 30 insertions(+), 48 deletions(-) diff --git a/WidgetExtension/Variants/FollowersCount/FollowersCountWidget.swift b/WidgetExtension/Variants/FollowersCount/FollowersCountWidget.swift index 3cdff147a..51baf30f0 100644 --- a/WidgetExtension/Variants/FollowersCount/FollowersCountWidget.swift +++ b/WidgetExtension/Variants/FollowersCount/FollowersCountWidget.swift @@ -13,9 +13,6 @@ struct FollowersCountWidgetProvider: IntentTimelineProvider { } func getSnapshot(for configuration: FollowersCountIntent, in context: Context, completion: @escaping (FollowersCountEntry) -> ()) { - guard !context.isPreview else { - return completion(.placeholder) - } loadCurrentEntry(for: configuration, in: context, completion: completion) } @@ -81,6 +78,9 @@ private extension FollowersCountWidgetProvider { .mastodonAuthenticationBoxes .first else { + guard !context.isPreview else { + return completion(.placeholder) + } return completion(.unconfigured) } diff --git a/WidgetExtension/Variants/LatestFollowers/LatestFollowersWidget.swift b/WidgetExtension/Variants/LatestFollowers/LatestFollowersWidget.swift index eb7994091..3b75e619c 100644 --- a/WidgetExtension/Variants/LatestFollowers/LatestFollowersWidget.swift +++ b/WidgetExtension/Variants/LatestFollowers/LatestFollowersWidget.swift @@ -11,9 +11,6 @@ struct LatestFollowersWidgetProvider: IntentTimelineProvider { } func getSnapshot(for configuration: LatestFollowersIntent, in context: Context, completion: @escaping (LatestFollowersEntry) -> ()) { - guard !context.isPreview else { - return completion(.placeholder) - } loadCurrentEntry(for: configuration, in: context, completion: completion) } @@ -85,19 +82,12 @@ private extension LatestFollowersWidgetProvider { .mastodonAuthenticationBoxes .first else { + guard !context.isPreview else { + return completion(.placeholder) + } return completion(.unconfigured) } - -// guard let desiredAccount: String = { -// guard let account = authBox.authenticationRecord.object(in: WidgetExtension.appContext.managedObjectContext)?.user.acct else { -// return nil -// } -// return account -// }() else { -// return completion(.unconfigured) -// } - - + var accounts = [LatestFollowersEntryAccountable]() let followers = try await WidgetExtension.appContext @@ -127,23 +117,6 @@ private extension LatestFollowersWidgetProvider { ) completion(entry) - -// for desiredAccount in desiredAccounts { -// let resultingAccount = try await WidgetExtension.appContext -// .apiService -// .search(query: .init(q: desiredAccount, type: .accounts), authenticationBox: authBox) -// .value -// .accounts -// .first! -// -// let imageData = try await URLSession.shared.data(from: resultingAccount.avatarImageURLWithFallback(domain: authBox.domain)).0 -// -// accounts.append(FollowersEntryAccount.from( -// mastodonAccount: resultingAccount, -// domain: authBox.domain, -// avatarImage: UIImage(data: imageData) ?? UIImage(named: "missingAvatar")! -// )) -// } } } } diff --git a/WidgetExtension/Variants/LatestFollowers/LatestFollowersWidgetView.swift b/WidgetExtension/Variants/LatestFollowers/LatestFollowersWidgetView.swift index 9a8e36c32..929f2a483 100644 --- a/WidgetExtension/Variants/LatestFollowers/LatestFollowersWidgetView.swift +++ b/WidgetExtension/Variants/LatestFollowers/LatestFollowersWidgetView.swift @@ -37,7 +37,10 @@ struct LatestFollowersWidgetView: View { } private func viewForSmallWidget(_ accounts: [LatestFollowersEntryAccountable], lastUpdate: Date) -> some View { - VStack(alignment: .leading, spacing: 0) { + VStack(alignment: .leading) { + Text("Latest followers") + .font(.system(size: UIFontMetrics.default.scaledValue(for: 16))) + ForEach(accounts, id: \.acct) { account in HStack { if let avatarImage = account.avatarImage { @@ -47,23 +50,29 @@ struct LatestFollowersWidgetView: View { .cornerRadius(5) } VStack(alignment: .leading) { - Text(account.note) - .font(.caption) - .lineLimit(1) - .truncationMode(.tail) - Text("@\(account.acct)") - .font(.caption2) - .foregroundColor(.secondary) - .lineLimit(1) - .truncationMode(.tail) + Text(account.displayNameWithFallback) + .font(.footnote.bold()) + .foregroundColor(.secondary) + .lineLimit(1) + .truncationMode(.tail) + + Text("@\(account.acct)") + .font(.caption2) + .foregroundColor(.secondary) + .lineLimit(1) + .truncationMode(.tail) + } Spacer() } - .padding(.leading, 20) } Spacer() + Text("Last update: \(dateFormatter.string(from: lastUpdate))") + .font(.caption2) + .foregroundColor(.secondary) } + .padding(.horizontal, 20) .padding(.vertical, 16) } diff --git a/WidgetExtension/Variants/MultiFollowersCount/MultiFollowersCountWidget.swift b/WidgetExtension/Variants/MultiFollowersCount/MultiFollowersCountWidget.swift index ca3d3efc0..518470a79 100644 --- a/WidgetExtension/Variants/MultiFollowersCount/MultiFollowersCountWidget.swift +++ b/WidgetExtension/Variants/MultiFollowersCount/MultiFollowersCountWidget.swift @@ -11,9 +11,6 @@ struct MultiFollowersCountWidgetProvider: IntentTimelineProvider { } func getSnapshot(for configuration: MultiFollowersCountIntent, in context: Context, completion: @escaping (MultiFollowersCountEntry) -> ()) { - guard !context.isPreview else { - return completion(.placeholder) - } loadCurrentEntry(for: configuration, in: context, completion: completion) } @@ -78,6 +75,9 @@ private extension MultiFollowersCountWidgetProvider { .mastodonAuthenticationBoxes .first else { + guard !context.isPreview else { + return completion(.placeholder) + } return completion(.unconfigured) } From 28e5a8fa90a3ab47cebaaeb02dfd7531ed067ea5 Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Mon, 6 Feb 2023 11:58:23 +0100 Subject: [PATCH 14/24] chore(Widget): Make font dynamic sizeable --- .../FollowersCount/FollowersCountWidgetView.swift | 10 +++++----- .../LatestFollowers/LatestFollowersWidgetView.swift | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/WidgetExtension/Variants/FollowersCount/FollowersCountWidgetView.swift b/WidgetExtension/Variants/FollowersCount/FollowersCountWidgetView.swift index 41648805d..ad35a48c4 100644 --- a/WidgetExtension/Variants/FollowersCount/FollowersCountWidgetView.swift +++ b/WidgetExtension/Variants/FollowersCount/FollowersCountWidgetView.swift @@ -52,7 +52,7 @@ struct FollowersCountWidgetView: View { .truncationMode(.tail) Text(account.displayNameWithFallback) - .font(.system(size: 13)) + .font(.system(size: UIFontMetrics.default.scaledValue(for: 13))) .lineLimit(1) .truncationMode(.tail) @@ -108,7 +108,7 @@ struct FollowersCountWidgetView: View { Spacer() if let increaseCount = followersHistory.increaseCountString(for: account) { Text("\(increaseCount) followers today") - .font(.system(size: 12)) + .font(.system(size: UIFontMetrics.default.scaledValue(for: 12))) .foregroundColor(.secondary) .lineLimit(1) .truncationMode(.tail) @@ -135,12 +135,12 @@ struct FollowersCountWidgetView: View { HStack(alignment: .center) { Image("BrandIcon") Text("FOLLOWERS") - .font(.system(size: 15, weight: .semibold)) + .font(.system(size: UIFontMetrics.default.scaledValue(for: 15), weight: .semibold)) } .padding(.top, 6) Text(account.followersCount.asAbbreviatedCountString()) - .font(.system(size: 43)) + .font(.system(size: UIFontMetrics.default.scaledValue(for: 43))) .lineLimit(1) .truncationMode(.tail) } @@ -157,7 +157,7 @@ struct FollowersCountWidgetView: View { Image("BrandIcon") Text(account.followersCount.asAbbreviatedCountString()) - .font(.system(size: 15)) + .font(.system(size: UIFontMetrics.default.scaledValue(for: 15))) .lineLimit(1) .truncationMode(.tail) } diff --git a/WidgetExtension/Variants/LatestFollowers/LatestFollowersWidgetView.swift b/WidgetExtension/Variants/LatestFollowers/LatestFollowersWidgetView.swift index 929f2a483..839422bdb 100644 --- a/WidgetExtension/Variants/LatestFollowers/LatestFollowersWidgetView.swift +++ b/WidgetExtension/Variants/LatestFollowers/LatestFollowersWidgetView.swift @@ -109,7 +109,7 @@ struct LatestFollowersWidgetView: View { .truncationMode(.tail) } - Text(account.noteWithoutHtmlTags!) + Text(account.noteWithoutHtmlTags ?? "") .font(.caption) .lineLimit(1) .truncationMode(.tail) From b5dca48cb2e7e266ba763baebdf4624b1d6155c6 Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Mon, 6 Feb 2023 12:13:29 +0100 Subject: [PATCH 15/24] feat(Widget): Implement L10n --- .gitignore | 3 +++ Localization/app.json | 14 +++++++++++ .../Generated/Strings.swift | 24 +++++++++++++++++++ .../Resources/Base.lproj/Localizable.strings | 6 +++++ .../FollowersCountWidgetView.swift | 9 +++---- .../LatestFollowersWidgetView.swift | 15 ++++++------ .../MultiFollowersCountWidgetView.swift | 5 ++-- 7 files changed, 62 insertions(+), 14 deletions(-) diff --git a/.gitignore b/.gitignore index 017cfd6fa..907bc4d97 100644 --- a/.gitignore +++ b/.gitignore @@ -131,3 +131,6 @@ env/**/** ## Ruby ### vendor/ .bundle/ + +## IntelliJ IDEA ## +.idea \ No newline at end of file diff --git a/Localization/app.json b/Localization/app.json index 3150c0d24..d6f19a3b0 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -816,5 +816,19 @@ "open_in": { "invalid_link_error": "This doesn't seem to be a valid Mastodon link." } + }, + "widget": { + "common": { + "unsupported_widget_family": "Sorry but this Widget family is unsupported.", + "user_not_logged_in": "Please open Mastodon to log in to an Account." + }, + "followers_count": { + "title": "FOLLOWERS", + "followers_today": "%s followers today" + }, + "latest_followers": { + "title": "Latest followers", + "last_update": "Last update: %s" + } } } diff --git a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift index d78e95231..1cab66ca5 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift +++ b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift @@ -1479,6 +1479,30 @@ public enum L10n { public static let newInMastodon = L10n.tr("Localizable", "Scene.Wizard.NewInMastodon", fallback: "New in Mastodon") } } + public enum Widget { + public enum Common { + /// Sorry but this Widget family is unsupported. + public static let unsupportedWidgetFamily = L10n.tr("Localizable", "Widget.Common.UnsupportedWidgetFamily", fallback: "Sorry but this Widget family is unsupported.") + /// Please open Mastodon to log in to an Account. + public static let userNotLoggedIn = L10n.tr("Localizable", "Widget.Common.UserNotLoggedIn", fallback: "Please open Mastodon to log in to an Account.") + } + public enum FollowersCount { + /// %@ followers today + public static func followersToday(_ p1: Any) -> String { + return L10n.tr("Localizable", "Widget.FollowersCount.FollowersToday", String(describing: p1), fallback: "%@ followers today") + } + /// FOLLOWERS + public static let title = L10n.tr("Localizable", "Widget.FollowersCount.Title", fallback: "FOLLOWERS") + } + public enum LatestFollowers { + /// Last update: %@ + public static func lastUpdate(_ p1: Any) -> String { + return L10n.tr("Localizable", "Widget.LatestFollowers.LastUpdate", String(describing: p1), fallback: "Last update: %@") + } + /// Latest followers + public static let title = L10n.tr("Localizable", "Widget.LatestFollowers.Title", fallback: "Latest followers") + } + } public enum A11y { public enum Plural { public enum Count { diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings index b795752ed..2083555bc 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings @@ -518,3 +518,9 @@ You can’t go wrong with any of our recommend servers, so regardless of which o "Scene.Privacy.Policy.Ios" = "Privacy Policy - Mastodon for iOS"; "Scene.Privacy.Policy.Server" = "Privacy Policy - %@"; "Extension.OpenIn.InvalidLinkError" = "This doesn't seem to be a valid Mastodon link."; +"Widget.Common.UnsupportedWidgetFamily" = "Sorry but this Widget family is unsupported."; +"Widget.Common.UserNotLoggedIn" = "Please open Mastodon to log in to an Account."; +"Widget.FollowersCount.Title" = "FOLLOWERS"; +"Widget.FollowersCount.FollowersToday" = "%@ followers today"; +"Widget.LatestFollowers.Title" = "Latest followers"; +"Widget.LatestFollowers.LastUpdate" = "Last update: %@"; diff --git a/WidgetExtension/Variants/FollowersCount/FollowersCountWidgetView.swift b/WidgetExtension/Variants/FollowersCount/FollowersCountWidgetView.swift index ad35a48c4..288fcdcdc 100644 --- a/WidgetExtension/Variants/FollowersCount/FollowersCountWidgetView.swift +++ b/WidgetExtension/Variants/FollowersCount/FollowersCountWidgetView.swift @@ -3,6 +3,7 @@ import SwiftUI import WidgetKit import MastodonAsset +import MastodonLocalization struct FollowersCountWidgetView: View { private let followersHistory = FollowersCountHistory.shared @@ -25,10 +26,10 @@ struct FollowersCountWidgetView: View { case .accessoryCircular: viewForAccessoryCircular(account) default: - Text("Sorry but this Widget family is unsupported.") + Text(L10n.Widget.Common.unsupportedWidgetFamily) } } else { - Text("Please open Mastodon to log in to an Account.") + Text(L10n.Widget.Common.userNotLoggedIn) .multilineTextAlignment(.center) .font(.caption) .padding(.all, 20) @@ -107,7 +108,7 @@ struct FollowersCountWidgetView: View { VStack(alignment: .leading, spacing: 0) { Spacer() if let increaseCount = followersHistory.increaseCountString(for: account) { - Text("\(increaseCount) followers today") + Text(L10n.Widget.FollowersCount.followersToday(increaseCount)) .font(.system(size: UIFontMetrics.default.scaledValue(for: 12))) .foregroundColor(.secondary) .lineLimit(1) @@ -134,7 +135,7 @@ struct FollowersCountWidgetView: View { VStack(alignment: .leading, spacing: 0) { HStack(alignment: .center) { Image("BrandIcon") - Text("FOLLOWERS") + Text(L10n.Widget.FollowersCount.title) .font(.system(size: UIFontMetrics.default.scaledValue(for: 15), weight: .semibold)) } .padding(.top, 6) diff --git a/WidgetExtension/Variants/LatestFollowers/LatestFollowersWidgetView.swift b/WidgetExtension/Variants/LatestFollowers/LatestFollowersWidgetView.swift index 839422bdb..d1f0493a5 100644 --- a/WidgetExtension/Variants/LatestFollowers/LatestFollowersWidgetView.swift +++ b/WidgetExtension/Variants/LatestFollowers/LatestFollowersWidgetView.swift @@ -5,6 +5,7 @@ import WidgetKit import MastodonSDK import MastodonAsset import MastodonUI +import MastodonLocalization struct LatestFollowersWidgetView: View { private let dateFormatter: DateFormatter = { @@ -26,10 +27,10 @@ struct LatestFollowersWidgetView: View { case .systemMedium: viewForMediumWidget(accounts, lastUpdate: entry.date) default: - Text("Sorry but this Widget family is unsupported.") + Text(L10n.Widget.Common.unsupportedWidgetFamily) } } else { - Text("Please open Mastodon to log in to an Account.") + Text(L10n.Widget.Common.userNotLoggedIn) .multilineTextAlignment(.center) .font(.caption) .padding(.all, 20) @@ -38,7 +39,7 @@ struct LatestFollowersWidgetView: View { private func viewForSmallWidget(_ accounts: [LatestFollowersEntryAccountable], lastUpdate: Date) -> some View { VStack(alignment: .leading) { - Text("Latest followers") + Text(L10n.Widget.LatestFollowers.title) .font(.system(size: UIFontMetrics.default.scaledValue(for: 16))) ForEach(accounts, id: \.acct) { account in @@ -53,7 +54,6 @@ struct LatestFollowersWidgetView: View { Text(account.displayNameWithFallback) .font(.footnote.bold()) - .foregroundColor(.secondary) .lineLimit(1) .truncationMode(.tail) @@ -68,7 +68,7 @@ struct LatestFollowersWidgetView: View { } } Spacer() - Text("Last update: \(dateFormatter.string(from: lastUpdate))") + Text(L10n.Widget.LatestFollowers.lastUpdate(dateFormatter.string(from: lastUpdate))) .font(.caption2) .foregroundColor(.secondary) } @@ -79,7 +79,7 @@ struct LatestFollowersWidgetView: View { private func viewForMediumWidget(_ accounts: [LatestFollowersEntryAccountable], lastUpdate: Date) -> some View { VStack(alignment: .leading) { HStack { - Text("Latest followers") + Text(L10n.Widget.LatestFollowers.title) .font(.system(size: UIFontMetrics.default.scaledValue(for: 16))) Spacer() Image("BrandIconColored") @@ -98,7 +98,6 @@ struct LatestFollowersWidgetView: View { HStack { Text(account.displayNameWithFallback) .font(.footnote.bold()) - .foregroundColor(.secondary) .lineLimit(1) .truncationMode(.tail) @@ -118,7 +117,7 @@ struct LatestFollowersWidgetView: View { } } Spacer() - Text("Last update: \(dateFormatter.string(from: lastUpdate))") + Text(L10n.Widget.LatestFollowers.lastUpdate(dateFormatter.string(from: lastUpdate))) .font(.caption2) .foregroundColor(.secondary) } diff --git a/WidgetExtension/Variants/MultiFollowersCount/MultiFollowersCountWidgetView.swift b/WidgetExtension/Variants/MultiFollowersCount/MultiFollowersCountWidgetView.swift index a73500d27..836490dd2 100644 --- a/WidgetExtension/Variants/MultiFollowersCount/MultiFollowersCountWidgetView.swift +++ b/WidgetExtension/Variants/MultiFollowersCount/MultiFollowersCountWidgetView.swift @@ -3,6 +3,7 @@ import SwiftUI import WidgetKit import MastodonAsset +import MastodonLocalization struct MultiFollowersCountWidgetView: View { @Environment(\.widgetFamily) var family @@ -17,10 +18,10 @@ struct MultiFollowersCountWidgetView: View { case .systemMedium: viewForMediumWidget(accounts) default: - Text("Sorry but this Widget family is unsupported.") + Text(L10n.Widget.Common.unsupportedWidgetFamily) } } else { - Text("Please open Mastodon to log in to an Account.") + Text(L10n.Widget.Common.userNotLoggedIn) .multilineTextAlignment(.center) .font(.caption) .padding(.all, 20) From c00e797996b557247cb3a47c5c356d4254d9a5d0 Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Mon, 6 Feb 2023 13:51:58 +0100 Subject: [PATCH 16/24] chore(Widget): Add another mock follower in multi followers widget --- .../LatestFollowersWidget.swift | 2 +- .../MultiFollowersCountWidget.swift | 22 ++++++++++++++----- .../MultiFollowersCountWidgetView.swift | 4 ++-- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/WidgetExtension/Variants/LatestFollowers/LatestFollowersWidget.swift b/WidgetExtension/Variants/LatestFollowers/LatestFollowersWidget.swift index 3b75e619c..66c5bd52f 100644 --- a/WidgetExtension/Variants/LatestFollowers/LatestFollowersWidget.swift +++ b/WidgetExtension/Variants/LatestFollowers/LatestFollowersWidget.swift @@ -52,7 +52,7 @@ struct LatestFollowersEntry: TimelineEntry { static var unconfigured: Self { LatestFollowersEntry( date: .now, - accounts: [], + accounts: nil, configuration: LatestFollowersIntent() ) } diff --git a/WidgetExtension/Variants/MultiFollowersCount/MultiFollowersCountWidget.swift b/WidgetExtension/Variants/MultiFollowersCount/MultiFollowersCountWidget.swift index 518470a79..92bb8b258 100644 --- a/WidgetExtension/Variants/MultiFollowersCount/MultiFollowersCountWidget.swift +++ b/WidgetExtension/Variants/MultiFollowersCount/MultiFollowersCountWidget.swift @@ -23,14 +23,14 @@ struct MultiFollowersCountWidgetProvider: IntentTimelineProvider { struct MultiFollowersCountEntry: TimelineEntry { let date: Date - let accounts: [FollowersEntryAccountable]? + let accounts: [MultiFollowersEntryAccountable]? let configuration: MultiFollowersCountIntent static var placeholder: Self { MultiFollowersCountEntry( date: .now, accounts: [ - FollowersEntryAccount( + MultiFollowersEntryAccount( followersCount: 99_900, displayNameWithFallback: "Mastodon", acct: "mastodon", @@ -45,7 +45,7 @@ struct MultiFollowersCountEntry: TimelineEntry { static var unconfigured: Self { MultiFollowersCountEntry( date: .now, - accounts: [], + accounts: nil, configuration: MultiFollowersCountIntent() ) } @@ -93,7 +93,7 @@ private extension MultiFollowersCountWidgetProvider { return completion(.unconfigured) } - var accounts = [FollowersEntryAccountable]() + var accounts = [MultiFollowersEntryAccountable]() for desiredAccount in desiredAccounts { let resultingAccount = try await WidgetExtension.appContext @@ -105,12 +105,24 @@ private extension MultiFollowersCountWidgetProvider { let imageData = try await URLSession.shared.data(from: resultingAccount.avatarImageURLWithFallback(domain: authBox.domain)).0 - accounts.append(FollowersEntryAccount.from( + accounts.append(MultiFollowersEntryAccount.from( mastodonAccount: resultingAccount, domain: authBox.domain, avatarImage: UIImage(data: imageData) ?? UIImage(named: "missingAvatar")! )) } + + if context.isPreview { + accounts.append( + MultiFollowersEntryAccount( + followersCount: 1_200, + displayNameWithFallback: "Another follower", + acct: "@another@follower.social", + avatarImage: UIImage(named: "missingAvatar")!, + domain: authBox.domain + ) + ) + } let entry = MultiFollowersCountEntry( date: Date(), diff --git a/WidgetExtension/Variants/MultiFollowersCount/MultiFollowersCountWidgetView.swift b/WidgetExtension/Variants/MultiFollowersCount/MultiFollowersCountWidgetView.swift index 836490dd2..bfc0f8495 100644 --- a/WidgetExtension/Variants/MultiFollowersCount/MultiFollowersCountWidgetView.swift +++ b/WidgetExtension/Variants/MultiFollowersCount/MultiFollowersCountWidgetView.swift @@ -28,7 +28,7 @@ struct MultiFollowersCountWidgetView: View { } } - private func viewForSmallWidget(_ accounts: [FollowersEntryAccountable]) -> some View { + private func viewForSmallWidget(_ accounts: [MultiFollowersEntryAccountable]) -> some View { VStack(alignment: .leading, spacing: 0) { ForEach(accounts, id: \.acct) { account in HStack { @@ -59,7 +59,7 @@ struct MultiFollowersCountWidgetView: View { .padding(.vertical, 16) } - private func viewForMediumWidget(_ accounts: [FollowersEntryAccountable]) -> some View { + private func viewForMediumWidget(_ accounts: [MultiFollowersEntryAccountable]) -> some View { VStack(alignment: .leading, spacing: 0) { LazyVGrid(columns: [ GridItem(.flexible()), From 998ed3b307fb2a9f365084f83909f1d250834b01 Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Mon, 6 Feb 2023 19:57:33 +0100 Subject: [PATCH 17/24] chore(Widget): Implement L10n for mock account --- Localization/app.json | 6 ++++++ .../Sources/MastodonLocalization/Generated/Strings.swift | 8 ++++++++ .../Resources/Base.lproj/Localizable.strings | 2 ++ .../MultiFollowersCount/MultiFollowersCountWidget.swift | 5 +++-- 4 files changed, 19 insertions(+), 2 deletions(-) diff --git a/Localization/app.json b/Localization/app.json index d6f19a3b0..48f31c813 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -826,6 +826,12 @@ "title": "FOLLOWERS", "followers_today": "%s followers today" }, + "multiple_followers": { + "mock_user": { + "display_name": "Another follower", + "account_name": "another@follower.social" + } + }, "latest_followers": { "title": "Latest followers", "last_update": "Last update: %s" diff --git a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift index 1cab66ca5..f0b3bc02e 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift +++ b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift @@ -1502,6 +1502,14 @@ public enum L10n { /// Latest followers public static let title = L10n.tr("Localizable", "Widget.LatestFollowers.Title", fallback: "Latest followers") } + public enum MultipleFollowers { + public enum MockUser { + /// another@follower.social + public static let accountName = L10n.tr("Localizable", "Widget.MultipleFollowers.MockUser.AccountName", fallback: "another@follower.social") + /// Another follower + public static let displayName = L10n.tr("Localizable", "Widget.MultipleFollowers.MockUser.DisplayName", fallback: "Another follower") + } + } } public enum A11y { public enum Plural { diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings index 2083555bc..d118b86b7 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings @@ -522,5 +522,7 @@ You can’t go wrong with any of our recommend servers, so regardless of which o "Widget.Common.UserNotLoggedIn" = "Please open Mastodon to log in to an Account."; "Widget.FollowersCount.Title" = "FOLLOWERS"; "Widget.FollowersCount.FollowersToday" = "%@ followers today"; +"Widget.MultipleFollowers.MockUser.DisplayName" = "Another follower"; +"Widget.MultipleFollowers.MockUser.AccountName" = "another@follower.social"; "Widget.LatestFollowers.Title" = "Latest followers"; "Widget.LatestFollowers.LastUpdate" = "Last update: %@"; diff --git a/WidgetExtension/Variants/MultiFollowersCount/MultiFollowersCountWidget.swift b/WidgetExtension/Variants/MultiFollowersCount/MultiFollowersCountWidget.swift index 92bb8b258..d5398b684 100644 --- a/WidgetExtension/Variants/MultiFollowersCount/MultiFollowersCountWidget.swift +++ b/WidgetExtension/Variants/MultiFollowersCount/MultiFollowersCountWidget.swift @@ -4,6 +4,7 @@ import WidgetKit import SwiftUI import Intents import MastodonSDK +import MastodonLocalization struct MultiFollowersCountWidgetProvider: IntentTimelineProvider { func placeholder(in context: Context) -> MultiFollowersCountEntry { @@ -116,8 +117,8 @@ private extension MultiFollowersCountWidgetProvider { accounts.append( MultiFollowersEntryAccount( followersCount: 1_200, - displayNameWithFallback: "Another follower", - acct: "@another@follower.social", + displayNameWithFallback: L10n.Widget.MultipleFollowers.MockUser.displayName, + acct: L10n.Widget.MultipleFollowers.MockUser.accountName, avatarImage: UIImage(named: "missingAvatar")!, domain: authBox.domain ) From c8944146e655b0e7793d8a8ed20a70de2842a50b Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Mon, 6 Feb 2023 19:59:59 +0100 Subject: [PATCH 18/24] Add modification notice for LightChart -> DataRepresentable --- WidgetExtension/LightChart/DataRepresentable.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/WidgetExtension/LightChart/DataRepresentable.swift b/WidgetExtension/LightChart/DataRepresentable.swift index cb96037ce..f585014db 100644 --- a/WidgetExtension/LightChart/DataRepresentable.swift +++ b/WidgetExtension/LightChart/DataRepresentable.swift @@ -4,6 +4,7 @@ // // Created by Alexey Pichukov on 19.08.2020. // +// Modified by Marcus Kida for Mastodon on 30.01.2023. import Foundation import CoreGraphics From cd64668c083342c8c5ae5ffacfa842e8d88fc0dd Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Tue, 7 Feb 2023 08:17:00 +0100 Subject: [PATCH 19/24] chore(Widget): Import LightChart fork using SPM --- Mastodon.xcodeproj/project.pbxproj | 60 +++---- .../xcshareddata/swiftpm/Package.resolved | 9 + WidgetExtension/LightChart/ChartType.swift | 25 --- .../LightChart/Charts/CurvedChart.swift | 170 ------------------ .../LightChart/Charts/LineChart.swift | 138 -------------- .../LightChart/DataRepresentable.swift | 43 ----- WidgetExtension/LightChart/LightChart.swift | 52 ------ WidgetExtension/LightChart/Math.swift | 34 ---- .../FollowersCountWidgetView.swift | 1 + 9 files changed, 30 insertions(+), 502 deletions(-) delete mode 100644 WidgetExtension/LightChart/ChartType.swift delete mode 100644 WidgetExtension/LightChart/Charts/CurvedChart.swift delete mode 100644 WidgetExtension/LightChart/Charts/LineChart.swift delete mode 100644 WidgetExtension/LightChart/DataRepresentable.swift delete mode 100644 WidgetExtension/LightChart/LightChart.swift delete mode 100644 WidgetExtension/LightChart/Math.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index fa1ec86e3..ed740fe04 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -25,12 +25,6 @@ 2A1FE47C2938BB2600784BF1 /* FollowedTagsViewModel+DiffableDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A1FE47B2938BB2600784BF1 /* FollowedTagsViewModel+DiffableDataSource.swift */; }; 2A1FE47E2938C11200784BF1 /* Collection+IsNotEmpty.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A1FE47D2938C11200784BF1 /* Collection+IsNotEmpty.swift */; }; 2A33062D2987DBFA001D4C51 /* FollowersCountHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A33062C2987DBFA001D4C51 /* FollowersCountHistory.swift */; }; - 2A33063629880835001D4C51 /* Math.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A33062F29880834001D4C51 /* Math.swift */; }; - 2A33063729880835001D4C51 /* DataRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A33063029880834001D4C51 /* DataRepresentable.swift */; }; - 2A33063829880835001D4C51 /* LineChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A33063229880834001D4C51 /* LineChart.swift */; }; - 2A33063929880835001D4C51 /* CurvedChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A33063329880834001D4C51 /* CurvedChart.swift */; }; - 2A33063A29880835001D4C51 /* LightChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A33063429880834001D4C51 /* LightChart.swift */; }; - 2A33063B29880835001D4C51 /* ChartType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A33063529880834001D4C51 /* ChartType.swift */; }; 2A33AB662982C4AF008A7FB1 /* FollowersCountWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A33AB652982C4AF008A7FB1 /* FollowersCountWidgetView.swift */; }; 2A3F6FE3292ECB5E002E6DA7 /* FollowedTagsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A3F6FE2292ECB5E002E6DA7 /* FollowedTagsViewModel.swift */; }; 2A3F6FE5292F6E44002E6DA7 /* FollowedTagsTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A3F6FE4292F6E44002E6DA7 /* FollowedTagsTableViewCell.swift */; }; @@ -60,6 +54,7 @@ 2A9D0666298C05A800BF38CB /* LatestFollowersWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A9D0665298C05A800BF38CB /* LatestFollowersWidgetView.swift */; }; 2A9D066F298D0FD100BF38CB /* MastodonSDKDynamic in Frameworks */ = {isa = PBXBuildFile; productRef = 2A9D066E298D0FD100BF38CB /* MastodonSDKDynamic */; }; 2AB12E4629362F27006BC925 /* DataSourceFacade+Translate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AB12E4529362F27006BC925 /* DataSourceFacade+Translate.swift */; }; + 2AB5011B2992322500346092 /* LightChart in Frameworks */ = {isa = PBXBuildFile; productRef = 2AB5011A2992322500346092 /* LightChart */; }; 2AE202AA297FE10B00F66E55 /* WidgetExtension.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 2A72812C297EA9D7004138C5 /* WidgetExtension.intentdefinition */; }; 2AE202AC297FE19100F66E55 /* FollowersCountIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AE202AB297FE19100F66E55 /* FollowersCountIntentHandler.swift */; }; 2AE202AD297FE1CD00F66E55 /* WidgetExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A72813E297EC762004138C5 /* WidgetExtension.swift */; }; @@ -618,12 +613,6 @@ 2A1FE47B2938BB2600784BF1 /* FollowedTagsViewModel+DiffableDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FollowedTagsViewModel+DiffableDataSource.swift"; sourceTree = ""; }; 2A1FE47D2938C11200784BF1 /* Collection+IsNotEmpty.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection+IsNotEmpty.swift"; sourceTree = ""; }; 2A33062C2987DBFA001D4C51 /* FollowersCountHistory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowersCountHistory.swift; sourceTree = ""; }; - 2A33062F29880834001D4C51 /* Math.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Math.swift; sourceTree = ""; }; - 2A33063029880834001D4C51 /* DataRepresentable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataRepresentable.swift; sourceTree = ""; }; - 2A33063229880834001D4C51 /* LineChart.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LineChart.swift; sourceTree = ""; }; - 2A33063329880834001D4C51 /* CurvedChart.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CurvedChart.swift; sourceTree = ""; }; - 2A33063429880834001D4C51 /* LightChart.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LightChart.swift; sourceTree = ""; }; - 2A33063529880834001D4C51 /* ChartType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChartType.swift; sourceTree = ""; }; 2A33625329759B4200481A90 /* OpenInActionExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = OpenInActionExtension.entitlements; sourceTree = ""; }; 2A33AB652982C4AF008A7FB1 /* FollowersCountWidgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowersCountWidgetView.swift; sourceTree = ""; }; 2A3F6FE2292ECB5E002E6DA7 /* FollowedTagsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowedTagsViewModel.swift; sourceTree = ""; }; @@ -1220,6 +1209,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 2AB5011B2992322500346092 /* LightChart in Frameworks */, 2A9D066F298D0FD100BF38CB /* MastodonSDKDynamic in Frameworks */, 2A728124297EA9D7004138C5 /* SwiftUI.framework in Frameworks */, 2A728122297EA9D7004138C5 /* WidgetKit.framework in Frameworks */, @@ -1398,27 +1388,6 @@ path = Pods; sourceTree = ""; }; - 2A33062E29880834001D4C51 /* LightChart */ = { - isa = PBXGroup; - children = ( - 2A33063129880834001D4C51 /* Charts */, - 2A33062F29880834001D4C51 /* Math.swift */, - 2A33063029880834001D4C51 /* DataRepresentable.swift */, - 2A33063429880834001D4C51 /* LightChart.swift */, - 2A33063529880834001D4C51 /* ChartType.swift */, - ); - path = LightChart; - sourceTree = ""; - }; - 2A33063129880834001D4C51 /* Charts */ = { - isa = PBXGroup; - children = ( - 2A33063229880834001D4C51 /* LineChart.swift */, - 2A33063329880834001D4C51 /* CurvedChart.swift */, - ); - path = Charts; - sourceTree = ""; - }; 2A506CF2292CD83B00059C37 /* FollowedTags */ = { isa = PBXGroup; children = ( @@ -1445,7 +1414,6 @@ 2A728125297EA9D7004138C5 /* WidgetExtension */ = { isa = PBXGroup; children = ( - 2A33062E29880834001D4C51 /* LightChart */, 2A86A14329892700007F1062 /* Variants */, 2AE202A9297FDDF500F66E55 /* WidgetExtension.entitlements */, 2A72813E297EC762004138C5 /* WidgetExtension.swift */, @@ -3034,6 +3002,7 @@ name = WidgetExtension; packageProductDependencies = ( 2A9D066E298D0FD100BF38CB /* MastodonSDKDynamic */, + 2AB5011A2992322500346092 /* LightChart */, ); productName = WidgetExtensionExtension; productReference = 2A728120297EA9D7004138C5 /* WidgetExtension.appex */; @@ -3247,6 +3216,7 @@ ); mainGroup = DB427DC925BAA00100D1B89D; packageReferences = ( + 2AB501192992322500346092 /* XCRemoteSwiftPackageReference "LightChart" */, ); productRefGroup = DB427DD325BAA00100D1B89D /* Products */; projectDirPath = ""; @@ -3512,22 +3482,16 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 2A33063729880835001D4C51 /* DataRepresentable.swift in Sources */, 2A33062D2987DBFA001D4C51 /* FollowersCountHistory.swift in Sources */, - 2A33063829880835001D4C51 /* LineChart.swift in Sources */, 2A9D0666298C05A800BF38CB /* LatestFollowersWidgetView.swift in Sources */, 2A728130297EA9D8004138C5 /* WidgetExtension.intentdefinition in Sources */, 2A86A14B2989326E007F1062 /* MultiFollowersCountWidgetView.swift in Sources */, 2A72813F297EC762004138C5 /* WidgetExtension.swift in Sources */, - 2A33063A29880835001D4C51 /* LightChart.swift in Sources */, - 2A33063B29880835001D4C51 /* ChartType.swift in Sources */, - 2A33063629880835001D4C51 /* Math.swift in Sources */, 2A86A14929892B3A007F1062 /* MultiFollowersCountWidget.swift in Sources */, 2A33AB662982C4AF008A7FB1 /* FollowersCountWidgetView.swift in Sources */, 2A9D0664298C048800BF38CB /* LatestFollowersWidget.swift in Sources */, 2A728127297EA9D7004138C5 /* WidgetExtensionBundle.swift in Sources */, 2A72812B297EA9D7004138C5 /* FollowersCountWidget.swift in Sources */, - 2A33063929880835001D4C51 /* CurvedChart.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -5330,6 +5294,17 @@ }; /* End XCConfigurationList section */ +/* Begin XCRemoteSwiftPackageReference section */ + 2AB501192992322500346092 /* XCRemoteSwiftPackageReference "LightChart" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/Bearologics/LightChart.git"; + requirement = { + branch = master; + kind = branch; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + /* Begin XCSwiftPackageProductDependency section */ 2A90A156296EEE500026C155 /* MastodonSDKDynamic */ = { isa = XCSwiftPackageProductDependency; @@ -5339,6 +5314,11 @@ isa = XCSwiftPackageProductDependency; productName = MastodonSDKDynamic; }; + 2AB5011A2992322500346092 /* LightChart */ = { + isa = XCSwiftPackageProductDependency; + package = 2AB501192992322500346092 /* XCRemoteSwiftPackageReference "LightChart" */; + productName = LightChart; + }; 357FEEAE29523D470021C9DC /* MastodonSDKDynamic */ = { isa = XCSwiftPackageProductDependency; productName = MastodonSDKDynamic; diff --git a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved index cd7395c7e..232ec9c87 100644 --- a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -73,6 +73,15 @@ "version": "4.2.2" } }, + { + "package": "LightChart", + "repositoryURL": "https://github.com/Bearologics/LightChart.git", + "state": { + "branch": "master", + "revision": "a7e724e9ec3cdcaa2d0840b95780e66b870dbf1e", + "version": null + } + }, { "package": "MetaTextKit", "repositoryURL": "https://github.com/TwidereProject/MetaTextKit.git", diff --git a/WidgetExtension/LightChart/ChartType.swift b/WidgetExtension/LightChart/ChartType.swift deleted file mode 100644 index 1a83b0b4c..000000000 --- a/WidgetExtension/LightChart/ChartType.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// ChartType.swift -// -// -// Created by Alexey Pichukov on 19.08.2020. -// - -import SwiftUI - -public enum ChartType { - case line - case curved -} - -public enum ChartVisualType { - case outline(color: Color, lineWidth: CGFloat) - case filled(color: Color, lineWidth: CGFloat) - case customFilled(color: Color, lineWidth: CGFloat, fillGradient: LinearGradient) -} - -public enum CurrentValueLineType { - case none - case line(color: Color, lineWidth: CGFloat) - case dash(color: Color, lineWidth: CGFloat, dash: [CGFloat]) -} diff --git a/WidgetExtension/LightChart/Charts/CurvedChart.swift b/WidgetExtension/LightChart/Charts/CurvedChart.swift deleted file mode 100644 index c6df1d934..000000000 --- a/WidgetExtension/LightChart/Charts/CurvedChart.swift +++ /dev/null @@ -1,170 +0,0 @@ -// -// File.swift -// -// -// Created by Alexey Pichukov on 20.08.2020. -// - -import SwiftUI - -public struct CurvedChart: View { - - private let data: [Double] - private let frame: CGRect - private let offset: Double - private let type: ChartVisualType - private let currentValueLineType: CurrentValueLineType - private var points: [CGPoint] = [] - - /// Creates a new `CurvedChart` - /// - /// - Parameters: - /// - data: A data set that should be presented on the chart - /// - frame: A frame from the parent view - /// - visualType: A type of chart, `.outline` by default - /// - offset: An offset for the chart, a space below the chart in percentage (0 - 1) - /// For example `offset: 0.2` means that the chart will occupy 80% of the upper - /// part of the view - /// - currentValueLineType: A type of current value line (`none` for no line on chart) - public init(data: [Double], - frame: CGRect, - visualType: ChartVisualType = .outline(color: .red, lineWidth: 2), - offset: Double = 0, - currentValueLineType: CurrentValueLineType = .none) { - self.data = data - self.frame = frame - self.type = visualType - self.offset = offset - self.currentValueLineType = currentValueLineType - self.points = points(forData: data, - frame: frame, - offset: offset, - lineWidth: lineWidth(visualType: visualType)) - } - - public var body: some View { - ZStack { - chart - .rotationEffect(.degrees(180), anchor: .center) - .rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0)) - .drawingGroup() - line - .rotationEffect(.degrees(180), anchor: .center) - .rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0)) - .drawingGroup() - } - } - - private var chart: some View { - switch type { - case .outline(let color, let lineWidth): - return AnyView(curvedPath(points: points) - .stroke(color, style: StrokeStyle(lineWidth: lineWidth, lineJoin: .round))) - case .filled(let color, let lineWidth): - return AnyView(ZStack { - curvedPathGradient(points: points) - .fill(LinearGradient( - gradient: .init(colors: [color.opacity(0.2), color.opacity(0.02)]), - startPoint: .init(x: 0.5, y: 1), - endPoint: .init(x: 0.5, y: 0) - )) - curvedPath(points: points) - .stroke(color, style: StrokeStyle(lineWidth: lineWidth, lineJoin: .round)) - }) - case .customFilled(let color, let lineWidth, let fillGradient): - return AnyView(ZStack { - curvedPathGradient(points: points) - .fill(fillGradient) - curvedPath(points: points) - .stroke(color, style: StrokeStyle(lineWidth: lineWidth, lineJoin: .round)) - }) - } - } - - private var line: some View { - switch currentValueLineType { - case .none: - return AnyView(EmptyView()) - case .line(let color, let lineWidth): - return AnyView( - currentValueLinePath(points: points) - .stroke(color, style: StrokeStyle(lineWidth: lineWidth)) - ) - case .dash(let color, let lineWidth, let dash): - return AnyView( - currentValueLinePath(points: points) - .stroke(color, style: StrokeStyle(lineWidth: lineWidth, dash: dash)) - ) - } - } - - // MARK: private functions - - private func curvedPath(points: [CGPoint]) -> Path { - func mid(_ point1: CGPoint, _ point2: CGPoint) -> CGPoint { - return CGPoint(x: (point1.x + point2.x) / 2, y:(point1.y + point2.y) / 2) - } - - func control(_ point1: CGPoint, _ point2: CGPoint) -> CGPoint { - var controlPoint = mid(point1, point2) - let delta = abs(point2.y - controlPoint.y) - - if point1.y < point2.y { - controlPoint.y += delta - } else if point1.y > point2.y { - controlPoint.y -= delta - } - - return controlPoint - } - - var path = Path() - guard points.count > 1 else { - return path - } - - var startPoint = points[0] - path.move(to: startPoint) - - guard points.count > 2 else { - path.addLine(to: points[1]) - return path - } - - for i in 1.. Path { - var path = curvedPath(points: points) - guard let lastPoint = points.last else { - return path - } - path.addLine(to: CGPoint(x: lastPoint.x, y: 0)) - path.addLine(to: CGPoint(x: 0, y: 0)) - path.addLine(to: CGPoint(x: 0, y: points[0].y)) - - return path - } - - private func currentValueLinePath(points: [CGPoint]) -> Path { - var path = Path() - guard let lastPoint = points.last else { - return path - } - path.move(to: CGPoint(x: 0, y: lastPoint.y)) - path.addLine(to: lastPoint) - return path - } -} - -extension CurvedChart: DataRepresentable { } diff --git a/WidgetExtension/LightChart/Charts/LineChart.swift b/WidgetExtension/LightChart/Charts/LineChart.swift deleted file mode 100644 index b2a0a3ce9..000000000 --- a/WidgetExtension/LightChart/Charts/LineChart.swift +++ /dev/null @@ -1,138 +0,0 @@ -// -// LineChart.swift -// -// -// Created by Alexey Pichukov on 19.08.2020. -// - -import SwiftUI - -public struct LineChart: View { - - private let data: [Double] - private let frame: CGRect - private let offset: Double - private let type: ChartVisualType - private let currentValueLineType: CurrentValueLineType - private var points: [CGPoint] = [] - - /// Creates a new `LineChart` - /// - /// - Parameters: - /// - data: A data set that should be presented on the chart - /// - frame: A frame from the parent view - /// - visualType: A type of chart, `.outline` by default - /// - offset: An offset for the chart, a space below the chart in percentage (0 - 1) - /// For example `offset: 0.2` means that the chart will occupy 80% of the upper - /// part of the view - /// - currentValueLineType: A type of current value line (`none` for no line on chart) - public init(data: [Double], - frame: CGRect, - visualType: ChartVisualType = .outline(color: .red, lineWidth: 2), - offset: Double = 0, - currentValueLineType: CurrentValueLineType = .none) { - self.data = data - self.frame = frame - self.type = visualType - self.offset = offset - self.currentValueLineType = currentValueLineType - self.points = points(forData: data, - frame: frame, - offset: offset, - lineWidth: lineWidth(visualType: visualType)) - } - - public var body: some View { - ZStack { - chart - .rotationEffect(.degrees(180), anchor: .center) - .rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0)) - .drawingGroup() - line - .rotationEffect(.degrees(180), anchor: .center) - .rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0)) - .drawingGroup() - } - } - - private var chart: some View { - switch type { - case .outline(let color, let lineWidth): - return AnyView(linePath(points: points) - .stroke(color, style: StrokeStyle(lineWidth: lineWidth, lineJoin: .round))) - case .filled(let color, let lineWidth): - return AnyView(ZStack { - linePathGradient(points: points) - .fill(LinearGradient( - gradient: .init(colors: [color.opacity(0.2), color.opacity(0.02)]), - startPoint: .init(x: 0.5, y: 1), - endPoint: .init(x: 0.5, y: 0) - )) - linePath(points: points) - .stroke(color, style: StrokeStyle(lineWidth: lineWidth, lineJoin: .round)) - }) - case .customFilled(let color, let lineWidth, let fillGradient): - return AnyView(ZStack { - linePathGradient(points: points) - .fill(fillGradient) - linePath(points: points) - .stroke(color, style: StrokeStyle(lineWidth: lineWidth, lineJoin: .round)) - }) - } - } - - private var line: some View { - switch currentValueLineType { - case .none: - return AnyView(EmptyView()) - case .line(let color, let lineWidth): - return AnyView( - currentValueLinePath(points: points) - .stroke(color, style: StrokeStyle(lineWidth: lineWidth)) - ) - case .dash(let color, let lineWidth, let dash): - return AnyView( - currentValueLinePath(points: points) - .stroke(color, style: StrokeStyle(lineWidth: lineWidth, dash: dash)) - ) - } - } - - // MARK: private functions - - private func linePath(points: [CGPoint]) -> Path { - var path = Path() - guard points.count > 1 else { - return path - } - path.move(to: points[0]) - for i in 1.. Path { - var path = linePath(points: points) - guard let lastPoint = points.last else { - return path - } - path.addLine(to: CGPoint(x: lastPoint.x, y: 0)) - path.addLine(to: CGPoint(x: 0, y: 0)) - path.addLine(to: CGPoint(x: 0, y: points[0].y)) - - return path - } - - private func currentValueLinePath(points: [CGPoint]) -> Path { - var path = Path() - guard let lastPoint = points.last else { - return path - } - path.move(to: CGPoint(x: 0, y: lastPoint.y)) - path.addLine(to: lastPoint) - return path - } -} - -extension LineChart: DataRepresentable { } diff --git a/WidgetExtension/LightChart/DataRepresentable.swift b/WidgetExtension/LightChart/DataRepresentable.swift deleted file mode 100644 index f585014db..000000000 --- a/WidgetExtension/LightChart/DataRepresentable.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// DataRepresentable.swift -// -// -// Created by Alexey Pichukov on 19.08.2020. -// -// Modified by Marcus Kida for Mastodon on 30.01.2023. - -import Foundation -import CoreGraphics - -protocol DataRepresentable { - func points(forData data: [Double], frame: CGRect, offset: Double, lineWidth: CGFloat) -> [CGPoint] - func lineWidth(visualType: ChartVisualType) -> CGFloat -} - -extension DataRepresentable { - - func points(forData data: [Double], frame: CGRect, offset: Double, lineWidth: CGFloat) -> [CGPoint] { - var vector = Math.stretchOut(Math.norm(data)) - if offset != 0 { - vector = Math.stretchIn(vector, offset: offset) - } - var points: [CGPoint] = [] - for i in 0.. CGFloat { - switch visualType { - case .outline(_, let lineWidth): - return lineWidth - case .filled(_, let lineWidth): - return lineWidth - case .customFilled(_, let lineWidth, _): - return lineWidth - } - } -} diff --git a/WidgetExtension/LightChart/LightChart.swift b/WidgetExtension/LightChart/LightChart.swift deleted file mode 100644 index e7b868730..000000000 --- a/WidgetExtension/LightChart/LightChart.swift +++ /dev/null @@ -1,52 +0,0 @@ -import SwiftUI - -public struct LightChartView: View { - - private let data: [Double] - private let type: ChartType - private let visualType: ChartVisualType - private let offset: Double - private let currentValueLineType: CurrentValueLineType - - public init(data: [Double], - type: ChartType = .line, - visualType: ChartVisualType = .outline(color: .red, lineWidth: 2), - offset: Double = 0, - currentValueLineType: CurrentValueLineType = .none) { - self.data = data - self.type = type - self.visualType = visualType - self.offset = offset - self.currentValueLineType = currentValueLineType - } - - public var body: some View { - GeometryReader { reader in - chart(withFrame: CGRect(x: 0, - y: 0, - width: reader.frame(in: .local).width , - height: reader.frame(in: .local).height)) - } - } - - private func chart(withFrame frame: CGRect) -> AnyView { - switch type { - case .line: - return AnyView( - LineChart(data: data, - frame: frame, - visualType: visualType, - offset: offset, - currentValueLineType: currentValueLineType) - ) - case .curved: - return AnyView( - CurvedChart(data: data, - frame: frame, - visualType: visualType, - offset: offset, - currentValueLineType: currentValueLineType) - ) - } - } -} diff --git a/WidgetExtension/LightChart/Math.swift b/WidgetExtension/LightChart/Math.swift deleted file mode 100644 index 837af4ceb..000000000 --- a/WidgetExtension/LightChart/Math.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// Math.swift -// -// -// Created by Alexey Pichukov on 19.08.2020. -// - -import Foundation -import CoreGraphics - -struct Math { - - static func norm(_ vector: [Double]) -> [Double] { - let norm = sqrt(Double(vector.reduce(0) { $0 + $1 * $1 })) - return norm == 0 ? vector : vector.map { $0 / norm } - } - - static func stretchOut(_ vector: [Double]) -> [Double] { - guard let min = vector.min(), - let rawMax = vector.max() else { - return vector - } - let max = rawMax - min - return vector.map { ($0 - min) / (max != 0 ? max : 1) } - } - - static func stretchIn(_ vector: [Double], offset: Double) -> [Double] { - guard let max = vector.max() else { - return vector - } - let newMax = max - offset - return vector.map { $0 * newMax + offset } - } -} diff --git a/WidgetExtension/Variants/FollowersCount/FollowersCountWidgetView.swift b/WidgetExtension/Variants/FollowersCount/FollowersCountWidgetView.swift index 288fcdcdc..2f5b93610 100644 --- a/WidgetExtension/Variants/FollowersCount/FollowersCountWidgetView.swift +++ b/WidgetExtension/Variants/FollowersCount/FollowersCountWidgetView.swift @@ -4,6 +4,7 @@ import SwiftUI import WidgetKit import MastodonAsset import MastodonLocalization +import LightChart struct FollowersCountWidgetView: View { private let followersHistory = FollowersCountHistory.shared From 0e303418d7f5b602f1973f598e8eb7e3552d1b40 Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Tue, 7 Feb 2023 09:07:37 +0100 Subject: [PATCH 20/24] chore(Widget): Fix picked account not desired account --- .../FollowersCount/FollowersCountWidget.swift | 27 +++++++------- .../MultiFollowersCountWidget.swift | 36 ++++++++++--------- 2 files changed, 35 insertions(+), 28 deletions(-) diff --git a/WidgetExtension/Variants/FollowersCount/FollowersCountWidget.swift b/WidgetExtension/Variants/FollowersCount/FollowersCountWidget.swift index 51baf30f0..6f9a6e81c 100644 --- a/WidgetExtension/Variants/FollowersCount/FollowersCountWidget.swift +++ b/WidgetExtension/Variants/FollowersCount/FollowersCountWidget.swift @@ -84,21 +84,24 @@ private extension FollowersCountWidgetProvider { return completion(.unconfigured) } - guard let desiredAccount: String = { - guard let account = configuration.account else { - return authBox.authenticationRecord.object(in: WidgetExtension.appContext.managedObjectContext)?.user.acct - } - return account - }() else { + guard + let desiredAccount = configuration.account ?? authBox.authenticationRecord.object( + in: WidgetExtension.appContext.managedObjectContext + )?.user.acct + else { return completion(.unconfigured) } - let resultingAccount = try await WidgetExtension.appContext - .apiService - .search(query: .init(q: desiredAccount, type: .accounts), authenticationBox: authBox) - .value - .accounts - .first! + guard + let resultingAccount = try await WidgetExtension.appContext + .apiService + .search(query: .init(q: desiredAccount, type: .accounts), authenticationBox: authBox) + .value + .accounts + .first(where: { $0.acct == desiredAccount }) + else { + return completion(.unconfigured) + } let imageData = try await URLSession.shared.data(from: resultingAccount.avatarImageURLWithFallback(domain: authBox.domain)).0 diff --git a/WidgetExtension/Variants/MultiFollowersCount/MultiFollowersCountWidget.swift b/WidgetExtension/Variants/MultiFollowersCount/MultiFollowersCountWidget.swift index d5398b684..f39e548c4 100644 --- a/WidgetExtension/Variants/MultiFollowersCount/MultiFollowersCountWidget.swift +++ b/WidgetExtension/Variants/MultiFollowersCount/MultiFollowersCountWidget.swift @@ -82,27 +82,31 @@ private extension MultiFollowersCountWidgetProvider { return completion(.unconfigured) } - guard let desiredAccounts: [String] = { - guard let account = configuration.accounts?.compactMap({ $0 }) else { - if let acct = authBox.authenticationRecord.object(in: WidgetExtension.appContext.managedObjectContext)?.user.acct { - return [acct] - } - return nil - } - return account - }() else { + let desiredAccounts: [String] + + if let configuredAccounts = configuration.accounts?.compactMap({ $0 }) { + desiredAccounts = configuredAccounts + } else if let currentlyLoggedInAccount = authBox.authenticationRecord.object( + in: WidgetExtension.appContext.managedObjectContext + )?.user.acct { + desiredAccounts = [currentlyLoggedInAccount] + } else { return completion(.unconfigured) } - + var accounts = [MultiFollowersEntryAccountable]() for desiredAccount in desiredAccounts { - let resultingAccount = try await WidgetExtension.appContext - .apiService - .search(query: .init(q: desiredAccount, type: .accounts), authenticationBox: authBox) - .value - .accounts - .first! + guard + let resultingAccount = try await WidgetExtension.appContext + .apiService + .search(query: .init(q: desiredAccount, type: .accounts), authenticationBox: authBox) + .value + .accounts + .first(where: { $0.acct == desiredAccount }) + else { + continue + } let imageData = try await URLSession.shared.data(from: resultingAccount.avatarImageURLWithFallback(domain: authBox.domain)).0 From e00b50ea869ddc1dc3feb25258ac2705557204aa Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Tue, 7 Feb 2023 09:14:28 +0100 Subject: [PATCH 21/24] chore(Widget): Use SVG icons --- .../BrandIcon.imageset/Contents.json | 42 ++---------------- .../BrandIcon.imageset/Logo 1.png | Bin 284 -> 0 bytes .../BrandIcon.imageset/Logo 1.svg | 3 ++ .../BrandIcon.imageset/Logo.png | Bin 286 -> 0 bytes .../BrandIcon.imageset/Logo.svg | 3 ++ .../BrandIcon.imageset/Logo@2x 1.png | Bin 484 -> 0 bytes .../BrandIcon.imageset/Logo@2x.png | Bin 444 -> 0 bytes .../BrandIcon.imageset/Logo@3x 1.png | Bin 694 -> 0 bytes .../BrandIcon.imageset/Logo@3x.png | Bin 636 -> 0 bytes .../BrandIconColored.imageset/Contents.json | 15 +------ .../BrandIconColored.imageset/Logo.png | Bin 576 -> 0 bytes .../BrandIconColored.imageset/Logo.svg | 4 ++ .../BrandIconColored.imageset/Logo@2x.png | Bin 1036 -> 0 bytes .../BrandIconColored.imageset/Logo@3x.png | Bin 1444 -> 0 bytes 14 files changed, 16 insertions(+), 51 deletions(-) delete mode 100644 WidgetExtension/Assets.xcassets/BrandIcon.imageset/Logo 1.png create mode 100644 WidgetExtension/Assets.xcassets/BrandIcon.imageset/Logo 1.svg delete mode 100644 WidgetExtension/Assets.xcassets/BrandIcon.imageset/Logo.png create mode 100644 WidgetExtension/Assets.xcassets/BrandIcon.imageset/Logo.svg delete mode 100644 WidgetExtension/Assets.xcassets/BrandIcon.imageset/Logo@2x 1.png delete mode 100644 WidgetExtension/Assets.xcassets/BrandIcon.imageset/Logo@2x.png delete mode 100644 WidgetExtension/Assets.xcassets/BrandIcon.imageset/Logo@3x 1.png delete mode 100644 WidgetExtension/Assets.xcassets/BrandIcon.imageset/Logo@3x.png delete mode 100644 WidgetExtension/Assets.xcassets/BrandIconColored.imageset/Logo.png create mode 100644 WidgetExtension/Assets.xcassets/BrandIconColored.imageset/Logo.svg delete mode 100644 WidgetExtension/Assets.xcassets/BrandIconColored.imageset/Logo@2x.png delete mode 100644 WidgetExtension/Assets.xcassets/BrandIconColored.imageset/Logo@3x.png diff --git a/WidgetExtension/Assets.xcassets/BrandIcon.imageset/Contents.json b/WidgetExtension/Assets.xcassets/BrandIcon.imageset/Contents.json index e3acfa8de..ec6e92dd6 100644 --- a/WidgetExtension/Assets.xcassets/BrandIcon.imageset/Contents.json +++ b/WidgetExtension/Assets.xcassets/BrandIcon.imageset/Contents.json @@ -1,9 +1,8 @@ { "images" : [ { - "filename" : "Logo.png", - "idiom" : "universal", - "scale" : "1x" + "filename" : "Logo.svg", + "idiom" : "universal" }, { "appearances" : [ @@ -12,41 +11,8 @@ "value" : "dark" } ], - "filename" : "Logo 1.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "Logo@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "Logo@2x 1.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "Logo@3x.png", - "idiom" : "universal", - "scale" : "3x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "Logo@3x 1.png", - "idiom" : "universal", - "scale" : "3x" + "filename" : "Logo 1.svg", + "idiom" : "universal" } ], "info" : { diff --git a/WidgetExtension/Assets.xcassets/BrandIcon.imageset/Logo 1.png b/WidgetExtension/Assets.xcassets/BrandIcon.imageset/Logo 1.png deleted file mode 100644 index 0cd6d0b8b956ff9e1f425e44d4124104ce7980ba..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 284 zcmeAS@N?(olHy`uVBq!ia0vp^JRmj)8<3o<+3y6TI14-?iy0WWg+Z8+Vb&Z8pde#$ zkh>GZx^prwfgF}}M_)$E)e-c@N{EeP2jv*C{Qv(n39x~u55%_Q9Wp#}@UuYhvCB9TzlQ@#Di3EUrBuwaJlM4zV({9;DJ-j7f3SoNiM<6rRtXJ*?;%NKq) zd*9$ii3|T6&x)p~OwX5(w{5YUVpn6d|DM2x$_<;E%S`XB{GZJ6(9kRCTW8d%W5KnT ZrL|9_8vdE`ni1%D22WQ%mvv4FO#p`2XQ2Q9 diff --git a/WidgetExtension/Assets.xcassets/BrandIcon.imageset/Logo 1.svg b/WidgetExtension/Assets.xcassets/BrandIcon.imageset/Logo 1.svg new file mode 100644 index 000000000..1a5328cd2 --- /dev/null +++ b/WidgetExtension/Assets.xcassets/BrandIcon.imageset/Logo 1.svg @@ -0,0 +1,3 @@ + + + diff --git a/WidgetExtension/Assets.xcassets/BrandIcon.imageset/Logo.png b/WidgetExtension/Assets.xcassets/BrandIcon.imageset/Logo.png deleted file mode 100644 index 12f3c23d34370c29fe24be9f1c5c5d5946453bd8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 286 zcmeAS@N?(olHy`uVBq!ia0vp^JRmj)8<3o<+3y6TI14-?iy0WWg+Z8+Vb&Z8pde#$ zkh>GZx^prwfgF}}M_)$E)e-c@N{LP*&jv*C{Qzz~gJmkRRV(O50f>@+ zgU~t_uLZ&{8iX_swg~KGdGX-J=5RSh9_CqSwNbT`bYAdQhBfQ(Lwq@x1Ydq=Mit=2_?Fg!@J<_}csA{EPv`TCAFb~zZQR-y evaRaF+1gKw diff --git a/WidgetExtension/Assets.xcassets/BrandIcon.imageset/Logo.svg b/WidgetExtension/Assets.xcassets/BrandIcon.imageset/Logo.svg new file mode 100644 index 000000000..74dc7b51e --- /dev/null +++ b/WidgetExtension/Assets.xcassets/BrandIcon.imageset/Logo.svg @@ -0,0 +1,3 @@ + + + diff --git a/WidgetExtension/Assets.xcassets/BrandIcon.imageset/Logo@2x 1.png b/WidgetExtension/Assets.xcassets/BrandIcon.imageset/Logo@2x 1.png deleted file mode 100644 index 496d567732aee87f2b2b2b3ac4a9a34d0d2cfa4a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 484 zcmV06OwPn4f zC{_y!V6CnAamUom(oJiU(R)wBJQPLIik9qHGmrNF=$N(SytB(<2j7Jg=n`r8E0D6= zEj!;woVUz_GsL^N_v|qD$~?Ocl0fZk`1a=Cy!ppqkMe{k*a(ptZ>+qrj*LUQ2{yDc zAkfo#Fl$F(wlj=NuY}9oO%O1dws?^h%|kB!Td%1`WA%VP=1Q$;)$Ej zHAQDy7REV$!4C+0CXB~5REH}&300000HV*BT$_x!n&gXdmyJ``}y`OF1MJ10&daI}iSN0@5k z*(E{H#|JiU6(8>;a1;hQEMbDk06FP<$oar2>FlAS2{YDU1(A!4mW}> m9*h4x@Q6wL7#y2xPJRKKoA^XREjLC00000<3V9i0_IhdksL|B#DCU(0sTX;?wRBy1`CCL z0A^-v%-k}a>{rT;_&OGjS)claL3d{vYs-qC289HRy-14~=qmIVV1ab@rtxgf9DxI* zx5}T+P1mnIK*Jyp_5PF6jyjb(V?ExvdDc$UekcdwYgA7~J%Y-=cIEY2KRYU#OIt-ml!MSr1frujbIpd-+8k?IjNXYx&P--qJbT&ky_!DNt3pt^=lr&sBXrFsS( z7^K6u)LX#@WiTLyW>DJ2E9;cO8T17A6XNZ+?k=ZhgHo`6g-z_CJF=m@vfQ8x42QGH zhbGcDXaax7m--4ddWSM}7y=x1OHs}QXv!MY?=pxb_wpe0Z1+9;RP4{au^>C;)4@f0`5;O5|*la;wXOZ{YREXs)6Qj@5}pg6-i|%Lf@jn=w6CMXd65)rqXU`LTbU cbeo@vA7dL)HB<7XWdHyG07*qoM6N<$g4tL;*8l(j diff --git a/WidgetExtension/Assets.xcassets/BrandIcon.imageset/Logo@3x.png b/WidgetExtension/Assets.xcassets/BrandIcon.imageset/Logo@3x.png deleted file mode 100644 index 02abd6055f1512d0798b57dc63e1482894f26d6f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 636 zcmV-?0)zdDP)K~#7F%~;`i zf-n$$_Ur$$fpLPeLFoo;pc~XqP&$EbP&d#GXeY2v08Aho*qnxvrpXZmmG^xQl5iw1 z6*}yWi*M{xS+j1)A}SdOJh(nUzaLYNJ3M`dSzDYF|bn6++g~5jUrymy2{~r9*C*;H zYY*xksb>>(fIz=TGZ}h90zXIh8RFg`aN71}X9%IrEYZ=2I7EuJ6IpZe+o*Kw9nBB2 W-0yoRL)mx$0000Axqs51IaQqrJaWsPNCtTS16RXHfV>wMA8OF^WS2Ug(S(q05Dtv`ul0$g5ph%~TZ@#@BgJUh?M0DXN%9~?Bc<_$1D)^RKpfaR4GnQQUl#M;P+A!#&# zqhk|6iKDx}@7Vqn%@0aLDI!}TaWayQV0X`?wCx$*LPjP2mCU4maoLE#-pBrzJ- zM&2~L?|ql37btG90N(&BYs1_;vca96Nz`m26U153#H2pu_kUuL6Ou=YoGkQ9$SEd& zq6uRX(N(-5i=b + + + diff --git a/WidgetExtension/Assets.xcassets/BrandIconColored.imageset/Logo@2x.png b/WidgetExtension/Assets.xcassets/BrandIconColored.imageset/Logo@2x.png deleted file mode 100644 index d2299c42f8df94996e6e28e3f937f269b8a6994f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1036 zcmV+n1oQieP)SU0b*sq4aU7P&CDAivPd zW%L3>?2(GZ)|MzXU$v5%mpSUv;EOAZUm7@ zM64MVK!ypVQozVad)fXz@coDL-Q+7^YRV?nic%R^U-xeWgLq6U{6GMi)m3jYR^^kx z(z|AU>NPMs+fJtbueA@<%fs-6P%y4gKaKhsa7%}V5Qumj7#p)IWcwg6(b+ft<+S!` z5sEdhLau--a)b47abdT#-V;ou+%gngUM}0QyLUOy;phlnjG{~^>QQ6~*p1KY%^xx0 zr6rpfr#Ud(TCs*|%{h$mm=7V?)84a(cviLI@X+}auGlST#@?P&xNOJbZ^hsse4!vD zc&Oa)YUSzQ_IFIG6<(t6n2;ab)A;f~x zr#tgOn`apCO5GJ9zZLg3ASa;6n`>8u?h>@Qo+_&)?;pGe+7Mx4I*VYjh z$26=vxn}&Pg|0)`tqOfn>>kXFXP@Z(VQhkEBo)L}_vigRE{SyfG z%NXSH#;HXy!9iNifo9!?xNkz5Hy=jr_QR-3{N{zP`V@J6L-^{}A|y@Z$pv|f5ZK8G z9rp-pbETQKix68#kPmmKge)Dwo_2wRfcN!FfqNyy*vX0ws`1+m_FAQi19ra=0(=9P zPRkfNfMe50CZ$G8zyT%0{G>p-%hPb5z_iv9O<|D>ykO#_%b0cTN21=&h?fY7wq2}7 zYJ-xYL>{7sjtI4#7+Fu-S&^icrXadspG1e}E#D(7PjA{^67<8BTN2|_#O9TXnNB_u zVGUWxCTU9*80Y&WX)mN&qjEaNC<+Q>b3zN?ZqU_l5Uv4my+oU8eyw}}0000yWIRa`@((!BdnUN^3^s_rAegjdq7c;&Ci-g7o{*`aH}TkG~M!YY_=<-A>VBqr%I#2-)ZdZs5%m$Obb>jxafQ=O{v-V0eZ3NP(c(#MH1~`w+Qx=VOY%7{|F|BojFe1Gc0Rv; zgH-h69s;KUv0BX$=2$Bsw`YN+C183QxOMCLwUZOz_haDrIQ||B3qY}${vH98%4J<} zoX1*)flUIw`VzQzFTR{KZm&uUyuNvD>5KM*i9@XS=uz-J!l0<{b8eq3n^sy~0Tvg{ z%1%wuDz||bFW4(CI_FJA31Z3OJWyw(cz$kHhsypE=TR~&?lkDm9V9}!s?_t4`lg~h zWc;m^&CAnbMT>#$oJnE1dC~@XNVgS%f?s43#PX&OmE)>lL7Rn5CSxiJXIy^X?w)B` zNYGpIbQ?3uVDTbPEDZ5n;=#_&fXT^D_w$R8`kvMN9RA#{FXKn*N;!E5VQr9m#yS$2 ztSKtiVlse(8s&-F-Rpdp28zH}^oS21UU%c&E?w&R-MLry4L#wv-vs^~W@pVjq&SaQ zVbh<6Uy~;ugt+OWqwsn%6@<-EFJHC=2H~H`qp;8e^5aoYFKTj?#z!H-gD@SVM#3gT zZWcGMh}FW%oc$SD)qKthp(l%(5Z1u+cNi!G7Z%39?=es;y88yQM{{Tv8z?HP!Z5@! zHfSvO)pF(#i)B_==8%nyJO+%!i`~Jm&}(E7hX^~x06F>vLm`u0kt^tbWCfi$=fQPz zJ(KQUTNH*2y89M|<+;z>R52q>7L<+UK(5 zfP!jK1+TUvxU2a$(j=@XJcf-LZC~OK&hhya(T~Iz-A)waU%kbllH7MI! z!b;sIs@qNy$xtW$t0?jE(n&K)K3Q`w%1M41F|LHkL`|zbMhY%_$PpP7*W!9Ma2qLH zza)*Uuj?X4h-<N}eXl4% zB`w!XZMil@iU=HgCYcJo5cyJImvI=M-aN)ImW+L`C}E6bZt}^enhMfj_uHQq`9+03>4PVJj}cHN01HM4;6|$++)#E8E8_J#z!dDmSt y0$SI0000 Date: Tue, 7 Feb 2023 09:33:47 +0100 Subject: [PATCH 22/24] chore(Widget): Add missing L10n --- Localization/app.json | 6 ++ Mastodon.xcodeproj/project.pbxproj | 76 +++++++++++++++++-- .../Generated/Strings.swift | 12 +++ .../Resources/Base.lproj/Localizable.strings | 6 ++ .../WidgetExtension.intentdefinition | 0 .../FollowersCount/FollowersCountWidget.swift | 5 +- .../LatestFollowersWidget.swift | 5 +- .../MultiFollowersCountWidget.swift | 4 +- .../ar.lproj/WidgetExtension.strings | 28 +++++++ .../ca.lproj/WidgetExtension.strings | 28 +++++++ .../ckb.lproj/WidgetExtension.strings | 28 +++++++ .../cs.lproj/WidgetExtension.strings | 28 +++++++ .../de.lproj/WidgetExtension.strings | 28 +++++++ .../en.lproj/WidgetExtension.strings | 28 +++++++ .../es-AR.lproj/WidgetExtension.strings | 28 +++++++ .../es.lproj/WidgetExtension.strings | 28 +++++++ .../eu.lproj/WidgetExtension.strings | 28 +++++++ .../fi.lproj/WidgetExtension.strings | 28 +++++++ .../fr.lproj/WidgetExtension.strings | 28 +++++++ .../gd.lproj/WidgetExtension.strings | 28 +++++++ .../gl.lproj/WidgetExtension.strings | 28 +++++++ .../it.lproj/WidgetExtension.strings | 28 +++++++ .../ja.lproj/WidgetExtension.strings | 28 +++++++ .../kab.lproj/WidgetExtension.strings | 28 +++++++ .../ku.lproj/WidgetExtension.strings | 28 +++++++ .../nl.lproj/WidgetExtension.strings | 28 +++++++ .../ru.lproj/WidgetExtension.strings | 28 +++++++ .../sl.lproj/WidgetExtension.strings | 28 +++++++ .../sv.lproj/WidgetExtension.strings | 28 +++++++ .../th.lproj/WidgetExtension.strings | 28 +++++++ .../tr.lproj/WidgetExtension.strings | 28 +++++++ .../vi.lproj/WidgetExtension.strings | 28 +++++++ .../zh-Hans.lproj/WidgetExtension.strings | 28 +++++++ .../zh-Hant.lproj/WidgetExtension.strings | 28 +++++++ 34 files changed, 828 insertions(+), 14 deletions(-) rename WidgetExtension/{ => Base.lproj}/WidgetExtension.intentdefinition (100%) create mode 100644 WidgetExtension/ar.lproj/WidgetExtension.strings create mode 100644 WidgetExtension/ca.lproj/WidgetExtension.strings create mode 100644 WidgetExtension/ckb.lproj/WidgetExtension.strings create mode 100644 WidgetExtension/cs.lproj/WidgetExtension.strings create mode 100644 WidgetExtension/de.lproj/WidgetExtension.strings create mode 100644 WidgetExtension/en.lproj/WidgetExtension.strings create mode 100644 WidgetExtension/es-AR.lproj/WidgetExtension.strings create mode 100644 WidgetExtension/es.lproj/WidgetExtension.strings create mode 100644 WidgetExtension/eu.lproj/WidgetExtension.strings create mode 100644 WidgetExtension/fi.lproj/WidgetExtension.strings create mode 100644 WidgetExtension/fr.lproj/WidgetExtension.strings create mode 100644 WidgetExtension/gd.lproj/WidgetExtension.strings create mode 100644 WidgetExtension/gl.lproj/WidgetExtension.strings create mode 100644 WidgetExtension/it.lproj/WidgetExtension.strings create mode 100644 WidgetExtension/ja.lproj/WidgetExtension.strings create mode 100644 WidgetExtension/kab.lproj/WidgetExtension.strings create mode 100644 WidgetExtension/ku.lproj/WidgetExtension.strings create mode 100644 WidgetExtension/nl.lproj/WidgetExtension.strings create mode 100644 WidgetExtension/ru.lproj/WidgetExtension.strings create mode 100644 WidgetExtension/sl.lproj/WidgetExtension.strings create mode 100644 WidgetExtension/sv.lproj/WidgetExtension.strings create mode 100644 WidgetExtension/th.lproj/WidgetExtension.strings create mode 100644 WidgetExtension/tr.lproj/WidgetExtension.strings create mode 100644 WidgetExtension/vi.lproj/WidgetExtension.strings create mode 100644 WidgetExtension/zh-Hans.lproj/WidgetExtension.strings create mode 100644 WidgetExtension/zh-Hant.lproj/WidgetExtension.strings diff --git a/Localization/app.json b/Localization/app.json index 48f31c813..a171b3e2d 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -823,16 +823,22 @@ "user_not_logged_in": "Please open Mastodon to log in to an Account." }, "followers_count": { + "configuration_display_name": "Followers", + "configuration_description": "Show number of followers.", "title": "FOLLOWERS", "followers_today": "%s followers today" }, "multiple_followers": { + "configuration_display_name": "Multiple followers", + "configuration_description": "Show number of followers for multiple accounts.", "mock_user": { "display_name": "Another follower", "account_name": "another@follower.social" } }, "latest_followers": { + "configuration_display_name": "Latest followers", + "configuration_description": "Show latest followers.", "title": "Latest followers", "last_update": "Last update: %s" } diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index ed740fe04..4f75ca4e2 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -40,8 +40,6 @@ 2A728127297EA9D7004138C5 /* WidgetExtensionBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A728126297EA9D7004138C5 /* WidgetExtensionBundle.swift */; }; 2A72812B297EA9D7004138C5 /* FollowersCountWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A72812A297EA9D7004138C5 /* FollowersCountWidget.swift */; }; 2A72812E297EA9D8004138C5 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2A72812D297EA9D8004138C5 /* Assets.xcassets */; }; - 2A728130297EA9D8004138C5 /* WidgetExtension.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 2A72812C297EA9D7004138C5 /* WidgetExtension.intentdefinition */; }; - 2A728131297EA9D8004138C5 /* WidgetExtension.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 2A72812C297EA9D7004138C5 /* WidgetExtension.intentdefinition */; }; 2A728134297EA9D8004138C5 /* WidgetExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 2A728120297EA9D7004138C5 /* WidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 2A72813F297EC762004138C5 /* WidgetExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A72813E297EC762004138C5 /* WidgetExtension.swift */; }; 2A76F75C2930D94700B3388D /* HashtagTimelineHeaderViewActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A76F75B2930D94700B3388D /* HashtagTimelineHeaderViewActionButton.swift */; }; @@ -55,7 +53,9 @@ 2A9D066F298D0FD100BF38CB /* MastodonSDKDynamic in Frameworks */ = {isa = PBXBuildFile; productRef = 2A9D066E298D0FD100BF38CB /* MastodonSDKDynamic */; }; 2AB12E4629362F27006BC925 /* DataSourceFacade+Translate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AB12E4529362F27006BC925 /* DataSourceFacade+Translate.swift */; }; 2AB5011B2992322500346092 /* LightChart in Frameworks */ = {isa = PBXBuildFile; productRef = 2AB5011A2992322500346092 /* LightChart */; }; - 2AE202AA297FE10B00F66E55 /* WidgetExtension.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 2A72812C297EA9D7004138C5 /* WidgetExtension.intentdefinition */; }; + 2AB5011C299243FB00346092 /* WidgetExtension.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 2AB50120299243FB00346092 /* WidgetExtension.intentdefinition */; }; + 2AB5011D299243FB00346092 /* WidgetExtension.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 2AB50120299243FB00346092 /* WidgetExtension.intentdefinition */; }; + 2AB5011E299243FB00346092 /* WidgetExtension.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 2AB50120299243FB00346092 /* WidgetExtension.intentdefinition */; }; 2AE202AC297FE19100F66E55 /* FollowersCountIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AE202AB297FE19100F66E55 /* FollowersCountIntentHandler.swift */; }; 2AE202AD297FE1CD00F66E55 /* WidgetExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A72813E297EC762004138C5 /* WidgetExtension.swift */; }; 2AE244482927831100BDBF7C /* UIImage+SFSymbols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AE244472927831100BDBF7C /* UIImage+SFSymbols.swift */; }; @@ -630,7 +630,6 @@ 2A728123297EA9D7004138C5 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; 2A728126297EA9D7004138C5 /* WidgetExtensionBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetExtensionBundle.swift; sourceTree = ""; }; 2A72812A297EA9D7004138C5 /* FollowersCountWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowersCountWidget.swift; sourceTree = ""; }; - 2A72812C297EA9D7004138C5 /* WidgetExtension.intentdefinition */ = {isa = PBXFileReference; lastKnownFileType = file.intentdefinition; path = WidgetExtension.intentdefinition; sourceTree = ""; }; 2A72812D297EA9D8004138C5 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 2A72812F297EA9D8004138C5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 2A72813E297EC762004138C5 /* WidgetExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetExtension.swift; sourceTree = ""; }; @@ -642,6 +641,33 @@ 2A9D0663298C048800BF38CB /* LatestFollowersWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LatestFollowersWidget.swift; sourceTree = ""; }; 2A9D0665298C05A800BF38CB /* LatestFollowersWidgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LatestFollowersWidgetView.swift; sourceTree = ""; }; 2AB12E4529362F27006BC925 /* DataSourceFacade+Translate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Translate.swift"; sourceTree = ""; }; + 2AB5011F299243FB00346092 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.intentdefinition; name = Base; path = Base.lproj/WidgetExtension.intentdefinition; sourceTree = ""; }; + 2AB501222992440200346092 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/WidgetExtension.strings; sourceTree = ""; }; + 2AB501242992443100346092 /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = vi; path = vi.lproj/WidgetExtension.strings; sourceTree = ""; }; + 2AB501262992443100346092 /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/WidgetExtension.strings; sourceTree = ""; }; + 2AB501282992443200346092 /* th */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = th; path = th.lproj/WidgetExtension.strings; sourceTree = ""; }; + 2AB5012A2992443200346092 /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/WidgetExtension.strings; sourceTree = ""; }; + 2AB5012C2992443300346092 /* es-AR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-AR"; path = "es-AR.lproj/WidgetExtension.strings"; sourceTree = ""; }; + 2AB5012E2992443300346092 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/WidgetExtension.strings; sourceTree = ""; }; + 2AB501302992443400346092 /* sl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sl; path = sl.lproj/WidgetExtension.strings; sourceTree = ""; }; + 2AB501322992443400346092 /* gd */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = gd; path = gd.lproj/WidgetExtension.strings; sourceTree = ""; }; + 2AB501342992443500346092 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/WidgetExtension.strings; sourceTree = ""; }; + 2AB501362992443600346092 /* ckb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ckb; path = ckb.lproj/WidgetExtension.strings; sourceTree = ""; }; + 2AB501382992443600346092 /* ku */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ku; path = ku.lproj/WidgetExtension.strings; sourceTree = ""; }; + 2AB5013A2992443700346092 /* kab */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = kab; path = kab.lproj/WidgetExtension.strings; sourceTree = ""; }; + 2AB5013C2992443700346092 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/WidgetExtension.strings; sourceTree = ""; }; + 2AB5013E2992443800346092 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/WidgetExtension.strings; sourceTree = ""; }; + 2AB501402992443800346092 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/WidgetExtension.strings; sourceTree = ""; }; + 2AB501422992443900346092 /* gl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = gl; path = gl.lproj/WidgetExtension.strings; sourceTree = ""; }; + 2AB501442992443900346092 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/WidgetExtension.strings; sourceTree = ""; }; + 2AB501462992443900346092 /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/WidgetExtension.strings; sourceTree = ""; }; + 2AB501482992443A00346092 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/WidgetExtension.strings; sourceTree = ""; }; + 2AB5014A2992443A00346092 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/WidgetExtension.strings; sourceTree = ""; }; + 2AB5014C2992443B00346092 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/WidgetExtension.strings"; sourceTree = ""; }; + 2AB5014E2992443B00346092 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/WidgetExtension.strings"; sourceTree = ""; }; + 2AB501502992443C00346092 /* ca */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ca; path = ca.lproj/WidgetExtension.strings; sourceTree = ""; }; + 2AB501522992443C00346092 /* eu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = eu; path = eu.lproj/WidgetExtension.strings; sourceTree = ""; }; + 2AB501542992443D00346092 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/WidgetExtension.strings; sourceTree = ""; }; 2AE202A9297FDDF500F66E55 /* WidgetExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WidgetExtension.entitlements; sourceTree = ""; }; 2AE202AB297FE19100F66E55 /* FollowersCountIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowersCountIntentHandler.swift; sourceTree = ""; }; 2AE244472927831100BDBF7C /* UIImage+SFSymbols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+SFSymbols.swift"; sourceTree = ""; }; @@ -1418,7 +1444,7 @@ 2AE202A9297FDDF500F66E55 /* WidgetExtension.entitlements */, 2A72813E297EC762004138C5 /* WidgetExtension.swift */, 2A728126297EA9D7004138C5 /* WidgetExtensionBundle.swift */, - 2A72812C297EA9D7004138C5 /* WidgetExtension.intentdefinition */, + 2AB50120299243FB00346092 /* WidgetExtension.intentdefinition */, 2A72812D297EA9D8004138C5 /* Assets.xcassets */, 2A72812F297EA9D8004138C5 /* Info.plist */, ); @@ -3484,7 +3510,7 @@ files = ( 2A33062D2987DBFA001D4C51 /* FollowersCountHistory.swift in Sources */, 2A9D0666298C05A800BF38CB /* LatestFollowersWidgetView.swift in Sources */, - 2A728130297EA9D8004138C5 /* WidgetExtension.intentdefinition in Sources */, + 2AB5011E299243FB00346092 /* WidgetExtension.intentdefinition in Sources */, 2A86A14B2989326E007F1062 /* MultiFollowersCountWidgetView.swift in Sources */, 2A72813F297EC762004138C5 /* WidgetExtension.swift in Sources */, 2A86A14929892B3A007F1062 /* MultiFollowersCountWidget.swift in Sources */, @@ -3789,7 +3815,7 @@ DB98EB5627B0FF1B0082E365 /* ReportViewControllerAppearance.swift in Sources */, DB3EA8E6281B79E200598866 /* DiscoveryCommunityViewController.swift in Sources */, 2D206B8625F5FB0900143C56 /* Double.swift in Sources */, - 2A728131297EA9D8004138C5 /* WidgetExtension.intentdefinition in Sources */, + 2AB5011C299243FB00346092 /* WidgetExtension.intentdefinition in Sources */, DB9F58F126EF512300E7BBE9 /* AccountListTableViewCell.swift in Sources */, 2D76319F25C1521200929FB9 /* StatusSection.swift in Sources */, DB938F0326240EA300E5B6C1 /* CachedThreadViewModel.swift in Sources */, @@ -3927,7 +3953,7 @@ DB0009A726AEE5DC009B9D2D /* Intents.intentdefinition in Sources */, 2AE202AD297FE1CD00F66E55 /* WidgetExtension.swift in Sources */, DBB8AB4626AECDE200F6D281 /* SendPostIntentHandler.swift in Sources */, - 2AE202AA297FE10B00F66E55 /* WidgetExtension.intentdefinition in Sources */, + 2AB5011D299243FB00346092 /* WidgetExtension.intentdefinition in Sources */, 2AE202AC297FE19100F66E55 /* FollowersCountIntentHandler.swift in Sources */, DB64BA452851F23000ADF1B7 /* MastodonAuthentication+Fetch.swift in Sources */, DB64BA482851F29300ADF1B7 /* Account+Fetch.swift in Sources */, @@ -3998,6 +4024,40 @@ /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ + 2AB50120299243FB00346092 /* WidgetExtension.intentdefinition */ = { + isa = PBXVariantGroup; + children = ( + 2AB5011F299243FB00346092 /* Base */, + 2AB501222992440200346092 /* en */, + 2AB501242992443100346092 /* vi */, + 2AB501262992443100346092 /* tr */, + 2AB501282992443200346092 /* th */, + 2AB5012A2992443200346092 /* sv */, + 2AB5012C2992443300346092 /* es-AR */, + 2AB5012E2992443300346092 /* es */, + 2AB501302992443400346092 /* sl */, + 2AB501322992443400346092 /* gd */, + 2AB501342992443500346092 /* ru */, + 2AB501362992443600346092 /* ckb */, + 2AB501382992443600346092 /* ku */, + 2AB5013A2992443700346092 /* kab */, + 2AB5013C2992443700346092 /* ja */, + 2AB5013E2992443800346092 /* it */, + 2AB501402992443800346092 /* de */, + 2AB501422992443900346092 /* gl */, + 2AB501442992443900346092 /* fr */, + 2AB501462992443900346092 /* fi */, + 2AB501482992443A00346092 /* nl */, + 2AB5014A2992443A00346092 /* cs */, + 2AB5014C2992443B00346092 /* zh-Hant */, + 2AB5014E2992443B00346092 /* zh-Hans */, + 2AB501502992443C00346092 /* ca */, + 2AB501522992443C00346092 /* eu */, + 2AB501542992443D00346092 /* ar */, + ); + name = WidgetExtension.intentdefinition; + sourceTree = ""; + }; DB0009A926AEE5DC009B9D2D /* Intents.intentdefinition */ = { isa = PBXVariantGroup; children = ( diff --git a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift index f0b3bc02e..1b3eb4dc6 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift +++ b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift @@ -1487,6 +1487,10 @@ public enum L10n { public static let userNotLoggedIn = L10n.tr("Localizable", "Widget.Common.UserNotLoggedIn", fallback: "Please open Mastodon to log in to an Account.") } public enum FollowersCount { + /// Show number of followers. + public static let configurationDescription = L10n.tr("Localizable", "Widget.FollowersCount.ConfigurationDescription", fallback: "Show number of followers.") + /// Followers + public static let configurationDisplayName = L10n.tr("Localizable", "Widget.FollowersCount.ConfigurationDisplayName", fallback: "Followers") /// %@ followers today public static func followersToday(_ p1: Any) -> String { return L10n.tr("Localizable", "Widget.FollowersCount.FollowersToday", String(describing: p1), fallback: "%@ followers today") @@ -1495,6 +1499,10 @@ public enum L10n { public static let title = L10n.tr("Localizable", "Widget.FollowersCount.Title", fallback: "FOLLOWERS") } public enum LatestFollowers { + /// Show latest followers. + public static let configurationDescription = L10n.tr("Localizable", "Widget.LatestFollowers.ConfigurationDescription", fallback: "Show latest followers.") + /// Latest followers + public static let configurationDisplayName = L10n.tr("Localizable", "Widget.LatestFollowers.ConfigurationDisplayName", fallback: "Latest followers") /// Last update: %@ public static func lastUpdate(_ p1: Any) -> String { return L10n.tr("Localizable", "Widget.LatestFollowers.LastUpdate", String(describing: p1), fallback: "Last update: %@") @@ -1503,6 +1511,10 @@ public enum L10n { public static let title = L10n.tr("Localizable", "Widget.LatestFollowers.Title", fallback: "Latest followers") } public enum MultipleFollowers { + /// Show number of followers for multiple accounts. + public static let configurationDescription = L10n.tr("Localizable", "Widget.MultipleFollowers.ConfigurationDescription", fallback: "Show number of followers for multiple accounts.") + /// Multiple followers + public static let configurationDisplayName = L10n.tr("Localizable", "Widget.MultipleFollowers.ConfigurationDisplayName", fallback: "Multiple followers") public enum MockUser { /// another@follower.social public static let accountName = L10n.tr("Localizable", "Widget.MultipleFollowers.MockUser.AccountName", fallback: "another@follower.social") diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings index d118b86b7..f1823554f 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings @@ -520,9 +520,15 @@ You can’t go wrong with any of our recommend servers, so regardless of which o "Extension.OpenIn.InvalidLinkError" = "This doesn't seem to be a valid Mastodon link."; "Widget.Common.UnsupportedWidgetFamily" = "Sorry but this Widget family is unsupported."; "Widget.Common.UserNotLoggedIn" = "Please open Mastodon to log in to an Account."; +"Widget.FollowersCount.ConfigurationDisplayName" = "Followers"; +"Widget.FollowersCount.ConfigurationDescription" = "Show number of followers."; "Widget.FollowersCount.Title" = "FOLLOWERS"; "Widget.FollowersCount.FollowersToday" = "%@ followers today"; +"Widget.MultipleFollowers.ConfigurationDisplayName" = "Multiple followers"; +"Widget.MultipleFollowers.ConfigurationDescription" = "Show number of followers for multiple accounts."; "Widget.MultipleFollowers.MockUser.DisplayName" = "Another follower"; "Widget.MultipleFollowers.MockUser.AccountName" = "another@follower.social"; +"Widget.LatestFollowers.ConfigurationDisplayName" = "Latest followers"; +"Widget.LatestFollowers.ConfigurationDescription" = "Show latest followers."; "Widget.LatestFollowers.Title" = "Latest followers"; "Widget.LatestFollowers.LastUpdate" = "Last update: %@"; diff --git a/WidgetExtension/WidgetExtension.intentdefinition b/WidgetExtension/Base.lproj/WidgetExtension.intentdefinition similarity index 100% rename from WidgetExtension/WidgetExtension.intentdefinition rename to WidgetExtension/Base.lproj/WidgetExtension.intentdefinition diff --git a/WidgetExtension/Variants/FollowersCount/FollowersCountWidget.swift b/WidgetExtension/Variants/FollowersCount/FollowersCountWidget.swift index 6f9a6e81c..d490d2084 100644 --- a/WidgetExtension/Variants/FollowersCount/FollowersCountWidget.swift +++ b/WidgetExtension/Variants/FollowersCount/FollowersCountWidget.swift @@ -4,6 +4,7 @@ import WidgetKit import SwiftUI import Intents import MastodonSDK +import MastodonLocalization struct FollowersCountWidgetProvider: IntentTimelineProvider { private let followersHistory = FollowersCountHistory.shared @@ -63,8 +64,8 @@ struct FollowersCountWidget: Widget { IntentConfiguration(kind: "Followers", intent: FollowersCountIntent.self, provider: FollowersCountWidgetProvider()) { entry in FollowersCountWidgetView(entry: entry) } - .configurationDisplayName("Followers") - .description("Show number of followers.") + .configurationDisplayName(L10n.Widget.FollowersCount.configurationDisplayName) + .description(L10n.Widget.FollowersCount.configurationDescription) .supportedFamilies(availableFamilies) } } diff --git a/WidgetExtension/Variants/LatestFollowers/LatestFollowersWidget.swift b/WidgetExtension/Variants/LatestFollowers/LatestFollowersWidget.swift index 66c5bd52f..d2bf73b5a 100644 --- a/WidgetExtension/Variants/LatestFollowers/LatestFollowersWidget.swift +++ b/WidgetExtension/Variants/LatestFollowers/LatestFollowersWidget.swift @@ -4,6 +4,7 @@ import WidgetKit import SwiftUI import Intents import MastodonSDK +import MastodonLocalization struct LatestFollowersWidgetProvider: IntentTimelineProvider { func placeholder(in context: Context) -> LatestFollowersEntry { @@ -67,8 +68,8 @@ struct LatestFollowersWidget: Widget { IntentConfiguration(kind: "Latest followers", intent: LatestFollowersIntent.self, provider: LatestFollowersWidgetProvider()) { entry in LatestFollowersWidgetView(entry: entry) } - .configurationDisplayName("Latest followers") - .description("Show latest followers.") + .configurationDisplayName(L10n.Widget.LatestFollowers.configurationDisplayName) + .description(L10n.Widget.LatestFollowers.configurationDescription) .supportedFamilies(availableFamilies) } } diff --git a/WidgetExtension/Variants/MultiFollowersCount/MultiFollowersCountWidget.swift b/WidgetExtension/Variants/MultiFollowersCount/MultiFollowersCountWidget.swift index f39e548c4..b53c9af6c 100644 --- a/WidgetExtension/Variants/MultiFollowersCount/MultiFollowersCountWidget.swift +++ b/WidgetExtension/Variants/MultiFollowersCount/MultiFollowersCountWidget.swift @@ -61,8 +61,8 @@ struct MultiFollowersCountWidget: Widget { IntentConfiguration(kind: "Multiple followers", intent: MultiFollowersCountIntent.self, provider: MultiFollowersCountWidgetProvider()) { entry in MultiFollowersCountWidgetView(entry: entry) } - .configurationDisplayName("Multiple followers") - .description("Show number of followers for multiple accounts.") + .configurationDisplayName(L10n.Widget.MultipleFollowers.configurationDisplayName) + .description(L10n.Widget.MultipleFollowers.configurationDescription) .supportedFamilies(availableFamilies) } } diff --git a/WidgetExtension/ar.lproj/WidgetExtension.strings b/WidgetExtension/ar.lproj/WidgetExtension.strings new file mode 100644 index 000000000..37a67e62d --- /dev/null +++ b/WidgetExtension/ar.lproj/WidgetExtension.strings @@ -0,0 +1,28 @@ +"3d6HSO" = "Enter username"; + +"7DZrRA" = "Username"; + +"82L4Nj" = "Yes"; + +"BFppgH" = "Username"; + +"OL6lkx" = "Account"; + +"ZLZ6sg" = "Latest Followers"; + +"e0W2wo" = "Multi Followers Count"; + +"fovmPX" = "Accounts"; + +"gpCwrM" = "Followers Count"; + +"jg9D5P" = "No"; + +"sOLUtG" = "Enter follower Username"; + +"tVvJ9c" = "Followers"; + +"xVtyec" = "Show chart"; + +"zeJo4f" = "Should the Widget show a chart?"; + diff --git a/WidgetExtension/ca.lproj/WidgetExtension.strings b/WidgetExtension/ca.lproj/WidgetExtension.strings new file mode 100644 index 000000000..37a67e62d --- /dev/null +++ b/WidgetExtension/ca.lproj/WidgetExtension.strings @@ -0,0 +1,28 @@ +"3d6HSO" = "Enter username"; + +"7DZrRA" = "Username"; + +"82L4Nj" = "Yes"; + +"BFppgH" = "Username"; + +"OL6lkx" = "Account"; + +"ZLZ6sg" = "Latest Followers"; + +"e0W2wo" = "Multi Followers Count"; + +"fovmPX" = "Accounts"; + +"gpCwrM" = "Followers Count"; + +"jg9D5P" = "No"; + +"sOLUtG" = "Enter follower Username"; + +"tVvJ9c" = "Followers"; + +"xVtyec" = "Show chart"; + +"zeJo4f" = "Should the Widget show a chart?"; + diff --git a/WidgetExtension/ckb.lproj/WidgetExtension.strings b/WidgetExtension/ckb.lproj/WidgetExtension.strings new file mode 100644 index 000000000..37a67e62d --- /dev/null +++ b/WidgetExtension/ckb.lproj/WidgetExtension.strings @@ -0,0 +1,28 @@ +"3d6HSO" = "Enter username"; + +"7DZrRA" = "Username"; + +"82L4Nj" = "Yes"; + +"BFppgH" = "Username"; + +"OL6lkx" = "Account"; + +"ZLZ6sg" = "Latest Followers"; + +"e0W2wo" = "Multi Followers Count"; + +"fovmPX" = "Accounts"; + +"gpCwrM" = "Followers Count"; + +"jg9D5P" = "No"; + +"sOLUtG" = "Enter follower Username"; + +"tVvJ9c" = "Followers"; + +"xVtyec" = "Show chart"; + +"zeJo4f" = "Should the Widget show a chart?"; + diff --git a/WidgetExtension/cs.lproj/WidgetExtension.strings b/WidgetExtension/cs.lproj/WidgetExtension.strings new file mode 100644 index 000000000..37a67e62d --- /dev/null +++ b/WidgetExtension/cs.lproj/WidgetExtension.strings @@ -0,0 +1,28 @@ +"3d6HSO" = "Enter username"; + +"7DZrRA" = "Username"; + +"82L4Nj" = "Yes"; + +"BFppgH" = "Username"; + +"OL6lkx" = "Account"; + +"ZLZ6sg" = "Latest Followers"; + +"e0W2wo" = "Multi Followers Count"; + +"fovmPX" = "Accounts"; + +"gpCwrM" = "Followers Count"; + +"jg9D5P" = "No"; + +"sOLUtG" = "Enter follower Username"; + +"tVvJ9c" = "Followers"; + +"xVtyec" = "Show chart"; + +"zeJo4f" = "Should the Widget show a chart?"; + diff --git a/WidgetExtension/de.lproj/WidgetExtension.strings b/WidgetExtension/de.lproj/WidgetExtension.strings new file mode 100644 index 000000000..37a67e62d --- /dev/null +++ b/WidgetExtension/de.lproj/WidgetExtension.strings @@ -0,0 +1,28 @@ +"3d6HSO" = "Enter username"; + +"7DZrRA" = "Username"; + +"82L4Nj" = "Yes"; + +"BFppgH" = "Username"; + +"OL6lkx" = "Account"; + +"ZLZ6sg" = "Latest Followers"; + +"e0W2wo" = "Multi Followers Count"; + +"fovmPX" = "Accounts"; + +"gpCwrM" = "Followers Count"; + +"jg9D5P" = "No"; + +"sOLUtG" = "Enter follower Username"; + +"tVvJ9c" = "Followers"; + +"xVtyec" = "Show chart"; + +"zeJo4f" = "Should the Widget show a chart?"; + diff --git a/WidgetExtension/en.lproj/WidgetExtension.strings b/WidgetExtension/en.lproj/WidgetExtension.strings new file mode 100644 index 000000000..37a67e62d --- /dev/null +++ b/WidgetExtension/en.lproj/WidgetExtension.strings @@ -0,0 +1,28 @@ +"3d6HSO" = "Enter username"; + +"7DZrRA" = "Username"; + +"82L4Nj" = "Yes"; + +"BFppgH" = "Username"; + +"OL6lkx" = "Account"; + +"ZLZ6sg" = "Latest Followers"; + +"e0W2wo" = "Multi Followers Count"; + +"fovmPX" = "Accounts"; + +"gpCwrM" = "Followers Count"; + +"jg9D5P" = "No"; + +"sOLUtG" = "Enter follower Username"; + +"tVvJ9c" = "Followers"; + +"xVtyec" = "Show chart"; + +"zeJo4f" = "Should the Widget show a chart?"; + diff --git a/WidgetExtension/es-AR.lproj/WidgetExtension.strings b/WidgetExtension/es-AR.lproj/WidgetExtension.strings new file mode 100644 index 000000000..37a67e62d --- /dev/null +++ b/WidgetExtension/es-AR.lproj/WidgetExtension.strings @@ -0,0 +1,28 @@ +"3d6HSO" = "Enter username"; + +"7DZrRA" = "Username"; + +"82L4Nj" = "Yes"; + +"BFppgH" = "Username"; + +"OL6lkx" = "Account"; + +"ZLZ6sg" = "Latest Followers"; + +"e0W2wo" = "Multi Followers Count"; + +"fovmPX" = "Accounts"; + +"gpCwrM" = "Followers Count"; + +"jg9D5P" = "No"; + +"sOLUtG" = "Enter follower Username"; + +"tVvJ9c" = "Followers"; + +"xVtyec" = "Show chart"; + +"zeJo4f" = "Should the Widget show a chart?"; + diff --git a/WidgetExtension/es.lproj/WidgetExtension.strings b/WidgetExtension/es.lproj/WidgetExtension.strings new file mode 100644 index 000000000..37a67e62d --- /dev/null +++ b/WidgetExtension/es.lproj/WidgetExtension.strings @@ -0,0 +1,28 @@ +"3d6HSO" = "Enter username"; + +"7DZrRA" = "Username"; + +"82L4Nj" = "Yes"; + +"BFppgH" = "Username"; + +"OL6lkx" = "Account"; + +"ZLZ6sg" = "Latest Followers"; + +"e0W2wo" = "Multi Followers Count"; + +"fovmPX" = "Accounts"; + +"gpCwrM" = "Followers Count"; + +"jg9D5P" = "No"; + +"sOLUtG" = "Enter follower Username"; + +"tVvJ9c" = "Followers"; + +"xVtyec" = "Show chart"; + +"zeJo4f" = "Should the Widget show a chart?"; + diff --git a/WidgetExtension/eu.lproj/WidgetExtension.strings b/WidgetExtension/eu.lproj/WidgetExtension.strings new file mode 100644 index 000000000..37a67e62d --- /dev/null +++ b/WidgetExtension/eu.lproj/WidgetExtension.strings @@ -0,0 +1,28 @@ +"3d6HSO" = "Enter username"; + +"7DZrRA" = "Username"; + +"82L4Nj" = "Yes"; + +"BFppgH" = "Username"; + +"OL6lkx" = "Account"; + +"ZLZ6sg" = "Latest Followers"; + +"e0W2wo" = "Multi Followers Count"; + +"fovmPX" = "Accounts"; + +"gpCwrM" = "Followers Count"; + +"jg9D5P" = "No"; + +"sOLUtG" = "Enter follower Username"; + +"tVvJ9c" = "Followers"; + +"xVtyec" = "Show chart"; + +"zeJo4f" = "Should the Widget show a chart?"; + diff --git a/WidgetExtension/fi.lproj/WidgetExtension.strings b/WidgetExtension/fi.lproj/WidgetExtension.strings new file mode 100644 index 000000000..37a67e62d --- /dev/null +++ b/WidgetExtension/fi.lproj/WidgetExtension.strings @@ -0,0 +1,28 @@ +"3d6HSO" = "Enter username"; + +"7DZrRA" = "Username"; + +"82L4Nj" = "Yes"; + +"BFppgH" = "Username"; + +"OL6lkx" = "Account"; + +"ZLZ6sg" = "Latest Followers"; + +"e0W2wo" = "Multi Followers Count"; + +"fovmPX" = "Accounts"; + +"gpCwrM" = "Followers Count"; + +"jg9D5P" = "No"; + +"sOLUtG" = "Enter follower Username"; + +"tVvJ9c" = "Followers"; + +"xVtyec" = "Show chart"; + +"zeJo4f" = "Should the Widget show a chart?"; + diff --git a/WidgetExtension/fr.lproj/WidgetExtension.strings b/WidgetExtension/fr.lproj/WidgetExtension.strings new file mode 100644 index 000000000..37a67e62d --- /dev/null +++ b/WidgetExtension/fr.lproj/WidgetExtension.strings @@ -0,0 +1,28 @@ +"3d6HSO" = "Enter username"; + +"7DZrRA" = "Username"; + +"82L4Nj" = "Yes"; + +"BFppgH" = "Username"; + +"OL6lkx" = "Account"; + +"ZLZ6sg" = "Latest Followers"; + +"e0W2wo" = "Multi Followers Count"; + +"fovmPX" = "Accounts"; + +"gpCwrM" = "Followers Count"; + +"jg9D5P" = "No"; + +"sOLUtG" = "Enter follower Username"; + +"tVvJ9c" = "Followers"; + +"xVtyec" = "Show chart"; + +"zeJo4f" = "Should the Widget show a chart?"; + diff --git a/WidgetExtension/gd.lproj/WidgetExtension.strings b/WidgetExtension/gd.lproj/WidgetExtension.strings new file mode 100644 index 000000000..37a67e62d --- /dev/null +++ b/WidgetExtension/gd.lproj/WidgetExtension.strings @@ -0,0 +1,28 @@ +"3d6HSO" = "Enter username"; + +"7DZrRA" = "Username"; + +"82L4Nj" = "Yes"; + +"BFppgH" = "Username"; + +"OL6lkx" = "Account"; + +"ZLZ6sg" = "Latest Followers"; + +"e0W2wo" = "Multi Followers Count"; + +"fovmPX" = "Accounts"; + +"gpCwrM" = "Followers Count"; + +"jg9D5P" = "No"; + +"sOLUtG" = "Enter follower Username"; + +"tVvJ9c" = "Followers"; + +"xVtyec" = "Show chart"; + +"zeJo4f" = "Should the Widget show a chart?"; + diff --git a/WidgetExtension/gl.lproj/WidgetExtension.strings b/WidgetExtension/gl.lproj/WidgetExtension.strings new file mode 100644 index 000000000..37a67e62d --- /dev/null +++ b/WidgetExtension/gl.lproj/WidgetExtension.strings @@ -0,0 +1,28 @@ +"3d6HSO" = "Enter username"; + +"7DZrRA" = "Username"; + +"82L4Nj" = "Yes"; + +"BFppgH" = "Username"; + +"OL6lkx" = "Account"; + +"ZLZ6sg" = "Latest Followers"; + +"e0W2wo" = "Multi Followers Count"; + +"fovmPX" = "Accounts"; + +"gpCwrM" = "Followers Count"; + +"jg9D5P" = "No"; + +"sOLUtG" = "Enter follower Username"; + +"tVvJ9c" = "Followers"; + +"xVtyec" = "Show chart"; + +"zeJo4f" = "Should the Widget show a chart?"; + diff --git a/WidgetExtension/it.lproj/WidgetExtension.strings b/WidgetExtension/it.lproj/WidgetExtension.strings new file mode 100644 index 000000000..37a67e62d --- /dev/null +++ b/WidgetExtension/it.lproj/WidgetExtension.strings @@ -0,0 +1,28 @@ +"3d6HSO" = "Enter username"; + +"7DZrRA" = "Username"; + +"82L4Nj" = "Yes"; + +"BFppgH" = "Username"; + +"OL6lkx" = "Account"; + +"ZLZ6sg" = "Latest Followers"; + +"e0W2wo" = "Multi Followers Count"; + +"fovmPX" = "Accounts"; + +"gpCwrM" = "Followers Count"; + +"jg9D5P" = "No"; + +"sOLUtG" = "Enter follower Username"; + +"tVvJ9c" = "Followers"; + +"xVtyec" = "Show chart"; + +"zeJo4f" = "Should the Widget show a chart?"; + diff --git a/WidgetExtension/ja.lproj/WidgetExtension.strings b/WidgetExtension/ja.lproj/WidgetExtension.strings new file mode 100644 index 000000000..37a67e62d --- /dev/null +++ b/WidgetExtension/ja.lproj/WidgetExtension.strings @@ -0,0 +1,28 @@ +"3d6HSO" = "Enter username"; + +"7DZrRA" = "Username"; + +"82L4Nj" = "Yes"; + +"BFppgH" = "Username"; + +"OL6lkx" = "Account"; + +"ZLZ6sg" = "Latest Followers"; + +"e0W2wo" = "Multi Followers Count"; + +"fovmPX" = "Accounts"; + +"gpCwrM" = "Followers Count"; + +"jg9D5P" = "No"; + +"sOLUtG" = "Enter follower Username"; + +"tVvJ9c" = "Followers"; + +"xVtyec" = "Show chart"; + +"zeJo4f" = "Should the Widget show a chart?"; + diff --git a/WidgetExtension/kab.lproj/WidgetExtension.strings b/WidgetExtension/kab.lproj/WidgetExtension.strings new file mode 100644 index 000000000..37a67e62d --- /dev/null +++ b/WidgetExtension/kab.lproj/WidgetExtension.strings @@ -0,0 +1,28 @@ +"3d6HSO" = "Enter username"; + +"7DZrRA" = "Username"; + +"82L4Nj" = "Yes"; + +"BFppgH" = "Username"; + +"OL6lkx" = "Account"; + +"ZLZ6sg" = "Latest Followers"; + +"e0W2wo" = "Multi Followers Count"; + +"fovmPX" = "Accounts"; + +"gpCwrM" = "Followers Count"; + +"jg9D5P" = "No"; + +"sOLUtG" = "Enter follower Username"; + +"tVvJ9c" = "Followers"; + +"xVtyec" = "Show chart"; + +"zeJo4f" = "Should the Widget show a chart?"; + diff --git a/WidgetExtension/ku.lproj/WidgetExtension.strings b/WidgetExtension/ku.lproj/WidgetExtension.strings new file mode 100644 index 000000000..37a67e62d --- /dev/null +++ b/WidgetExtension/ku.lproj/WidgetExtension.strings @@ -0,0 +1,28 @@ +"3d6HSO" = "Enter username"; + +"7DZrRA" = "Username"; + +"82L4Nj" = "Yes"; + +"BFppgH" = "Username"; + +"OL6lkx" = "Account"; + +"ZLZ6sg" = "Latest Followers"; + +"e0W2wo" = "Multi Followers Count"; + +"fovmPX" = "Accounts"; + +"gpCwrM" = "Followers Count"; + +"jg9D5P" = "No"; + +"sOLUtG" = "Enter follower Username"; + +"tVvJ9c" = "Followers"; + +"xVtyec" = "Show chart"; + +"zeJo4f" = "Should the Widget show a chart?"; + diff --git a/WidgetExtension/nl.lproj/WidgetExtension.strings b/WidgetExtension/nl.lproj/WidgetExtension.strings new file mode 100644 index 000000000..37a67e62d --- /dev/null +++ b/WidgetExtension/nl.lproj/WidgetExtension.strings @@ -0,0 +1,28 @@ +"3d6HSO" = "Enter username"; + +"7DZrRA" = "Username"; + +"82L4Nj" = "Yes"; + +"BFppgH" = "Username"; + +"OL6lkx" = "Account"; + +"ZLZ6sg" = "Latest Followers"; + +"e0W2wo" = "Multi Followers Count"; + +"fovmPX" = "Accounts"; + +"gpCwrM" = "Followers Count"; + +"jg9D5P" = "No"; + +"sOLUtG" = "Enter follower Username"; + +"tVvJ9c" = "Followers"; + +"xVtyec" = "Show chart"; + +"zeJo4f" = "Should the Widget show a chart?"; + diff --git a/WidgetExtension/ru.lproj/WidgetExtension.strings b/WidgetExtension/ru.lproj/WidgetExtension.strings new file mode 100644 index 000000000..37a67e62d --- /dev/null +++ b/WidgetExtension/ru.lproj/WidgetExtension.strings @@ -0,0 +1,28 @@ +"3d6HSO" = "Enter username"; + +"7DZrRA" = "Username"; + +"82L4Nj" = "Yes"; + +"BFppgH" = "Username"; + +"OL6lkx" = "Account"; + +"ZLZ6sg" = "Latest Followers"; + +"e0W2wo" = "Multi Followers Count"; + +"fovmPX" = "Accounts"; + +"gpCwrM" = "Followers Count"; + +"jg9D5P" = "No"; + +"sOLUtG" = "Enter follower Username"; + +"tVvJ9c" = "Followers"; + +"xVtyec" = "Show chart"; + +"zeJo4f" = "Should the Widget show a chart?"; + diff --git a/WidgetExtension/sl.lproj/WidgetExtension.strings b/WidgetExtension/sl.lproj/WidgetExtension.strings new file mode 100644 index 000000000..37a67e62d --- /dev/null +++ b/WidgetExtension/sl.lproj/WidgetExtension.strings @@ -0,0 +1,28 @@ +"3d6HSO" = "Enter username"; + +"7DZrRA" = "Username"; + +"82L4Nj" = "Yes"; + +"BFppgH" = "Username"; + +"OL6lkx" = "Account"; + +"ZLZ6sg" = "Latest Followers"; + +"e0W2wo" = "Multi Followers Count"; + +"fovmPX" = "Accounts"; + +"gpCwrM" = "Followers Count"; + +"jg9D5P" = "No"; + +"sOLUtG" = "Enter follower Username"; + +"tVvJ9c" = "Followers"; + +"xVtyec" = "Show chart"; + +"zeJo4f" = "Should the Widget show a chart?"; + diff --git a/WidgetExtension/sv.lproj/WidgetExtension.strings b/WidgetExtension/sv.lproj/WidgetExtension.strings new file mode 100644 index 000000000..37a67e62d --- /dev/null +++ b/WidgetExtension/sv.lproj/WidgetExtension.strings @@ -0,0 +1,28 @@ +"3d6HSO" = "Enter username"; + +"7DZrRA" = "Username"; + +"82L4Nj" = "Yes"; + +"BFppgH" = "Username"; + +"OL6lkx" = "Account"; + +"ZLZ6sg" = "Latest Followers"; + +"e0W2wo" = "Multi Followers Count"; + +"fovmPX" = "Accounts"; + +"gpCwrM" = "Followers Count"; + +"jg9D5P" = "No"; + +"sOLUtG" = "Enter follower Username"; + +"tVvJ9c" = "Followers"; + +"xVtyec" = "Show chart"; + +"zeJo4f" = "Should the Widget show a chart?"; + diff --git a/WidgetExtension/th.lproj/WidgetExtension.strings b/WidgetExtension/th.lproj/WidgetExtension.strings new file mode 100644 index 000000000..37a67e62d --- /dev/null +++ b/WidgetExtension/th.lproj/WidgetExtension.strings @@ -0,0 +1,28 @@ +"3d6HSO" = "Enter username"; + +"7DZrRA" = "Username"; + +"82L4Nj" = "Yes"; + +"BFppgH" = "Username"; + +"OL6lkx" = "Account"; + +"ZLZ6sg" = "Latest Followers"; + +"e0W2wo" = "Multi Followers Count"; + +"fovmPX" = "Accounts"; + +"gpCwrM" = "Followers Count"; + +"jg9D5P" = "No"; + +"sOLUtG" = "Enter follower Username"; + +"tVvJ9c" = "Followers"; + +"xVtyec" = "Show chart"; + +"zeJo4f" = "Should the Widget show a chart?"; + diff --git a/WidgetExtension/tr.lproj/WidgetExtension.strings b/WidgetExtension/tr.lproj/WidgetExtension.strings new file mode 100644 index 000000000..37a67e62d --- /dev/null +++ b/WidgetExtension/tr.lproj/WidgetExtension.strings @@ -0,0 +1,28 @@ +"3d6HSO" = "Enter username"; + +"7DZrRA" = "Username"; + +"82L4Nj" = "Yes"; + +"BFppgH" = "Username"; + +"OL6lkx" = "Account"; + +"ZLZ6sg" = "Latest Followers"; + +"e0W2wo" = "Multi Followers Count"; + +"fovmPX" = "Accounts"; + +"gpCwrM" = "Followers Count"; + +"jg9D5P" = "No"; + +"sOLUtG" = "Enter follower Username"; + +"tVvJ9c" = "Followers"; + +"xVtyec" = "Show chart"; + +"zeJo4f" = "Should the Widget show a chart?"; + diff --git a/WidgetExtension/vi.lproj/WidgetExtension.strings b/WidgetExtension/vi.lproj/WidgetExtension.strings new file mode 100644 index 000000000..37a67e62d --- /dev/null +++ b/WidgetExtension/vi.lproj/WidgetExtension.strings @@ -0,0 +1,28 @@ +"3d6HSO" = "Enter username"; + +"7DZrRA" = "Username"; + +"82L4Nj" = "Yes"; + +"BFppgH" = "Username"; + +"OL6lkx" = "Account"; + +"ZLZ6sg" = "Latest Followers"; + +"e0W2wo" = "Multi Followers Count"; + +"fovmPX" = "Accounts"; + +"gpCwrM" = "Followers Count"; + +"jg9D5P" = "No"; + +"sOLUtG" = "Enter follower Username"; + +"tVvJ9c" = "Followers"; + +"xVtyec" = "Show chart"; + +"zeJo4f" = "Should the Widget show a chart?"; + diff --git a/WidgetExtension/zh-Hans.lproj/WidgetExtension.strings b/WidgetExtension/zh-Hans.lproj/WidgetExtension.strings new file mode 100644 index 000000000..37a67e62d --- /dev/null +++ b/WidgetExtension/zh-Hans.lproj/WidgetExtension.strings @@ -0,0 +1,28 @@ +"3d6HSO" = "Enter username"; + +"7DZrRA" = "Username"; + +"82L4Nj" = "Yes"; + +"BFppgH" = "Username"; + +"OL6lkx" = "Account"; + +"ZLZ6sg" = "Latest Followers"; + +"e0W2wo" = "Multi Followers Count"; + +"fovmPX" = "Accounts"; + +"gpCwrM" = "Followers Count"; + +"jg9D5P" = "No"; + +"sOLUtG" = "Enter follower Username"; + +"tVvJ9c" = "Followers"; + +"xVtyec" = "Show chart"; + +"zeJo4f" = "Should the Widget show a chart?"; + diff --git a/WidgetExtension/zh-Hant.lproj/WidgetExtension.strings b/WidgetExtension/zh-Hant.lproj/WidgetExtension.strings new file mode 100644 index 000000000..37a67e62d --- /dev/null +++ b/WidgetExtension/zh-Hant.lproj/WidgetExtension.strings @@ -0,0 +1,28 @@ +"3d6HSO" = "Enter username"; + +"7DZrRA" = "Username"; + +"82L4Nj" = "Yes"; + +"BFppgH" = "Username"; + +"OL6lkx" = "Account"; + +"ZLZ6sg" = "Latest Followers"; + +"e0W2wo" = "Multi Followers Count"; + +"fovmPX" = "Accounts"; + +"gpCwrM" = "Followers Count"; + +"jg9D5P" = "No"; + +"sOLUtG" = "Enter follower Username"; + +"tVvJ9c" = "Followers"; + +"xVtyec" = "Show chart"; + +"zeJo4f" = "Should the Widget show a chart?"; + From f4d066c8f4f27e401729fa24382004b1167dc4c7 Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Tue, 7 Feb 2023 09:38:27 +0100 Subject: [PATCH 23/24] chore(Widget): Add comment regarding LatestFollowersEntryAccountable.noteWithoutHtmlTags --- .../Variants/LatestFollowers/LatestFollowersWidgetView.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/WidgetExtension/Variants/LatestFollowers/LatestFollowersWidgetView.swift b/WidgetExtension/Variants/LatestFollowers/LatestFollowersWidgetView.swift index d1f0493a5..3bdc38a77 100644 --- a/WidgetExtension/Variants/LatestFollowers/LatestFollowersWidgetView.swift +++ b/WidgetExtension/Variants/LatestFollowers/LatestFollowersWidgetView.swift @@ -126,6 +126,10 @@ struct LatestFollowersWidgetView: View { } } +/// This code is used to strip HTML tags from the bio description as the widgets currently dont support +/// rich text rendering due to the lack of SwiftUI-only components for this purpose. +/// todo: Implement rich text rendering for bio description and remove this code +/// https://github.com/mastodon/mastodon-ios/issues/921 private extension LatestFollowersEntryAccountable { var noteWithoutHtmlTags: String? { do { From 736c3b515fbf45c88e5b116582207fa798e7b7a8 Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Tue, 7 Feb 2023 12:36:13 +0100 Subject: [PATCH 24/24] fix(Widget): User lookup for widget does not yield correct result --- .../Handler/FollowersCountIntentHandler.swift | 11 +---------- .../Handler/MultiFollowersCountIntentHandler.swift | 2 +- .../MastodonSDK/Entity/Mastodon+Entity+Account.swift | 11 +++++++++-- .../FollowersCount/FollowersCountWidget.swift | 4 ++-- .../MultiFollowersCountWidget.swift | 4 ++-- 5 files changed, 15 insertions(+), 17 deletions(-) diff --git a/MastodonIntent/Handler/FollowersCountIntentHandler.swift b/MastodonIntent/Handler/FollowersCountIntentHandler.swift index 170508325..fa5b53bc2 100644 --- a/MastodonIntent/Handler/FollowersCountIntentHandler.swift +++ b/MastodonIntent/Handler/FollowersCountIntentHandler.swift @@ -30,15 +30,6 @@ class FollowersCountIntentHandler: INExtension, FollowersCountIntentHandling { .apiService .search(query: .init(q: searchTerm), authenticationBox: authenticationBox) - return INObjectCollection(items: results.value.accounts.map { $0.acctWithDomain(localDomain: authenticationBox.domain) as NSString }) - } -} - -extension Mastodon.Entity.Account { - func acctWithDomain(localDomain: String) -> String { - guard acct.contains("@") else { - return "\(acct)@\(localDomain)" - } - return acct + return INObjectCollection(items: results.value.accounts.map { $0.acctWithDomainIfMissing(authenticationBox.domain) as NSString }) } } diff --git a/MastodonIntent/Handler/MultiFollowersCountIntentHandler.swift b/MastodonIntent/Handler/MultiFollowersCountIntentHandler.swift index 785949e2d..f49398630 100644 --- a/MastodonIntent/Handler/MultiFollowersCountIntentHandler.swift +++ b/MastodonIntent/Handler/MultiFollowersCountIntentHandler.swift @@ -22,6 +22,6 @@ class MultiFollowersCountIntentHandler: INExtension, MultiFollowersCountIntentHa .apiService .search(query: .init(q: searchTerm), authenticationBox: authenticationBox) - return INObjectCollection(items: results.value.accounts.map { $0.acctWithDomain(localDomain: authenticationBox.domain) as NSString }) + return INObjectCollection(items: results.value.accounts.map { $0.acctWithDomainIfMissing(authenticationBox.domain) as NSString }) } } diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Account.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Account.swift index 13f3c0a71..0e90a5ccd 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Account.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Account.swift @@ -82,6 +82,13 @@ extension Mastodon.Entity { case muteExpiresAt = "mute_expires_at" } } - - +} + +extension Mastodon.Entity.Account { + public func acctWithDomainIfMissing(_ localDomain: String) -> String { + guard acct.contains("@") else { + return "\(acct)@\(localDomain)" + } + return acct + } } diff --git a/WidgetExtension/Variants/FollowersCount/FollowersCountWidget.swift b/WidgetExtension/Variants/FollowersCount/FollowersCountWidget.swift index d490d2084..c8e53fb1c 100644 --- a/WidgetExtension/Variants/FollowersCount/FollowersCountWidget.swift +++ b/WidgetExtension/Variants/FollowersCount/FollowersCountWidget.swift @@ -88,7 +88,7 @@ private extension FollowersCountWidgetProvider { guard let desiredAccount = configuration.account ?? authBox.authenticationRecord.object( in: WidgetExtension.appContext.managedObjectContext - )?.user.acct + )?.user.acctWithDomain else { return completion(.unconfigured) } @@ -99,7 +99,7 @@ private extension FollowersCountWidgetProvider { .search(query: .init(q: desiredAccount, type: .accounts), authenticationBox: authBox) .value .accounts - .first(where: { $0.acct == desiredAccount }) + .first(where: { $0.acctWithDomainIfMissing(authBox.domain) == desiredAccount }) else { return completion(.unconfigured) } diff --git a/WidgetExtension/Variants/MultiFollowersCount/MultiFollowersCountWidget.swift b/WidgetExtension/Variants/MultiFollowersCount/MultiFollowersCountWidget.swift index b53c9af6c..9049a5a79 100644 --- a/WidgetExtension/Variants/MultiFollowersCount/MultiFollowersCountWidget.swift +++ b/WidgetExtension/Variants/MultiFollowersCount/MultiFollowersCountWidget.swift @@ -88,7 +88,7 @@ private extension MultiFollowersCountWidgetProvider { desiredAccounts = configuredAccounts } else if let currentlyLoggedInAccount = authBox.authenticationRecord.object( in: WidgetExtension.appContext.managedObjectContext - )?.user.acct { + )?.user.acctWithDomain { desiredAccounts = [currentlyLoggedInAccount] } else { return completion(.unconfigured) @@ -103,7 +103,7 @@ private extension MultiFollowersCountWidgetProvider { .search(query: .init(q: desiredAccount, type: .accounts), authenticationBox: authBox) .value .accounts - .first(where: { $0.acct == desiredAccount }) + .first(where: { $0.acctWithDomainIfMissing(authBox.domain) == desiredAccount }) else { continue }