mirror of
https://github.com/mastodon/mastodon-ios.git
synced 2025-02-02 18:36:44 +01:00
Add LatestFollowersWidget
This commit is contained in:
parent
8438bbc032
commit
d685b9e365
@ -49,7 +49,6 @@
|
||||
2A728130297EA9D8004138C5 /* WidgetExtension.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 2A72812C297EA9D7004138C5 /* WidgetExtension.intentdefinition */; };
|
||||
2A728131297EA9D8004138C5 /* WidgetExtension.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 2A72812C297EA9D7004138C5 /* WidgetExtension.intentdefinition */; };
|
||||
2A728134297EA9D8004138C5 /* WidgetExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 2A728120297EA9D7004138C5 /* WidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
2A72813B297EC6F7004138C5 /* MastodonSDKDynamic in Frameworks */ = {isa = PBXBuildFile; productRef = 2A72813A297EC6F7004138C5 /* MastodonSDKDynamic */; };
|
||||
2A72813F297EC762004138C5 /* WidgetExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A72813E297EC762004138C5 /* WidgetExtension.swift */; };
|
||||
2A76F75C2930D94700B3388D /* HashtagTimelineHeaderViewActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A76F75B2930D94700B3388D /* HashtagTimelineHeaderViewActionButton.swift */; };
|
||||
2A82294F29262EE000D2A1F7 /* AppContext+NextAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A82294E29262EE000D2A1F7 /* AppContext+NextAccount.swift */; };
|
||||
@ -57,6 +56,9 @@
|
||||
2A86A14929892B3A007F1062 /* MultiFollowersCountWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A86A14829892B3A007F1062 /* MultiFollowersCountWidget.swift */; };
|
||||
2A86A14B2989326E007F1062 /* MultiFollowersCountWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A86A14A2989326E007F1062 /* MultiFollowersCountWidgetView.swift */; };
|
||||
2A90A157296EEE500026C155 /* MastodonSDKDynamic in Frameworks */ = {isa = PBXBuildFile; productRef = 2A90A156296EEE500026C155 /* MastodonSDKDynamic */; };
|
||||
2A9D0664298C048800BF38CB /* LatestFollowersWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A9D0663298C048800BF38CB /* LatestFollowersWidget.swift */; };
|
||||
2A9D0666298C05A800BF38CB /* LatestFollowersWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A9D0665298C05A800BF38CB /* LatestFollowersWidgetView.swift */; };
|
||||
2A9D066F298D0FD100BF38CB /* MastodonSDKDynamic in Frameworks */ = {isa = PBXBuildFile; productRef = 2A9D066E298D0FD100BF38CB /* MastodonSDKDynamic */; };
|
||||
2AB12E4629362F27006BC925 /* DataSourceFacade+Translate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AB12E4529362F27006BC925 /* DataSourceFacade+Translate.swift */; };
|
||||
2AE202AA297FE10B00F66E55 /* WidgetExtension.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 2A72812C297EA9D7004138C5 /* WidgetExtension.intentdefinition */; };
|
||||
2AE202AC297FE19100F66E55 /* FollowersCountIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AE202AB297FE19100F66E55 /* FollowersCountIntentHandler.swift */; };
|
||||
@ -648,6 +650,8 @@
|
||||
2A86A14529892944007F1062 /* MultiFollowersCountIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiFollowersCountIntentHandler.swift; sourceTree = "<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>"; };
|
||||
2A9D0663298C048800BF38CB /* LatestFollowersWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LatestFollowersWidget.swift; sourceTree = "<group>"; };
|
||||
2A9D0665298C05A800BF38CB /* LatestFollowersWidgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LatestFollowersWidgetView.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>"; };
|
||||
@ -1216,7 +1220,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
2A72813B297EC6F7004138C5 /* MastodonSDKDynamic in Frameworks */,
|
||||
2A9D066F298D0FD100BF38CB /* MastodonSDKDynamic in Frameworks */,
|
||||
2A728124297EA9D7004138C5 /* SwiftUI.framework in Frameworks */,
|
||||
2A728122297EA9D7004138C5 /* WidgetKit.framework in Frameworks */,
|
||||
);
|
||||
@ -1458,6 +1462,7 @@
|
||||
children = (
|
||||
2A86A14429892709007F1062 /* FollowersCount */,
|
||||
2A86A14729892B1B007F1062 /* MultiFollowersCount */,
|
||||
2A9D0662298C045000BF38CB /* LatestFollowers */,
|
||||
);
|
||||
path = Variants;
|
||||
sourceTree = "<group>";
|
||||
@ -1481,6 +1486,15 @@
|
||||
path = MultiFollowersCount;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
2A9D0662298C045000BF38CB /* LatestFollowers */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
2A9D0663298C048800BF38CB /* LatestFollowersWidget.swift */,
|
||||
2A9D0665298C05A800BF38CB /* LatestFollowersWidgetView.swift */,
|
||||
);
|
||||
path = LatestFollowers;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
2D152A8A25C295B8009AA50C /* Content */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -3019,7 +3033,7 @@
|
||||
);
|
||||
name = WidgetExtension;
|
||||
packageProductDependencies = (
|
||||
2A72813A297EC6F7004138C5 /* MastodonSDKDynamic */,
|
||||
2A9D066E298D0FD100BF38CB /* MastodonSDKDynamic */,
|
||||
);
|
||||
productName = WidgetExtensionExtension;
|
||||
productReference = 2A728120297EA9D7004138C5 /* WidgetExtension.appex */;
|
||||
@ -3501,6 +3515,7 @@
|
||||
2A33063729880835001D4C51 /* DataRepresentable.swift in Sources */,
|
||||
2A33062D2987DBFA001D4C51 /* FollowersCountHistory.swift in Sources */,
|
||||
2A33063829880835001D4C51 /* LineChart.swift in Sources */,
|
||||
2A9D0666298C05A800BF38CB /* LatestFollowersWidgetView.swift in Sources */,
|
||||
2A728130297EA9D8004138C5 /* WidgetExtension.intentdefinition in Sources */,
|
||||
2A86A14B2989326E007F1062 /* MultiFollowersCountWidgetView.swift in Sources */,
|
||||
2A72813F297EC762004138C5 /* WidgetExtension.swift in Sources */,
|
||||
@ -3509,6 +3524,7 @@
|
||||
2A33063629880835001D4C51 /* Math.swift in Sources */,
|
||||
2A86A14929892B3A007F1062 /* MultiFollowersCountWidget.swift in Sources */,
|
||||
2A33AB662982C4AF008A7FB1 /* FollowersCountWidgetView.swift in Sources */,
|
||||
2A9D0664298C048800BF38CB /* LatestFollowersWidget.swift in Sources */,
|
||||
2A728127297EA9D7004138C5 /* WidgetExtensionBundle.swift in Sources */,
|
||||
2A72812B297EA9D7004138C5 /* FollowersCountWidget.swift in Sources */,
|
||||
2A33063929880835001D4C51 /* CurvedChart.swift in Sources */,
|
||||
@ -5315,11 +5331,11 @@
|
||||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
2A72813A297EC6F7004138C5 /* MastodonSDKDynamic */ = {
|
||||
2A90A156296EEE500026C155 /* MastodonSDKDynamic */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = MastodonSDKDynamic;
|
||||
};
|
||||
2A90A156296EEE500026C155 /* MastodonSDKDynamic */ = {
|
||||
2A9D066E298D0FD100BF38CB /* MastodonSDKDynamic */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = MastodonSDKDynamic;
|
||||
};
|
||||
|
@ -63,6 +63,7 @@
|
||||
<array>
|
||||
<string>FollowersCountIntent</string>
|
||||
<string>MultiFollowersCountIntent</string>
|
||||
<string>LatestFollowersIntent</string>
|
||||
<string>SendPostIntent</string>
|
||||
</array>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
|
@ -27,3 +27,9 @@ extension Collection where Iterator.Element: NSManagedObject {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Collection {
|
||||
public subscript (safe index: Index) -> Element? {
|
||||
return indices.contains(index) ? self[index] : nil
|
||||
}
|
||||
}
|
||||
|
23
WidgetExtension/Assets.xcassets/BrandIconColored.imageset/Contents.json
vendored
Normal file
23
WidgetExtension/Assets.xcassets/BrandIconColored.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Logo.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "Logo@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "Logo@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
WidgetExtension/Assets.xcassets/BrandIconColored.imageset/Logo.png
vendored
Normal file
BIN
WidgetExtension/Assets.xcassets/BrandIconColored.imageset/Logo.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 576 B |
BIN
WidgetExtension/Assets.xcassets/BrandIconColored.imageset/Logo@2x.png
vendored
Normal file
BIN
WidgetExtension/Assets.xcassets/BrandIconColored.imageset/Logo@2x.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.0 KiB |
BIN
WidgetExtension/Assets.xcassets/BrandIconColored.imageset/Logo@3x.png
vendored
Normal file
BIN
WidgetExtension/Assets.xcassets/BrandIconColored.imageset/Logo@3x.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
@ -94,9 +94,9 @@ class FollowersCountHistory {
|
||||
let relevantDays = elapsedFollowersCountDateStrings()
|
||||
let today = relevantDays.last!
|
||||
let yesterday = relevantDays[relevantDays.count - 2]
|
||||
|
||||
|
||||
let followersToday = history.first(where: { $0.dstring == today })?.count ?? account.followersCount
|
||||
let followersYesterday = history.first(where: { $0.dstring == yesterday })?.count ?? account.followersCount
|
||||
let followersYesterday = history[safe: history.count-2]?.count ?? account.followersCount
|
||||
|
||||
let followersChange = followersToday - followersYesterday
|
||||
|
||||
|
@ -0,0 +1,175 @@
|
||||
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
|
||||
|
||||
import WidgetKit
|
||||
import SwiftUI
|
||||
import Intents
|
||||
import MastodonSDK
|
||||
|
||||
struct LatestFollowersWidgetProvider: IntentTimelineProvider {
|
||||
func placeholder(in context: Context) -> LatestFollowersEntry {
|
||||
.placeholder
|
||||
}
|
||||
|
||||
func getSnapshot(for configuration: LatestFollowersIntent, in context: Context, completion: @escaping (LatestFollowersEntry) -> ()) {
|
||||
guard !context.isPreview else {
|
||||
return completion(.placeholder)
|
||||
}
|
||||
loadCurrentEntry(for: configuration, in: context, completion: completion)
|
||||
}
|
||||
|
||||
func getTimeline(for configuration: LatestFollowersIntent, in context: Context, completion: @escaping (Timeline<LatestFollowersEntry>) -> ()) {
|
||||
loadCurrentEntry(for: configuration, in: context) { entry in
|
||||
completion(Timeline(entries: [entry], policy: .after(.now)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct LatestFollowersEntry: TimelineEntry {
|
||||
let date: Date
|
||||
let accounts: [LatestFollowersEntryAccountable]?
|
||||
let configuration: LatestFollowersIntent
|
||||
|
||||
static var placeholder: Self {
|
||||
LatestFollowersEntry(
|
||||
date: .now,
|
||||
accounts: [
|
||||
LatestFollowersEntryAccount(
|
||||
note: "Just another Mastodon user",
|
||||
displayNameWithFallback: "Mastodon",
|
||||
acct: "mastodon",
|
||||
avatarImage: UIImage(named: "missingAvatar")!,
|
||||
domain: "mastodon"
|
||||
),
|
||||
LatestFollowersEntryAccount(
|
||||
note: "Yet another Mastodon user",
|
||||
displayNameWithFallback: "Mastodon",
|
||||
acct: "mastodon",
|
||||
avatarImage: UIImage(named: "missingAvatar")!,
|
||||
domain: "mastodon"
|
||||
)
|
||||
],
|
||||
configuration: LatestFollowersIntent()
|
||||
)
|
||||
}
|
||||
|
||||
static var unconfigured: Self {
|
||||
LatestFollowersEntry(
|
||||
date: .now,
|
||||
accounts: [],
|
||||
configuration: LatestFollowersIntent()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct LatestFollowersWidget: Widget {
|
||||
private var availableFamilies: [WidgetFamily] {
|
||||
return [.systemSmall, .systemMedium]
|
||||
}
|
||||
|
||||
var body: some WidgetConfiguration {
|
||||
IntentConfiguration(kind: "Latest followers", intent: LatestFollowersIntent.self, provider: LatestFollowersWidgetProvider()) { entry in
|
||||
LatestFollowersWidgetView(entry: entry)
|
||||
}
|
||||
.configurationDisplayName("Latest followers")
|
||||
.description("Show latest followers.")
|
||||
.supportedFamilies(availableFamilies)
|
||||
}
|
||||
}
|
||||
|
||||
private extension LatestFollowersWidgetProvider {
|
||||
func loadCurrentEntry(for configuration: LatestFollowersIntent, in context: Context, completion: @escaping (LatestFollowersEntry) -> Void) {
|
||||
Task { @MainActor in
|
||||
guard
|
||||
let authBox = WidgetExtension.appContext
|
||||
.authenticationService
|
||||
.mastodonAuthenticationBoxes
|
||||
.first
|
||||
else {
|
||||
return completion(.unconfigured)
|
||||
}
|
||||
|
||||
// guard let desiredAccount: String = {
|
||||
// guard let account = authBox.authenticationRecord.object(in: WidgetExtension.appContext.managedObjectContext)?.user.acct else {
|
||||
// return nil
|
||||
// }
|
||||
// return account
|
||||
// }() else {
|
||||
// return completion(.unconfigured)
|
||||
// }
|
||||
|
||||
|
||||
var accounts = [LatestFollowersEntryAccountable]()
|
||||
|
||||
let followers = try await WidgetExtension.appContext
|
||||
.apiService
|
||||
.followers(userID: authBox.userID, maxID: nil, authenticationBox: authBox)
|
||||
.value
|
||||
.prefix(2) // X most recent followers
|
||||
|
||||
for follower in followers {
|
||||
let imageData = try await URLSession.shared.data(from: follower.avatarImageURLWithFallback(domain: authBox.domain)).0
|
||||
|
||||
accounts.append(
|
||||
LatestFollowersEntryAccount(
|
||||
note: follower.note,
|
||||
displayNameWithFallback: follower.displayNameWithFallback,
|
||||
acct: follower.acct,
|
||||
avatarImage: UIImage(data: imageData) ?? UIImage(named: "missingAvatar")!,
|
||||
domain: authBox.domain
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
let entry = LatestFollowersEntry(
|
||||
date: Date(),
|
||||
accounts: accounts,
|
||||
configuration: configuration
|
||||
)
|
||||
|
||||
completion(entry)
|
||||
|
||||
// for desiredAccount in desiredAccounts {
|
||||
// let resultingAccount = try await WidgetExtension.appContext
|
||||
// .apiService
|
||||
// .search(query: .init(q: desiredAccount, type: .accounts), authenticationBox: authBox)
|
||||
// .value
|
||||
// .accounts
|
||||
// .first!
|
||||
//
|
||||
// let imageData = try await URLSession.shared.data(from: resultingAccount.avatarImageURLWithFallback(domain: authBox.domain)).0
|
||||
//
|
||||
// accounts.append(FollowersEntryAccount.from(
|
||||
// mastodonAccount: resultingAccount,
|
||||
// domain: authBox.domain,
|
||||
// avatarImage: UIImage(data: imageData) ?? UIImage(named: "missingAvatar")!
|
||||
// ))
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protocol LatestFollowersEntryAccountable {
|
||||
var note: String { get }
|
||||
var displayNameWithFallback: String { get }
|
||||
var acct: String { get }
|
||||
var avatarImage: UIImage { get }
|
||||
var domain: String { get }
|
||||
}
|
||||
|
||||
struct LatestFollowersEntryAccount: LatestFollowersEntryAccountable {
|
||||
let note: String
|
||||
let displayNameWithFallback: String
|
||||
let acct: String
|
||||
let avatarImage: UIImage
|
||||
let domain: String
|
||||
|
||||
static func from(mastodonAccount: Mastodon.Entity.Account, domain: String, avatarImage: UIImage) -> Self {
|
||||
LatestFollowersEntryAccount(
|
||||
note: mastodonAccount.header,
|
||||
displayNameWithFallback: mastodonAccount.displayNameWithFallback,
|
||||
acct: mastodonAccount.acct,
|
||||
avatarImage: avatarImage,
|
||||
domain: domain
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,132 @@
|
||||
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
|
||||
|
||||
import SwiftUI
|
||||
import WidgetKit
|
||||
import MastodonSDK
|
||||
import MastodonAsset
|
||||
import MastodonUI
|
||||
|
||||
struct LatestFollowersWidgetView: View {
|
||||
private let dateFormatter: DateFormatter = {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateStyle = .none
|
||||
formatter.timeStyle = .short
|
||||
return formatter
|
||||
}()
|
||||
|
||||
@Environment(\.widgetFamily) var family
|
||||
|
||||
var entry: LatestFollowersWidgetProvider.Entry
|
||||
|
||||
var body: some View {
|
||||
if let accounts = entry.accounts {
|
||||
switch family {
|
||||
case .systemSmall:
|
||||
viewForSmallWidget(accounts, lastUpdate: entry.date)
|
||||
case .systemMedium:
|
||||
viewForMediumWidget(accounts, lastUpdate: entry.date)
|
||||
default:
|
||||
Text("Sorry but this Widget family is unsupported.")
|
||||
}
|
||||
} else {
|
||||
Text("Please open Mastodon to log in to an Account.")
|
||||
.multilineTextAlignment(.center)
|
||||
.font(.caption)
|
||||
.padding(.all, 20)
|
||||
}
|
||||
}
|
||||
|
||||
private func viewForSmallWidget(_ accounts: [LatestFollowersEntryAccountable], lastUpdate: Date) -> some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
ForEach(accounts, id: \.acct) { account in
|
||||
HStack {
|
||||
if let avatarImage = account.avatarImage {
|
||||
Image(uiImage: avatarImage)
|
||||
.resizable()
|
||||
.frame(width: 32, height: 32)
|
||||
.cornerRadius(5)
|
||||
}
|
||||
VStack(alignment: .leading) {
|
||||
Text(account.note)
|
||||
.font(.caption)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
|
||||
Text("@\(account.acct)")
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.padding(.leading, 20)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.padding(.vertical, 16)
|
||||
}
|
||||
|
||||
private func viewForMediumWidget(_ accounts: [LatestFollowersEntryAccountable], lastUpdate: Date) -> some View {
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
Text("Latest followers")
|
||||
.font(.system(size: UIFontMetrics.default.scaledValue(for: 16)))
|
||||
Spacer()
|
||||
Image("BrandIconColored")
|
||||
}
|
||||
|
||||
ForEach(accounts, id: \.acct) { account in
|
||||
HStack {
|
||||
if let avatarImage = account.avatarImage {
|
||||
Image(uiImage: avatarImage)
|
||||
.resizable()
|
||||
.frame(width: 32, height: 32)
|
||||
.cornerRadius(5)
|
||||
}
|
||||
VStack(alignment: .leading) {
|
||||
|
||||
HStack {
|
||||
Text(account.displayNameWithFallback)
|
||||
.font(.footnote.bold())
|
||||
.foregroundColor(.secondary)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
|
||||
Text("@\(account.acct)")
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
}
|
||||
|
||||
Text(account.noteWithoutHtmlTags!)
|
||||
.font(.caption)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
Text("Last update: \(dateFormatter.string(from: lastUpdate))")
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.vertical, 16)
|
||||
}
|
||||
}
|
||||
|
||||
private extension LatestFollowersEntryAccountable {
|
||||
var noteWithoutHtmlTags: String? {
|
||||
do {
|
||||
let regex = "<[^>]+>"
|
||||
let expr = try NSRegularExpression(pattern: regex, options: NSRegularExpression.Options.caseInsensitive)
|
||||
let result = expr.stringByReplacingMatches(in: note, options: [], range: NSMakeRange(0, note.count), withTemplate: "")
|
||||
return result
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
@ -278,26 +278,6 @@
|
||||
<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/>
|
||||
@ -359,6 +339,42 @@
|
||||
<key>INIntentVerb</key>
|
||||
<string>View</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>INIntentCategory</key>
|
||||
<string>information</string>
|
||||
<key>INIntentDescriptionID</key>
|
||||
<string>5KZ2fm</string>
|
||||
<key>INIntentEligibleForWidgets</key>
|
||||
<true/>
|
||||
<key>INIntentIneligibleForSuggestions</key>
|
||||
<true/>
|
||||
<key>INIntentName</key>
|
||||
<string>LatestFollowers</string>
|
||||
<key>INIntentResponse</key>
|
||||
<dict>
|
||||
<key>INIntentResponseCodes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>INIntentResponseCodeName</key>
|
||||
<string>success</string>
|
||||
<key>INIntentResponseCodeSuccess</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>INIntentResponseCodeName</key>
|
||||
<string>failure</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
<key>INIntentTitle</key>
|
||||
<string>Latest Followers</string>
|
||||
<key>INIntentTitleID</key>
|
||||
<string>ZLZ6sg</string>
|
||||
<key>INIntentType</key>
|
||||
<string>Custom</string>
|
||||
<key>INIntentVerb</key>
|
||||
<string>View</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>INTypes</key>
|
||||
<array/>
|
||||
|
@ -8,5 +8,6 @@ struct WidgetExtensionBundle: WidgetBundle {
|
||||
var body: some Widget {
|
||||
FollowersCountWidget()
|
||||
MultiFollowersCountWidget()
|
||||
LatestFollowersWidget()
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user