Follower Goal widget
This commit is contained in:
parent
dd1b13e39b
commit
1175e328f7
|
@ -46,6 +46,7 @@
|
||||||
B999DE5C2B76F8CB00509868 /* ContactsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B999DE5B2B76F8CB00509868 /* ContactsView.swift */; };
|
B999DE5C2B76F8CB00509868 /* ContactsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B999DE5B2B76F8CB00509868 /* ContactsView.swift */; };
|
||||||
B999DE5E2B76F9D100509868 /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = B999DE5D2B76F9D100509868 /* Message.swift */; };
|
B999DE5E2B76F9D100509868 /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = B999DE5D2B76F9D100509868 /* Message.swift */; };
|
||||||
B999DE602B76FB3E00509868 /* ContactRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = B999DE5F2B76FB3E00509868 /* ContactRow.swift */; };
|
B999DE602B76FB3E00509868 /* ContactRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = B999DE5F2B76FB3E00509868 /* ContactRow.swift */; };
|
||||||
|
B9B469B02B9A275F00AD5585 /* FollowGoalWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9B469AF2B9A275F00AD5585 /* FollowGoalWidget.swift */; };
|
||||||
B9B63B212B442D1500BBC82D /* DynamicTextEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9B63B202B442D1500BBC82D /* DynamicTextEditor.swift */; };
|
B9B63B212B442D1500BBC82D /* DynamicTextEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9B63B202B442D1500BBC82D /* DynamicTextEditor.swift */; };
|
||||||
B9B63B232B447B8000BBC82D /* PostCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9B63B222B447B8000BBC82D /* PostCardView.swift */; };
|
B9B63B232B447B8000BBC82D /* PostCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9B63B222B447B8000BBC82D /* PostCardView.swift */; };
|
||||||
B9B63B252B44997400BBC82D /* QuotePostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9B63B242B44997400BBC82D /* QuotePostView.swift */; };
|
B9B63B252B44997400BBC82D /* QuotePostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9B63B242B44997400BBC82D /* QuotePostView.swift */; };
|
||||||
|
@ -181,6 +182,7 @@
|
||||||
B999DE5B2B76F8CB00509868 /* ContactsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsView.swift; sourceTree = "<group>"; };
|
B999DE5B2B76F8CB00509868 /* ContactsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsView.swift; sourceTree = "<group>"; };
|
||||||
B999DE5D2B76F9D100509868 /* Message.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Message.swift; sourceTree = "<group>"; };
|
B999DE5D2B76F9D100509868 /* Message.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Message.swift; sourceTree = "<group>"; };
|
||||||
B999DE5F2B76FB3E00509868 /* ContactRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactRow.swift; sourceTree = "<group>"; };
|
B999DE5F2B76FB3E00509868 /* ContactRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactRow.swift; sourceTree = "<group>"; };
|
||||||
|
B9B469AF2B9A275F00AD5585 /* FollowGoalWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowGoalWidget.swift; sourceTree = "<group>"; };
|
||||||
B9B63B202B442D1500BBC82D /* DynamicTextEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicTextEditor.swift; sourceTree = "<group>"; };
|
B9B63B202B442D1500BBC82D /* DynamicTextEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicTextEditor.swift; sourceTree = "<group>"; };
|
||||||
B9B63B222B447B8000BBC82D /* PostCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostCardView.swift; sourceTree = "<group>"; };
|
B9B63B222B447B8000BBC82D /* PostCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostCardView.swift; sourceTree = "<group>"; };
|
||||||
B9B63B242B44997400BBC82D /* QuotePostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuotePostView.swift; sourceTree = "<group>"; };
|
B9B63B242B44997400BBC82D /* QuotePostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuotePostView.swift; sourceTree = "<group>"; };
|
||||||
|
@ -326,6 +328,7 @@
|
||||||
B9C20D582B923CDD004DC9B3 /* ThreadedWidgetsExtension.entitlements */,
|
B9C20D582B923CDD004DC9B3 /* ThreadedWidgetsExtension.entitlements */,
|
||||||
B9C20D0F2B921C78004DC9B3 /* ThreadedWidgetsBundle.swift */,
|
B9C20D0F2B921C78004DC9B3 /* ThreadedWidgetsBundle.swift */,
|
||||||
B9C20D112B921C78004DC9B3 /* FollowCountWidget.swift */,
|
B9C20D112B921C78004DC9B3 /* FollowCountWidget.swift */,
|
||||||
|
B9B469AF2B9A275F00AD5585 /* FollowGoalWidget.swift */,
|
||||||
B9C20D132B921C78004DC9B3 /* AppIntent.swift */,
|
B9C20D132B921C78004DC9B3 /* AppIntent.swift */,
|
||||||
B9C20D602B949AD7004DC9B3 /* Redeclarations.swift */,
|
B9C20D602B949AD7004DC9B3 /* Redeclarations.swift */,
|
||||||
B9C20D152B921C7B004DC9B3 /* Assets.xcassets */,
|
B9C20D152B921C7B004DC9B3 /* Assets.xcassets */,
|
||||||
|
@ -684,6 +687,7 @@
|
||||||
B9C20D142B921C78004DC9B3 /* AppIntent.swift in Sources */,
|
B9C20D142B921C78004DC9B3 /* AppIntent.swift in Sources */,
|
||||||
B9C20D372B9229EC004DC9B3 /* AppInfo.swift in Sources */,
|
B9C20D372B9229EC004DC9B3 /* AppInfo.swift in Sources */,
|
||||||
B9C20D3D2B9229EC004DC9B3 /* Emoji.swift in Sources */,
|
B9C20D3D2B9229EC004DC9B3 /* Emoji.swift in Sources */,
|
||||||
|
B9B469B02B9A275F00AD5585 /* FollowGoalWidget.swift in Sources */,
|
||||||
B9C20D342B9229EC004DC9B3 /* Client.swift in Sources */,
|
B9C20D342B9229EC004DC9B3 /* Client.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
|
|
@ -5,15 +5,26 @@ import SwiftData
|
||||||
import WidgetKit
|
import WidgetKit
|
||||||
import AppIntents
|
import AppIntents
|
||||||
|
|
||||||
/// Widgets that require to select an account will use this `ConfigurationIntent`
|
/// Widgets that require to select only an account will use this `ConfigurationIntent`
|
||||||
struct AccountAppIntent: WidgetConfigurationIntent {
|
struct AccountAppIntent: WidgetConfigurationIntent {
|
||||||
static var title: LocalizedStringResource = "widget.follow-count"
|
static var title: LocalizedStringResource = "widget.follow-count"
|
||||||
static var description = IntentDescription("widget.follow-count.description")
|
static var description = IntentDescription("widget.follow-count.description")
|
||||||
|
|
||||||
@Parameter(title: "widget.select-account")
|
@Parameter(title: "widget.select-account")
|
||||||
var account: AccountEntity?
|
var account: AccountEntity?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct AccountGoalAppIntent: WidgetConfigurationIntent {
|
||||||
|
static var title: LocalizedStringResource = "widget.follow-goal"
|
||||||
|
static var description = IntentDescription("widget.follow-goal.description")
|
||||||
|
|
||||||
|
@Parameter(title: "widget.select-account")
|
||||||
|
var account: AccountEntity?
|
||||||
|
|
||||||
|
@Parameter(title: "widget.set-goal", default: 1_000)
|
||||||
|
var goal: Int
|
||||||
|
}
|
||||||
|
|
||||||
struct AccountEntity: AppEntity {
|
struct AccountEntity: AppEntity {
|
||||||
let client: Client
|
let client: Client
|
||||||
let id: String
|
let id: String
|
||||||
|
|
|
@ -4,67 +4,9 @@ import WidgetKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import SwiftData
|
import SwiftData
|
||||||
|
|
||||||
struct Provider: AppIntentTimelineProvider {
|
|
||||||
func placeholder(in context: Context) -> SimpleEntry {
|
|
||||||
let placeholder: UIImage = UIImage(systemName: "person.crop.circle") ?? UIImage()
|
|
||||||
placeholder.withTintColor(UIColor.label)
|
|
||||||
return SimpleEntry(date: Date(), pfp: placeholder, followers: 38, configuration: AccountAppIntent())
|
|
||||||
}
|
|
||||||
|
|
||||||
func snapshot(for configuration: AccountAppIntent, in context: Context) async -> SimpleEntry {
|
|
||||||
let data = await getData(configuration: configuration)
|
|
||||||
return SimpleEntry(date: Date(), pfp: data.0, followers: data.1, configuration: configuration)
|
|
||||||
}
|
|
||||||
|
|
||||||
func timeline(for configuration: AccountAppIntent, in context: Context) async -> Timeline<SimpleEntry> {
|
|
||||||
var entries: [SimpleEntry] = []
|
|
||||||
|
|
||||||
let data = await getData(configuration: configuration)
|
|
||||||
|
|
||||||
// Generate a timeline consisting of two entries an hour apart, starting from the current date.
|
|
||||||
let currentDate = Date()
|
|
||||||
for hourOffset in 0 ..< 2 {
|
|
||||||
let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
|
|
||||||
let entry = SimpleEntry(date: entryDate, pfp: data.0, followers: data.1, configuration: configuration)
|
|
||||||
entries.append(entry)
|
|
||||||
}
|
|
||||||
|
|
||||||
return Timeline(entries: entries, policy: .atEnd)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func getData(configuration: AccountAppIntent) async -> (UIImage, Int) {
|
|
||||||
var pfp: UIImage = UIImage(systemName: "person.crop.circle") ?? UIImage()
|
|
||||||
pfp.withTintColor(UIColor.label)
|
|
||||||
if let account = configuration.account {
|
|
||||||
do {
|
|
||||||
let acc = try await account.client.getString(endpoint: Accounts.verifyCredentials, forceVersion: .v1)
|
|
||||||
|
|
||||||
if let serialized: [String : Any] = try JSONSerialization.jsonObject(with: acc.data(using: String.Encoding.utf8) ?? Data()) as? [String : Any] {
|
|
||||||
let avatar: String = serialized["avatar"] as! String
|
|
||||||
let task = try await URLSession.shared.data(from: URL(string: avatar)!)
|
|
||||||
pfp = UIImage(data: task.0) ?? UIImage()
|
|
||||||
|
|
||||||
let followers: Int = serialized["followers_count"] as! Int
|
|
||||||
return (pfp, followers)
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
print(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return (pfp, 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct SimpleEntry: TimelineEntry {
|
|
||||||
let date: Date
|
|
||||||
let pfp: UIImage
|
|
||||||
let followers: Int
|
|
||||||
let configuration: AccountAppIntent
|
|
||||||
}
|
|
||||||
|
|
||||||
struct FollowCountWidgetView: View {
|
struct FollowCountWidgetView: View {
|
||||||
@Environment(\.widgetFamily) private var family: WidgetFamily
|
@Environment(\.widgetFamily) private var family: WidgetFamily
|
||||||
var entry: Provider.Entry
|
var entry: FollowCountWidget.Provider.Entry
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if let account = entry.configuration.account {
|
if let account = entry.configuration.account {
|
||||||
|
@ -157,6 +99,7 @@ struct FollowCountWidgetView: View {
|
||||||
.multilineTextAlignment(.leading)
|
.multilineTextAlignment(.leading)
|
||||||
.font(.system(size: 32, weight: .bold).monospacedDigit())
|
.font(.system(size: 32, weight: .bold).monospacedDigit())
|
||||||
.contentTransition(.numericText())
|
.contentTransition(.numericText())
|
||||||
|
.redacted(reason: .privacy)
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 7.5)
|
.padding(.horizontal, 7.5)
|
||||||
|
|
||||||
|
@ -183,4 +126,62 @@ struct FollowCountWidget: Widget {
|
||||||
.description("widget.follow-count.description")
|
.description("widget.follow-count.description")
|
||||||
.supportedFamilies([.systemSmall, .systemMedium, .accessoryRectangular])
|
.supportedFamilies([.systemSmall, .systemMedium, .accessoryRectangular])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct Provider: AppIntentTimelineProvider {
|
||||||
|
func placeholder(in context: Context) -> SimpleEntry {
|
||||||
|
let placeholder: UIImage = UIImage(systemName: "person.crop.circle") ?? UIImage()
|
||||||
|
placeholder.withTintColor(UIColor.label)
|
||||||
|
return SimpleEntry(date: Date(), pfp: placeholder, followers: 38, configuration: AccountAppIntent())
|
||||||
|
}
|
||||||
|
|
||||||
|
func snapshot(for configuration: AccountAppIntent, in context: Context) async -> SimpleEntry {
|
||||||
|
let data = await getData(configuration: configuration)
|
||||||
|
return SimpleEntry(date: Date(), pfp: data.0, followers: data.1, configuration: configuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
func timeline(for configuration: AccountAppIntent, in context: Context) async -> Timeline<SimpleEntry> {
|
||||||
|
var entries: [SimpleEntry] = []
|
||||||
|
|
||||||
|
let data = await getData(configuration: configuration)
|
||||||
|
|
||||||
|
// Generate a timeline consisting of two entries an hour apart, starting from the current date.
|
||||||
|
let currentDate = Date()
|
||||||
|
for hourOffset in 0 ..< 2 {
|
||||||
|
let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
|
||||||
|
let entry = SimpleEntry(date: entryDate, pfp: data.0, followers: data.1, configuration: configuration)
|
||||||
|
entries.append(entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Timeline(entries: entries, policy: .atEnd)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getData(configuration: AccountAppIntent) async -> (UIImage, Int) {
|
||||||
|
var pfp: UIImage = UIImage(systemName: "person.crop.circle") ?? UIImage()
|
||||||
|
pfp.withTintColor(UIColor.label)
|
||||||
|
if let account = configuration.account {
|
||||||
|
do {
|
||||||
|
let acc = try await account.client.getString(endpoint: Accounts.verifyCredentials, forceVersion: .v1)
|
||||||
|
|
||||||
|
if let serialized: [String : Any] = try JSONSerialization.jsonObject(with: acc.data(using: String.Encoding.utf8) ?? Data()) as? [String : Any] {
|
||||||
|
let avatar: String = serialized["avatar"] as! String
|
||||||
|
let task = try await URLSession.shared.data(from: URL(string: avatar)!)
|
||||||
|
pfp = UIImage(data: task.0) ?? UIImage()
|
||||||
|
|
||||||
|
let followers: Int = serialized["followers_count"] as! Int
|
||||||
|
return (pfp, followers)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (pfp, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SimpleEntry: TimelineEntry {
|
||||||
|
let date: Date
|
||||||
|
let pfp: UIImage
|
||||||
|
let followers: Int
|
||||||
|
let configuration: AccountAppIntent
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,198 @@
|
||||||
|
//Made by Lumaa
|
||||||
|
|
||||||
|
import WidgetKit
|
||||||
|
import SwiftUI
|
||||||
|
import SwiftData
|
||||||
|
|
||||||
|
struct FollowGoalWidgetView: View {
|
||||||
|
@Environment(\.widgetFamily) private var family: WidgetFamily
|
||||||
|
var entry: FollowGoalWidget.Provider.Entry
|
||||||
|
|
||||||
|
var maxGauge: Double {
|
||||||
|
return entry.followers >= entry.configuration.goal ? Double(entry.followers) : Double(entry.configuration.goal)
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
if let account = entry.configuration.account {
|
||||||
|
ZStack {
|
||||||
|
if family == WidgetFamily.systemMedium {
|
||||||
|
medium
|
||||||
|
} else if family == WidgetFamily.accessoryRectangular {
|
||||||
|
rectangular
|
||||||
|
} else if family == WidgetFamily.accessoryCircular {
|
||||||
|
circular
|
||||||
|
} else {
|
||||||
|
Text(String("Unsupported WidgetFamily"))
|
||||||
|
.font(.caption)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.modelContainer(for: [LoggedAccount.self])
|
||||||
|
} else {
|
||||||
|
Text("widget.select-account")
|
||||||
|
.font(.caption)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var medium: some View {
|
||||||
|
VStack(alignment: .center) {
|
||||||
|
HStack(alignment: .center, spacing: 7.5) {
|
||||||
|
Image(uiImage: entry.pfp)
|
||||||
|
.resizable()
|
||||||
|
.scaledToFit()
|
||||||
|
.frame(width: 30, height: 30)
|
||||||
|
.foregroundStyle(Color.white)
|
||||||
|
.clipShape(Circle())
|
||||||
|
|
||||||
|
Text("@\(entry.configuration.account!.username)")
|
||||||
|
.redacted(reason: .privacy)
|
||||||
|
.font(.caption.bold())
|
||||||
|
.lineLimit(1)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 7.5)
|
||||||
|
|
||||||
|
|
||||||
|
HStack(alignment: .center, spacing: 7.5) {
|
||||||
|
Text(entry.followers, format: .number.notation(.compactName))
|
||||||
|
.font(.title.monospacedDigit().bold())
|
||||||
|
.contentTransition(.numericText())
|
||||||
|
Text("widget.followers")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(Color.gray)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 7.5)
|
||||||
|
|
||||||
|
Gauge(value: Double(entry.followers), in: 0...maxGauge) {
|
||||||
|
EmptyView()
|
||||||
|
} currentValueLabel: {
|
||||||
|
EmptyView()
|
||||||
|
} minimumValueLabel: {
|
||||||
|
Text(0, format: .number.notation(.compactName))
|
||||||
|
} maximumValueLabel: {
|
||||||
|
Text(entry.configuration.goal, format: .number.notation(.compactName))
|
||||||
|
}
|
||||||
|
.gaugeStyle(.linearCapacity)
|
||||||
|
.tint(Double(entry.followers) >= maxGauge ? Color.green : Color.blue)
|
||||||
|
.labelsHidden()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var rectangular: some View {
|
||||||
|
Gauge(value: Double(entry.followers), in: 0...maxGauge) {
|
||||||
|
Text("@\(entry.configuration.account!.username)")
|
||||||
|
.multilineTextAlignment(.leading)
|
||||||
|
.font(.caption)
|
||||||
|
.opacity(0.7)
|
||||||
|
} currentValueLabel: {
|
||||||
|
HStack {
|
||||||
|
Text(entry.followers, format: .number.notation(.compactName))
|
||||||
|
.font(.caption.monospacedDigit().bold())
|
||||||
|
.contentTransition(.numericText())
|
||||||
|
Text("widget.followers")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(Color.gray)
|
||||||
|
}
|
||||||
|
} minimumValueLabel: {
|
||||||
|
Text(0, format: .number.notation(.compactName))
|
||||||
|
} maximumValueLabel: {
|
||||||
|
Text(entry.configuration.goal, format: .number.notation(.compactName))
|
||||||
|
}
|
||||||
|
.gaugeStyle(.accessoryLinearCapacity)
|
||||||
|
}
|
||||||
|
|
||||||
|
var circular: some View {
|
||||||
|
Gauge(value: Double(entry.followers), in: 0...maxGauge) {
|
||||||
|
EmptyView()
|
||||||
|
} currentValueLabel: {
|
||||||
|
Text(entry.followers, format: .number.notation(.compactName))
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
} minimumValueLabel: {
|
||||||
|
EmptyView()
|
||||||
|
} maximumValueLabel: {
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
.gaugeStyle(.accessoryCircularCapacity)
|
||||||
|
.tint(Double(entry.followers) >= maxGauge ? Color.green : Color.blue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FollowGoalWidget: Widget {
|
||||||
|
let kind: String = "FollowGoalWidget"
|
||||||
|
let modelContainer: ModelContainer
|
||||||
|
|
||||||
|
init() {
|
||||||
|
guard let modelContainer: ModelContainer = try? .init(for: LoggedAccount.self, configurations: ModelConfiguration(isStoredInMemoryOnly: true)) else { fatalError("Couldn't get LoggedAccounts") }
|
||||||
|
self.modelContainer = modelContainer
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some WidgetConfiguration {
|
||||||
|
AppIntentConfiguration(kind: kind, intent: AccountGoalAppIntent.self, provider: Provider()) { entry in
|
||||||
|
FollowGoalWidgetView(entry: entry)
|
||||||
|
.containerBackground(Color("WidgetBackground"), for: .widget)
|
||||||
|
}
|
||||||
|
.configurationDisplayName("widget.follow-goal")
|
||||||
|
.description("widget.follow-goal.description")
|
||||||
|
.supportedFamilies([.systemMedium, .accessoryRectangular, .accessoryCircular])
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Provider: AppIntentTimelineProvider {
|
||||||
|
func placeholder(in context: Context) -> SimpleEntry {
|
||||||
|
let placeholder: UIImage = UIImage(systemName: "person.crop.circle") ?? UIImage()
|
||||||
|
placeholder.withTintColor(UIColor.label)
|
||||||
|
return SimpleEntry(date: Date(), pfp: placeholder, followers: 38, configuration: AccountGoalAppIntent())
|
||||||
|
}
|
||||||
|
|
||||||
|
func snapshot(for configuration: AccountGoalAppIntent, in context: Context) async -> SimpleEntry {
|
||||||
|
let data = await getData(configuration: configuration)
|
||||||
|
return SimpleEntry(date: Date(), pfp: data.0, followers: data.1, configuration: configuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
func timeline(for configuration: AccountGoalAppIntent, in context: Context) async -> Timeline<SimpleEntry> {
|
||||||
|
var entries: [SimpleEntry] = []
|
||||||
|
|
||||||
|
let data = await getData(configuration: configuration)
|
||||||
|
|
||||||
|
// Generate a timeline consisting of two entries an hour apart, starting from the current date.
|
||||||
|
let currentDate = Date()
|
||||||
|
for hourOffset in 0 ..< 2 {
|
||||||
|
let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
|
||||||
|
let entry = SimpleEntry(date: entryDate, pfp: data.0, followers: data.1, configuration: configuration)
|
||||||
|
entries.append(entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Timeline(entries: entries, policy: .atEnd)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getData(configuration: AccountGoalAppIntent) async -> (UIImage, Int) {
|
||||||
|
var pfp: UIImage = UIImage(systemName: "person.crop.circle") ?? UIImage()
|
||||||
|
pfp.withTintColor(UIColor.label)
|
||||||
|
if let account = configuration.account {
|
||||||
|
do {
|
||||||
|
let acc = try await account.client.getString(endpoint: Accounts.verifyCredentials, forceVersion: .v1)
|
||||||
|
|
||||||
|
if let serialized: [String : Any] = try JSONSerialization.jsonObject(with: acc.data(using: String.Encoding.utf8) ?? Data()) as? [String : Any] {
|
||||||
|
let avatar: String = serialized["avatar"] as! String
|
||||||
|
let task = try await URLSession.shared.data(from: URL(string: avatar)!)
|
||||||
|
pfp = UIImage(data: task.0) ?? UIImage()
|
||||||
|
|
||||||
|
let followers: Int = serialized["followers_count"] as! Int
|
||||||
|
return (pfp, followers)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (pfp, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SimpleEntry: TimelineEntry {
|
||||||
|
let date: Date
|
||||||
|
let pfp: UIImage
|
||||||
|
let followers: Int
|
||||||
|
let configuration: AccountGoalAppIntent
|
||||||
|
}
|
||||||
|
}
|
|
@ -34,6 +34,26 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"widget.follow-goal" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Follower Goal"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"widget.follow-goal.description" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Set a follower goal and see the progress"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"widget.followers" : {
|
"widget.followers" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
|
@ -53,6 +73,16 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"widget.set-goal" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Goal"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"version" : "1.0"
|
"version" : "1.0"
|
||||||
|
|
|
@ -8,5 +8,6 @@ import UIKit
|
||||||
struct ThreadedWidgetsBundle: WidgetBundle {
|
struct ThreadedWidgetsBundle: WidgetBundle {
|
||||||
var body: some Widget {
|
var body: some Widget {
|
||||||
FollowCountWidget()
|
FollowCountWidget()
|
||||||
|
FollowGoalWidget()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue