From 1ca4a74ff071c9d3b0ea6862dacd96f3c5c0daeb Mon Sep 17 00:00:00 2001 From: Thomas Ricouard Date: Sun, 5 May 2024 13:12:19 +0200 Subject: [PATCH 01/20] Initial widget support --- IceCubesApp.xcodeproj/project.pbxproj | 242 +++++++++++++++++- .../Localization/Localizable.xcstrings | 23 +- IceCubesAppIntents/AppAccountEntity.swift | 44 ++++ IceCubesAppIntents/AppShortcuts.swift | 8 +- IceCubesAppIntents/InlinePostIntent.swift | 41 +-- IceCubesAppIntents/PostImageIntent.swift | 5 +- IceCubesAppIntents/PostIntent.swift | 7 +- IceCubesAppIntents/TabIntent.swift | 5 +- IceCubesAppIntents/TimelineFilterEntity.swift | 37 +++ .../AccentColor.colorset/Contents.json | 20 ++ .../AppIcon.appiconset/Contents.json | 13 + .../Assets.xcassets/Contents.json | 6 + .../WidgetBackground.colorset/Contents.json | 38 +++ .../IceCubesAppWidgetsExtensionBundle.swift | 9 + .../IceCubesWidgetConfigurationIntent.swift | 22 ++ IceCubesAppWidgetsExtension/Info.plist | 11 + .../LatestPostsWidget.swift | 152 +++++++++++ ...sAppWidgetsExtensionExtension.entitlements | 14 + ...idgetsExtensionExtensionDebug.entitlements | 18 ++ .../Sources/Timeline/TimelineFilter.swift | 4 +- 20 files changed, 654 insertions(+), 65 deletions(-) create mode 100644 IceCubesAppIntents/AppAccountEntity.swift create mode 100644 IceCubesAppIntents/TimelineFilterEntity.swift create mode 100644 IceCubesAppWidgetsExtension/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 IceCubesAppWidgetsExtension/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 IceCubesAppWidgetsExtension/Assets.xcassets/Contents.json create mode 100644 IceCubesAppWidgetsExtension/Assets.xcassets/WidgetBackground.colorset/Contents.json create mode 100644 IceCubesAppWidgetsExtension/IceCubesAppWidgetsExtensionBundle.swift create mode 100644 IceCubesAppWidgetsExtension/IceCubesWidgetConfigurationIntent.swift create mode 100644 IceCubesAppWidgetsExtension/Info.plist create mode 100644 IceCubesAppWidgetsExtension/LatestPostsWidget.swift create mode 100644 IceCubesAppWidgetsExtensionExtension.entitlements create mode 100644 IceCubesAppWidgetsExtensionExtensionDebug.entitlements diff --git a/IceCubesApp.xcodeproj/project.pbxproj b/IceCubesApp.xcodeproj/project.pbxproj index 86970016..d2100fd1 100644 --- a/IceCubesApp.xcodeproj/project.pbxproj +++ b/IceCubesApp.xcodeproj/project.pbxproj @@ -67,6 +67,23 @@ 9F7335F22967608F00AFF0BA /* AddRemoteTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7335F12967608F00AFF0BA /* AddRemoteTimelineView.swift */; }; 9F7335F92968576500AFF0BA /* DisplaySettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7335F82968576500AFF0BA /* DisplaySettingsView.swift */; }; 9F7788C02BE63935004E6BEF /* InlinePostIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7788BF2BE63935004E6BEF /* InlinePostIntent.swift */; }; + 9F7788C72BE652B1004E6BEF /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9F7788C62BE652B1004E6BEF /* WidgetKit.framework */; }; + 9F7788C92BE652B1004E6BEF /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9F7788C82BE652B1004E6BEF /* SwiftUI.framework */; }; + 9F7788CC2BE652B1004E6BEF /* IceCubesAppWidgetsExtensionBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7788CB2BE652B1004E6BEF /* IceCubesAppWidgetsExtensionBundle.swift */; }; + 9F7788CE2BE652B1004E6BEF /* LatestPostsWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7788CD2BE652B1004E6BEF /* LatestPostsWidget.swift */; }; + 9F7788D02BE652B1004E6BEF /* IceCubesWidgetConfigurationIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7788CF2BE652B1004E6BEF /* IceCubesWidgetConfigurationIntent.swift */; }; + 9F7788D22BE652B2004E6BEF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9F7788D12BE652B2004E6BEF /* Assets.xcassets */; }; + 9F7788D62BE652B2004E6BEF /* IceCubesAppWidgetsExtensionExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 9F7788C52BE652B1004E6BEF /* IceCubesAppWidgetsExtensionExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 9F7788DE2BE6543D004E6BEF /* Account in Frameworks */ = {isa = PBXBuildFile; productRef = 9F7788DD2BE6543D004E6BEF /* Account */; }; + 9F7788E02BE6543D004E6BEF /* AppAccount in Frameworks */ = {isa = PBXBuildFile; productRef = 9F7788DF2BE6543D004E6BEF /* AppAccount */; }; + 9F7788E22BE6543D004E6BEF /* Env in Frameworks */ = {isa = PBXBuildFile; productRef = 9F7788E12BE6543D004E6BEF /* Env */; }; + 9F7788E42BE6543D004E6BEF /* Models in Frameworks */ = {isa = PBXBuildFile; productRef = 9F7788E32BE6543D004E6BEF /* Models */; }; + 9F7788E62BE6543D004E6BEF /* Network in Frameworks */ = {isa = PBXBuildFile; productRef = 9F7788E52BE6543D004E6BEF /* Network */; }; + 9F7788E82BE65533004E6BEF /* AppAccountEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7788E72BE65533004E6BEF /* AppAccountEntity.swift */; }; + 9F7788EA2BE65585004E6BEF /* AppAccountEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7788E72BE65533004E6BEF /* AppAccountEntity.swift */; }; + 9F7788ED2BE78D75004E6BEF /* TimelineFilterEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7788EC2BE78D75004E6BEF /* TimelineFilterEntity.swift */; }; + 9F7788EE2BE78D7B004E6BEF /* TimelineFilterEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7788EC2BE78D75004E6BEF /* TimelineFilterEntity.swift */; }; + 9F7788F02BE78E77004E6BEF /* Timeline in Frameworks */ = {isa = PBXBuildFile; productRef = 9F7788EF2BE78E77004E6BEF /* Timeline */; }; 9F7D93942980063100EE6B7A /* AppAccount in Frameworks */ = {isa = PBXBuildFile; productRef = 9F7D93932980063100EE6B7A /* AppAccount */; }; 9F7D939A29805DBD00EE6B7A /* AccountSettingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7D939929805DBD00EE6B7A /* AccountSettingView.swift */; }; 9FA6FD6229C04A8800E2312C /* TranslationSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA6FD6129C04A8800E2312C /* TranslationSettingsView.swift */; }; @@ -128,6 +145,13 @@ remoteGlobalIDString = 9F2A5415296AB631009B2D7C; remoteInfo = IceCubesNotifications; }; + 9F7788D42BE652B2004E6BEF /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 9FBFE631292A715500C250E9 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 9F7788C42BE652B1004E6BEF; + remoteInfo = IceCubesAppWidgetsExtensionExtension; + }; 9FAD859029743F7400496AB1 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 9FBFE631292A715500C250E9 /* Project object */; @@ -154,6 +178,7 @@ E9DF420729830FEC0003AAD2 /* IceCubesActionExtension.appex in Embed Foundation Extensions */, 9F2A541D296AB631009B2D7C /* IceCubesNotifications.appex in Embed Foundation Extensions */, 9FAD859229743F7400496AB1 /* IceCubesShareExtension.appex in Embed Foundation Extensions */, + 9F7788D62BE652B2004E6BEF /* IceCubesAppWidgetsExtensionExtension.appex in Embed Foundation Extensions */, ); name = "Embed Foundation Extensions"; runOnlyForDeploymentPostprocessing = 0; @@ -226,6 +251,18 @@ 9F7335F12967608F00AFF0BA /* AddRemoteTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddRemoteTimelineView.swift; sourceTree = ""; }; 9F7335F82968576500AFF0BA /* DisplaySettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplaySettingsView.swift; sourceTree = ""; }; 9F7788BF2BE63935004E6BEF /* InlinePostIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlinePostIntent.swift; sourceTree = ""; }; + 9F7788C52BE652B1004E6BEF /* IceCubesAppWidgetsExtensionExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = IceCubesAppWidgetsExtensionExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 9F7788C62BE652B1004E6BEF /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; }; + 9F7788C82BE652B1004E6BEF /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; + 9F7788CB2BE652B1004E6BEF /* IceCubesAppWidgetsExtensionBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IceCubesAppWidgetsExtensionBundle.swift; sourceTree = ""; }; + 9F7788CD2BE652B1004E6BEF /* LatestPostsWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LatestPostsWidget.swift; sourceTree = ""; }; + 9F7788CF2BE652B1004E6BEF /* IceCubesWidgetConfigurationIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IceCubesWidgetConfigurationIntent.swift; sourceTree = ""; }; + 9F7788D12BE652B2004E6BEF /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 9F7788D32BE652B2004E6BEF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 9F7788D72BE652B2004E6BEF /* IceCubesAppWidgetsExtensionExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = IceCubesAppWidgetsExtensionExtension.entitlements; sourceTree = ""; }; + 9F7788E72BE65533004E6BEF /* AppAccountEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppAccountEntity.swift; sourceTree = ""; }; + 9F7788EB2BE65689004E6BEF /* IceCubesAppWidgetsExtensionExtensionDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = IceCubesAppWidgetsExtensionExtensionDebug.entitlements; sourceTree = ""; }; + 9F7788EC2BE78D75004E6BEF /* TimelineFilterEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineFilterEntity.swift; sourceTree = ""; }; 9F7D939529800B0300EE6B7A /* IceCubesApp-release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "IceCubesApp-release.xcconfig"; sourceTree = ""; }; 9F7D939929805DBD00EE6B7A /* AccountSettingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSettingView.swift; sourceTree = ""; }; 9FA6FD6129C04A8800E2312C /* TranslationSettingsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TranslationSettingsView.swift; sourceTree = ""; }; @@ -289,6 +326,21 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 9F7788C22BE652B1004E6BEF /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 9F7788E42BE6543D004E6BEF /* Models in Frameworks */, + 9F7788E62BE6543D004E6BEF /* Network in Frameworks */, + 9F7788E02BE6543D004E6BEF /* AppAccount in Frameworks */, + 9F7788DE2BE6543D004E6BEF /* Account in Frameworks */, + 9F7788E22BE6543D004E6BEF /* Env in Frameworks */, + 9F7788C92BE652B1004E6BEF /* SwiftUI.framework in Frameworks */, + 9F7788C72BE652B1004E6BEF /* WidgetKit.framework in Frameworks */, + 9F7788F02BE78E77004E6BEF /* Timeline in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 9FAD858529743F7400496AB1 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -364,12 +416,14 @@ 9F37BDD92BE36E08007F28AD /* IceCubesAppIntents */ = { isa = PBXGroup; children = ( - 9F37BDDA2BE36E22007F28AD /* PostIntent.swift */, + 9F7788E72BE65533004E6BEF /* AppAccountEntity.swift */, 9F37BDDC2BE37193007F28AD /* AppIntentService.swift */, - 9F37BDDE2BE37C35007F28AD /* TabIntent.swift */, - 9F37BDE02BE38646007F28AD /* PostImageIntent.swift */, 9F37BDE22BE393A7007F28AD /* AppShortcuts.swift */, 9F7788BF2BE63935004E6BEF /* InlinePostIntent.swift */, + 9F37BDE02BE38646007F28AD /* PostImageIntent.swift */, + 9F37BDDA2BE36E22007F28AD /* PostIntent.swift */, + 9F37BDDE2BE37C35007F28AD /* TabIntent.swift */, + 9F7788EC2BE78D75004E6BEF /* TimelineFilterEntity.swift */, ); path = IceCubesAppIntents; sourceTree = ""; @@ -415,6 +469,18 @@ path = Timeline; sourceTree = ""; }; + 9F7788CA2BE652B1004E6BEF /* IceCubesAppWidgetsExtension */ = { + isa = PBXGroup; + children = ( + 9F7788CB2BE652B1004E6BEF /* IceCubesAppWidgetsExtensionBundle.swift */, + 9F7788CD2BE652B1004E6BEF /* LatestPostsWidget.swift */, + 9F7788CF2BE652B1004E6BEF /* IceCubesWidgetConfigurationIntent.swift */, + 9F7788D12BE652B2004E6BEF /* Assets.xcassets */, + 9F7788D32BE652B2004E6BEF /* Info.plist */, + ); + path = IceCubesAppWidgetsExtension; + sourceTree = ""; + }; 9FA0D2AC29921C1F008A143B /* Embeds */ = { isa = PBXGroup; children = ( @@ -476,6 +542,8 @@ 9FBFE630292A715500C250E9 = { isa = PBXGroup; children = ( + 9F7788EB2BE65689004E6BEF /* IceCubesAppWidgetsExtensionExtensionDebug.entitlements */, + 9F7788D72BE652B2004E6BEF /* IceCubesAppWidgetsExtensionExtension.entitlements */, DD31E2E5297FB68B00A4BE29 /* IceCubesApp.xcconfig */, 9F7D939529800B0300EE6B7A /* IceCubesApp-release.xcconfig */, 9FBFE63B292A715500C250E9 /* IceCubesApp */, @@ -483,6 +551,7 @@ E9DF41FD29830FEC0003AAD2 /* IceCubesActionExtension */, 9F2A5417296AB631009B2D7C /* IceCubesNotifications */, 9FAD858929743F7400496AB1 /* IceCubesShareExtension */, + 9F7788CA2BE652B1004E6BEF /* IceCubesAppWidgetsExtension */, 9FBFE63A292A715500C250E9 /* Products */, 9FBFE64C292A72BD00C250E9 /* Frameworks */, 9FE3DB55296FEF5800628CB0 /* AppAccount */, @@ -508,6 +577,7 @@ 9F2A5416296AB631009B2D7C /* IceCubesNotifications.appex */, 9FAD858829743F7400496AB1 /* IceCubesShareExtension.appex */, E9DF41FA29830FEC0003AAD2 /* IceCubesActionExtension.appex */, + 9F7788C52BE652B1004E6BEF /* IceCubesAppWidgetsExtensionExtension.appex */, ); name = Products; sourceTree = ""; @@ -533,6 +603,8 @@ 9F7335EE29674F7100AFF0BA /* QuickLook.framework */, 9F7335EB2967461B00AFF0BA /* AVKit.framework */, E9DF41FB29830FEC0003AAD2 /* UniformTypeIdentifiers.framework */, + 9F7788C62BE652B1004E6BEF /* WidgetKit.framework */, + 9F7788C82BE652B1004E6BEF /* SwiftUI.framework */, ); name = Frameworks; sourceTree = ""; @@ -613,6 +685,31 @@ productReference = 9F2A5416296AB631009B2D7C /* IceCubesNotifications.appex */; productType = "com.apple.product-type.app-extension"; }; + 9F7788C42BE652B1004E6BEF /* IceCubesAppWidgetsExtensionExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = 9F7788D82BE652B2004E6BEF /* Build configuration list for PBXNativeTarget "IceCubesAppWidgetsExtensionExtension" */; + buildPhases = ( + 9F7788C12BE652B1004E6BEF /* Sources */, + 9F7788C22BE652B1004E6BEF /* Frameworks */, + 9F7788C32BE652B1004E6BEF /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = IceCubesAppWidgetsExtensionExtension; + packageProductDependencies = ( + 9F7788DD2BE6543D004E6BEF /* Account */, + 9F7788DF2BE6543D004E6BEF /* AppAccount */, + 9F7788E12BE6543D004E6BEF /* Env */, + 9F7788E32BE6543D004E6BEF /* Models */, + 9F7788E52BE6543D004E6BEF /* Network */, + 9F7788EF2BE78E77004E6BEF /* Timeline */, + ); + productName = IceCubesAppWidgetsExtensionExtension; + productReference = 9F7788C52BE652B1004E6BEF /* IceCubesAppWidgetsExtensionExtension.appex */; + productType = "com.apple.product-type.app-extension"; + }; 9FAD858729743F7400496AB1 /* IceCubesShareExtension */ = { isa = PBXNativeTarget; buildConfigurationList = 9FAD859329743F7400496AB1 /* Build configuration list for PBXNativeTarget "IceCubesShareExtension" */; @@ -655,6 +752,7 @@ 9F2A541C296AB631009B2D7C /* PBXTargetDependency */, 9FAD859129743F7400496AB1 /* PBXTargetDependency */, E9DF420629830FEC0003AAD2 /* PBXTargetDependency */, + 9F7788D52BE652B2004E6BEF /* PBXTargetDependency */, ); name = IceCubesApp; packageProductDependencies = ( @@ -706,12 +804,15 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1420; + LastSwiftUpdateCheck = 1530; LastUpgradeCheck = 1500; TargetAttributes = { 9F2A5415296AB631009B2D7C = { CreatedOnToolsVersion = 14.2; }; + 9F7788C42BE652B1004E6BEF = { + CreatedOnToolsVersion = 15.3; + }; 9FAD858729743F7400496AB1 = { CreatedOnToolsVersion = 14.2; }; @@ -764,6 +865,7 @@ E9DF41F929830FEC0003AAD2 /* IceCubesActionExtension */, 9F2A5415296AB631009B2D7C /* IceCubesNotifications */, 9FAD858729743F7400496AB1 /* IceCubesShareExtension */, + 9F7788C42BE652B1004E6BEF /* IceCubesAppWidgetsExtensionExtension */, ); }; /* End PBXProject section */ @@ -779,6 +881,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 9F7788C32BE652B1004E6BEF /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 9F7788D22BE652B2004E6BEF /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 9FAD858629743F7400496AB1 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -833,6 +943,18 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 9F7788C12BE652B1004E6BEF /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 9F7788EA2BE65585004E6BEF /* AppAccountEntity.swift in Sources */, + 9F7788CE2BE652B1004E6BEF /* LatestPostsWidget.swift in Sources */, + 9F7788EE2BE78D7B004E6BEF /* TimelineFilterEntity.swift in Sources */, + 9F7788CC2BE652B1004E6BEF /* IceCubesAppWidgetsExtensionBundle.swift in Sources */, + 9F7788D02BE652B1004E6BEF /* IceCubesWidgetConfigurationIntent.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 9FAD858429743F7400496AB1 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -877,10 +999,12 @@ 9F2B92FA295DA7D700DE16D0 /* AddAccountsView.swift in Sources */, 639CDF9C296AC82F00C35E58 /* SafariRouter.swift in Sources */, 9F35DB4729506F6600B3281A /* NotificationTab.swift in Sources */, + 9F7788ED2BE78D75004E6BEF /* TimelineFilterEntity.swift in Sources */, 9F37BDE32BE393A7007F28AD /* AppShortcuts.swift in Sources */, 9F654BEF299AC45B00D27FA5 /* ReportView.swift in Sources */, D08A9C3529956CFA00204A4A /* SwipeActionsSettingsView.swift in Sources */, 9F7335F22967608F00AFF0BA /* AddRemoteTimelineView.swift in Sources */, + 9F7788E82BE65533004E6BEF /* AppAccountEntity.swift in Sources */, 9FC14EF62B494DFF0006CEE1 /* RecenTagsSettingView.swift in Sources */, 9F6028562B3F36AE00476078 /* AppView.swift in Sources */, 9F55C68D2955968700F94077 /* ExploreTab.swift in Sources */, @@ -907,6 +1031,11 @@ target = 9F2A5415296AB631009B2D7C /* IceCubesNotifications */; targetProxy = 9F2A541B296AB631009B2D7C /* PBXContainerItemProxy */; }; + 9F7788D52BE652B2004E6BEF /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 9F7788C42BE652B1004E6BEF /* IceCubesAppWidgetsExtensionExtension */; + targetProxy = 9F7788D42BE652B2004E6BEF /* PBXContainerItemProxy */; + }; 9FAD859129743F7400496AB1 /* PBXTargetDependency */ = { isa = PBXTargetDependency; platformFilters = ( @@ -1021,6 +1150,78 @@ }; name = Release; }; + 9F7788D92BE652B2004E6BEF /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CODE_SIGN_ENTITLEMENTS = IceCubesAppWidgetsExtensionExtensionDebug.entitlements; + CODE_SIGN_IDENTITY = "-"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = IceCubesAppWidgetsExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = IceCubesAppWidgetsExtension; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 17.4; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.thomasricouard.IceCubesApp.IceCubesAppWidgetsExtension; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SUPPORTS_MACCATALYST = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 9F7788DA2BE652B2004E6BEF /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CODE_SIGN_ENTITLEMENTS = IceCubesAppWidgetsExtensionExtension.entitlements; + CODE_SIGN_IDENTITY = "-"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = IceCubesAppWidgetsExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = IceCubesAppWidgetsExtension; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 17.4; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.thomasricouard.IceCubesApp.IceCubesAppWidgetsExtension; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SUPPORTS_MACCATALYST = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; 9FAD859429743F7400496AB1 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -1408,6 +1609,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 9F7788D82BE652B2004E6BEF /* Build configuration list for PBXNativeTarget "IceCubesAppWidgetsExtensionExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 9F7788D92BE652B2004E6BEF /* Debug */, + 9F7788DA2BE652B2004E6BEF /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 9FAD859329743F7400496AB1 /* Build configuration list for PBXNativeTarget "IceCubesShareExtension" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -1523,6 +1733,30 @@ isa = XCSwiftPackageProductDependency; productName = Conversations; }; + 9F7788DD2BE6543D004E6BEF /* Account */ = { + isa = XCSwiftPackageProductDependency; + productName = Account; + }; + 9F7788DF2BE6543D004E6BEF /* AppAccount */ = { + isa = XCSwiftPackageProductDependency; + productName = AppAccount; + }; + 9F7788E12BE6543D004E6BEF /* Env */ = { + isa = XCSwiftPackageProductDependency; + productName = Env; + }; + 9F7788E32BE6543D004E6BEF /* Models */ = { + isa = XCSwiftPackageProductDependency; + productName = Models; + }; + 9F7788E52BE6543D004E6BEF /* Network */ = { + isa = XCSwiftPackageProductDependency; + productName = Network; + }; + 9F7788EF2BE78E77004E6BEF /* Timeline */ = { + isa = XCSwiftPackageProductDependency; + productName = Timeline; + }; 9F7D93932980063100EE6B7A /* AppAccount */ = { isa = XCSwiftPackageProductDependency; productName = AppAccount; diff --git a/IceCubesApp/Resources/Localization/Localizable.xcstrings b/IceCubesApp/Resources/Localization/Localizable.xcstrings index 77eca02c..0839af4e 100644 --- a/IceCubesApp/Resources/Localization/Localizable.xcstrings +++ b/IceCubesApp/Resources/Localization/Localizable.xcstrings @@ -20539,7 +20539,10 @@ } } }, - "Compose a status" : { + "Compose a post" : { + + }, + "Compose a post to Mastodon" : { }, "Content of the post to be sent to Mastodon" : { @@ -40167,6 +40170,7 @@ } }, "Post status to Mastodon" : { + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -40692,13 +40696,13 @@ } } }, - "Send a text status" : { + "Send a post" : { }, - "Send a text status to Mastodon using Ice Cubes" : { + "Send a text post to Mastodon with Ice Cubes" : { }, - "Send text status to Mastodon" : { + "Send post to Mastodon" : { }, "Settings" : { @@ -80417,6 +80421,9 @@ } } } + }, + "TimelineFilter" : { + }, "Trending Links" : { "localizations" : { @@ -80796,8 +80803,15 @@ } } } + }, + "Use Ice Cubes to compose a post for Mastodon" : { + + }, + "Use Ice Cubes to compose a post with an image to Mastodon" : { + }, "Use Ice Cubes to post a status to Mastodon" : { + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -80808,6 +80822,7 @@ } }, "Use Ice Cubes to post a status with an image to Mastodon" : { + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { diff --git a/IceCubesAppIntents/AppAccountEntity.swift b/IceCubesAppIntents/AppAccountEntity.swift new file mode 100644 index 00000000..5b87adb6 --- /dev/null +++ b/IceCubesAppIntents/AppAccountEntity.swift @@ -0,0 +1,44 @@ +import AppIntents +import AppAccount +import Env +import Foundation +import Models +import Network + + +extension IntentDescription: @unchecked Sendable { } +extension TypeDisplayRepresentation: @unchecked Sendable { } + +public struct AppAccountEntity: Identifiable, AppEntity { + public var id: String { account.id } + + public let account: AppAccount + + public static let defaultQuery = DefaultAppAccountEntityQuery() + + public static let typeDisplayRepresentation: TypeDisplayRepresentation = "AppAccount" + + public var displayRepresentation: DisplayRepresentation { + DisplayRepresentation(title: "\(account.accountName ?? account.server)") + } +} + +public struct DefaultAppAccountEntityQuery: EntityQuery { + public init() { } + + public func entities(for identifiers: [AppAccountEntity.ID]) async throws -> [AppAccountEntity] { + return await AppAccountsManager.shared.availableAccounts.filter { account in + identifiers.contains { id in + id == account.id + } + }.map { AppAccountEntity(account: $0) } + } + + public func suggestedEntities() async throws -> [AppAccountEntity] { + await AppAccountsManager.shared.availableAccounts.map { .init(account: $0) } + } + + public func defaultResult() async -> AppAccountEntity? { + await .init(account: AppAccountsManager.shared.currentAccount) + } +} diff --git a/IceCubesAppIntents/AppShortcuts.swift b/IceCubesAppIntents/AppShortcuts.swift index 0d297ab4..7b5ded40 100644 --- a/IceCubesAppIntents/AppShortcuts.swift +++ b/IceCubesAppIntents/AppShortcuts.swift @@ -8,16 +8,16 @@ struct AppShortcuts: AppShortcutsProvider { "Post \(\.$content) in \(.applicationName)", "Post a status on Mastodon with \(.applicationName)", ], - shortTitle: "Compose a status", + shortTitle: "Compose a post", systemImageName: "square.and.pencil" ) AppShortcut( intent: InlinePostIntent(), phrases: [ - "Write a status with \(.applicationName)", - "Send on Status on Mastodon with \(.applicationName)", + "Write a post with \(.applicationName)", + "Send on post on Mastodon with \(.applicationName)", ], - shortTitle: "Send a text status", + shortTitle: "Send a post", systemImageName: "square.and.pencil" ) AppShortcut( diff --git a/IceCubesAppIntents/InlinePostIntent.swift b/IceCubesAppIntents/InlinePostIntent.swift index 69c316ba..93cb19ca 100644 --- a/IceCubesAppIntents/InlinePostIntent.swift +++ b/IceCubesAppIntents/InlinePostIntent.swift @@ -33,48 +33,13 @@ enum PostVisibility: String, AppEnum { } } -struct AppAccountWrapper: Identifiable, AppEntity { - var id: String { account.id } - - let account: AppAccount - - static var defaultQuery = DefaultAppAccountQuery() - - static var typeDisplayRepresentation: TypeDisplayRepresentation = "AppAccount" - - var displayRepresentation: DisplayRepresentation { - DisplayRepresentation(title: "\(account.accountName ?? account.server)") - } -} - -struct DefaultAppAccountQuery: EntityQuery { - func entities(for identifiers: [AppAccountWrapper.ID]) async throws -> [AppAccountWrapper] { - return await AppAccountsManager.shared.availableAccounts.filter { account in - identifiers.contains { id in - id == account.id - } - }.map { AppAccountWrapper(account: $0) } - } - - func suggestedEntities() async throws -> [AppAccountWrapper] { - await AppAccountsManager.shared.availableAccounts.map { .init(account: $0) } - } - - func defaultResult() async -> AppAccountWrapper? { - await .init(account: AppAccountsManager.shared.currentAccount) - } -} - struct InlinePostIntent: AppIntent { - static let title: LocalizedStringResource = "Send text status to Mastodon" - static var description: IntentDescription { - "Send a text status to Mastodon using Ice Cubes" - } - + static let title: LocalizedStringResource = "Send post to Mastodon" + static let description: IntentDescription = "Send a text post to Mastodon with Ice Cubes" static let openAppWhenRun: Bool = false @Parameter(title: "Account", requestValueDialog: IntentDialog("Account")) - var account: AppAccountWrapper + var account: AppAccountEntity @Parameter(title: "Post visibility", requestValueDialog: IntentDialog("Visibility of your post")) var visibility: PostVisibility diff --git a/IceCubesAppIntents/PostImageIntent.swift b/IceCubesAppIntents/PostImageIntent.swift index ab27e1dd..8b312a7b 100644 --- a/IceCubesAppIntents/PostImageIntent.swift +++ b/IceCubesAppIntents/PostImageIntent.swift @@ -3,10 +3,7 @@ import Foundation struct PostImageIntent: AppIntent { static let title: LocalizedStringResource = "Post an image to Mastodon" - static var description: IntentDescription { - "Use Ice Cubes to post a status with an image to Mastodon" - } - + static let description: IntentDescription = "Use Ice Cubes to compose a post with an image to Mastodon" static let openAppWhenRun: Bool = true @Parameter(title: "Image", diff --git a/IceCubesAppIntents/PostIntent.swift b/IceCubesAppIntents/PostIntent.swift index ebb3ff63..e305fa25 100644 --- a/IceCubesAppIntents/PostIntent.swift +++ b/IceCubesAppIntents/PostIntent.swift @@ -2,11 +2,8 @@ import AppIntents import Foundation struct PostIntent: AppIntent { - static let title: LocalizedStringResource = "Post status to Mastodon" - static var description: IntentDescription { - "Use Ice Cubes to post a status to Mastodon" - } - + static let title: LocalizedStringResource = "Compose a post to Mastodon" + static let description: IntentDescription = "Use Ice Cubes to compose a post for Mastodon" static let openAppWhenRun: Bool = true @Parameter(title: "Post content", inputConnectionBehavior: .connectToPreviousIntentResult) diff --git a/IceCubesAppIntents/TabIntent.swift b/IceCubesAppIntents/TabIntent.swift index 61cc415e..b4bb34ae 100644 --- a/IceCubesAppIntents/TabIntent.swift +++ b/IceCubesAppIntents/TabIntent.swift @@ -75,10 +75,7 @@ enum TabEnum: String, AppEnum, Sendable { struct TabIntent: AppIntent { static let title: LocalizedStringResource = "Open on a tab" - static var description: IntentDescription { - "Open the app on a specific tab" - } - + static let description: IntentDescription = "Open the app on a specific tab" static let openAppWhenRun: Bool = true @Parameter(title: "Selected tab") diff --git a/IceCubesAppIntents/TimelineFilterEntity.swift b/IceCubesAppIntents/TimelineFilterEntity.swift new file mode 100644 index 00000000..9a1c348a --- /dev/null +++ b/IceCubesAppIntents/TimelineFilterEntity.swift @@ -0,0 +1,37 @@ +import AppIntents +import AppAccount +import Env +import Foundation +import Models +import Network +import Timeline + +public struct TimelineFilterEntity: Identifiable, AppEntity { + public var id: String { timeline.id } + + public let timeline: TimelineFilter + + public static let defaultQuery = DefaultTimelineEntityQuery() + + public static let typeDisplayRepresentation: TypeDisplayRepresentation = "TimelineFilter" + + public var displayRepresentation: DisplayRepresentation { + DisplayRepresentation(title: "\(timeline.title)") + } +} + +public struct DefaultTimelineEntityQuery: EntityQuery { + public init() { } + + public func entities(for identifiers: [TimelineFilter.ID]) async throws -> [TimelineFilterEntity] { + [.home, .trending, .federated, .local].map{ .init(timeline: $0) } + } + + public func suggestedEntities() async throws -> [TimelineFilterEntity] { + [.home, .trending, .federated, .local].map{ .init(timeline: $0) } + } + + public func defaultResult() async -> TimelineFilterEntity? { + .init(timeline: .home) + } +} diff --git a/IceCubesAppWidgetsExtension/Assets.xcassets/AccentColor.colorset/Contents.json b/IceCubesAppWidgetsExtension/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..8691996d --- /dev/null +++ b/IceCubesAppWidgetsExtension/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0.859", + "green" : "0.267", + "red" : "0.675" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IceCubesAppWidgetsExtension/Assets.xcassets/AppIcon.appiconset/Contents.json b/IceCubesAppWidgetsExtension/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..13613e3e --- /dev/null +++ b/IceCubesAppWidgetsExtension/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/IceCubesAppWidgetsExtension/Assets.xcassets/Contents.json b/IceCubesAppWidgetsExtension/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/IceCubesAppWidgetsExtension/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IceCubesAppWidgetsExtension/Assets.xcassets/WidgetBackground.colorset/Contents.json b/IceCubesAppWidgetsExtension/Assets.xcassets/WidgetBackground.colorset/Contents.json new file mode 100644 index 00000000..470890bc --- /dev/null +++ b/IceCubesAppWidgetsExtension/Assets.xcassets/WidgetBackground.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0.949", + "green" : "0.945", + "red" : "0.941" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0.133", + "green" : "0.082", + "red" : "0.067" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IceCubesAppWidgetsExtension/IceCubesAppWidgetsExtensionBundle.swift b/IceCubesAppWidgetsExtension/IceCubesAppWidgetsExtensionBundle.swift new file mode 100644 index 00000000..fe7d3461 --- /dev/null +++ b/IceCubesAppWidgetsExtension/IceCubesAppWidgetsExtensionBundle.swift @@ -0,0 +1,9 @@ +import WidgetKit +import SwiftUI + +@main +struct IceCubesAppWidgetsExtensionBundle: WidgetBundle { + var body: some Widget { + LatestPostsWidget() + } +} diff --git a/IceCubesAppWidgetsExtension/IceCubesWidgetConfigurationIntent.swift b/IceCubesAppWidgetsExtension/IceCubesWidgetConfigurationIntent.swift new file mode 100644 index 00000000..924bf169 --- /dev/null +++ b/IceCubesAppWidgetsExtension/IceCubesWidgetConfigurationIntent.swift @@ -0,0 +1,22 @@ +import WidgetKit +import AppIntents + +struct IceCubesWidgetConfigurationIntent: WidgetConfigurationIntent { + static let title: LocalizedStringResource = "Configuration" + static let description = IntentDescription("Choose the account and timeline for this widget") + + @Parameter(title: "Account") + var account: AppAccountEntity? + + @Parameter(title: "Timeline") + var timeline: TimelineFilterEntity? +} + +extension IceCubesWidgetConfigurationIntent { + static var previewAccount: IceCubesWidgetConfigurationIntent { + let intent = IceCubesWidgetConfigurationIntent() + intent.account = .init(account: .init(server: "Test", accountName: "Test account")) + intent.timeline = .init(timeline: .home) + return intent + } +} diff --git a/IceCubesAppWidgetsExtension/Info.plist b/IceCubesAppWidgetsExtension/Info.plist new file mode 100644 index 00000000..0f118fb7 --- /dev/null +++ b/IceCubesAppWidgetsExtension/Info.plist @@ -0,0 +1,11 @@ + + + + + NSExtension + + NSExtensionPointIdentifier + com.apple.widgetkit-extension + + + diff --git a/IceCubesAppWidgetsExtension/LatestPostsWidget.swift b/IceCubesAppWidgetsExtension/LatestPostsWidget.swift new file mode 100644 index 00000000..24f7c3d2 --- /dev/null +++ b/IceCubesAppWidgetsExtension/LatestPostsWidget.swift @@ -0,0 +1,152 @@ +import WidgetKit +import SwiftUI +import Network +import DesignSystem +import Models +import Timeline + +struct LatestPostsWidgetProvider: AppIntentTimelineProvider { + func placeholder(in context: Context) -> LatestPostWidgetEntry { + .init(date: Date(), configuration: IceCubesWidgetConfigurationIntent(), timeline: .home, statuses: [ + .placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder() + ]) + } + + func snapshot(for configuration: IceCubesWidgetConfigurationIntent, in context: Context) async -> LatestPostWidgetEntry { + if let entry = await timeline(for: configuration, context: context).entries.first { + return entry + } + return .init(date: Date(), configuration: configuration, timeline: .home, statuses: []) + } + + func timeline(for configuration: IceCubesWidgetConfigurationIntent, in context: Context) async -> Timeline { + await timeline(for: configuration, context: context) + } + + private func timeline(for configuration: IceCubesWidgetConfigurationIntent, context: Context) async -> Timeline { + guard let account = configuration.account, let timeline = configuration.timeline else { + return Timeline(entries: [.init(date: Date(), + configuration: configuration, + timeline: .home, + statuses: [])], + policy: .atEnd) + } + let client = Client(server: account.account.server, oauthToken: account.account.oauthToken) + do { + var statuses: [Status] = try await client.get(endpoint: timeline.timeline.endpoint(sinceId: nil, + maxId: nil, + minId: nil, + offset: nil)) + statuses = statuses.filter{ $0.reblog == nil && !$0.content.asRawText.isEmpty } + switch context.family { + case .systemMedium: + if statuses.count >= 2 { + statuses = statuses.prefix(upTo: 2).map{ $0 } + } + case .systemLarge: + if statuses.count >= 4 { + statuses = statuses.prefix(upTo: 4).map{ $0 } + } + case .systemExtraLarge: + if statuses.count >= 6 { + statuses = statuses.prefix(upTo: 6).map{ $0 } + } + default: + break + } + return Timeline(entries: [.init(date: Date(), configuration: configuration, + timeline: timeline.timeline, + statuses: statuses)], policy: .atEnd) + } catch { + return Timeline(entries: [.init(date: Date(), + configuration: configuration, + timeline: .home, + statuses: [])], policy: .atEnd) + } + } +} + +struct LatestPostWidgetEntry: TimelineEntry { + let date: Date + let configuration: IceCubesWidgetConfigurationIntent + let timeline: TimelineFilter + let statuses: [Status] +} + +struct LatestPostsWidgetView : View { + var entry: LatestPostsWidgetProvider.Entry + + @Environment(\.widgetFamily) var family + @Environment(\.redactionReasons) var redacted + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + headerView + ForEach(entry.statuses) { status in + makeStatusView(status) + } + } + .frame(maxWidth: .infinity) + } + + private var headerView: some View { + HStack { + Text(entry.timeline.title) + Spacer() + Image(systemName: "cube") + } + .font(.subheadline) + .fontWeight(.bold) + .foregroundStyle(Color("AccentColor")) + } + + @ViewBuilder + private func makeStatusView(_ status: Status) -> some View { + if let url = URL(string: status.url ?? "") { + Link(destination: url, label: { + VStack(alignment: .leading, spacing: 4) { + HStack(spacing: 0) { + Text(status.account.safeDisplayName) + Text(" @") + Text(status.account.username) + Spacer() + } + .font(.subheadline) + .fontWeight(.semibold) + .foregroundStyle(.secondary) + .lineLimit(1) + + Text(status.content.asRawText) + .font(.body) + .lineLimit(2) + } + }) + } + } +} + +struct LatestPostsWidget: Widget { + let kind: String = "LatestPostsWidget" + + var body: some WidgetConfiguration { + AppIntentConfiguration(kind: kind, + intent: IceCubesWidgetConfigurationIntent.self, + provider: LatestPostsWidgetProvider()) { entry in + LatestPostsWidgetView(entry: entry) + .containerBackground(Color("WidgetBackground").gradient, for: .widget) + } + .configurationDisplayName("Latest posts") + .description("Show the latest post for the selected timeline") + .supportedFamilies([.systemMedium, .systemLarge, .systemExtraLarge]) + } +} + + +#Preview(as: .systemMedium) { + LatestPostsWidget() +} timeline: { + LatestPostWidgetEntry(date: .now, + configuration: .previewAccount, + timeline: .home, + statuses: [.placeholder(), .placeholder(), .placeholder(), .placeholder()]) +} diff --git a/IceCubesAppWidgetsExtensionExtension.entitlements b/IceCubesAppWidgetsExtensionExtension.entitlements new file mode 100644 index 00000000..a31bf1a4 --- /dev/null +++ b/IceCubesAppWidgetsExtensionExtension.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.application-groups + + group.com.thomasricouard.IceCubesApp + + com.apple.security.network.client + + + diff --git a/IceCubesAppWidgetsExtensionExtensionDebug.entitlements b/IceCubesAppWidgetsExtensionExtensionDebug.entitlements new file mode 100644 index 00000000..d59836e7 --- /dev/null +++ b/IceCubesAppWidgetsExtensionExtensionDebug.entitlements @@ -0,0 +1,18 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.application-groups + + group.com.thomasricouard.IceCubesApp + + com.apple.security.network.client + + keychain-access-groups + + $(AppIdentifierPrefix)$(BUNDLE_ID_PREFIX).IceCubesApp + + + diff --git a/Packages/Timeline/Sources/Timeline/TimelineFilter.swift b/Packages/Timeline/Sources/Timeline/TimelineFilter.swift index d1277709..2c7fe901 100644 --- a/Packages/Timeline/Sources/Timeline/TimelineFilter.swift +++ b/Packages/Timeline/Sources/Timeline/TimelineFilter.swift @@ -3,7 +3,7 @@ import Models import Network import SwiftUI -public enum RemoteTimelineFilter: String, CaseIterable, Hashable, Equatable { +public enum RemoteTimelineFilter: String, CaseIterable, Hashable, Equatable, Sendable { case local, federated, trending public func localizedTitle() -> LocalizedStringKey { @@ -29,7 +29,7 @@ public enum RemoteTimelineFilter: String, CaseIterable, Hashable, Equatable { } } -public enum TimelineFilter: Hashable, Equatable, Identifiable { +public enum TimelineFilter: Hashable, Equatable, Identifiable, Sendable { case home, local, federated, trending case hashtag(tag: String, accountId: String?) case tagGroup(title: String, tags: [String], symbolName: String?) From 6bd14e0f8db4c6112e76167bdf05f1b134e861a3 Mon Sep 17 00:00:00 2001 From: Thomas Ricouard Date: Sun, 5 May 2024 13:34:22 +0200 Subject: [PATCH 02/20] Don't embed widgets on visionOS --- IceCubesApp.xcodeproj/project.pbxproj | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/IceCubesApp.xcodeproj/project.pbxproj b/IceCubesApp.xcodeproj/project.pbxproj index d2100fd1..c50de191 100644 --- a/IceCubesApp.xcodeproj/project.pbxproj +++ b/IceCubesApp.xcodeproj/project.pbxproj @@ -73,7 +73,7 @@ 9F7788CE2BE652B1004E6BEF /* LatestPostsWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7788CD2BE652B1004E6BEF /* LatestPostsWidget.swift */; }; 9F7788D02BE652B1004E6BEF /* IceCubesWidgetConfigurationIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7788CF2BE652B1004E6BEF /* IceCubesWidgetConfigurationIntent.swift */; }; 9F7788D22BE652B2004E6BEF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9F7788D12BE652B2004E6BEF /* Assets.xcassets */; }; - 9F7788D62BE652B2004E6BEF /* IceCubesAppWidgetsExtensionExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 9F7788C52BE652B1004E6BEF /* IceCubesAppWidgetsExtensionExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 9F7788D62BE652B2004E6BEF /* IceCubesAppWidgetsExtensionExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 9F7788C52BE652B1004E6BEF /* IceCubesAppWidgetsExtensionExtension.appex */; platformFilters = (ios, maccatalyst, ); settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 9F7788DE2BE6543D004E6BEF /* Account in Frameworks */ = {isa = PBXBuildFile; productRef = 9F7788DD2BE6543D004E6BEF /* Account */; }; 9F7788E02BE6543D004E6BEF /* AppAccount in Frameworks */ = {isa = PBXBuildFile; productRef = 9F7788DF2BE6543D004E6BEF /* AppAccount */; }; 9F7788E22BE6543D004E6BEF /* Env in Frameworks */ = {isa = PBXBuildFile; productRef = 9F7788E12BE6543D004E6BEF /* Env */; }; @@ -1033,6 +1033,10 @@ }; 9F7788D52BE652B2004E6BEF /* PBXTargetDependency */ = { isa = PBXTargetDependency; + platformFilters = ( + ios, + maccatalyst, + ); target = 9F7788C42BE652B1004E6BEF /* IceCubesAppWidgetsExtensionExtension */; targetProxy = 9F7788D42BE652B2004E6BEF /* PBXContainerItemProxy */; }; From dd1615f0e385a3f6d56d95363407a338e37044e7 Mon Sep 17 00:00:00 2001 From: Thomas Ricouard Date: Sun, 5 May 2024 17:30:09 +0200 Subject: [PATCH 03/20] Fix widget entitlements --- IceCubesApp.xcodeproj/project.pbxproj | 8 +++----- ...eCubesAppWidgetsExtensionExtension.entitlements | 8 ++++---- .../LatestPostsWidget.swift | 1 + IceCubesAppWidgetsExtensionExtension.entitlements | 14 -------------- 4 files changed, 8 insertions(+), 23 deletions(-) rename IceCubesAppWidgetsExtensionExtensionDebug.entitlements => IceCubesAppWidgetsExtension/IceCubesAppWidgetsExtensionExtension.entitlements (100%) delete mode 100644 IceCubesAppWidgetsExtensionExtension.entitlements diff --git a/IceCubesApp.xcodeproj/project.pbxproj b/IceCubesApp.xcodeproj/project.pbxproj index c50de191..931488fa 100644 --- a/IceCubesApp.xcodeproj/project.pbxproj +++ b/IceCubesApp.xcodeproj/project.pbxproj @@ -261,7 +261,6 @@ 9F7788D32BE652B2004E6BEF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 9F7788D72BE652B2004E6BEF /* IceCubesAppWidgetsExtensionExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = IceCubesAppWidgetsExtensionExtension.entitlements; sourceTree = ""; }; 9F7788E72BE65533004E6BEF /* AppAccountEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppAccountEntity.swift; sourceTree = ""; }; - 9F7788EB2BE65689004E6BEF /* IceCubesAppWidgetsExtensionExtensionDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = IceCubesAppWidgetsExtensionExtensionDebug.entitlements; sourceTree = ""; }; 9F7788EC2BE78D75004E6BEF /* TimelineFilterEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineFilterEntity.swift; sourceTree = ""; }; 9F7D939529800B0300EE6B7A /* IceCubesApp-release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "IceCubesApp-release.xcconfig"; sourceTree = ""; }; 9F7D939929805DBD00EE6B7A /* AccountSettingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSettingView.swift; sourceTree = ""; }; @@ -472,6 +471,7 @@ 9F7788CA2BE652B1004E6BEF /* IceCubesAppWidgetsExtension */ = { isa = PBXGroup; children = ( + 9F7788D72BE652B2004E6BEF /* IceCubesAppWidgetsExtensionExtension.entitlements */, 9F7788CB2BE652B1004E6BEF /* IceCubesAppWidgetsExtensionBundle.swift */, 9F7788CD2BE652B1004E6BEF /* LatestPostsWidget.swift */, 9F7788CF2BE652B1004E6BEF /* IceCubesWidgetConfigurationIntent.swift */, @@ -542,8 +542,6 @@ 9FBFE630292A715500C250E9 = { isa = PBXGroup; children = ( - 9F7788EB2BE65689004E6BEF /* IceCubesAppWidgetsExtensionExtensionDebug.entitlements */, - 9F7788D72BE652B2004E6BEF /* IceCubesAppWidgetsExtensionExtension.entitlements */, DD31E2E5297FB68B00A4BE29 /* IceCubesApp.xcconfig */, 9F7D939529800B0300EE6B7A /* IceCubesApp-release.xcconfig */, 9FBFE63B292A715500C250E9 /* IceCubesApp */, @@ -1159,7 +1157,7 @@ buildSettings = { ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; - CODE_SIGN_ENTITLEMENTS = IceCubesAppWidgetsExtensionExtensionDebug.entitlements; + CODE_SIGN_ENTITLEMENTS = IceCubesAppWidgetsExtension/IceCubesAppWidgetsExtensionExtension.entitlements; CODE_SIGN_IDENTITY = "-"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; @@ -1195,7 +1193,7 @@ buildSettings = { ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; - CODE_SIGN_ENTITLEMENTS = IceCubesAppWidgetsExtensionExtension.entitlements; + CODE_SIGN_ENTITLEMENTS = IceCubesAppWidgetsExtension/IceCubesAppWidgetsExtensionExtension.entitlements; CODE_SIGN_IDENTITY = "-"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; diff --git a/IceCubesAppWidgetsExtensionExtensionDebug.entitlements b/IceCubesAppWidgetsExtension/IceCubesAppWidgetsExtensionExtension.entitlements similarity index 100% rename from IceCubesAppWidgetsExtensionExtensionDebug.entitlements rename to IceCubesAppWidgetsExtension/IceCubesAppWidgetsExtensionExtension.entitlements index d59836e7..6169e196 100644 --- a/IceCubesAppWidgetsExtensionExtensionDebug.entitlements +++ b/IceCubesAppWidgetsExtension/IceCubesAppWidgetsExtensionExtension.entitlements @@ -2,6 +2,10 @@ + keychain-access-groups + + $(AppIdentifierPrefix)$(BUNDLE_ID_PREFIX).IceCubesApp + com.apple.security.app-sandbox com.apple.security.application-groups @@ -10,9 +14,5 @@ com.apple.security.network.client - keychain-access-groups - - $(AppIdentifierPrefix)$(BUNDLE_ID_PREFIX).IceCubesApp - diff --git a/IceCubesAppWidgetsExtension/LatestPostsWidget.swift b/IceCubesAppWidgetsExtension/LatestPostsWidget.swift index 24f7c3d2..8aa9a32e 100644 --- a/IceCubesAppWidgetsExtension/LatestPostsWidget.swift +++ b/IceCubesAppWidgetsExtension/LatestPostsWidget.swift @@ -85,6 +85,7 @@ struct LatestPostsWidgetView : View { ForEach(entry.statuses) { status in makeStatusView(status) } + Spacer() } .frame(maxWidth: .infinity) } diff --git a/IceCubesAppWidgetsExtensionExtension.entitlements b/IceCubesAppWidgetsExtensionExtension.entitlements deleted file mode 100644 index a31bf1a4..00000000 --- a/IceCubesAppWidgetsExtensionExtension.entitlements +++ /dev/null @@ -1,14 +0,0 @@ - - - - - com.apple.security.app-sandbox - - com.apple.security.application-groups - - group.com.thomasricouard.IceCubesApp - - com.apple.security.network.client - - - From 73651cb7f174daefb06c53211847cf4d1ce693e8 Mon Sep 17 00:00:00 2001 From: Thomas Ricouard Date: Sun, 5 May 2024 17:47:08 +0200 Subject: [PATCH 04/20] Polish on timeline widget --- .../LatestPostsWidget.swift | 91 ++++++++++++++----- 1 file changed, 68 insertions(+), 23 deletions(-) diff --git a/IceCubesAppWidgetsExtension/LatestPostsWidget.swift b/IceCubesAppWidgetsExtension/LatestPostsWidget.swift index 8aa9a32e..1fec4639 100644 --- a/IceCubesAppWidgetsExtension/LatestPostsWidget.swift +++ b/IceCubesAppWidgetsExtension/LatestPostsWidget.swift @@ -7,16 +7,21 @@ import Timeline struct LatestPostsWidgetProvider: AppIntentTimelineProvider { func placeholder(in context: Context) -> LatestPostWidgetEntry { - .init(date: Date(), configuration: IceCubesWidgetConfigurationIntent(), timeline: .home, statuses: [ - .placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder() - ]) + .init(date: Date(), + configuration: IceCubesWidgetConfigurationIntent(), + timeline: .home, + statuses: [.placeholder(), .placeholder()], + images: [:]) } func snapshot(for configuration: IceCubesWidgetConfigurationIntent, in context: Context) async -> LatestPostWidgetEntry { if let entry = await timeline(for: configuration, context: context).entries.first { return entry } - return .init(date: Date(), configuration: configuration, timeline: .home, statuses: []) + return .init(date: Date(), + configuration: configuration, + timeline: .home, statuses: [], + images: [:]) } func timeline(for configuration: IceCubesWidgetConfigurationIntent, in context: Context) async -> Timeline { @@ -28,7 +33,8 @@ struct LatestPostsWidgetProvider: AppIntentTimelineProvider { return Timeline(entries: [.init(date: Date(), configuration: configuration, timeline: .home, - statuses: [])], + statuses: [], + images: [:])], policy: .atEnd) } let client = Client(server: account.account.server, oauthToken: account.account.oauthToken) @@ -44,24 +50,46 @@ struct LatestPostsWidgetProvider: AppIntentTimelineProvider { statuses = statuses.prefix(upTo: 2).map{ $0 } } case .systemLarge: - if statuses.count >= 4 { - statuses = statuses.prefix(upTo: 4).map{ $0 } + if statuses.count >= 5 { + statuses = statuses.prefix(upTo: 5).map{ $0 } } case .systemExtraLarge: - if statuses.count >= 6 { - statuses = statuses.prefix(upTo: 6).map{ $0 } + if statuses.count >= 8 { + statuses = statuses.prefix(upTo: 8).map{ $0 } } default: break } + let images = try await loadImages(urls: statuses.map{ $0.account.avatar }) return Timeline(entries: [.init(date: Date(), configuration: configuration, timeline: timeline.timeline, - statuses: statuses)], policy: .atEnd) + statuses: statuses, + images: images)], policy: .atEnd) } catch { return Timeline(entries: [.init(date: Date(), configuration: configuration, timeline: .home, - statuses: [])], policy: .atEnd) + statuses: [], + images: [:])], policy: .atEnd) + } + } + + private func loadImages(urls: [URL]) async throws -> [URL: UIImage] { + try await withThrowingTaskGroup(of: (URL, UIImage?).self) { group in + for url in urls { + group.addTask { + let response = try await URLSession.shared.data(from: url) + return (url, UIImage(data: response.0)) + } + } + + var images: [URL: UIImage] = [:] + + for try await (url, image) in group { + images[url] = image + } + + return images } } } @@ -71,6 +99,7 @@ struct LatestPostWidgetEntry: TimelineEntry { let configuration: IceCubesWidgetConfigurationIntent let timeline: TimelineFilter let statuses: [Status] + let images: [URL: UIImage] } struct LatestPostsWidgetView : View { @@ -106,17 +135,7 @@ struct LatestPostsWidgetView : View { if let url = URL(string: status.url ?? "") { Link(destination: url, label: { VStack(alignment: .leading, spacing: 4) { - HStack(spacing: 0) { - Text(status.account.safeDisplayName) - Text(" @") - Text(status.account.username) - Spacer() - } - .font(.subheadline) - .fontWeight(.semibold) - .foregroundStyle(.secondary) - .lineLimit(1) - + makeStatusHeaderView(status) Text(status.content.asRawText) .font(.body) .lineLimit(2) @@ -124,6 +143,31 @@ struct LatestPostsWidgetView : View { }) } } + + private func makeStatusHeaderView(_ status: Status) -> some View { + HStack(spacing: 4) { + if let image = entry.images[status.account.avatar] { + Image(uiImage: image) + .resizable() + .frame(width: 16, height: 16) + .clipShape(Circle()) + } else { + Circle() + .foregroundStyle(.secondary) + .frame(width: 16, height: 16) + } + HStack(spacing: 0) { + Text(status.account.safeDisplayName) + Text(" @") + Text(status.account.username) + Spacer() + } + .font(.subheadline) + .fontWeight(.semibold) + .foregroundStyle(.secondary) + .lineLimit(1) + } + } } struct LatestPostsWidget: Widget { @@ -149,5 +193,6 @@ struct LatestPostsWidget: Widget { LatestPostWidgetEntry(date: .now, configuration: .previewAccount, timeline: .home, - statuses: [.placeholder(), .placeholder(), .placeholder(), .placeholder()]) + statuses: [.placeholder(), .placeholder(), .placeholder(), .placeholder()], + images: [:]) } From c4dee39efecb4da104e63b92c736a22a043005f2 Mon Sep 17 00:00:00 2001 From: Thomas Ricouard Date: Sun, 5 May 2024 18:06:47 +0200 Subject: [PATCH 05/20] More fix for timeline widget --- .../LatestPostsWidget.swift | 44 +++++++++++++------ 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/IceCubesAppWidgetsExtension/LatestPostsWidget.swift b/IceCubesAppWidgetsExtension/LatestPostsWidget.swift index 1fec4639..551d2588 100644 --- a/IceCubesAppWidgetsExtension/LatestPostsWidget.swift +++ b/IceCubesAppWidgetsExtension/LatestPostsWidget.swift @@ -10,7 +10,7 @@ struct LatestPostsWidgetProvider: AppIntentTimelineProvider { .init(date: Date(), configuration: IceCubesWidgetConfigurationIntent(), timeline: .home, - statuses: [.placeholder(), .placeholder()], + statuses: [.placeholder()], images: [:]) } @@ -45,17 +45,17 @@ struct LatestPostsWidgetProvider: AppIntentTimelineProvider { offset: nil)) statuses = statuses.filter{ $0.reblog == nil && !$0.content.asRawText.isEmpty } switch context.family { - case .systemMedium: - if statuses.count >= 2 { - statuses = statuses.prefix(upTo: 2).map{ $0 } + case .systemSmall, .systemMedium: + if statuses.count >= 1 { + statuses = statuses.prefix(upTo: 1).map{ $0 } } case .systemLarge: - if statuses.count >= 5 { - statuses = statuses.prefix(upTo: 5).map{ $0 } + if statuses.count >= 4 { + statuses = statuses.prefix(upTo: 4).map{ $0 } } case .systemExtraLarge: - if statuses.count >= 8 { - statuses = statuses.prefix(upTo: 8).map{ $0 } + if statuses.count >= 6 { + statuses = statuses.prefix(upTo: 6).map{ $0 } } default: break @@ -108,11 +108,24 @@ struct LatestPostsWidgetView : View { @Environment(\.widgetFamily) var family @Environment(\.redactionReasons) var redacted + var contentLineLimit: Int { + switch family { + case .systemSmall, .systemMedium: + return 4 + default: + return 2 + } + } var body: some View { VStack(alignment: .leading, spacing: 8) { headerView ForEach(entry.statuses) { status in - makeStatusView(status) + VStack(spacing: 4) { + makeStatusView(status) + if entry.statuses.last?.id != status.id { + Divider() + } + } } Spacer() } @@ -134,11 +147,12 @@ struct LatestPostsWidgetView : View { private func makeStatusView(_ status: Status) -> some View { if let url = URL(string: status.url ?? "") { Link(destination: url, label: { - VStack(alignment: .leading, spacing: 4) { + VStack(alignment: .leading, spacing: 2) { makeStatusHeaderView(status) - Text(status.content.asRawText) + Text(status.content.asSafeMarkdownAttributedString) .font(.body) - .lineLimit(2) + .lineLimit(contentLineLimit) + .fixedSize(horizontal: false, vertical: true) } }) } @@ -157,7 +171,9 @@ struct LatestPostsWidgetView : View { .frame(width: 16, height: 16) } HStack(spacing: 0) { - Text(status.account.safeDisplayName) + if family != .systemSmall { + Text(status.account.safeDisplayName) + } Text(" @") Text(status.account.username) Spacer() @@ -182,7 +198,7 @@ struct LatestPostsWidget: Widget { } .configurationDisplayName("Latest posts") .description("Show the latest post for the selected timeline") - .supportedFamilies([.systemMedium, .systemLarge, .systemExtraLarge]) + .supportedFamilies([.systemSmall, .systemMedium, .systemLarge, .systemExtraLarge]) } } From 88218cd6ecd9db5b0130d67eb3ad756214ae39f0 Mon Sep 17 00:00:00 2001 From: Thomas Ricouard Date: Sun, 5 May 2024 18:10:32 +0200 Subject: [PATCH 06/20] Style fix --- IceCubesAppWidgetsExtension/LatestPostsWidget.swift | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/IceCubesAppWidgetsExtension/LatestPostsWidget.swift b/IceCubesAppWidgetsExtension/LatestPostsWidget.swift index 551d2588..09d0225a 100644 --- a/IceCubesAppWidgetsExtension/LatestPostsWidget.swift +++ b/IceCubesAppWidgetsExtension/LatestPostsWidget.swift @@ -171,16 +171,18 @@ struct LatestPostsWidgetView : View { .frame(width: 16, height: 16) } HStack(spacing: 0) { + Text(status.account.safeDisplayName) + .foregroundStyle(.primary) if family != .systemSmall { - Text(status.account.safeDisplayName) + Text(" @") + .foregroundStyle(.tertiary) + Text(status.account.username) + .foregroundStyle(.tertiary) } - Text(" @") - Text(status.account.username) Spacer() } .font(.subheadline) .fontWeight(.semibold) - .foregroundStyle(.secondary) .lineLimit(1) } } From a2afd4f58fb75344d484110254846c007120bd00 Mon Sep 17 00:00:00 2001 From: Thomas Ricouard Date: Sun, 5 May 2024 18:16:20 +0200 Subject: [PATCH 07/20] Fix widget bundle identifier --- IceCubesApp.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/IceCubesApp.xcodeproj/project.pbxproj b/IceCubesApp.xcodeproj/project.pbxproj index 931488fa..648ab394 100644 --- a/IceCubesApp.xcodeproj/project.pbxproj +++ b/IceCubesApp.xcodeproj/project.pbxproj @@ -1176,7 +1176,7 @@ "@executable_path/../../Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.thomasricouard.IceCubesApp.IceCubesAppWidgetsExtension; + PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesAppWidgetsExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; SKIP_INSTALL = YES; @@ -1212,7 +1212,7 @@ "@executable_path/../../Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.thomasricouard.IceCubesApp.IceCubesAppWidgetsExtension; + PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesAppWidgetsExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; SKIP_INSTALL = YES; From 7aebe530dd4f99fec7feccaaf6b7a598254df505 Mon Sep 17 00:00:00 2001 From: Thomas Ricouard Date: Sun, 5 May 2024 18:29:48 +0200 Subject: [PATCH 08/20] Widget: More UI refinements --- .../LatestPostsWidget.swift | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/IceCubesAppWidgetsExtension/LatestPostsWidget.swift b/IceCubesAppWidgetsExtension/LatestPostsWidget.swift index 09d0225a..47131b2f 100644 --- a/IceCubesAppWidgetsExtension/LatestPostsWidget.swift +++ b/IceCubesAppWidgetsExtension/LatestPostsWidget.swift @@ -49,14 +49,10 @@ struct LatestPostsWidgetProvider: AppIntentTimelineProvider { if statuses.count >= 1 { statuses = statuses.prefix(upTo: 1).map{ $0 } } - case .systemLarge: + case .systemLarge, .systemExtraLarge: if statuses.count >= 4 { statuses = statuses.prefix(upTo: 4).map{ $0 } } - case .systemExtraLarge: - if statuses.count >= 6 { - statuses = statuses.prefix(upTo: 6).map{ $0 } - } default: break } @@ -119,12 +115,9 @@ struct LatestPostsWidgetView : View { var body: some View { VStack(alignment: .leading, spacing: 8) { headerView - ForEach(entry.statuses) { status in - VStack(spacing: 4) { + VStack(alignment: .leading, spacing: 16) { + ForEach(entry.statuses) { status in makeStatusView(status) - if entry.statuses.last?.id != status.id { - Divider() - } } } Spacer() @@ -153,13 +146,14 @@ struct LatestPostsWidgetView : View { .font(.body) .lineLimit(contentLineLimit) .fixedSize(horizontal: false, vertical: true) + .padding(.leading, 20) } }) } } private func makeStatusHeaderView(_ status: Status) -> some View { - HStack(spacing: 4) { + HStack(alignment: .center, spacing: 4) { if let image = entry.images[status.account.avatar] { Image(uiImage: image) .resizable() From 8ab7b5ac69c7ddc0c507a8264eb100cfccf17bd0 Mon Sep 17 00:00:00 2001 From: Thomas Ricouard Date: Sun, 5 May 2024 19:03:25 +0200 Subject: [PATCH 09/20] Fix app group --- .../IceCubesAppWidgetsExtensionExtension.entitlements | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IceCubesAppWidgetsExtension/IceCubesAppWidgetsExtensionExtension.entitlements b/IceCubesAppWidgetsExtension/IceCubesAppWidgetsExtensionExtension.entitlements index 6169e196..7666b43d 100644 --- a/IceCubesAppWidgetsExtension/IceCubesAppWidgetsExtensionExtension.entitlements +++ b/IceCubesAppWidgetsExtension/IceCubesAppWidgetsExtensionExtension.entitlements @@ -10,7 +10,7 @@ com.apple.security.application-groups - group.com.thomasricouard.IceCubesApp + group.$(BUNDLE_ID_PREFIX).IceCubesApp com.apple.security.network.client From ea31cda3c2ab510e709e8c96a98306517cee3f4f Mon Sep 17 00:00:00 2001 From: Thomas Ricouard Date: Sun, 5 May 2024 19:31:28 +0200 Subject: [PATCH 10/20] Widget: Add Hashtag widget --- IceCubesApp.xcodeproj/project.pbxproj | 50 ++++- .../HashtagPostsWidget.swift | 81 +++++++ .../HashtagPostsWidgetConfiguration.swift | 22 ++ .../IceCubesAppWidgetsExtensionBundle.swift | 1 + .../LatestPosts/LatestPostsWidget.swift | 100 +++++++++ .../LatestPostsWidgetConfiguration.swift} | 8 +- .../LatestPostsWidget.swift | 210 ------------------ .../Shared/LastestPostsUI.swift | 97 ++++++++ .../Shared/SharedUtils.swift | 55 +++++ 9 files changed, 405 insertions(+), 219 deletions(-) create mode 100644 IceCubesAppWidgetsExtension/HashtagPostsWidget/HashtagPostsWidget.swift create mode 100644 IceCubesAppWidgetsExtension/HashtagPostsWidget/HashtagPostsWidgetConfiguration.swift create mode 100644 IceCubesAppWidgetsExtension/LatestPosts/LatestPostsWidget.swift rename IceCubesAppWidgetsExtension/{IceCubesWidgetConfigurationIntent.swift => LatestPosts/LatestPostsWidgetConfiguration.swift} (67%) delete mode 100644 IceCubesAppWidgetsExtension/LatestPostsWidget.swift create mode 100644 IceCubesAppWidgetsExtension/Shared/LastestPostsUI.swift create mode 100644 IceCubesAppWidgetsExtension/Shared/SharedUtils.swift diff --git a/IceCubesApp.xcodeproj/project.pbxproj b/IceCubesApp.xcodeproj/project.pbxproj index 648ab394..c0ccb757 100644 --- a/IceCubesApp.xcodeproj/project.pbxproj +++ b/IceCubesApp.xcodeproj/project.pbxproj @@ -71,7 +71,7 @@ 9F7788C92BE652B1004E6BEF /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9F7788C82BE652B1004E6BEF /* SwiftUI.framework */; }; 9F7788CC2BE652B1004E6BEF /* IceCubesAppWidgetsExtensionBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7788CB2BE652B1004E6BEF /* IceCubesAppWidgetsExtensionBundle.swift */; }; 9F7788CE2BE652B1004E6BEF /* LatestPostsWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7788CD2BE652B1004E6BEF /* LatestPostsWidget.swift */; }; - 9F7788D02BE652B1004E6BEF /* IceCubesWidgetConfigurationIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7788CF2BE652B1004E6BEF /* IceCubesWidgetConfigurationIntent.swift */; }; + 9F7788D02BE652B1004E6BEF /* LatestPostsWidgetConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7788CF2BE652B1004E6BEF /* LatestPostsWidgetConfiguration.swift */; }; 9F7788D22BE652B2004E6BEF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9F7788D12BE652B2004E6BEF /* Assets.xcassets */; }; 9F7788D62BE652B2004E6BEF /* IceCubesAppWidgetsExtensionExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 9F7788C52BE652B1004E6BEF /* IceCubesAppWidgetsExtensionExtension.appex */; platformFilters = (ios, maccatalyst, ); settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 9F7788DE2BE6543D004E6BEF /* Account in Frameworks */ = {isa = PBXBuildFile; productRef = 9F7788DD2BE6543D004E6BEF /* Account */; }; @@ -120,6 +120,10 @@ 9FE4CCAB2B4C848A00DA5F13 /* GiphyUISDK in Frameworks */ = {isa = PBXBuildFile; platformFilters = (ios, maccatalyst, ); productRef = 9FE4CCAA2B4C848A00DA5F13 /* GiphyUISDK */; }; 9FE4CCAD2B4C849F00DA5F13 /* GiphyUISDK in Frameworks */ = {isa = PBXBuildFile; productRef = 9FE4CCAC2B4C849F00DA5F13 /* GiphyUISDK */; }; 9FE6A42E2BD043A90055D388 /* RevenueCat in Frameworks */ = {isa = PBXBuildFile; productRef = 9FE6A42D2BD043A90055D388 /* RevenueCat */; }; + 9FF2FB622BE7F5D5001560CE /* HashtagPostsWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF2FB602BE7F5A7001560CE /* HashtagPostsWidget.swift */; }; + 9FF2FB632BE7F5D9001560CE /* HashtagPostsWidgetConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF2FB5E2BE7F56F001560CE /* HashtagPostsWidgetConfiguration.swift */; }; + 9FF2FB672BE7F816001560CE /* LastestPostsUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF2FB652BE7F805001560CE /* LastestPostsUI.swift */; }; + 9FF2FB6A2BE7F84E001560CE /* SharedUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF2FB682BE7F842001560CE /* SharedUtils.swift */; }; 9FFF677C299B7B2C00FE700A /* Notifications in Frameworks */ = {isa = PBXBuildFile; productRef = 9FFF677B299B7B2C00FE700A /* Notifications */; }; 9FFF6780299B7D2B00FE700A /* DesignSystem in Frameworks */ = {isa = PBXBuildFile; productRef = 9FFF677F299B7D2B00FE700A /* DesignSystem */; }; 9FFF6782299B7D3A00FE700A /* Account in Frameworks */ = {isa = PBXBuildFile; productRef = 9FFF6781299B7D3A00FE700A /* Account */; }; @@ -256,7 +260,7 @@ 9F7788C82BE652B1004E6BEF /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; 9F7788CB2BE652B1004E6BEF /* IceCubesAppWidgetsExtensionBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IceCubesAppWidgetsExtensionBundle.swift; sourceTree = ""; }; 9F7788CD2BE652B1004E6BEF /* LatestPostsWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LatestPostsWidget.swift; sourceTree = ""; }; - 9F7788CF2BE652B1004E6BEF /* IceCubesWidgetConfigurationIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IceCubesWidgetConfigurationIntent.swift; sourceTree = ""; }; + 9F7788CF2BE652B1004E6BEF /* LatestPostsWidgetConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LatestPostsWidgetConfiguration.swift; sourceTree = ""; }; 9F7788D12BE652B2004E6BEF /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 9F7788D32BE652B2004E6BEF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 9F7788D72BE652B2004E6BEF /* IceCubesAppWidgetsExtensionExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = IceCubesAppWidgetsExtensionExtension.entitlements; sourceTree = ""; }; @@ -290,6 +294,10 @@ 9FE0346A2ADD59AC00529EA8 /* MediaUI */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = MediaUI; path = Packages/MediaUI; sourceTree = ""; }; 9FE151A5293C90F900E9683D /* IconSelectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconSelectorView.swift; sourceTree = ""; }; 9FE3DB55296FEF5800628CB0 /* AppAccount */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = AppAccount; path = Packages/AppAccount; sourceTree = ""; }; + 9FF2FB5E2BE7F56F001560CE /* HashtagPostsWidgetConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagPostsWidgetConfiguration.swift; sourceTree = ""; }; + 9FF2FB602BE7F5A7001560CE /* HashtagPostsWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagPostsWidget.swift; sourceTree = ""; }; + 9FF2FB652BE7F805001560CE /* LastestPostsUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LastestPostsUI.swift; sourceTree = ""; }; + 9FF2FB682BE7F842001560CE /* SharedUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedUtils.swift; sourceTree = ""; }; B0BAB49E29B3D7A9008F54D7 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/InfoPlist.strings"; sourceTree = ""; }; C4CBB90B298A0DA3007E1707 /* en-GB */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "en-GB"; path = "en-GB.lproj/InfoPlist.strings"; sourceTree = ""; }; C4FBCF6F298FD88A0015DF22 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/InfoPlist.strings"; sourceTree = ""; }; @@ -471,10 +479,11 @@ 9F7788CA2BE652B1004E6BEF /* IceCubesAppWidgetsExtension */ = { isa = PBXGroup; children = ( + 9FF2FB642BE7F7FA001560CE /* Shared */, + 9FF2FB5D2BE7F559001560CE /* HashtagPostsWidget */, + 9FF2FB5C2BE7F549001560CE /* LatestPosts */, 9F7788D72BE652B2004E6BEF /* IceCubesAppWidgetsExtensionExtension.entitlements */, 9F7788CB2BE652B1004E6BEF /* IceCubesAppWidgetsExtensionBundle.swift */, - 9F7788CD2BE652B1004E6BEF /* LatestPostsWidget.swift */, - 9F7788CF2BE652B1004E6BEF /* IceCubesWidgetConfigurationIntent.swift */, 9F7788D12BE652B2004E6BEF /* Assets.xcassets */, 9F7788D32BE652B2004E6BEF /* Info.plist */, ); @@ -632,6 +641,33 @@ path = Settings; sourceTree = ""; }; + 9FF2FB5C2BE7F549001560CE /* LatestPosts */ = { + isa = PBXGroup; + children = ( + 9F7788CD2BE652B1004E6BEF /* LatestPostsWidget.swift */, + 9F7788CF2BE652B1004E6BEF /* LatestPostsWidgetConfiguration.swift */, + ); + path = LatestPosts; + sourceTree = ""; + }; + 9FF2FB5D2BE7F559001560CE /* HashtagPostsWidget */ = { + isa = PBXGroup; + children = ( + 9FF2FB5E2BE7F56F001560CE /* HashtagPostsWidgetConfiguration.swift */, + 9FF2FB602BE7F5A7001560CE /* HashtagPostsWidget.swift */, + ); + path = HashtagPostsWidget; + sourceTree = ""; + }; + 9FF2FB642BE7F7FA001560CE /* Shared */ = { + isa = PBXGroup; + children = ( + 9FF2FB652BE7F805001560CE /* LastestPostsUI.swift */, + 9FF2FB682BE7F842001560CE /* SharedUtils.swift */, + ); + path = Shared; + sourceTree = ""; + }; E9B576C029743F2A00BCE646 /* Localization */ = { isa = PBXGroup; children = ( @@ -945,11 +981,15 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 9FF2FB622BE7F5D5001560CE /* HashtagPostsWidget.swift in Sources */, 9F7788EA2BE65585004E6BEF /* AppAccountEntity.swift in Sources */, + 9FF2FB6A2BE7F84E001560CE /* SharedUtils.swift in Sources */, 9F7788CE2BE652B1004E6BEF /* LatestPostsWidget.swift in Sources */, 9F7788EE2BE78D7B004E6BEF /* TimelineFilterEntity.swift in Sources */, 9F7788CC2BE652B1004E6BEF /* IceCubesAppWidgetsExtensionBundle.swift in Sources */, - 9F7788D02BE652B1004E6BEF /* IceCubesWidgetConfigurationIntent.swift in Sources */, + 9F7788D02BE652B1004E6BEF /* LatestPostsWidgetConfiguration.swift in Sources */, + 9FF2FB672BE7F816001560CE /* LastestPostsUI.swift in Sources */, + 9FF2FB632BE7F5D9001560CE /* HashtagPostsWidgetConfiguration.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/IceCubesAppWidgetsExtension/HashtagPostsWidget/HashtagPostsWidget.swift b/IceCubesAppWidgetsExtension/HashtagPostsWidget/HashtagPostsWidget.swift new file mode 100644 index 00000000..fdf8ea9d --- /dev/null +++ b/IceCubesAppWidgetsExtension/HashtagPostsWidget/HashtagPostsWidget.swift @@ -0,0 +1,81 @@ +import WidgetKit +import SwiftUI +import Network +import DesignSystem +import Models +import Timeline + +struct HashtagPostsWidgetProvider: AppIntentTimelineProvider { + func placeholder(in context: Context) -> LatestPostWidgetEntry { + .init(date: Date(), + timeline: .hashtag(tag: "Mastodon", accountId: nil), + statuses: [.placeholder()], + images: [:]) + } + + func snapshot(for configuration: HashtagPostsWidgetConfiguration, in context: Context) async -> LatestPostWidgetEntry { + if let entry = await timeline(for: configuration, context: context).entries.first { + return entry + } + return .init(date: Date(), + timeline: .hashtag(tag: "Mastodon", accountId: nil), + statuses: [], + images: [:]) + } + + func timeline(for configuration: HashtagPostsWidgetConfiguration, in context: Context) async -> Timeline { + await timeline(for: configuration, context: context) + } + + private func timeline(for configuration: HashtagPostsWidgetConfiguration, context: Context) async -> Timeline { + guard let account = configuration.account, let hashgtag = configuration.hashgtag else { + return Timeline(entries: [.init(date: Date(), + timeline: .hashtag(tag: "Mastodon", accountId: nil), + statuses: [], + images: [:])], + policy: .atEnd) + } + do { + let statuses = await loadStatuses(for: .hashtag(tag: hashgtag, accountId: nil), + account: account, + widgetFamily: context.family) + let images = try await loadImages(urls: statuses.map{ $0.account.avatar } ) + return Timeline(entries: [.init(date: Date(), + timeline: .hashtag(tag: hashgtag, accountId: nil), + statuses: statuses, + images: images)], policy: .atEnd) + } catch { + return Timeline(entries: [.init(date: Date(), + timeline: .hashtag(tag: "Mastodon", accountId: nil), + statuses: [], + images: [:])], + policy: .atEnd) + } + } +} + +struct HashtagPostsWidget: Widget { + let kind: String = "HashtagPostsWidget" + + var body: some WidgetConfiguration { + AppIntentConfiguration(kind: kind, + intent: HashtagPostsWidgetConfiguration.self, + provider: HashtagPostsWidgetProvider()) { entry in + LatestPostsWidgetView(entry: entry) + .containerBackground(Color("WidgetBackground").gradient, for: .widget) + } + .configurationDisplayName("Hashtag timeline") + .description("Show the latest post for the selected hashtag") + .supportedFamilies([.systemSmall, .systemMedium, .systemLarge, .systemExtraLarge]) + } +} + + +#Preview(as: .systemMedium) { + HashtagPostsWidget() +} timeline: { + LatestPostWidgetEntry(date: .now, + timeline: .hashtag(tag: "Matodon", accountId: nil), + statuses: [.placeholder(), .placeholder(), .placeholder(), .placeholder()], + images: [:]) +} diff --git a/IceCubesAppWidgetsExtension/HashtagPostsWidget/HashtagPostsWidgetConfiguration.swift b/IceCubesAppWidgetsExtension/HashtagPostsWidget/HashtagPostsWidgetConfiguration.swift new file mode 100644 index 00000000..71804530 --- /dev/null +++ b/IceCubesAppWidgetsExtension/HashtagPostsWidget/HashtagPostsWidgetConfiguration.swift @@ -0,0 +1,22 @@ +import WidgetKit +import AppIntents + +struct HashtagPostsWidgetConfiguration: WidgetConfigurationIntent { + static let title: LocalizedStringResource = "Configuration" + static let description = IntentDescription("Choose the account and hashtag for this widget") + + @Parameter(title: "Account") + var account: AppAccountEntity? + + @Parameter(title: "Hashtag") + var hashgtag: String? +} + +extension HashtagPostsWidgetConfiguration { + static var previewAccount: HashtagPostsWidgetConfiguration { + let intent = HashtagPostsWidgetConfiguration() + intent.account = .init(account: .init(server: "Test", accountName: "Test account")) + intent.hashgtag = "Mastodon" + return intent + } +} diff --git a/IceCubesAppWidgetsExtension/IceCubesAppWidgetsExtensionBundle.swift b/IceCubesAppWidgetsExtension/IceCubesAppWidgetsExtensionBundle.swift index fe7d3461..b950fdb1 100644 --- a/IceCubesAppWidgetsExtension/IceCubesAppWidgetsExtensionBundle.swift +++ b/IceCubesAppWidgetsExtension/IceCubesAppWidgetsExtensionBundle.swift @@ -5,5 +5,6 @@ import SwiftUI struct IceCubesAppWidgetsExtensionBundle: WidgetBundle { var body: some Widget { LatestPostsWidget() + HashtagPostsWidget() } } diff --git a/IceCubesAppWidgetsExtension/LatestPosts/LatestPostsWidget.swift b/IceCubesAppWidgetsExtension/LatestPosts/LatestPostsWidget.swift new file mode 100644 index 00000000..3ecd9283 --- /dev/null +++ b/IceCubesAppWidgetsExtension/LatestPosts/LatestPostsWidget.swift @@ -0,0 +1,100 @@ +import WidgetKit +import SwiftUI +import Network +import DesignSystem +import Models +import Timeline + +struct LatestPostsWidgetProvider: AppIntentTimelineProvider { + func placeholder(in context: Context) -> LatestPostWidgetEntry { + .init(date: Date(), + timeline: .home, + statuses: [.placeholder()], + images: [:]) + } + + func snapshot(for configuration: LatestPostsWidgetConfiguration, in context: Context) async -> LatestPostWidgetEntry { + if let entry = await timeline(for: configuration, context: context).entries.first { + return entry + } + return .init(date: Date(), + timeline: .home, statuses: [], + images: [:]) + } + + func timeline(for configuration: LatestPostsWidgetConfiguration, in context: Context) async -> Timeline { + await timeline(for: configuration, context: context) + } + + private func timeline(for configuration: LatestPostsWidgetConfiguration, context: Context) async -> Timeline { + guard let account = configuration.account, let timeline = configuration.timeline else { + return Timeline(entries: [.init(date: Date(), + timeline: .home, + statuses: [], + images: [:])], + policy: .atEnd) + } + + do { + let statuses = await loadStatuses(for: timeline.timeline, + account: account, + widgetFamily: context.family) + let images = try await loadImages(urls: statuses.map{ $0.account.avatar } ) + return Timeline(entries: [.init(date: Date(), + timeline: timeline.timeline, + statuses: statuses, + images: images)], policy: .atEnd) + } catch { + return Timeline(entries: [.init(date: Date(), + timeline: .home, + statuses: [], + images: [:])], + policy: .atEnd) + } + } + + private func loadImages(urls: [URL]) async throws -> [URL: UIImage] { + try await withThrowingTaskGroup(of: (URL, UIImage?).self) { group in + for url in urls { + group.addTask { + let response = try await URLSession.shared.data(from: url) + return (url, UIImage(data: response.0)) + } + } + + var images: [URL: UIImage] = [:] + + for try await (url, image) in group { + images[url] = image + } + + return images + } + } +} + +struct LatestPostsWidget: Widget { + let kind: String = "LatestPostsWidget" + + var body: some WidgetConfiguration { + AppIntentConfiguration(kind: kind, + intent: LatestPostsWidgetConfiguration.self, + provider: LatestPostsWidgetProvider()) { entry in + LatestPostsWidgetView(entry: entry) + .containerBackground(Color("WidgetBackground").gradient, for: .widget) + } + .configurationDisplayName("Latest posts") + .description("Show the latest post for the selected timeline") + .supportedFamilies([.systemSmall, .systemMedium, .systemLarge, .systemExtraLarge]) + } +} + + +#Preview(as: .systemMedium) { + LatestPostsWidget() +} timeline: { + LatestPostWidgetEntry(date: .now, + timeline: .home, + statuses: [.placeholder(), .placeholder(), .placeholder(), .placeholder()], + images: [:]) +} diff --git a/IceCubesAppWidgetsExtension/IceCubesWidgetConfigurationIntent.swift b/IceCubesAppWidgetsExtension/LatestPosts/LatestPostsWidgetConfiguration.swift similarity index 67% rename from IceCubesAppWidgetsExtension/IceCubesWidgetConfigurationIntent.swift rename to IceCubesAppWidgetsExtension/LatestPosts/LatestPostsWidgetConfiguration.swift index 924bf169..ac6c3818 100644 --- a/IceCubesAppWidgetsExtension/IceCubesWidgetConfigurationIntent.swift +++ b/IceCubesAppWidgetsExtension/LatestPosts/LatestPostsWidgetConfiguration.swift @@ -1,7 +1,7 @@ import WidgetKit import AppIntents -struct IceCubesWidgetConfigurationIntent: WidgetConfigurationIntent { +struct LatestPostsWidgetConfiguration: WidgetConfigurationIntent { static let title: LocalizedStringResource = "Configuration" static let description = IntentDescription("Choose the account and timeline for this widget") @@ -12,9 +12,9 @@ struct IceCubesWidgetConfigurationIntent: WidgetConfigurationIntent { var timeline: TimelineFilterEntity? } -extension IceCubesWidgetConfigurationIntent { - static var previewAccount: IceCubesWidgetConfigurationIntent { - let intent = IceCubesWidgetConfigurationIntent() +extension LatestPostsWidgetConfiguration { + static var previewAccount: LatestPostsWidgetConfiguration { + let intent = LatestPostsWidgetConfiguration() intent.account = .init(account: .init(server: "Test", accountName: "Test account")) intent.timeline = .init(timeline: .home) return intent diff --git a/IceCubesAppWidgetsExtension/LatestPostsWidget.swift b/IceCubesAppWidgetsExtension/LatestPostsWidget.swift deleted file mode 100644 index 47131b2f..00000000 --- a/IceCubesAppWidgetsExtension/LatestPostsWidget.swift +++ /dev/null @@ -1,210 +0,0 @@ -import WidgetKit -import SwiftUI -import Network -import DesignSystem -import Models -import Timeline - -struct LatestPostsWidgetProvider: AppIntentTimelineProvider { - func placeholder(in context: Context) -> LatestPostWidgetEntry { - .init(date: Date(), - configuration: IceCubesWidgetConfigurationIntent(), - timeline: .home, - statuses: [.placeholder()], - images: [:]) - } - - func snapshot(for configuration: IceCubesWidgetConfigurationIntent, in context: Context) async -> LatestPostWidgetEntry { - if let entry = await timeline(for: configuration, context: context).entries.first { - return entry - } - return .init(date: Date(), - configuration: configuration, - timeline: .home, statuses: [], - images: [:]) - } - - func timeline(for configuration: IceCubesWidgetConfigurationIntent, in context: Context) async -> Timeline { - await timeline(for: configuration, context: context) - } - - private func timeline(for configuration: IceCubesWidgetConfigurationIntent, context: Context) async -> Timeline { - guard let account = configuration.account, let timeline = configuration.timeline else { - return Timeline(entries: [.init(date: Date(), - configuration: configuration, - timeline: .home, - statuses: [], - images: [:])], - policy: .atEnd) - } - let client = Client(server: account.account.server, oauthToken: account.account.oauthToken) - do { - var statuses: [Status] = try await client.get(endpoint: timeline.timeline.endpoint(sinceId: nil, - maxId: nil, - minId: nil, - offset: nil)) - statuses = statuses.filter{ $0.reblog == nil && !$0.content.asRawText.isEmpty } - switch context.family { - case .systemSmall, .systemMedium: - if statuses.count >= 1 { - statuses = statuses.prefix(upTo: 1).map{ $0 } - } - case .systemLarge, .systemExtraLarge: - if statuses.count >= 4 { - statuses = statuses.prefix(upTo: 4).map{ $0 } - } - default: - break - } - let images = try await loadImages(urls: statuses.map{ $0.account.avatar }) - return Timeline(entries: [.init(date: Date(), configuration: configuration, - timeline: timeline.timeline, - statuses: statuses, - images: images)], policy: .atEnd) - } catch { - return Timeline(entries: [.init(date: Date(), - configuration: configuration, - timeline: .home, - statuses: [], - images: [:])], policy: .atEnd) - } - } - - private func loadImages(urls: [URL]) async throws -> [URL: UIImage] { - try await withThrowingTaskGroup(of: (URL, UIImage?).self) { group in - for url in urls { - group.addTask { - let response = try await URLSession.shared.data(from: url) - return (url, UIImage(data: response.0)) - } - } - - var images: [URL: UIImage] = [:] - - for try await (url, image) in group { - images[url] = image - } - - return images - } - } -} - -struct LatestPostWidgetEntry: TimelineEntry { - let date: Date - let configuration: IceCubesWidgetConfigurationIntent - let timeline: TimelineFilter - let statuses: [Status] - let images: [URL: UIImage] -} - -struct LatestPostsWidgetView : View { - var entry: LatestPostsWidgetProvider.Entry - - @Environment(\.widgetFamily) var family - @Environment(\.redactionReasons) var redacted - - var contentLineLimit: Int { - switch family { - case .systemSmall, .systemMedium: - return 4 - default: - return 2 - } - } - var body: some View { - VStack(alignment: .leading, spacing: 8) { - headerView - VStack(alignment: .leading, spacing: 16) { - ForEach(entry.statuses) { status in - makeStatusView(status) - } - } - Spacer() - } - .frame(maxWidth: .infinity) - } - - private var headerView: some View { - HStack { - Text(entry.timeline.title) - Spacer() - Image(systemName: "cube") - } - .font(.subheadline) - .fontWeight(.bold) - .foregroundStyle(Color("AccentColor")) - } - - @ViewBuilder - private func makeStatusView(_ status: Status) -> some View { - if let url = URL(string: status.url ?? "") { - Link(destination: url, label: { - VStack(alignment: .leading, spacing: 2) { - makeStatusHeaderView(status) - Text(status.content.asSafeMarkdownAttributedString) - .font(.body) - .lineLimit(contentLineLimit) - .fixedSize(horizontal: false, vertical: true) - .padding(.leading, 20) - } - }) - } - } - - private func makeStatusHeaderView(_ status: Status) -> some View { - HStack(alignment: .center, spacing: 4) { - if let image = entry.images[status.account.avatar] { - Image(uiImage: image) - .resizable() - .frame(width: 16, height: 16) - .clipShape(Circle()) - } else { - Circle() - .foregroundStyle(.secondary) - .frame(width: 16, height: 16) - } - HStack(spacing: 0) { - Text(status.account.safeDisplayName) - .foregroundStyle(.primary) - if family != .systemSmall { - Text(" @") - .foregroundStyle(.tertiary) - Text(status.account.username) - .foregroundStyle(.tertiary) - } - Spacer() - } - .font(.subheadline) - .fontWeight(.semibold) - .lineLimit(1) - } - } -} - -struct LatestPostsWidget: Widget { - let kind: String = "LatestPostsWidget" - - var body: some WidgetConfiguration { - AppIntentConfiguration(kind: kind, - intent: IceCubesWidgetConfigurationIntent.self, - provider: LatestPostsWidgetProvider()) { entry in - LatestPostsWidgetView(entry: entry) - .containerBackground(Color("WidgetBackground").gradient, for: .widget) - } - .configurationDisplayName("Latest posts") - .description("Show the latest post for the selected timeline") - .supportedFamilies([.systemSmall, .systemMedium, .systemLarge, .systemExtraLarge]) - } -} - - -#Preview(as: .systemMedium) { - LatestPostsWidget() -} timeline: { - LatestPostWidgetEntry(date: .now, - configuration: .previewAccount, - timeline: .home, - statuses: [.placeholder(), .placeholder(), .placeholder(), .placeholder()], - images: [:]) -} diff --git a/IceCubesAppWidgetsExtension/Shared/LastestPostsUI.swift b/IceCubesAppWidgetsExtension/Shared/LastestPostsUI.swift new file mode 100644 index 00000000..78634d98 --- /dev/null +++ b/IceCubesAppWidgetsExtension/Shared/LastestPostsUI.swift @@ -0,0 +1,97 @@ +import WidgetKit +import SwiftUI +import Network +import DesignSystem +import Models +import Timeline + +struct LatestPostWidgetEntry: TimelineEntry { + let date: Date + let timeline: TimelineFilter + let statuses: [Status] + let images: [URL: UIImage] +} + +struct LatestPostsWidgetView : View { + var entry: LatestPostsWidgetProvider.Entry + + @Environment(\.widgetFamily) var family + @Environment(\.redactionReasons) var redacted + + var contentLineLimit: Int { + switch family { + case .systemSmall, .systemMedium: + return 4 + default: + return 2 + } + } + var body: some View { + VStack(alignment: .leading, spacing: 8) { + headerView + VStack(alignment: .leading, spacing: 16) { + ForEach(entry.statuses) { status in + makeStatusView(status) + } + } + Spacer() + } + .frame(maxWidth: .infinity) + } + + private var headerView: some View { + HStack { + Text(entry.timeline.title) + Spacer() + Image(systemName: "cube") + } + .font(.subheadline) + .fontWeight(.bold) + .foregroundStyle(Color("AccentColor")) + } + + @ViewBuilder + private func makeStatusView(_ status: Status) -> some View { + if let url = URL(string: status.url ?? "") { + Link(destination: url, label: { + VStack(alignment: .leading, spacing: 2) { + makeStatusHeaderView(status) + Text(status.content.asSafeMarkdownAttributedString) + .font(.body) + .lineLimit(contentLineLimit) + .fixedSize(horizontal: false, vertical: true) + .padding(.leading, 20) + } + }) + } + } + + private func makeStatusHeaderView(_ status: Status) -> some View { + HStack(alignment: .center, spacing: 4) { + if let image = entry.images[status.account.avatar] { + Image(uiImage: image) + .resizable() + .frame(width: 16, height: 16) + .clipShape(Circle()) + } else { + Circle() + .foregroundStyle(.secondary) + .frame(width: 16, height: 16) + } + HStack(spacing: 0) { + Text(status.account.safeDisplayName) + .foregroundStyle(.primary) + if family != .systemSmall { + Text(" @") + .foregroundStyle(.tertiary) + Text(status.account.username) + .foregroundStyle(.tertiary) + } + Spacer() + } + .font(.subheadline) + .fontWeight(.semibold) + .lineLimit(1) + } + } +} diff --git a/IceCubesAppWidgetsExtension/Shared/SharedUtils.swift b/IceCubesAppWidgetsExtension/Shared/SharedUtils.swift new file mode 100644 index 00000000..477b01f0 --- /dev/null +++ b/IceCubesAppWidgetsExtension/Shared/SharedUtils.swift @@ -0,0 +1,55 @@ +import StatusKit +import WidgetKit +import Timeline +import Foundation +import UIKit +import AppAccount +import Models +import Network + +func loadStatuses(for timeline: TimelineFilter, + account: AppAccountEntity, + widgetFamily: WidgetFamily) async -> [Status] { + let client = Client(server: account.account.server, oauthToken: account.account.oauthToken) + do { + var statuses: [Status] = try await client.get(endpoint: timeline.endpoint(sinceId: nil, + maxId: nil, + minId: nil, + offset: nil)) + statuses = statuses.filter{ $0.reblog == nil && !$0.content.asRawText.isEmpty } + switch widgetFamily { + case .systemSmall, .systemMedium: + if statuses.count >= 1 { + statuses = statuses.prefix(upTo: 1).map{ $0 } + } + case .systemLarge, .systemExtraLarge: + if statuses.count >= 4 { + statuses = statuses.prefix(upTo: 4).map{ $0 } + } + default: + break + } + return statuses + } catch { + return [] + } +} + +func loadImages(urls: [URL]) async throws -> [URL: UIImage] { + try await withThrowingTaskGroup(of: (URL, UIImage?).self) { group in + for url in urls { + group.addTask { + let response = try await URLSession.shared.data(from: url) + return (url, UIImage(data: response.0)) + } + } + + var images: [URL: UIImage] = [:] + + for try await (url, image) in group { + images[url] = image + } + + return images + } +} From a6fd8d1137b2f4c64f8ffce0286884bdff319119 Mon Sep 17 00:00:00 2001 From: Thomas Ricouard Date: Sun, 5 May 2024 19:37:06 +0200 Subject: [PATCH 11/20] Widget: Renaming --- IceCubesApp.xcodeproj/project.pbxproj | 8 ++++---- .../HashtagPostsWidget/HashtagPostsWidget.swift | 12 ++++++------ .../LatestPosts/LatestPostsWidget.swift | 12 ++++++------ .../{LastestPostsUI.swift => PostsWidgetView.swift} | 4 ++-- 4 files changed, 18 insertions(+), 18 deletions(-) rename IceCubesAppWidgetsExtension/Shared/{LastestPostsUI.swift => PostsWidgetView.swift} (96%) diff --git a/IceCubesApp.xcodeproj/project.pbxproj b/IceCubesApp.xcodeproj/project.pbxproj index c0ccb757..a5cc6b79 100644 --- a/IceCubesApp.xcodeproj/project.pbxproj +++ b/IceCubesApp.xcodeproj/project.pbxproj @@ -122,7 +122,7 @@ 9FE6A42E2BD043A90055D388 /* RevenueCat in Frameworks */ = {isa = PBXBuildFile; productRef = 9FE6A42D2BD043A90055D388 /* RevenueCat */; }; 9FF2FB622BE7F5D5001560CE /* HashtagPostsWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF2FB602BE7F5A7001560CE /* HashtagPostsWidget.swift */; }; 9FF2FB632BE7F5D9001560CE /* HashtagPostsWidgetConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF2FB5E2BE7F56F001560CE /* HashtagPostsWidgetConfiguration.swift */; }; - 9FF2FB672BE7F816001560CE /* LastestPostsUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF2FB652BE7F805001560CE /* LastestPostsUI.swift */; }; + 9FF2FB672BE7F816001560CE /* PostsWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF2FB652BE7F805001560CE /* PostsWidgetView.swift */; }; 9FF2FB6A2BE7F84E001560CE /* SharedUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF2FB682BE7F842001560CE /* SharedUtils.swift */; }; 9FFF677C299B7B2C00FE700A /* Notifications in Frameworks */ = {isa = PBXBuildFile; productRef = 9FFF677B299B7B2C00FE700A /* Notifications */; }; 9FFF6780299B7D2B00FE700A /* DesignSystem in Frameworks */ = {isa = PBXBuildFile; productRef = 9FFF677F299B7D2B00FE700A /* DesignSystem */; }; @@ -296,7 +296,7 @@ 9FE3DB55296FEF5800628CB0 /* AppAccount */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = AppAccount; path = Packages/AppAccount; sourceTree = ""; }; 9FF2FB5E2BE7F56F001560CE /* HashtagPostsWidgetConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagPostsWidgetConfiguration.swift; sourceTree = ""; }; 9FF2FB602BE7F5A7001560CE /* HashtagPostsWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagPostsWidget.swift; sourceTree = ""; }; - 9FF2FB652BE7F805001560CE /* LastestPostsUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LastestPostsUI.swift; sourceTree = ""; }; + 9FF2FB652BE7F805001560CE /* PostsWidgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostsWidgetView.swift; sourceTree = ""; }; 9FF2FB682BE7F842001560CE /* SharedUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedUtils.swift; sourceTree = ""; }; B0BAB49E29B3D7A9008F54D7 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/InfoPlist.strings"; sourceTree = ""; }; C4CBB90B298A0DA3007E1707 /* en-GB */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "en-GB"; path = "en-GB.lproj/InfoPlist.strings"; sourceTree = ""; }; @@ -662,7 +662,7 @@ 9FF2FB642BE7F7FA001560CE /* Shared */ = { isa = PBXGroup; children = ( - 9FF2FB652BE7F805001560CE /* LastestPostsUI.swift */, + 9FF2FB652BE7F805001560CE /* PostsWidgetView.swift */, 9FF2FB682BE7F842001560CE /* SharedUtils.swift */, ); path = Shared; @@ -988,7 +988,7 @@ 9F7788EE2BE78D7B004E6BEF /* TimelineFilterEntity.swift in Sources */, 9F7788CC2BE652B1004E6BEF /* IceCubesAppWidgetsExtensionBundle.swift in Sources */, 9F7788D02BE652B1004E6BEF /* LatestPostsWidgetConfiguration.swift in Sources */, - 9FF2FB672BE7F816001560CE /* LastestPostsUI.swift in Sources */, + 9FF2FB672BE7F816001560CE /* PostsWidgetView.swift in Sources */, 9FF2FB632BE7F5D9001560CE /* HashtagPostsWidgetConfiguration.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/IceCubesAppWidgetsExtension/HashtagPostsWidget/HashtagPostsWidget.swift b/IceCubesAppWidgetsExtension/HashtagPostsWidget/HashtagPostsWidget.swift index fdf8ea9d..2c8a33e5 100644 --- a/IceCubesAppWidgetsExtension/HashtagPostsWidget/HashtagPostsWidget.swift +++ b/IceCubesAppWidgetsExtension/HashtagPostsWidget/HashtagPostsWidget.swift @@ -6,14 +6,14 @@ import Models import Timeline struct HashtagPostsWidgetProvider: AppIntentTimelineProvider { - func placeholder(in context: Context) -> LatestPostWidgetEntry { + func placeholder(in context: Context) -> PostsWidgetEntry { .init(date: Date(), timeline: .hashtag(tag: "Mastodon", accountId: nil), statuses: [.placeholder()], images: [:]) } - func snapshot(for configuration: HashtagPostsWidgetConfiguration, in context: Context) async -> LatestPostWidgetEntry { + func snapshot(for configuration: HashtagPostsWidgetConfiguration, in context: Context) async -> PostsWidgetEntry { if let entry = await timeline(for: configuration, context: context).entries.first { return entry } @@ -23,11 +23,11 @@ struct HashtagPostsWidgetProvider: AppIntentTimelineProvider { images: [:]) } - func timeline(for configuration: HashtagPostsWidgetConfiguration, in context: Context) async -> Timeline { + func timeline(for configuration: HashtagPostsWidgetConfiguration, in context: Context) async -> Timeline { await timeline(for: configuration, context: context) } - private func timeline(for configuration: HashtagPostsWidgetConfiguration, context: Context) async -> Timeline { + private func timeline(for configuration: HashtagPostsWidgetConfiguration, context: Context) async -> Timeline { guard let account = configuration.account, let hashgtag = configuration.hashgtag else { return Timeline(entries: [.init(date: Date(), timeline: .hashtag(tag: "Mastodon", accountId: nil), @@ -61,7 +61,7 @@ struct HashtagPostsWidget: Widget { AppIntentConfiguration(kind: kind, intent: HashtagPostsWidgetConfiguration.self, provider: HashtagPostsWidgetProvider()) { entry in - LatestPostsWidgetView(entry: entry) + PostsWidgetView(entry: entry) .containerBackground(Color("WidgetBackground").gradient, for: .widget) } .configurationDisplayName("Hashtag timeline") @@ -74,7 +74,7 @@ struct HashtagPostsWidget: Widget { #Preview(as: .systemMedium) { HashtagPostsWidget() } timeline: { - LatestPostWidgetEntry(date: .now, + PostsWidgetEntry(date: .now, timeline: .hashtag(tag: "Matodon", accountId: nil), statuses: [.placeholder(), .placeholder(), .placeholder(), .placeholder()], images: [:]) diff --git a/IceCubesAppWidgetsExtension/LatestPosts/LatestPostsWidget.swift b/IceCubesAppWidgetsExtension/LatestPosts/LatestPostsWidget.swift index 3ecd9283..526ee947 100644 --- a/IceCubesAppWidgetsExtension/LatestPosts/LatestPostsWidget.swift +++ b/IceCubesAppWidgetsExtension/LatestPosts/LatestPostsWidget.swift @@ -6,14 +6,14 @@ import Models import Timeline struct LatestPostsWidgetProvider: AppIntentTimelineProvider { - func placeholder(in context: Context) -> LatestPostWidgetEntry { + func placeholder(in context: Context) -> PostsWidgetEntry { .init(date: Date(), timeline: .home, statuses: [.placeholder()], images: [:]) } - func snapshot(for configuration: LatestPostsWidgetConfiguration, in context: Context) async -> LatestPostWidgetEntry { + func snapshot(for configuration: LatestPostsWidgetConfiguration, in context: Context) async -> PostsWidgetEntry { if let entry = await timeline(for: configuration, context: context).entries.first { return entry } @@ -22,11 +22,11 @@ struct LatestPostsWidgetProvider: AppIntentTimelineProvider { images: [:]) } - func timeline(for configuration: LatestPostsWidgetConfiguration, in context: Context) async -> Timeline { + func timeline(for configuration: LatestPostsWidgetConfiguration, in context: Context) async -> Timeline { await timeline(for: configuration, context: context) } - private func timeline(for configuration: LatestPostsWidgetConfiguration, context: Context) async -> Timeline { + private func timeline(for configuration: LatestPostsWidgetConfiguration, context: Context) async -> Timeline { guard let account = configuration.account, let timeline = configuration.timeline else { return Timeline(entries: [.init(date: Date(), timeline: .home, @@ -80,7 +80,7 @@ struct LatestPostsWidget: Widget { AppIntentConfiguration(kind: kind, intent: LatestPostsWidgetConfiguration.self, provider: LatestPostsWidgetProvider()) { entry in - LatestPostsWidgetView(entry: entry) + PostsWidgetView(entry: entry) .containerBackground(Color("WidgetBackground").gradient, for: .widget) } .configurationDisplayName("Latest posts") @@ -93,7 +93,7 @@ struct LatestPostsWidget: Widget { #Preview(as: .systemMedium) { LatestPostsWidget() } timeline: { - LatestPostWidgetEntry(date: .now, + PostsWidgetEntry(date: .now, timeline: .home, statuses: [.placeholder(), .placeholder(), .placeholder(), .placeholder()], images: [:]) diff --git a/IceCubesAppWidgetsExtension/Shared/LastestPostsUI.swift b/IceCubesAppWidgetsExtension/Shared/PostsWidgetView.swift similarity index 96% rename from IceCubesAppWidgetsExtension/Shared/LastestPostsUI.swift rename to IceCubesAppWidgetsExtension/Shared/PostsWidgetView.swift index 78634d98..77309ec5 100644 --- a/IceCubesAppWidgetsExtension/Shared/LastestPostsUI.swift +++ b/IceCubesAppWidgetsExtension/Shared/PostsWidgetView.swift @@ -5,14 +5,14 @@ import DesignSystem import Models import Timeline -struct LatestPostWidgetEntry: TimelineEntry { +struct PostsWidgetEntry: TimelineEntry { let date: Date let timeline: TimelineFilter let statuses: [Status] let images: [URL: UIImage] } -struct LatestPostsWidgetView : View { +struct PostsWidgetView : View { var entry: LatestPostsWidgetProvider.Entry @Environment(\.widgetFamily) var family From 7328c00006c121bcc17608eba852de99ac152767 Mon Sep 17 00:00:00 2001 From: Thomas Ricouard Date: Sun, 5 May 2024 19:41:04 +0200 Subject: [PATCH 12/20] Widget: Remove optional parameters --- .../HashtagPostsWidget/HashtagPostsWidget.swift | 14 ++++---------- .../HashtagPostsWidgetConfiguration.swift | 4 ++-- .../LatestPosts/LatestPostsWidget.swift | 16 ++++------------ .../LatestPostsWidgetConfiguration.swift | 4 ++-- 4 files changed, 12 insertions(+), 26 deletions(-) diff --git a/IceCubesAppWidgetsExtension/HashtagPostsWidget/HashtagPostsWidget.swift b/IceCubesAppWidgetsExtension/HashtagPostsWidget/HashtagPostsWidget.swift index 2c8a33e5..7144d5be 100644 --- a/IceCubesAppWidgetsExtension/HashtagPostsWidget/HashtagPostsWidget.swift +++ b/IceCubesAppWidgetsExtension/HashtagPostsWidget/HashtagPostsWidget.swift @@ -28,20 +28,14 @@ struct HashtagPostsWidgetProvider: AppIntentTimelineProvider { } private func timeline(for configuration: HashtagPostsWidgetConfiguration, context: Context) async -> Timeline { - guard let account = configuration.account, let hashgtag = configuration.hashgtag else { - return Timeline(entries: [.init(date: Date(), - timeline: .hashtag(tag: "Mastodon", accountId: nil), - statuses: [], - images: [:])], - policy: .atEnd) - } do { - let statuses = await loadStatuses(for: .hashtag(tag: hashgtag, accountId: nil), - account: account, + let statuses = await loadStatuses(for: .hashtag(tag: configuration.hashgtag, accountId: nil), + account: configuration.account, widgetFamily: context.family) let images = try await loadImages(urls: statuses.map{ $0.account.avatar } ) return Timeline(entries: [.init(date: Date(), - timeline: .hashtag(tag: hashgtag, accountId: nil), + timeline: .hashtag(tag: configuration.hashgtag, + accountId: nil), statuses: statuses, images: images)], policy: .atEnd) } catch { diff --git a/IceCubesAppWidgetsExtension/HashtagPostsWidget/HashtagPostsWidgetConfiguration.swift b/IceCubesAppWidgetsExtension/HashtagPostsWidget/HashtagPostsWidgetConfiguration.swift index 71804530..d638eb39 100644 --- a/IceCubesAppWidgetsExtension/HashtagPostsWidget/HashtagPostsWidgetConfiguration.swift +++ b/IceCubesAppWidgetsExtension/HashtagPostsWidget/HashtagPostsWidgetConfiguration.swift @@ -6,10 +6,10 @@ struct HashtagPostsWidgetConfiguration: WidgetConfigurationIntent { static let description = IntentDescription("Choose the account and hashtag for this widget") @Parameter(title: "Account") - var account: AppAccountEntity? + var account: AppAccountEntity @Parameter(title: "Hashtag") - var hashgtag: String? + var hashgtag: String } extension HashtagPostsWidgetConfiguration { diff --git a/IceCubesAppWidgetsExtension/LatestPosts/LatestPostsWidget.swift b/IceCubesAppWidgetsExtension/LatestPosts/LatestPostsWidget.swift index 526ee947..20181a7f 100644 --- a/IceCubesAppWidgetsExtension/LatestPosts/LatestPostsWidget.swift +++ b/IceCubesAppWidgetsExtension/LatestPosts/LatestPostsWidget.swift @@ -27,21 +27,13 @@ struct LatestPostsWidgetProvider: AppIntentTimelineProvider { } private func timeline(for configuration: LatestPostsWidgetConfiguration, context: Context) async -> Timeline { - guard let account = configuration.account, let timeline = configuration.timeline else { - return Timeline(entries: [.init(date: Date(), - timeline: .home, - statuses: [], - images: [:])], - policy: .atEnd) - } - do { - let statuses = await loadStatuses(for: timeline.timeline, - account: account, - widgetFamily: context.family) + let statuses = await loadStatuses(for: configuration.timeline.timeline, + account: configuration.account, + widgetFamily: context.family) let images = try await loadImages(urls: statuses.map{ $0.account.avatar } ) return Timeline(entries: [.init(date: Date(), - timeline: timeline.timeline, + timeline: configuration.timeline.timeline, statuses: statuses, images: images)], policy: .atEnd) } catch { diff --git a/IceCubesAppWidgetsExtension/LatestPosts/LatestPostsWidgetConfiguration.swift b/IceCubesAppWidgetsExtension/LatestPosts/LatestPostsWidgetConfiguration.swift index ac6c3818..b92cbb81 100644 --- a/IceCubesAppWidgetsExtension/LatestPosts/LatestPostsWidgetConfiguration.swift +++ b/IceCubesAppWidgetsExtension/LatestPosts/LatestPostsWidgetConfiguration.swift @@ -6,10 +6,10 @@ struct LatestPostsWidgetConfiguration: WidgetConfigurationIntent { static let description = IntentDescription("Choose the account and timeline for this widget") @Parameter(title: "Account") - var account: AppAccountEntity? + var account: AppAccountEntity @Parameter(title: "Timeline") - var timeline: TimelineFilterEntity? + var timeline: TimelineFilterEntity } extension LatestPostsWidgetConfiguration { From ee6f003073f74032d6681d13e21784cea2fcecfb Mon Sep 17 00:00:00 2001 From: Thomas Ricouard Date: Sun, 5 May 2024 20:03:12 +0200 Subject: [PATCH 13/20] Widget: Improve text size --- .../Shared/PostsWidgetView.swift | 12 +++++------- IceCubesAppWidgetsExtension/Shared/SharedUtils.swift | 4 ++-- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/IceCubesAppWidgetsExtension/Shared/PostsWidgetView.swift b/IceCubesAppWidgetsExtension/Shared/PostsWidgetView.swift index 77309ec5..dcda3780 100644 --- a/IceCubesAppWidgetsExtension/Shared/PostsWidgetView.swift +++ b/IceCubesAppWidgetsExtension/Shared/PostsWidgetView.swift @@ -21,7 +21,7 @@ struct PostsWidgetView : View { var contentLineLimit: Int { switch family { case .systemSmall, .systemMedium: - return 4 + return 5 default: return 2 } @@ -29,10 +29,8 @@ struct PostsWidgetView : View { var body: some View { VStack(alignment: .leading, spacing: 8) { headerView - VStack(alignment: .leading, spacing: 16) { - ForEach(entry.statuses) { status in - makeStatusView(status) - } + ForEach(entry.statuses) { status in + makeStatusView(status) } Spacer() } @@ -57,7 +55,7 @@ struct PostsWidgetView : View { VStack(alignment: .leading, spacing: 2) { makeStatusHeaderView(status) Text(status.content.asSafeMarkdownAttributedString) - .font(.body) + .font(.footnote) .lineLimit(contentLineLimit) .fixedSize(horizontal: false, vertical: true) .padding(.leading, 20) @@ -89,7 +87,7 @@ struct PostsWidgetView : View { } Spacer() } - .font(.subheadline) + .font(.footnote) .fontWeight(.semibold) .lineLimit(1) } diff --git a/IceCubesAppWidgetsExtension/Shared/SharedUtils.swift b/IceCubesAppWidgetsExtension/Shared/SharedUtils.swift index 477b01f0..db1aca41 100644 --- a/IceCubesAppWidgetsExtension/Shared/SharedUtils.swift +++ b/IceCubesAppWidgetsExtension/Shared/SharedUtils.swift @@ -23,8 +23,8 @@ func loadStatuses(for timeline: TimelineFilter, statuses = statuses.prefix(upTo: 1).map{ $0 } } case .systemLarge, .systemExtraLarge: - if statuses.count >= 4 { - statuses = statuses.prefix(upTo: 4).map{ $0 } + if statuses.count >= 5 { + statuses = statuses.prefix(upTo: 5).map{ $0 } } default: break From 24d5ecd1196bc1f055abcc1bc9a767dc62644b4b Mon Sep 17 00:00:00 2001 From: Thomas Ricouard Date: Sun, 5 May 2024 21:13:34 +0200 Subject: [PATCH 14/20] Shortcuts: Fix image --- Packages/StatusKit/Sources/StatusKit/Editor/ViewModel.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Packages/StatusKit/Sources/StatusKit/Editor/ViewModel.swift b/Packages/StatusKit/Sources/StatusKit/Editor/ViewModel.swift index 8c049832..3eb630ec 100644 --- a/Packages/StatusKit/Sources/StatusKit/Editor/ViewModel.swift +++ b/Packages/StatusKit/Sources/StatusKit/Editor/ViewModel.swift @@ -752,7 +752,7 @@ public extension StatusEditor { for url in urls { let compressor = Compressor() - + _ = url.startAccessingSecurityScopedResource() if let compressedData = await compressor.compressImageFrom(url: url), let image = UIImage(data: compressedData) { @@ -765,6 +765,8 @@ public extension StatusEditor { error: nil )) } + + url.stopAccessingSecurityScopedResource() } return containers From 189e10f2b4b5b3ef69da8a66a41dbaf080978119 Mon Sep 17 00:00:00 2001 From: Thomas Ricouard Date: Mon, 6 May 2024 08:37:58 +0200 Subject: [PATCH 15/20] Widget: Add mentions widget --- IceCubesApp.xcodeproj/project.pbxproj | 22 +++- ...CubesAppWidgetsExtensionExtension.xcscheme | 114 ++++++++++++++++++ .../HashtagPostsWidget.swift | 14 +-- .../IceCubesAppWidgetsExtensionBundle.swift | 1 + .../LatestPostsWidget.swift | 15 +-- .../LatestPostsWidgetConfiguration.swift | 0 .../MentionWidget/MentionWidget.swift | 81 +++++++++++++ .../MentionWidgetConfiguration.swift | 18 +++ .../Shared/PostsWidgetView.swift | 4 +- 9 files changed, 250 insertions(+), 19 deletions(-) create mode 100644 IceCubesApp.xcodeproj/xcshareddata/xcschemes/IceCubesAppWidgetsExtensionExtension.xcscheme rename IceCubesAppWidgetsExtension/{LatestPosts => LatestPostsWidget}/LatestPostsWidget.swift (86%) rename IceCubesAppWidgetsExtension/{LatestPosts => LatestPostsWidget}/LatestPostsWidgetConfiguration.swift (100%) create mode 100644 IceCubesAppWidgetsExtension/MentionWidget/MentionWidget.swift create mode 100644 IceCubesAppWidgetsExtension/MentionWidget/MentionWidgetConfiguration.swift diff --git a/IceCubesApp.xcodeproj/project.pbxproj b/IceCubesApp.xcodeproj/project.pbxproj index a5cc6b79..c2ec3166 100644 --- a/IceCubesApp.xcodeproj/project.pbxproj +++ b/IceCubesApp.xcodeproj/project.pbxproj @@ -124,6 +124,8 @@ 9FF2FB632BE7F5D9001560CE /* HashtagPostsWidgetConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF2FB5E2BE7F56F001560CE /* HashtagPostsWidgetConfiguration.swift */; }; 9FF2FB672BE7F816001560CE /* PostsWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF2FB652BE7F805001560CE /* PostsWidgetView.swift */; }; 9FF2FB6A2BE7F84E001560CE /* SharedUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF2FB682BE7F842001560CE /* SharedUtils.swift */; }; + 9FF2FB702BE8AE9D001560CE /* MentionWidgetConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF2FB6E2BE8AE9B001560CE /* MentionWidgetConfiguration.swift */; }; + 9FF2FB712BE8AEA0001560CE /* MentionWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF2FB6C2BE8AE90001560CE /* MentionWidget.swift */; }; 9FFF677C299B7B2C00FE700A /* Notifications in Frameworks */ = {isa = PBXBuildFile; productRef = 9FFF677B299B7B2C00FE700A /* Notifications */; }; 9FFF6780299B7D2B00FE700A /* DesignSystem in Frameworks */ = {isa = PBXBuildFile; productRef = 9FFF677F299B7D2B00FE700A /* DesignSystem */; }; 9FFF6782299B7D3A00FE700A /* Account in Frameworks */ = {isa = PBXBuildFile; productRef = 9FFF6781299B7D3A00FE700A /* Account */; }; @@ -298,6 +300,8 @@ 9FF2FB602BE7F5A7001560CE /* HashtagPostsWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagPostsWidget.swift; sourceTree = ""; }; 9FF2FB652BE7F805001560CE /* PostsWidgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostsWidgetView.swift; sourceTree = ""; }; 9FF2FB682BE7F842001560CE /* SharedUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedUtils.swift; sourceTree = ""; }; + 9FF2FB6C2BE8AE90001560CE /* MentionWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionWidget.swift; sourceTree = ""; }; + 9FF2FB6E2BE8AE9B001560CE /* MentionWidgetConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionWidgetConfiguration.swift; sourceTree = ""; }; B0BAB49E29B3D7A9008F54D7 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/InfoPlist.strings"; sourceTree = ""; }; C4CBB90B298A0DA3007E1707 /* en-GB */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "en-GB"; path = "en-GB.lproj/InfoPlist.strings"; sourceTree = ""; }; C4FBCF6F298FD88A0015DF22 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/InfoPlist.strings"; sourceTree = ""; }; @@ -479,9 +483,10 @@ 9F7788CA2BE652B1004E6BEF /* IceCubesAppWidgetsExtension */ = { isa = PBXGroup; children = ( + 9FF2FB6B2BE8AE78001560CE /* MentionWidget */, 9FF2FB642BE7F7FA001560CE /* Shared */, 9FF2FB5D2BE7F559001560CE /* HashtagPostsWidget */, - 9FF2FB5C2BE7F549001560CE /* LatestPosts */, + 9FF2FB5C2BE7F549001560CE /* LatestPostsWidget */, 9F7788D72BE652B2004E6BEF /* IceCubesAppWidgetsExtensionExtension.entitlements */, 9F7788CB2BE652B1004E6BEF /* IceCubesAppWidgetsExtensionBundle.swift */, 9F7788D12BE652B2004E6BEF /* Assets.xcassets */, @@ -641,13 +646,13 @@ path = Settings; sourceTree = ""; }; - 9FF2FB5C2BE7F549001560CE /* LatestPosts */ = { + 9FF2FB5C2BE7F549001560CE /* LatestPostsWidget */ = { isa = PBXGroup; children = ( 9F7788CD2BE652B1004E6BEF /* LatestPostsWidget.swift */, 9F7788CF2BE652B1004E6BEF /* LatestPostsWidgetConfiguration.swift */, ); - path = LatestPosts; + path = LatestPostsWidget; sourceTree = ""; }; 9FF2FB5D2BE7F559001560CE /* HashtagPostsWidget */ = { @@ -668,6 +673,15 @@ path = Shared; sourceTree = ""; }; + 9FF2FB6B2BE8AE78001560CE /* MentionWidget */ = { + isa = PBXGroup; + children = ( + 9FF2FB6C2BE8AE90001560CE /* MentionWidget.swift */, + 9FF2FB6E2BE8AE9B001560CE /* MentionWidgetConfiguration.swift */, + ); + path = MentionWidget; + sourceTree = ""; + }; E9B576C029743F2A00BCE646 /* Localization */ = { isa = PBXGroup; children = ( @@ -982,11 +996,13 @@ buildActionMask = 2147483647; files = ( 9FF2FB622BE7F5D5001560CE /* HashtagPostsWidget.swift in Sources */, + 9FF2FB712BE8AEA0001560CE /* MentionWidget.swift in Sources */, 9F7788EA2BE65585004E6BEF /* AppAccountEntity.swift in Sources */, 9FF2FB6A2BE7F84E001560CE /* SharedUtils.swift in Sources */, 9F7788CE2BE652B1004E6BEF /* LatestPostsWidget.swift in Sources */, 9F7788EE2BE78D7B004E6BEF /* TimelineFilterEntity.swift in Sources */, 9F7788CC2BE652B1004E6BEF /* IceCubesAppWidgetsExtensionBundle.swift in Sources */, + 9FF2FB702BE8AE9D001560CE /* MentionWidgetConfiguration.swift in Sources */, 9F7788D02BE652B1004E6BEF /* LatestPostsWidgetConfiguration.swift in Sources */, 9FF2FB672BE7F816001560CE /* PostsWidgetView.swift in Sources */, 9FF2FB632BE7F5D9001560CE /* HashtagPostsWidgetConfiguration.swift in Sources */, diff --git a/IceCubesApp.xcodeproj/xcshareddata/xcschemes/IceCubesAppWidgetsExtensionExtension.xcscheme b/IceCubesApp.xcodeproj/xcshareddata/xcschemes/IceCubesAppWidgetsExtensionExtension.xcscheme new file mode 100644 index 00000000..31b0b112 --- /dev/null +++ b/IceCubesApp.xcodeproj/xcshareddata/xcschemes/IceCubesAppWidgetsExtensionExtension.xcscheme @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/IceCubesAppWidgetsExtension/HashtagPostsWidget/HashtagPostsWidget.swift b/IceCubesAppWidgetsExtension/HashtagPostsWidget/HashtagPostsWidget.swift index 7144d5be..08478b6c 100644 --- a/IceCubesAppWidgetsExtension/HashtagPostsWidget/HashtagPostsWidget.swift +++ b/IceCubesAppWidgetsExtension/HashtagPostsWidget/HashtagPostsWidget.swift @@ -8,7 +8,7 @@ import Timeline struct HashtagPostsWidgetProvider: AppIntentTimelineProvider { func placeholder(in context: Context) -> PostsWidgetEntry { .init(date: Date(), - timeline: .hashtag(tag: "Mastodon", accountId: nil), + title: "#Mastodon", statuses: [.placeholder()], images: [:]) } @@ -18,7 +18,7 @@ struct HashtagPostsWidgetProvider: AppIntentTimelineProvider { return entry } return .init(date: Date(), - timeline: .hashtag(tag: "Mastodon", accountId: nil), + title: "#Mastodon", statuses: [], images: [:]) } @@ -29,18 +29,18 @@ struct HashtagPostsWidgetProvider: AppIntentTimelineProvider { private func timeline(for configuration: HashtagPostsWidgetConfiguration, context: Context) async -> Timeline { do { - let statuses = await loadStatuses(for: .hashtag(tag: configuration.hashgtag, accountId: nil), + let timeline: TimelineFilter = .hashtag(tag: configuration.hashgtag, accountId: nil) + let statuses = await loadStatuses(for: timeline, account: configuration.account, widgetFamily: context.family) let images = try await loadImages(urls: statuses.map{ $0.account.avatar } ) return Timeline(entries: [.init(date: Date(), - timeline: .hashtag(tag: configuration.hashgtag, - accountId: nil), + title: timeline.title, statuses: statuses, images: images)], policy: .atEnd) } catch { return Timeline(entries: [.init(date: Date(), - timeline: .hashtag(tag: "Mastodon", accountId: nil), + title: "#Mastodon", statuses: [], images: [:])], policy: .atEnd) @@ -69,7 +69,7 @@ struct HashtagPostsWidget: Widget { HashtagPostsWidget() } timeline: { PostsWidgetEntry(date: .now, - timeline: .hashtag(tag: "Matodon", accountId: nil), + title: "#Mastodon", statuses: [.placeholder(), .placeholder(), .placeholder(), .placeholder()], images: [:]) } diff --git a/IceCubesAppWidgetsExtension/IceCubesAppWidgetsExtensionBundle.swift b/IceCubesAppWidgetsExtension/IceCubesAppWidgetsExtensionBundle.swift index b950fdb1..ba5f7cea 100644 --- a/IceCubesAppWidgetsExtension/IceCubesAppWidgetsExtensionBundle.swift +++ b/IceCubesAppWidgetsExtension/IceCubesAppWidgetsExtensionBundle.swift @@ -6,5 +6,6 @@ struct IceCubesAppWidgetsExtensionBundle: WidgetBundle { var body: some Widget { LatestPostsWidget() HashtagPostsWidget() + MentionsWidget() } } diff --git a/IceCubesAppWidgetsExtension/LatestPosts/LatestPostsWidget.swift b/IceCubesAppWidgetsExtension/LatestPostsWidget/LatestPostsWidget.swift similarity index 86% rename from IceCubesAppWidgetsExtension/LatestPosts/LatestPostsWidget.swift rename to IceCubesAppWidgetsExtension/LatestPostsWidget/LatestPostsWidget.swift index 20181a7f..a6484b1c 100644 --- a/IceCubesAppWidgetsExtension/LatestPosts/LatestPostsWidget.swift +++ b/IceCubesAppWidgetsExtension/LatestPostsWidget/LatestPostsWidget.swift @@ -8,7 +8,7 @@ import Timeline struct LatestPostsWidgetProvider: AppIntentTimelineProvider { func placeholder(in context: Context) -> PostsWidgetEntry { .init(date: Date(), - timeline: .home, + title: "Home", statuses: [.placeholder()], images: [:]) } @@ -18,7 +18,8 @@ struct LatestPostsWidgetProvider: AppIntentTimelineProvider { return entry } return .init(date: Date(), - timeline: .home, statuses: [], + title: configuration.timeline.timeline.title, + statuses: [], images: [:]) } @@ -33,12 +34,12 @@ struct LatestPostsWidgetProvider: AppIntentTimelineProvider { widgetFamily: context.family) let images = try await loadImages(urls: statuses.map{ $0.account.avatar } ) return Timeline(entries: [.init(date: Date(), - timeline: configuration.timeline.timeline, + title: configuration.timeline.timeline.title, statuses: statuses, images: images)], policy: .atEnd) } catch { return Timeline(entries: [.init(date: Date(), - timeline: .home, + title: configuration.timeline.timeline.title, statuses: [], images: [:])], policy: .atEnd) @@ -86,7 +87,7 @@ struct LatestPostsWidget: Widget { LatestPostsWidget() } timeline: { PostsWidgetEntry(date: .now, - timeline: .home, - statuses: [.placeholder(), .placeholder(), .placeholder(), .placeholder()], - images: [:]) + title: "Mastodon", + statuses: [.placeholder(), .placeholder(), .placeholder(), .placeholder()], + images: [:]) } diff --git a/IceCubesAppWidgetsExtension/LatestPosts/LatestPostsWidgetConfiguration.swift b/IceCubesAppWidgetsExtension/LatestPostsWidget/LatestPostsWidgetConfiguration.swift similarity index 100% rename from IceCubesAppWidgetsExtension/LatestPosts/LatestPostsWidgetConfiguration.swift rename to IceCubesAppWidgetsExtension/LatestPostsWidget/LatestPostsWidgetConfiguration.swift diff --git a/IceCubesAppWidgetsExtension/MentionWidget/MentionWidget.swift b/IceCubesAppWidgetsExtension/MentionWidget/MentionWidget.swift new file mode 100644 index 00000000..046dc74c --- /dev/null +++ b/IceCubesAppWidgetsExtension/MentionWidget/MentionWidget.swift @@ -0,0 +1,81 @@ +import WidgetKit +import SwiftUI +import Network +import DesignSystem +import Models +import Timeline + +struct MentionsWidgetProvider: AppIntentTimelineProvider { + func placeholder(in context: Context) -> PostsWidgetEntry { + .init(date: Date(), + title: "Mentions", + statuses: [.placeholder()], + images: [:]) + } + + func snapshot(for configuration: MentionsWidgetConfiguration, in context: Context) async -> PostsWidgetEntry { + if let entry = await timeline(for: configuration, context: context).entries.first { + return entry + } + return .init(date: Date(), + title: "Mentions", + statuses: [], + images: [:]) + } + + func timeline(for configuration: MentionsWidgetConfiguration, in context: Context) async -> Timeline { + await timeline(for: configuration, context: context) + } + + private func timeline(for configuration: MentionsWidgetConfiguration, context: Context) async -> Timeline { + do { + let client = Client(server: configuration.account.account.server, + oauthToken: configuration.account.account.oauthToken) + var excludedTypes = Models.Notification.NotificationType.allCases + excludedTypes.removeAll(where: { $0 == .mention }) + var notifications: [Models.Notification] = + try await client.get(endpoint: Notifications.notifications(minId: nil, + maxId: nil, + types: excludedTypes.map(\.rawValue), + limit: 5)) + let statuses = notifications.compactMap{ $0.status } + let images = try await loadImages(urls: statuses.map{ $0.account.avatar } ) + return Timeline(entries: [.init(date: Date(), + title: "Mentions", + statuses: statuses, + images: images)], policy: .atEnd) + } catch { + return Timeline(entries: [.init(date: Date(), + title: "Mentions", + statuses: [], + images: [:])], + policy: .atEnd) + } + } +} + +struct MentionsWidget: Widget { + let kind: String = "MentionsWidget" + + var body: some WidgetConfiguration { + AppIntentConfiguration(kind: kind, + intent: MentionsWidgetConfiguration.self, + provider: MentionsWidgetProvider()) { entry in + PostsWidgetView(entry: entry) + .containerBackground(Color("WidgetBackground").gradient, for: .widget) + } + .configurationDisplayName("Mentions") + .description("Show the latest mentions for the selected account.") + .supportedFamilies([.systemSmall, .systemMedium, .systemLarge, .systemExtraLarge]) + } +} + + +#Preview(as: .systemMedium) { + MentionsWidget() +} timeline: { + PostsWidgetEntry(date: .now, + title: "Mentions", + statuses: [.placeholder(), .placeholder(), .placeholder(), .placeholder()], + images: [:]) +} diff --git a/IceCubesAppWidgetsExtension/MentionWidget/MentionWidgetConfiguration.swift b/IceCubesAppWidgetsExtension/MentionWidget/MentionWidgetConfiguration.swift new file mode 100644 index 00000000..7ad55a49 --- /dev/null +++ b/IceCubesAppWidgetsExtension/MentionWidget/MentionWidgetConfiguration.swift @@ -0,0 +1,18 @@ +import WidgetKit +import AppIntents + +struct MentionsWidgetConfiguration: WidgetConfigurationIntent { + static let title: LocalizedStringResource = "Configuration" + static let description = IntentDescription("Choose the account for this widget") + + @Parameter(title: "Account") + var account: AppAccountEntity +} + +extension MentionsWidgetConfiguration { + static var previewAccount: MentionsWidgetConfiguration { + let intent = MentionsWidgetConfiguration() + intent.account = .init(account: .init(server: "Test", accountName: "Test account")) + return intent + } +} diff --git a/IceCubesAppWidgetsExtension/Shared/PostsWidgetView.swift b/IceCubesAppWidgetsExtension/Shared/PostsWidgetView.swift index dcda3780..7b6c6821 100644 --- a/IceCubesAppWidgetsExtension/Shared/PostsWidgetView.swift +++ b/IceCubesAppWidgetsExtension/Shared/PostsWidgetView.swift @@ -7,7 +7,7 @@ import Timeline struct PostsWidgetEntry: TimelineEntry { let date: Date - let timeline: TimelineFilter + let title: String let statuses: [Status] let images: [URL: UIImage] } @@ -39,7 +39,7 @@ struct PostsWidgetView : View { private var headerView: some View { HStack { - Text(entry.timeline.title) + Text(entry.title) Spacer() Image(systemName: "cube") } From a37316c56fabf612953916502c8a4d80b32c3d57 Mon Sep 17 00:00:00 2001 From: Thomas Ricouard Date: Mon, 6 May 2024 08:38:37 +0200 Subject: [PATCH 16/20] Lint --- IceCubesAppIntents/AppAccountEntity.swift | 11 +++-- IceCubesAppIntents/TimelineFilterEntity.swift | 12 +++--- .../HashtagPostsWidget.swift | 36 ++++++++-------- .../HashtagPostsWidgetConfiguration.swift | 6 +-- .../IceCubesAppWidgetsExtensionBundle.swift | 2 +- .../LatestPostsWidget/LatestPostsWidget.swift | 38 ++++++++--------- .../LatestPostsWidgetConfiguration.swift | 6 +-- .../MentionWidget/MentionWidget.swift | 42 +++++++++---------- .../MentionWidgetConfiguration.swift | 4 +- .../Shared/PostsWidgetView.swift | 19 +++++---- .../Shared/SharedUtils.swift | 27 ++++++------ .../Sources/StatusKit/Editor/ViewModel.swift | 2 +- 12 files changed, 103 insertions(+), 102 deletions(-) diff --git a/IceCubesAppIntents/AppAccountEntity.swift b/IceCubesAppIntents/AppAccountEntity.swift index 5b87adb6..e6cb0f09 100644 --- a/IceCubesAppIntents/AppAccountEntity.swift +++ b/IceCubesAppIntents/AppAccountEntity.swift @@ -1,13 +1,12 @@ -import AppIntents import AppAccount +import AppIntents import Env import Foundation import Models import Network - -extension IntentDescription: @unchecked Sendable { } -extension TypeDisplayRepresentation: @unchecked Sendable { } +extension IntentDescription: @unchecked Sendable {} +extension TypeDisplayRepresentation: @unchecked Sendable {} public struct AppAccountEntity: Identifiable, AppEntity { public var id: String { account.id } @@ -24,8 +23,8 @@ public struct AppAccountEntity: Identifiable, AppEntity { } public struct DefaultAppAccountEntityQuery: EntityQuery { - public init() { } - + public init() {} + public func entities(for identifiers: [AppAccountEntity.ID]) async throws -> [AppAccountEntity] { return await AppAccountsManager.shared.availableAccounts.filter { account in identifiers.contains { id in diff --git a/IceCubesAppIntents/TimelineFilterEntity.swift b/IceCubesAppIntents/TimelineFilterEntity.swift index 9a1c348a..97512cc8 100644 --- a/IceCubesAppIntents/TimelineFilterEntity.swift +++ b/IceCubesAppIntents/TimelineFilterEntity.swift @@ -1,5 +1,5 @@ -import AppIntents import AppAccount +import AppIntents import Env import Foundation import Models @@ -21,14 +21,14 @@ public struct TimelineFilterEntity: Identifiable, AppEntity { } public struct DefaultTimelineEntityQuery: EntityQuery { - public init() { } - - public func entities(for identifiers: [TimelineFilter.ID]) async throws -> [TimelineFilterEntity] { - [.home, .trending, .federated, .local].map{ .init(timeline: $0) } + public init() {} + + public func entities(for _: [TimelineFilter.ID]) async throws -> [TimelineFilterEntity] { + [.home, .trending, .federated, .local].map { .init(timeline: $0) } } public func suggestedEntities() async throws -> [TimelineFilterEntity] { - [.home, .trending, .federated, .local].map{ .init(timeline: $0) } + [.home, .trending, .federated, .local].map { .init(timeline: $0) } } public func defaultResult() async -> TimelineFilterEntity? { diff --git a/IceCubesAppWidgetsExtension/HashtagPostsWidget/HashtagPostsWidget.swift b/IceCubesAppWidgetsExtension/HashtagPostsWidget/HashtagPostsWidget.swift index 08478b6c..82b020d6 100644 --- a/IceCubesAppWidgetsExtension/HashtagPostsWidget/HashtagPostsWidget.swift +++ b/IceCubesAppWidgetsExtension/HashtagPostsWidget/HashtagPostsWidget.swift @@ -1,18 +1,18 @@ -import WidgetKit -import SwiftUI -import Network import DesignSystem import Models +import Network +import SwiftUI import Timeline +import WidgetKit struct HashtagPostsWidgetProvider: AppIntentTimelineProvider { - func placeholder(in context: Context) -> PostsWidgetEntry { + func placeholder(in _: Context) -> PostsWidgetEntry { .init(date: Date(), title: "#Mastodon", statuses: [.placeholder()], images: [:]) } - + func snapshot(for configuration: HashtagPostsWidgetConfiguration, in context: Context) async -> PostsWidgetEntry { if let entry = await timeline(for: configuration, context: context).entries.first { return entry @@ -22,39 +22,40 @@ struct HashtagPostsWidgetProvider: AppIntentTimelineProvider { statuses: [], images: [:]) } - + func timeline(for configuration: HashtagPostsWidgetConfiguration, in context: Context) async -> Timeline { await timeline(for: configuration, context: context) } - + private func timeline(for configuration: HashtagPostsWidgetConfiguration, context: Context) async -> Timeline { do { let timeline: TimelineFilter = .hashtag(tag: configuration.hashgtag, accountId: nil) let statuses = await loadStatuses(for: timeline, account: configuration.account, widgetFamily: context.family) - let images = try await loadImages(urls: statuses.map{ $0.account.avatar } ) + let images = try await loadImages(urls: statuses.map { $0.account.avatar }) return Timeline(entries: [.init(date: Date(), - title: timeline.title, - statuses: statuses, - images: images)], policy: .atEnd) + title: timeline.title, + statuses: statuses, + images: images)], policy: .atEnd) } catch { return Timeline(entries: [.init(date: Date(), title: "#Mastodon", statuses: [], images: [:])], - policy: .atEnd) + policy: .atEnd) } } } struct HashtagPostsWidget: Widget { let kind: String = "HashtagPostsWidget" - + var body: some WidgetConfiguration { AppIntentConfiguration(kind: kind, intent: HashtagPostsWidgetConfiguration.self, - provider: HashtagPostsWidgetProvider()) { entry in + provider: HashtagPostsWidgetProvider()) + { entry in PostsWidgetView(entry: entry) .containerBackground(Color("WidgetBackground").gradient, for: .widget) } @@ -64,12 +65,11 @@ struct HashtagPostsWidget: Widget { } } - #Preview(as: .systemMedium) { HashtagPostsWidget() } timeline: { PostsWidgetEntry(date: .now, - title: "#Mastodon", - statuses: [.placeholder(), .placeholder(), .placeholder(), .placeholder()], - images: [:]) + title: "#Mastodon", + statuses: [.placeholder(), .placeholder(), .placeholder(), .placeholder()], + images: [:]) } diff --git a/IceCubesAppWidgetsExtension/HashtagPostsWidget/HashtagPostsWidgetConfiguration.swift b/IceCubesAppWidgetsExtension/HashtagPostsWidget/HashtagPostsWidgetConfiguration.swift index d638eb39..8aa70af9 100644 --- a/IceCubesAppWidgetsExtension/HashtagPostsWidget/HashtagPostsWidgetConfiguration.swift +++ b/IceCubesAppWidgetsExtension/HashtagPostsWidget/HashtagPostsWidgetConfiguration.swift @@ -1,13 +1,13 @@ -import WidgetKit import AppIntents +import WidgetKit struct HashtagPostsWidgetConfiguration: WidgetConfigurationIntent { static let title: LocalizedStringResource = "Configuration" static let description = IntentDescription("Choose the account and hashtag for this widget") - + @Parameter(title: "Account") var account: AppAccountEntity - + @Parameter(title: "Hashtag") var hashgtag: String } diff --git a/IceCubesAppWidgetsExtension/IceCubesAppWidgetsExtensionBundle.swift b/IceCubesAppWidgetsExtension/IceCubesAppWidgetsExtensionBundle.swift index ba5f7cea..f4684eb0 100644 --- a/IceCubesAppWidgetsExtension/IceCubesAppWidgetsExtensionBundle.swift +++ b/IceCubesAppWidgetsExtension/IceCubesAppWidgetsExtensionBundle.swift @@ -1,5 +1,5 @@ -import WidgetKit import SwiftUI +import WidgetKit @main struct IceCubesAppWidgetsExtensionBundle: WidgetBundle { diff --git a/IceCubesAppWidgetsExtension/LatestPostsWidget/LatestPostsWidget.swift b/IceCubesAppWidgetsExtension/LatestPostsWidget/LatestPostsWidget.swift index a6484b1c..a79111b5 100644 --- a/IceCubesAppWidgetsExtension/LatestPostsWidget/LatestPostsWidget.swift +++ b/IceCubesAppWidgetsExtension/LatestPostsWidget/LatestPostsWidget.swift @@ -1,18 +1,18 @@ -import WidgetKit -import SwiftUI -import Network import DesignSystem import Models +import Network +import SwiftUI import Timeline +import WidgetKit struct LatestPostsWidgetProvider: AppIntentTimelineProvider { - func placeholder(in context: Context) -> PostsWidgetEntry { + func placeholder(in _: Context) -> PostsWidgetEntry { .init(date: Date(), title: "Home", statuses: [.placeholder()], images: [:]) } - + func snapshot(for configuration: LatestPostsWidgetConfiguration, in context: Context) async -> PostsWidgetEntry { if let entry = await timeline(for: configuration, context: context).entries.first { return entry @@ -22,30 +22,30 @@ struct LatestPostsWidgetProvider: AppIntentTimelineProvider { statuses: [], images: [:]) } - + func timeline(for configuration: LatestPostsWidgetConfiguration, in context: Context) async -> Timeline { await timeline(for: configuration, context: context) } - + private func timeline(for configuration: LatestPostsWidgetConfiguration, context: Context) async -> Timeline { do { let statuses = await loadStatuses(for: configuration.timeline.timeline, account: configuration.account, widgetFamily: context.family) - let images = try await loadImages(urls: statuses.map{ $0.account.avatar } ) + let images = try await loadImages(urls: statuses.map { $0.account.avatar }) return Timeline(entries: [.init(date: Date(), title: configuration.timeline.timeline.title, - statuses: statuses, - images: images)], policy: .atEnd) + statuses: statuses, + images: images)], policy: .atEnd) } catch { return Timeline(entries: [.init(date: Date(), title: configuration.timeline.timeline.title, statuses: [], images: [:])], - policy: .atEnd) + policy: .atEnd) } } - + private func loadImages(urls: [URL]) async throws -> [URL: UIImage] { try await withThrowingTaskGroup(of: (URL, UIImage?).self) { group in for url in urls { @@ -54,13 +54,13 @@ struct LatestPostsWidgetProvider: AppIntentTimelineProvider { return (url, UIImage(data: response.0)) } } - + var images: [URL: UIImage] = [:] - + for try await (url, image) in group { images[url] = image } - + return images } } @@ -68,11 +68,12 @@ struct LatestPostsWidgetProvider: AppIntentTimelineProvider { struct LatestPostsWidget: Widget { let kind: String = "LatestPostsWidget" - + var body: some WidgetConfiguration { AppIntentConfiguration(kind: kind, intent: LatestPostsWidgetConfiguration.self, - provider: LatestPostsWidgetProvider()) { entry in + provider: LatestPostsWidgetProvider()) + { entry in PostsWidgetView(entry: entry) .containerBackground(Color("WidgetBackground").gradient, for: .widget) } @@ -82,11 +83,10 @@ struct LatestPostsWidget: Widget { } } - #Preview(as: .systemMedium) { LatestPostsWidget() } timeline: { - PostsWidgetEntry(date: .now, + PostsWidgetEntry(date: .now, title: "Mastodon", statuses: [.placeholder(), .placeholder(), .placeholder(), .placeholder()], images: [:]) diff --git a/IceCubesAppWidgetsExtension/LatestPostsWidget/LatestPostsWidgetConfiguration.swift b/IceCubesAppWidgetsExtension/LatestPostsWidget/LatestPostsWidgetConfiguration.swift index b92cbb81..11453c01 100644 --- a/IceCubesAppWidgetsExtension/LatestPostsWidget/LatestPostsWidgetConfiguration.swift +++ b/IceCubesAppWidgetsExtension/LatestPostsWidget/LatestPostsWidgetConfiguration.swift @@ -1,13 +1,13 @@ -import WidgetKit import AppIntents +import WidgetKit struct LatestPostsWidgetConfiguration: WidgetConfigurationIntent { static let title: LocalizedStringResource = "Configuration" static let description = IntentDescription("Choose the account and timeline for this widget") - + @Parameter(title: "Account") var account: AppAccountEntity - + @Parameter(title: "Timeline") var timeline: TimelineFilterEntity } diff --git a/IceCubesAppWidgetsExtension/MentionWidget/MentionWidget.swift b/IceCubesAppWidgetsExtension/MentionWidget/MentionWidget.swift index 046dc74c..421044c0 100644 --- a/IceCubesAppWidgetsExtension/MentionWidget/MentionWidget.swift +++ b/IceCubesAppWidgetsExtension/MentionWidget/MentionWidget.swift @@ -1,18 +1,18 @@ -import WidgetKit -import SwiftUI -import Network import DesignSystem import Models +import Network +import SwiftUI import Timeline +import WidgetKit struct MentionsWidgetProvider: AppIntentTimelineProvider { - func placeholder(in context: Context) -> PostsWidgetEntry { + func placeholder(in _: Context) -> PostsWidgetEntry { .init(date: Date(), title: "Mentions", statuses: [.placeholder()], images: [:]) } - + func snapshot(for configuration: MentionsWidgetConfiguration, in context: Context) async -> PostsWidgetEntry { if let entry = await timeline(for: configuration, context: context).entries.first { return entry @@ -22,45 +22,46 @@ struct MentionsWidgetProvider: AppIntentTimelineProvider { statuses: [], images: [:]) } - + func timeline(for configuration: MentionsWidgetConfiguration, in context: Context) async -> Timeline { await timeline(for: configuration, context: context) } - - private func timeline(for configuration: MentionsWidgetConfiguration, context: Context) async -> Timeline { + + private func timeline(for configuration: MentionsWidgetConfiguration, context _: Context) async -> Timeline { do { let client = Client(server: configuration.account.account.server, oauthToken: configuration.account.account.oauthToken) var excludedTypes = Models.Notification.NotificationType.allCases excludedTypes.removeAll(where: { $0 == .mention }) var notifications: [Models.Notification] = - try await client.get(endpoint: Notifications.notifications(minId: nil, - maxId: nil, - types: excludedTypes.map(\.rawValue), - limit: 5)) - let statuses = notifications.compactMap{ $0.status } - let images = try await loadImages(urls: statuses.map{ $0.account.avatar } ) + try await client.get(endpoint: Notifications.notifications(minId: nil, + maxId: nil, + types: excludedTypes.map(\.rawValue), + limit: 5)) + let statuses = notifications.compactMap { $0.status } + let images = try await loadImages(urls: statuses.map { $0.account.avatar }) return Timeline(entries: [.init(date: Date(), - title: "Mentions", - statuses: statuses, - images: images)], policy: .atEnd) + title: "Mentions", + statuses: statuses, + images: images)], policy: .atEnd) } catch { return Timeline(entries: [.init(date: Date(), title: "Mentions", statuses: [], images: [:])], - policy: .atEnd) + policy: .atEnd) } } } struct MentionsWidget: Widget { let kind: String = "MentionsWidget" - + var body: some WidgetConfiguration { AppIntentConfiguration(kind: kind, intent: MentionsWidgetConfiguration.self, - provider: MentionsWidgetProvider()) { entry in + provider: MentionsWidgetProvider()) + { entry in PostsWidgetView(entry: entry) .containerBackground(Color("WidgetBackground").gradient, for: .widget) } @@ -70,7 +71,6 @@ struct MentionsWidget: Widget { } } - #Preview(as: .systemMedium) { MentionsWidget() } timeline: { diff --git a/IceCubesAppWidgetsExtension/MentionWidget/MentionWidgetConfiguration.swift b/IceCubesAppWidgetsExtension/MentionWidget/MentionWidgetConfiguration.swift index 7ad55a49..0f0fc81b 100644 --- a/IceCubesAppWidgetsExtension/MentionWidget/MentionWidgetConfiguration.swift +++ b/IceCubesAppWidgetsExtension/MentionWidget/MentionWidgetConfiguration.swift @@ -1,10 +1,10 @@ -import WidgetKit import AppIntents +import WidgetKit struct MentionsWidgetConfiguration: WidgetConfigurationIntent { static let title: LocalizedStringResource = "Configuration" static let description = IntentDescription("Choose the account for this widget") - + @Parameter(title: "Account") var account: AppAccountEntity } diff --git a/IceCubesAppWidgetsExtension/Shared/PostsWidgetView.swift b/IceCubesAppWidgetsExtension/Shared/PostsWidgetView.swift index 7b6c6821..eeeeccf0 100644 --- a/IceCubesAppWidgetsExtension/Shared/PostsWidgetView.swift +++ b/IceCubesAppWidgetsExtension/Shared/PostsWidgetView.swift @@ -1,9 +1,9 @@ -import WidgetKit -import SwiftUI -import Network import DesignSystem import Models +import Network +import SwiftUI import Timeline +import WidgetKit struct PostsWidgetEntry: TimelineEntry { let date: Date @@ -12,12 +12,12 @@ struct PostsWidgetEntry: TimelineEntry { let images: [URL: UIImage] } -struct PostsWidgetView : View { +struct PostsWidgetView: View { var entry: LatestPostsWidgetProvider.Entry - + @Environment(\.widgetFamily) var family @Environment(\.redactionReasons) var redacted - + var contentLineLimit: Int { switch family { case .systemSmall, .systemMedium: @@ -26,6 +26,7 @@ struct PostsWidgetView : View { return 2 } } + var body: some View { VStack(alignment: .leading, spacing: 8) { headerView @@ -36,7 +37,7 @@ struct PostsWidgetView : View { } .frame(maxWidth: .infinity) } - + private var headerView: some View { HStack { Text(entry.title) @@ -47,7 +48,7 @@ struct PostsWidgetView : View { .fontWeight(.bold) .foregroundStyle(Color("AccentColor")) } - + @ViewBuilder private func makeStatusView(_ status: Status) -> some View { if let url = URL(string: status.url ?? "") { @@ -63,7 +64,7 @@ struct PostsWidgetView : View { }) } } - + private func makeStatusHeaderView(_ status: Status) -> some View { HStack(alignment: .center, spacing: 4) { if let image = entry.images[status.account.avatar] { diff --git a/IceCubesAppWidgetsExtension/Shared/SharedUtils.swift b/IceCubesAppWidgetsExtension/Shared/SharedUtils.swift index db1aca41..b8c76f44 100644 --- a/IceCubesAppWidgetsExtension/Shared/SharedUtils.swift +++ b/IceCubesAppWidgetsExtension/Shared/SharedUtils.swift @@ -1,30 +1,31 @@ -import StatusKit -import WidgetKit -import Timeline -import Foundation -import UIKit import AppAccount +import Foundation import Models import Network +import StatusKit +import Timeline +import UIKit +import WidgetKit func loadStatuses(for timeline: TimelineFilter, account: AppAccountEntity, - widgetFamily: WidgetFamily) async -> [Status] { + widgetFamily: WidgetFamily) async -> [Status] +{ let client = Client(server: account.account.server, oauthToken: account.account.oauthToken) do { var statuses: [Status] = try await client.get(endpoint: timeline.endpoint(sinceId: nil, maxId: nil, minId: nil, offset: nil)) - statuses = statuses.filter{ $0.reblog == nil && !$0.content.asRawText.isEmpty } + statuses = statuses.filter { $0.reblog == nil && !$0.content.asRawText.isEmpty } switch widgetFamily { case .systemSmall, .systemMedium: if statuses.count >= 1 { - statuses = statuses.prefix(upTo: 1).map{ $0 } + statuses = statuses.prefix(upTo: 1).map { $0 } } case .systemLarge, .systemExtraLarge: if statuses.count >= 5 { - statuses = statuses.prefix(upTo: 5).map{ $0 } + statuses = statuses.prefix(upTo: 5).map { $0 } } default: break @@ -34,7 +35,7 @@ func loadStatuses(for timeline: TimelineFilter, return [] } } - + func loadImages(urls: [URL]) async throws -> [URL: UIImage] { try await withThrowingTaskGroup(of: (URL, UIImage?).self) { group in for url in urls { @@ -43,13 +44,13 @@ func loadImages(urls: [URL]) async throws -> [URL: UIImage] { return (url, UIImage(data: response.0)) } } - + var images: [URL: UIImage] = [:] - + for try await (url, image) in group { images[url] = image } - + return images } } diff --git a/Packages/StatusKit/Sources/StatusKit/Editor/ViewModel.swift b/Packages/StatusKit/Sources/StatusKit/Editor/ViewModel.swift index 3eb630ec..9d443b10 100644 --- a/Packages/StatusKit/Sources/StatusKit/Editor/ViewModel.swift +++ b/Packages/StatusKit/Sources/StatusKit/Editor/ViewModel.swift @@ -765,7 +765,7 @@ public extension StatusEditor { error: nil )) } - + url.stopAccessingSecurityScopedResource() } From 1d798325444f1e9924edf8cd52e8b6165a51015d Mon Sep 17 00:00:00 2001 From: Thomas Ricouard Date: Mon, 6 May 2024 08:41:33 +0200 Subject: [PATCH 17/20] Bump version to 1.10.38 --- IceCubesApp.xcodeproj/project.pbxproj | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/IceCubesApp.xcodeproj/project.pbxproj b/IceCubesApp.xcodeproj/project.pbxproj index c2ec3166..a3d89d16 100644 --- a/IceCubesApp.xcodeproj/project.pbxproj +++ b/IceCubesApp.xcodeproj/project.pbxproj @@ -1157,7 +1157,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.10.37; + MARKETING_VERSION = 1.10.38; PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesNotifications"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -1192,7 +1192,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.10.37; + MARKETING_VERSION = 1.10.38; PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesNotifications"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -1300,7 +1300,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.10.37; + MARKETING_VERSION = 1.10.38; PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesShareExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -1334,7 +1334,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.10.37; + MARKETING_VERSION = 1.10.38; PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesShareExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -1515,7 +1515,7 @@ LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 1.10.37; + MARKETING_VERSION = 1.10.38; PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp"; PRODUCT_NAME = "Ice Cubes"; SDKROOT = auto; @@ -1570,7 +1570,7 @@ LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 1.10.37; + MARKETING_VERSION = 1.10.38; PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp"; PRODUCT_NAME = "Ice Cubes"; SDKROOT = auto; @@ -1605,7 +1605,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.10.37; + MARKETING_VERSION = 1.10.38; PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesActionExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -1640,7 +1640,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.10.37; + MARKETING_VERSION = 1.10.38; PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesActionExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; From c29de44d8ca1c7250a824dba923cc6d12078c527 Mon Sep 17 00:00:00 2001 From: Thomas Ricouard Date: Mon, 6 May 2024 09:20:01 +0200 Subject: [PATCH 18/20] Widget: Mentions only allow large size --- IceCubesAppWidgetsExtension/MentionWidget/MentionWidget.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IceCubesAppWidgetsExtension/MentionWidget/MentionWidget.swift b/IceCubesAppWidgetsExtension/MentionWidget/MentionWidget.swift index 421044c0..8612cbec 100644 --- a/IceCubesAppWidgetsExtension/MentionWidget/MentionWidget.swift +++ b/IceCubesAppWidgetsExtension/MentionWidget/MentionWidget.swift @@ -67,7 +67,7 @@ struct MentionsWidget: Widget { } .configurationDisplayName("Mentions") .description("Show the latest mentions for the selected account.") - .supportedFamilies([.systemSmall, .systemMedium, .systemLarge, .systemExtraLarge]) + .supportedFamilies([.systemLarge, .systemExtraLarge]) } } From 51fecb01f59ef45a0872e8efd5cd92beec2ac566 Mon Sep 17 00:00:00 2001 From: Thomas Ricouard Date: Mon, 6 May 2024 09:25:36 +0200 Subject: [PATCH 19/20] Bump version to 1.10.39 --- IceCubesApp.xcodeproj/project.pbxproj | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/IceCubesApp.xcodeproj/project.pbxproj b/IceCubesApp.xcodeproj/project.pbxproj index a3d89d16..e4717abf 100644 --- a/IceCubesApp.xcodeproj/project.pbxproj +++ b/IceCubesApp.xcodeproj/project.pbxproj @@ -1157,7 +1157,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.10.38; + MARKETING_VERSION = 1.10.39; PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesNotifications"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -1192,7 +1192,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.10.38; + MARKETING_VERSION = 1.10.39; PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesNotifications"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -1300,7 +1300,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.10.38; + MARKETING_VERSION = 1.10.39; PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesShareExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -1334,7 +1334,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.10.38; + MARKETING_VERSION = 1.10.39; PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesShareExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -1515,7 +1515,7 @@ LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 1.10.38; + MARKETING_VERSION = 1.10.39; PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp"; PRODUCT_NAME = "Ice Cubes"; SDKROOT = auto; @@ -1570,7 +1570,7 @@ LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 1.10.38; + MARKETING_VERSION = 1.10.39; PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp"; PRODUCT_NAME = "Ice Cubes"; SDKROOT = auto; @@ -1605,7 +1605,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.10.38; + MARKETING_VERSION = 1.10.39; PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesActionExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -1640,7 +1640,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.10.38; + MARKETING_VERSION = 1.10.39; PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesActionExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; From ccad00a0942d0e90bd2011de6919d6dca5bcd776 Mon Sep 17 00:00:00 2001 From: Jerry Zhang Date: Wed, 8 May 2024 16:32:26 +0800 Subject: [PATCH 20/20] Update Simplified Chinese localization (#2052) * Update Simplified Chinese localization * Fix --------- Co-authored-by: Thomas Ricouard --- .../Localization/Localizable.xcstrings | 430 +++++++++++++----- 1 file changed, 305 insertions(+), 125 deletions(-) diff --git a/IceCubesApp/Resources/Localization/Localizable.xcstrings b/IceCubesApp/Resources/Localization/Localizable.xcstrings index 0839af4e..638421fd 100644 --- a/IceCubesApp/Resources/Localization/Localizable.xcstrings +++ b/IceCubesApp/Resources/Localization/Localizable.xcstrings @@ -7545,7 +7545,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "已静音" + "value" : "已免打扰" } }, "zh-Hant" : { @@ -11583,8 +11583,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Blocked accounts" + "state" : "translated", + "value" : "已屏蔽的用户" } }, "zh-Hant" : { @@ -17427,8 +17427,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Muted accounts" + "state" : "translated", + "value" : "已免打扰的账户" } }, "zh-Hant" : { @@ -18391,126 +18391,6 @@ } } }, - "action.cancel" : { - "comment" : "MARK: Common strings", - "extractionState" : "stale", - "localizations" : { - "be" : { - "stringUnit" : { - "state" : "translated", - "value" : "Скасаваць" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cancel·la" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Abbrechen" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cancel" - } - }, - "en-GB" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cancel" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cancelar" - } - }, - "eu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Utzi" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Annuler" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Annulla" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "キャンセル" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "취소" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Avbryt" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Annuleer" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Anuluj" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cancelar" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "İptal Et" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Відміна" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "取消" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "取消" - } - } - } - }, "action.delete" : { "extractionState" : "manual", "localizations" : { @@ -20536,6 +20416,12 @@ "state" : "translated", "value" : "Lesezeichen" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "书签" + } } } }, @@ -26464,6 +26350,12 @@ "state" : "translated", "value" : "Erkunden & Trends" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "探索 & 当下流行" + } } } }, @@ -28498,6 +28390,12 @@ "state" : "translated", "value" : "Favoriten" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "喜欢" + } } } }, @@ -28508,6 +28406,12 @@ "state" : "translated", "value" : "Föderierte Timeline" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "跨站时间线" + } } } }, @@ -30780,6 +30684,12 @@ "state" : "translated", "value" : "Gefolgte Tags" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "正在关注的标签" + } } } }, @@ -30793,6 +30703,12 @@ "state" : "translated", "value" : "Eigene Timeline" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "主页时间线" + } } } }, @@ -30803,6 +30719,12 @@ "state" : "translated", "value" : "Bild" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "图片" + } } } }, @@ -30813,6 +30735,12 @@ "state" : "translated", "value" : "Bild zum Veröffentlichen auf Mastodon" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "向 Mastodon 发布的图片" + } } } }, @@ -33072,6 +33000,12 @@ "state" : "translated", "value" : "Listen" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "列表" + } } } }, @@ -34035,6 +33969,12 @@ "state" : "translated", "value" : "Lokale Timeline" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "本地时间线" + } } } }, @@ -34045,6 +33985,12 @@ "state" : "translated", "value" : "Erwähnungen" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "提及" + } } } }, @@ -34764,6 +34710,12 @@ "state" : "translated", "value" : "Neuer Beitrag" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "新嘟文" + } } } }, @@ -34774,6 +34726,12 @@ "state" : "translated", "value" : "Benachrichtigungen" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "通知" + } } } }, @@ -39865,6 +39823,12 @@ "state" : "translated", "value" : "Ice Cubes öffnen" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "打开 Ice Cubes" + } } } }, @@ -39875,6 +39839,12 @@ "state" : "translated", "value" : "Mit einem Tab öffnen" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "打开标签页" + } } } }, @@ -39885,6 +39855,12 @@ "state" : "translated", "value" : "Die App in einem vorgegebenen Tab öffnen" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "在特定标签页上打开应用" + } } } }, @@ -40132,6 +40108,12 @@ "state" : "translated", "value" : "Einen Status veröffentlichen" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "发布状态" + } } } }, @@ -40145,6 +40127,12 @@ "state" : "translated", "value" : "Ein Bild auf Mastodon veröffentlichen" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "向 Mastodon 发布图片" + } } } }, @@ -40155,6 +40143,12 @@ "state" : "translated", "value" : "Inhalt veröffentlichen" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "发布内容" + } } } }, @@ -40166,6 +40160,12 @@ "state" : "translated", "value" : "Bilder veröffentlichen" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "发布图片" + } } } }, @@ -40177,6 +40177,12 @@ "state" : "translated", "value" : "Status auf Mastodon veröffentlichen" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "向 Mastodon 发布状态" + } } } }, @@ -40193,6 +40199,12 @@ "state" : "translated", "value" : "Private Nachrichten" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "私信" + } } } }, @@ -40203,6 +40215,12 @@ "state" : "translated", "value" : "Profil" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "个人资料" + } } } }, @@ -40693,6 +40711,12 @@ "state" : "translated", "value" : "Ausgewählter Tab" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "选择标签页" + } } } }, @@ -40712,6 +40736,12 @@ "state" : "translated", "value" : "Einstellungen" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "设置" + } } } }, @@ -74897,6 +74927,12 @@ "state" : "translated", "value" : "Tab" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "标签页" + } } } }, @@ -80432,6 +80468,12 @@ "state" : "translated", "value" : "Links im Trend" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "当下流行的链接" + } } } }, @@ -80442,6 +80484,12 @@ "state" : "translated", "value" : "Timeline im Trend" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "当下流行时间线" + } } } }, @@ -80818,6 +80866,12 @@ "state" : "translated", "value" : "Ice Cubes benutzen, um einen Status auf Mastodon zu veröffentlichen" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "使用 Ice Cubes 向 Mastodon 发布状态" + } } } }, @@ -80829,6 +80883,132 @@ "state" : "translated", "value" : "Ice Cubes benutzen, um einen Status mit einem Bild auf Mastodon zu veröffentlichen" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "使用 Ice Cubes 向 Mastodon 发布带有图片的状态" + } + } + } + }, + "action.cancel" : { + "comment" : "MARK: Common strings", + "extractionState" : "stale", + "localizations" : { + "be" : { + "stringUnit" : { + "state" : "translated", + "value" : "Скасаваць" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cancel·la" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abbrechen" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cancel" + } + }, + "en-GB" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cancel" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cancelar" + } + }, + "eu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Utzi" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Annuler" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Annulla" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "キャンセル" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "취소" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Avbryt" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Annuleer" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Anuluj" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cancelar" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "İptal Et" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Відміна" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "取消" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "取消" + } } } },