From f40aeb9cac5d8da40da7e7cf8ec78e830b871d79 Mon Sep 17 00:00:00 2001 From: Thomas Ricouard Date: Fri, 17 May 2024 14:22:00 +0200 Subject: [PATCH] Add followers count widget --- IceCubesApp.xcodeproj/project.pbxproj | 20 ++++++ .../AccountWidget/AccountWidget.swift | 64 +++++++++++++++++++ .../AccountWidgetConfiguration.swift | 18 ++++++ .../AccountWidget/AccountWidgetView.swift | 34 ++++++++++ .../IceCubesAppWidgetsExtensionBundle.swift | 1 + 5 files changed, 137 insertions(+) create mode 100644 IceCubesAppWidgetsExtension/AccountWidget/AccountWidget.swift create mode 100644 IceCubesAppWidgetsExtension/AccountWidget/AccountWidgetConfiguration.swift create mode 100644 IceCubesAppWidgetsExtension/AccountWidget/AccountWidgetView.swift diff --git a/IceCubesApp.xcodeproj/project.pbxproj b/IceCubesApp.xcodeproj/project.pbxproj index 955cf53c..1e9acdfb 100644 --- a/IceCubesApp.xcodeproj/project.pbxproj +++ b/IceCubesApp.xcodeproj/project.pbxproj @@ -89,6 +89,9 @@ 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 */; }; + 9F8B92122BF77DBE003D37A2 /* AccountWidgetConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F8B920E2BF77DB4003D37A2 /* AccountWidgetConfiguration.swift */; }; + 9F8B92132BF77DBE003D37A2 /* AccountWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F8B92102BF77DBB003D37A2 /* AccountWidget.swift */; }; + 9F8B92162BF77F0B003D37A2 /* AccountWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F8B92142BF77F05003D37A2 /* AccountWidgetView.swift */; }; 9FA6FD6229C04A8800E2312C /* TranslationSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA6FD6129C04A8800E2312C /* TranslationSettingsView.swift */; }; 9FAD858B29743F7400496AB1 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FAD858A29743F7400496AB1 /* ShareViewController.swift */; }; 9FAD858E29743F7400496AB1 /* (null) in Resources */ = {isa = PBXBuildFile; }; @@ -275,6 +278,9 @@ 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 = ""; }; + 9F8B920E2BF77DB4003D37A2 /* AccountWidgetConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountWidgetConfiguration.swift; sourceTree = ""; }; + 9F8B92102BF77DBB003D37A2 /* AccountWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountWidget.swift; sourceTree = ""; }; + 9F8B92142BF77F05003D37A2 /* AccountWidgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountWidgetView.swift; sourceTree = ""; }; 9FA6FD6129C04A8800E2312C /* TranslationSettingsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TranslationSettingsView.swift; sourceTree = ""; }; 9FAD858829743F7400496AB1 /* IceCubesShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = IceCubesShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 9FAD858A29743F7400496AB1 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = ""; }; @@ -497,6 +503,7 @@ 9F7788CA2BE652B1004E6BEF /* IceCubesAppWidgetsExtension */ = { isa = PBXGroup; children = ( + 9F8B920D2BF77DA8003D37A2 /* AccountWidget */, 9F5BE6202BF48FB20074387E /* ListsWidget */, 9FF2FB6B2BE8AE78001560CE /* MentionWidget */, 9FF2FB642BE7F7FA001560CE /* Shared */, @@ -510,6 +517,16 @@ path = IceCubesAppWidgetsExtension; sourceTree = ""; }; + 9F8B920D2BF77DA8003D37A2 /* AccountWidget */ = { + isa = PBXGroup; + children = ( + 9F8B920E2BF77DB4003D37A2 /* AccountWidgetConfiguration.swift */, + 9F8B92102BF77DBB003D37A2 /* AccountWidget.swift */, + 9F8B92142BF77F05003D37A2 /* AccountWidgetView.swift */, + ); + path = AccountWidget; + sourceTree = ""; + }; 9FA0D2AC29921C1F008A143B /* Embeds */ = { isa = PBXGroup; children = ( @@ -1011,6 +1028,9 @@ 9F5BE6272BF492CF0074387E /* ListEntity.swift in Sources */, 9FF2FB622BE7F5D5001560CE /* HashtagPostsWidget.swift in Sources */, 9FF2FB712BE8AEA0001560CE /* MentionWidget.swift in Sources */, + 9F8B92162BF77F0B003D37A2 /* AccountWidgetView.swift in Sources */, + 9F8B92122BF77DBE003D37A2 /* AccountWidgetConfiguration.swift in Sources */, + 9F8B92132BF77DBE003D37A2 /* AccountWidget.swift in Sources */, 9F7788EA2BE65585004E6BEF /* AppAccountEntity.swift in Sources */, 9FF2FB6A2BE7F84E001560CE /* SharedUtils.swift in Sources */, 9F7788CE2BE652B1004E6BEF /* LatestPostsWidget.swift in Sources */, diff --git a/IceCubesAppWidgetsExtension/AccountWidget/AccountWidget.swift b/IceCubesAppWidgetsExtension/AccountWidget/AccountWidget.swift new file mode 100644 index 00000000..315bb925 --- /dev/null +++ b/IceCubesAppWidgetsExtension/AccountWidget/AccountWidget.swift @@ -0,0 +1,64 @@ +import DesignSystem +import Models +import Network +import SwiftUI +import Timeline +import WidgetKit + +struct AccountWidgetProvider: AppIntentTimelineProvider { + func placeholder(in _: Context) -> AccountWidgetEntry { + .init(date: Date(), account: .placeholder(), avatar: nil) + } + + func snapshot(for configuration: AccountWidgetConfiguration, in context: Context) async -> AccountWidgetEntry { + let account = await fetchAccount(configuration: configuration) + return .init(date: Date(), account: account, avatar: nil) + } + + func timeline(for configuration: AccountWidgetConfiguration, in context: Context) async -> Timeline { + let account = await fetchAccount(configuration: configuration) + let images = try? await loadImages(urls: [account.avatar]) + return .init(entries: [.init(date: Date(), account: account, avatar: images?.first?.value)], + policy: .atEnd) + } + + private func fetchAccount(configuration: AccountWidgetConfiguration) async -> Account { + let client = Client(server: configuration.account.account.server, + oauthToken: configuration.account.account.oauthToken) + do { + let account: Account = try await client.get(endpoint: Accounts.verifyCredentials) + return account + } catch { + return .placeholder() + } + } +} + +struct AccountWidgetEntry: TimelineEntry { + let date: Date + let account: Account + let avatar: UIImage? +} + +struct AccountWidget: Widget { + let kind: String = "AccountWidget" + + var body: some WidgetConfiguration { + AppIntentConfiguration(kind: kind, + intent: AccountWidgetConfiguration.self, + provider: AccountWidgetProvider()) + { entry in + AccountWidgetView(entry: entry) + .containerBackground(Color("WidgetBackground").gradient, for: .widget) + } + .configurationDisplayName("Account") + .description("Show information about your Mastodon account") + .supportedFamilies([.systemSmall]) + } +} + +#Preview(as: .systemSmall) { + AccountWidget() +} timeline: { + AccountWidgetEntry(date: Date(), account: .placeholder(), avatar: nil) +} diff --git a/IceCubesAppWidgetsExtension/AccountWidget/AccountWidgetConfiguration.swift b/IceCubesAppWidgetsExtension/AccountWidget/AccountWidgetConfiguration.swift new file mode 100644 index 00000000..7a26422f --- /dev/null +++ b/IceCubesAppWidgetsExtension/AccountWidget/AccountWidgetConfiguration.swift @@ -0,0 +1,18 @@ +import AppIntents +import WidgetKit + +struct AccountWidgetConfiguration: WidgetConfigurationIntent { + static let title: LocalizedStringResource = "Configuration" + static let description = IntentDescription("Choose the account for this widget") + + @Parameter(title: "Account") + var account: AppAccountEntity +} + +extension AccountWidgetConfiguration { + static var previewAccount: AccountWidgetConfiguration { + let intent = AccountWidgetConfiguration() + intent.account = .init(account: .init(server: "Test", accountName: "Test account")) + return intent + } +} diff --git a/IceCubesAppWidgetsExtension/AccountWidget/AccountWidgetView.swift b/IceCubesAppWidgetsExtension/AccountWidget/AccountWidgetView.swift new file mode 100644 index 00000000..6ac66577 --- /dev/null +++ b/IceCubesAppWidgetsExtension/AccountWidget/AccountWidgetView.swift @@ -0,0 +1,34 @@ +import DesignSystem +import Models +import Network +import SwiftUI +import Timeline +import WidgetKit + +struct AccountWidgetView: View { + var entry: AccountWidgetProvider.Entry + + @Environment(\.widgetFamily) var family + @Environment(\.redactionReasons) var redacted + + + var body: some View { + VStack(alignment: .center, spacing: 4) { + if let avatar = entry.avatar { + Image(uiImage: avatar) + .resizable() + .frame(width: 64, height: 64) + .clipShape(Circle()) + Text("\(entry.account.followersCount ?? 0)") + .font(.title) + .fontDesign(.rounded) + .fontWeight(.bold) + .monospacedDigit() + Text("Followers") + .font(.headline) + .foregroundStyle(.secondary) + } + } + .frame(maxWidth: .infinity) + } +} diff --git a/IceCubesAppWidgetsExtension/IceCubesAppWidgetsExtensionBundle.swift b/IceCubesAppWidgetsExtension/IceCubesAppWidgetsExtensionBundle.swift index f51a57cf..a7f68693 100644 --- a/IceCubesAppWidgetsExtension/IceCubesAppWidgetsExtensionBundle.swift +++ b/IceCubesAppWidgetsExtension/IceCubesAppWidgetsExtensionBundle.swift @@ -8,5 +8,6 @@ struct IceCubesAppWidgetsExtensionBundle: WidgetBundle { HashtagPostsWidget() ListsPostWidget() MentionsWidget() + AccountWidget() } }