feat(Widget): Implement MultiFollowersCountWidget for single Column

This commit is contained in:
Marcus Kida 2023-01-31 14:37:49 +01:00
parent e05a8602d5
commit 9eb26d4ed8
No known key found for this signature in database
GPG Key ID: 19FF64E08013CA40
11 changed files with 326 additions and 94 deletions

View File

@ -53,6 +53,9 @@
2A72813F297EC762004138C5 /* WidgetExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A72813E297EC762004138C5 /* WidgetExtension.swift */; };
2A76F75C2930D94700B3388D /* HashtagTimelineHeaderViewActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A76F75B2930D94700B3388D /* HashtagTimelineHeaderViewActionButton.swift */; };
2A82294F29262EE000D2A1F7 /* AppContext+NextAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A82294E29262EE000D2A1F7 /* AppContext+NextAccount.swift */; };
2A86A14629892944007F1062 /* MultiFollowersCountIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A86A14529892944007F1062 /* MultiFollowersCountIntentHandler.swift */; };
2A86A14929892B3A007F1062 /* MultiFollowersCountWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A86A14829892B3A007F1062 /* MultiFollowersCountWidget.swift */; };
2A86A14B2989326E007F1062 /* MultiFollowersCountWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A86A14A2989326E007F1062 /* MultiFollowersCountWidgetView.swift */; };
2A90A157296EEE500026C155 /* MastodonSDKDynamic in Frameworks */ = {isa = PBXBuildFile; productRef = 2A90A156296EEE500026C155 /* MastodonSDKDynamic */; };
2AB12E4629362F27006BC925 /* DataSourceFacade+Translate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AB12E4529362F27006BC925 /* DataSourceFacade+Translate.swift */; };
2AE202AA297FE10B00F66E55 /* WidgetExtension.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 2A72812C297EA9D7004138C5 /* WidgetExtension.intentdefinition */; };
@ -642,6 +645,9 @@
2A72813E297EC762004138C5 /* WidgetExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetExtension.swift; sourceTree = "<group>"; };
2A76F75B2930D94700B3388D /* HashtagTimelineHeaderViewActionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagTimelineHeaderViewActionButton.swift; sourceTree = "<group>"; };
2A82294E29262EE000D2A1F7 /* AppContext+NextAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppContext+NextAccount.swift"; sourceTree = "<group>"; };
2A86A14529892944007F1062 /* MultiFollowersCountIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiFollowersCountIntentHandler.swift; sourceTree = "<group>"; };
2A86A14829892B3A007F1062 /* MultiFollowersCountWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiFollowersCountWidget.swift; sourceTree = "<group>"; };
2A86A14A2989326E007F1062 /* MultiFollowersCountWidgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiFollowersCountWidgetView.swift; sourceTree = "<group>"; };
2AB12E4529362F27006BC925 /* DataSourceFacade+Translate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Translate.swift"; sourceTree = "<group>"; };
2AE202A9297FDDF500F66E55 /* WidgetExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WidgetExtension.entitlements; sourceTree = "<group>"; };
2AE202AB297FE19100F66E55 /* FollowersCountIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowersCountIntentHandler.swift; sourceTree = "<group>"; };
@ -1440,7 +1446,6 @@
2AE202A9297FDDF500F66E55 /* WidgetExtension.entitlements */,
2A72813E297EC762004138C5 /* WidgetExtension.swift */,
2A728126297EA9D7004138C5 /* WidgetExtensionBundle.swift */,
2A33062C2987DBFA001D4C51 /* FollowersCountHistory.swift */,
2A72812C297EA9D7004138C5 /* WidgetExtension.intentdefinition */,
2A72812D297EA9D8004138C5 /* Assets.xcassets */,
2A72812F297EA9D8004138C5 /* Info.plist */,
@ -1452,6 +1457,7 @@
isa = PBXGroup;
children = (
2A86A14429892709007F1062 /* FollowersCount */,
2A86A14729892B1B007F1062 /* MultiFollowersCount */,
);
path = Variants;
sourceTree = "<group>";
@ -1459,12 +1465,22 @@
2A86A14429892709007F1062 /* FollowersCount */ = {
isa = PBXGroup;
children = (
2A33062C2987DBFA001D4C51 /* FollowersCountHistory.swift */,
2A72812A297EA9D7004138C5 /* FollowersCountWidget.swift */,
2A33AB652982C4AF008A7FB1 /* FollowersCountWidgetView.swift */,
);
path = FollowersCount;
sourceTree = "<group>";
};
2A86A14729892B1B007F1062 /* MultiFollowersCount */ = {
isa = PBXGroup;
children = (
2A86A14829892B3A007F1062 /* MultiFollowersCountWidget.swift */,
2A86A14A2989326E007F1062 /* MultiFollowersCountWidgetView.swift */,
);
path = MultiFollowersCount;
sourceTree = "<group>";
};
2D152A8A25C295B8009AA50C /* Content */ = {
isa = PBXGroup;
children = (
@ -2291,6 +2307,7 @@
children = (
DBB8AB4526AECDE200F6D281 /* SendPostIntentHandler.swift */,
2AE202AB297FE19100F66E55 /* FollowersCountIntentHandler.swift */,
2A86A14529892944007F1062 /* MultiFollowersCountIntentHandler.swift */,
);
path = Handler;
sourceTree = "<group>";
@ -3485,10 +3502,12 @@
2A33062D2987DBFA001D4C51 /* FollowersCountHistory.swift in Sources */,
2A33063829880835001D4C51 /* LineChart.swift in Sources */,
2A728130297EA9D8004138C5 /* WidgetExtension.intentdefinition in Sources */,
2A86A14B2989326E007F1062 /* MultiFollowersCountWidgetView.swift in Sources */,
2A72813F297EC762004138C5 /* WidgetExtension.swift in Sources */,
2A33063A29880835001D4C51 /* LightChart.swift in Sources */,
2A33063B29880835001D4C51 /* ChartType.swift in Sources */,
2A33063629880835001D4C51 /* Math.swift in Sources */,
2A86A14929892B3A007F1062 /* MultiFollowersCountWidget.swift in Sources */,
2A33AB662982C4AF008A7FB1 /* FollowersCountWidgetView.swift in Sources */,
2A728127297EA9D7004138C5 /* WidgetExtensionBundle.swift in Sources */,
2A72812B297EA9D7004138C5 /* FollowersCountWidget.swift in Sources */,
@ -3932,6 +3951,7 @@
2AE202AC297FE19100F66E55 /* FollowersCountIntentHandler.swift in Sources */,
DB64BA452851F23000ADF1B7 /* MastodonAuthentication+Fetch.swift in Sources */,
DB64BA482851F29300ADF1B7 /* Account+Fetch.swift in Sources */,
2A86A14629892944007F1062 /* MultiFollowersCountIntentHandler.swift in Sources */,
DB8FABCA26AEC7B2008E5AF4 /* IntentHandler.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;

View File

@ -62,6 +62,7 @@
<key>NSUserActivityTypes</key>
<array>
<string>FollowersCountIntent</string>
<string>MultiFollowersCountSmallIntent</string>
<string>SendPostIntent</string>
</array>
<key>UIApplicationSceneManifest</key>

View File

@ -30,13 +30,11 @@ class FollowersCountIntentHandler: INExtension, FollowersCountIntentHandling {
.apiService
.search(query: .init(q: searchTerm), authenticationBox: authenticationBox)
debugPrint(results.value.statuses)
return INObjectCollection(items: results.value.accounts.map { $0.acctWithDomain(localDomain: authenticationBox.domain) as NSString })
}
}
private extension Mastodon.Entity.Account {
extension Mastodon.Entity.Account {
func acctWithDomain(localDomain: String) -> String {
guard acct.contains("@") else {
return "\(acct)@\(localDomain)"

View File

@ -0,0 +1,27 @@
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
import Foundation
import Intents
import MastodonCore
import MastodonSDK
import MastodonLocalization
class MultiFollowersCountIntentHandler: INExtension, MultiFollowersCountSmallIntentHandling {
func provideAccountsOptionsCollection(for intent: MultiFollowersCountSmallIntent, searchTerm: String?) async throws -> INObjectCollection<NSString> {
guard
let searchTerm = searchTerm,
let authenticationBox = WidgetExtension.appContext
.authenticationService
.mastodonAuthenticationBoxes
.first
else {
return INObjectCollection(items: [])
}
let results = try await WidgetExtension.appContext
.apiService
.search(query: .init(q: searchTerm), authenticationBox: authenticationBox)
return INObjectCollection(items: results.value.accounts.map { $0.acctWithDomain(localDomain: authenticationBox.domain) as NSString })
}
}

View File

@ -31,6 +31,7 @@
<key>IntentsSupported</key>
<array>
<string>FollowersCountIntent</string>
<string>MultiFollowersCountSmallIntent</string>
<string>SendPostIntent</string>
</array>
</dict>

View File

@ -17,6 +17,8 @@ class IntentHandler: INExtension {
return SendPostIntentHandler()
case is FollowersCountIntent:
return FollowersCountIntentHandler()
case is MultiFollowersCountSmallIntent:
return MultiFollowersCountIntentHandler()
default:
return self
}

View File

@ -0,0 +1,150 @@
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
import WidgetKit
import SwiftUI
import Intents
import MastodonSDK
struct MultiFollowersCountWidgetProvider: IntentTimelineProvider {
func placeholder(in context: Context) -> MultiFollowersCountEntry {
.placeholder
}
func getSnapshot(for configuration: MultiFollowersCountSmallIntent, in context: Context, completion: @escaping (MultiFollowersCountEntry) -> ()) {
guard !context.isPreview else {
return completion(.placeholder)
}
loadCurrentEntry(for: configuration, in: context, completion: completion)
}
func getTimeline(for configuration: MultiFollowersCountSmallIntent, in context: Context, completion: @escaping (Timeline<MultiFollowersCountEntry>) -> ()) {
loadCurrentEntry(for: configuration, in: context) { entry in
completion(Timeline(entries: [entry], policy: .after(.now)))
}
}
}
struct MultiFollowersCountEntry: TimelineEntry {
let date: Date
let accounts: [FollowersEntryAccountable]?
let configuration: MultiFollowersCountSmallIntent
static var placeholder: Self {
MultiFollowersCountEntry(
date: .now,
accounts: [
FollowersEntryAccount(
followersCount: 99_900,
displayNameWithFallback: "Mastodon",
acct: "mastodon",
avatarImage: UIImage(named: "missingAvatar")!,
domain: "mastodon"
)
],
configuration: MultiFollowersCountSmallIntent()
)
}
static var unconfigured: Self {
MultiFollowersCountEntry(
date: .now,
accounts: [],
configuration: MultiFollowersCountSmallIntent()
)
}
}
struct MultiFollowersCountWidget: Widget {
private var availableFamilies: [WidgetFamily] {
return [.systemSmall]
}
var body: some WidgetConfiguration {
IntentConfiguration(kind: "Multiple followers", intent: MultiFollowersCountSmallIntent.self, provider: MultiFollowersCountWidgetProvider()) { entry in
MultiFollowersCountWidgetView(entry: entry)
}
.configurationDisplayName("Multiple followers")
.description("Show number of followers for multiple accounts.")
.supportedFamilies(availableFamilies)
}
}
private extension MultiFollowersCountWidgetProvider {
func loadCurrentEntry(for configuration: MultiFollowersCountSmallIntent, in context: Context, completion: @escaping (MultiFollowersCountEntry) -> Void) {
Task {
guard
let authBox = WidgetExtension.appContext
.authenticationService
.mastodonAuthenticationBoxes
.first
else {
return completion(.unconfigured)
}
guard let desiredAccounts: [String] = {
guard let account = configuration.accounts?.compactMap({ $0 }) else {
if let acct = authBox.authenticationRecord.object(in: WidgetExtension.appContext.managedObjectContext)?.user.acct {
return [acct]
}
return nil
}
return account
}() else {
return completion(.unconfigured)
}
var accounts = [FollowersEntryAccountable]()
for desiredAccount in desiredAccounts {
let resultingAccount = try await WidgetExtension.appContext
.apiService
.search(query: .init(q: desiredAccount, type: .accounts), authenticationBox: authBox)
.value
.accounts
.first!
let imageData = try await URLSession.shared.data(from: resultingAccount.avatarImageURLWithFallback(domain: authBox.domain)).0
accounts.append(FollowersEntryAccount.from(
mastodonAccount: resultingAccount,
domain: authBox.domain,
avatarImage: UIImage(data: imageData) ?? UIImage(named: "missingAvatar")!
))
}
let entry = MultiFollowersCountEntry(
date: Date(),
accounts: accounts,
configuration: configuration
)
completion(entry)
}
}
}
protocol MultiFollowersEntryAccountable {
var followersCount: Int { get }
var displayNameWithFallback: String { get }
var acct: String { get }
var avatarImage: UIImage { get }
var domain: String { get }
}
struct MultiFollowersEntryAccount: MultiFollowersEntryAccountable {
let followersCount: Int
let displayNameWithFallback: String
let acct: String
let avatarImage: UIImage
let domain: String
static func from(mastodonAccount: Mastodon.Entity.Account, domain: String, avatarImage: UIImage) -> Self {
MultiFollowersEntryAccount(
followersCount: mastodonAccount.followersCount,
displayNameWithFallback: mastodonAccount.displayNameWithFallback,
acct: mastodonAccount.acct,
avatarImage: avatarImage,
domain: domain
)
}
}

View File

@ -0,0 +1,58 @@
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
import SwiftUI
import WidgetKit
import MastodonAsset
struct MultiFollowersCountWidgetView: View {
@Environment(\.widgetFamily) var family
var entry: MultiFollowersCountWidgetProvider.Entry
var body: some View {
if let accounts = entry.accounts {
switch family {
case .systemSmall:
viewForSmallWidgetNoChart(accounts)
default:
Text("Sorry but this Widget family is unsupported.")
}
} else {
Text("Please open Mastodon to log in to an Account.")
.multilineTextAlignment(.center)
.font(.caption)
.padding(.all, 20)
}
}
private func viewForSmallWidgetNoChart(_ accounts: [FollowersEntryAccountable]) -> some View {
VStack(alignment: .leading, spacing: 0) {
ForEach(accounts, id: \.acct) { account in
HStack {
if let avatarImage = account.avatarImage {
Image(uiImage: avatarImage)
.resizable()
.frame(width: 32, height: 32)
.cornerRadius(5)
}
VStack(alignment: .leading) {
Text(account.followersCount.asAbbreviatedCountString())
.font(.title2)
.lineLimit(1)
.truncationMode(.tail)
Text("@\(account.acct)")
.font(.caption2)
.foregroundColor(.secondary)
.lineLimit(1)
.truncationMode(.tail)
}
Spacer()
}
.padding(.leading, 20)
}
Spacer()
}
.padding(.vertical, 16)
}
}

View File

@ -184,9 +184,9 @@
<key>INIntentIneligibleForSuggestions</key>
<true/>
<key>INIntentLastParameterTag</key>
<integer>5</integer>
<integer>6</integer>
<key>INIntentName</key>
<string>MultiFollowCountSmall</string>
<string>MultiFollowersCountSmall</string>
<key>INIntentParameters</key>
<array>
<dict>
@ -251,17 +251,24 @@
<integer>1</integer>
<key>INIntentParameterFixedSizeArray</key>
<integer>1</integer>
<key>INIntentParameterMetadata</key>
<dict>
<key>INIntentParameterMetadataCapitalization</key>
<string>Sentences</string>
<key>INIntentParameterMetadataDefaultValueID</key>
<string>SNXOJo</string>
</dict>
<key>INIntentParameterName</key>
<string>accounts</string>
<key>INIntentParameterObjectType</key>
<string>MultiFollowAccountsSmall</string>
<key>INIntentParameterObjectTypeNamespace</key>
<string>88xZPY</string>
<key>INIntentParameterPromptDialogs</key>
<array>
<dict>
<key>INIntentParameterPromptDialogCustom</key>
<true/>
<key>INIntentParameterPromptDialogFormatString</key>
<string>Enter username</string>
<key>INIntentParameterPromptDialogFormatStringID</key>
<string>3d6HSO</string>
<key>INIntentParameterPromptDialogType</key>
<string>Configuration</string>
</dict>
@ -271,13 +278,37 @@
<key>INIntentParameterPromptDialogType</key>
<string>Primary</string>
</dict>
<dict>
<key>INIntentParameterPromptDialogCustom</key>
<true/>
<key>INIntentParameterPromptDialogFormatString</key>
<string>There are ${count} options matching ${accounts}.</string>
<key>INIntentParameterPromptDialogFormatStringID</key>
<string>3nWfxd</string>
<key>INIntentParameterPromptDialogType</key>
<string>DisambiguationIntroduction</string>
</dict>
<dict>
<key>INIntentParameterPromptDialogCustom</key>
<true/>
<key>INIntentParameterPromptDialogFormatString</key>
<string>Just to confirm, you wanted ${accounts}?</string>
<key>INIntentParameterPromptDialogFormatStringID</key>
<string>IP6ujX</string>
<key>INIntentParameterPromptDialogType</key>
<string>Confirmation</string>
</dict>
</array>
<key>INIntentParameterSupportsDynamicEnumeration</key>
<true/>
<key>INIntentParameterSupportsMultipleValues</key>
<true/>
<key>INIntentParameterSupportsSearch</key>
<true/>
<key>INIntentParameterTag</key>
<integer>5</integer>
<integer>6</integer>
<key>INIntentParameterType</key>
<string>Object</string>
<string>String</string>
</dict>
</array>
<key>INIntentResponse</key>
@ -295,9 +326,32 @@
<string>failure</string>
</dict>
</array>
<key>INIntentResponseLastParameterTag</key>
<integer>4</integer>
<key>INIntentResponseOutput</key>
<string>username</string>
<key>INIntentResponseParameters</key>
<array>
<dict>
<key>INIntentResponseParameterDisplayName</key>
<string>Username</string>
<key>INIntentResponseParameterDisplayNameID</key>
<string>7DZrRA</string>
<key>INIntentResponseParameterDisplayPriority</key>
<integer>1</integer>
<key>INIntentResponseParameterName</key>
<string>username</string>
<key>INIntentResponseParameterSupportsMultipleValues</key>
<true/>
<key>INIntentResponseParameterTag</key>
<integer>4</integer>
<key>INIntentResponseParameterType</key>
<string>Object</string>
</dict>
</array>
</dict>
<key>INIntentTitle</key>
<string>Multi Follow Count Small</string>
<string>Multi Followers Count Small</string>
<key>INIntentTitleID</key>
<string>e0W2wo</string>
<key>INIntentType</key>
@ -307,86 +361,6 @@
</dict>
</array>
<key>INTypes</key>
<array>
<dict>
<key>INTypeDisplayName</key>
<string>Account</string>
<key>INTypeDisplayNameID</key>
<string>LUrJ3D</string>
<key>INTypeLastPropertyTag</key>
<integer>101</integer>
<key>INTypeName</key>
<string>MultiFollowAccountsSmall</string>
<key>INTypeProperties</key>
<array>
<dict>
<key>INTypePropertyDefault</key>
<true/>
<key>INTypePropertyDisplayPriority</key>
<integer>1</integer>
<key>INTypePropertyName</key>
<string>identifier</string>
<key>INTypePropertyTag</key>
<integer>1</integer>
<key>INTypePropertyType</key>
<string>String</string>
</dict>
<dict>
<key>INTypePropertyDefault</key>
<true/>
<key>INTypePropertyDisplayPriority</key>
<integer>2</integer>
<key>INTypePropertyName</key>
<string>displayString</string>
<key>INTypePropertyTag</key>
<integer>2</integer>
<key>INTypePropertyType</key>
<string>String</string>
</dict>
<dict>
<key>INTypePropertyDefault</key>
<true/>
<key>INTypePropertyDisplayPriority</key>
<integer>3</integer>
<key>INTypePropertyName</key>
<string>pronunciationHint</string>
<key>INTypePropertyTag</key>
<integer>3</integer>
<key>INTypePropertyType</key>
<string>String</string>
</dict>
<dict>
<key>INTypePropertyDefault</key>
<true/>
<key>INTypePropertyDisplayPriority</key>
<integer>4</integer>
<key>INTypePropertyName</key>
<string>alternativeSpeakableMatches</string>
<key>INTypePropertySupportsMultipleValues</key>
<true/>
<key>INTypePropertyTag</key>
<integer>4</integer>
<key>INTypePropertyType</key>
<string>SpeakableString</string>
</dict>
<dict>
<key>INTypePropertyDisplayName</key>
<string>username</string>
<key>INTypePropertyDisplayNameID</key>
<string>TQNJZz</string>
<key>INTypePropertyDisplayPriority</key>
<integer>5</integer>
<key>INTypePropertyName</key>
<string>property</string>
<key>INTypePropertySupportsMultipleValues</key>
<true/>
<key>INTypePropertyTag</key>
<integer>101</integer>
<key>INTypePropertyType</key>
<string>String</string>
</dict>
</array>
</dict>
</array>
<array/>
</dict>
</plist>

View File

@ -7,5 +7,6 @@ import SwiftUI
struct WidgetExtensionBundle: WidgetBundle {
var body: some Widget {
FollowersCountWidget()
MultiFollowersCountWidget()
}
}