feat(Widget): Implement lockscreen widgets
This commit is contained in:
parent
4e591bcd1c
commit
aeaa3ea3ab
|
@ -24,6 +24,7 @@
|
|||
27D701F5292FC2D60031BCBB /* DataSourceFacade+URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27D701F4292FC2D60031BCBB /* DataSourceFacade+URL.swift */; };
|
||||
2A1FE47C2938BB2600784BF1 /* FollowedTagsViewModel+DiffableDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A1FE47B2938BB2600784BF1 /* FollowedTagsViewModel+DiffableDataSource.swift */; };
|
||||
2A1FE47E2938C11200784BF1 /* Collection+IsNotEmpty.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A1FE47D2938C11200784BF1 /* Collection+IsNotEmpty.swift */; };
|
||||
2A33AB662982C4AF008A7FB1 /* FollowCountWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A33AB652982C4AF008A7FB1 /* FollowCountWidgetView.swift */; };
|
||||
2A3F6FE3292ECB5E002E6DA7 /* FollowedTagsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A3F6FE2292ECB5E002E6DA7 /* FollowedTagsViewModel.swift */; };
|
||||
2A3F6FE5292F6E44002E6DA7 /* FollowedTagsTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A3F6FE4292F6E44002E6DA7 /* FollowedTagsTableViewCell.swift */; };
|
||||
2A506CF4292CD85800059C37 /* FollowedTagsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A506CF3292CD85800059C37 /* FollowedTagsViewController.swift */; };
|
||||
|
@ -605,6 +606,7 @@
|
|||
2A1FE47B2938BB2600784BF1 /* FollowedTagsViewModel+DiffableDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FollowedTagsViewModel+DiffableDataSource.swift"; sourceTree = "<group>"; };
|
||||
2A1FE47D2938C11200784BF1 /* Collection+IsNotEmpty.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection+IsNotEmpty.swift"; sourceTree = "<group>"; };
|
||||
2A33625329759B4200481A90 /* OpenInActionExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = OpenInActionExtension.entitlements; sourceTree = "<group>"; };
|
||||
2A33AB652982C4AF008A7FB1 /* FollowCountWidgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowCountWidgetView.swift; sourceTree = "<group>"; };
|
||||
2A3F6FE2292ECB5E002E6DA7 /* FollowedTagsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowedTagsViewModel.swift; sourceTree = "<group>"; };
|
||||
2A3F6FE4292F6E44002E6DA7 /* FollowedTagsTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowedTagsTableViewCell.swift; sourceTree = "<group>"; };
|
||||
2A506CF3292CD85800059C37 /* FollowedTagsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowedTagsViewController.swift; sourceTree = "<group>"; };
|
||||
|
@ -1372,6 +1374,14 @@
|
|||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
2A33AB642982C4A3008A7FB1 /* WidgetViews */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
2A33AB652982C4AF008A7FB1 /* FollowCountWidgetView.swift */,
|
||||
);
|
||||
path = WidgetViews;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
2A506CF2292CD83B00059C37 /* FollowedTags */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -1398,6 +1408,7 @@
|
|||
2A728125297EA9D7004138C5 /* WidgetExtension */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
2A33AB642982C4A3008A7FB1 /* WidgetViews */,
|
||||
2AE202A9297FDDF500F66E55 /* WidgetExtension.entitlements */,
|
||||
2A72813E297EC762004138C5 /* WidgetExtension.swift */,
|
||||
2A728126297EA9D7004138C5 /* WidgetExtensionBundle.swift */,
|
||||
|
@ -3427,6 +3438,7 @@
|
|||
files = (
|
||||
2A728130297EA9D8004138C5 /* WidgetExtension.intentdefinition in Sources */,
|
||||
2A72813F297EC762004138C5 /* WidgetExtension.swift in Sources */,
|
||||
2A33AB662982C4AF008A7FB1 /* FollowCountWidgetView.swift in Sources */,
|
||||
2A728127297EA9D7004138C5 /* WidgetExtensionBundle.swift in Sources */,
|
||||
2A72812B297EA9D7004138C5 /* FollowersWidgetExtension.swift in Sources */,
|
||||
);
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Logo.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "Logo 1.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "Logo@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "Logo@2x 1.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "Logo@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "Logo@3x 1.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 284 B |
Binary file not shown.
After Width: | Height: | Size: 286 B |
Binary file not shown.
After Width: | Height: | Size: 484 B |
Binary file not shown.
After Width: | Height: | Size: 444 B |
Binary file not shown.
After Width: | Height: | Size: 694 B |
Binary file not shown.
After Width: | Height: | Size: 636 B |
|
@ -17,7 +17,7 @@ struct FollowersProvider: IntentTimelineProvider {
|
|||
loadCurrentEntry(for: configuration, in: context, completion: completion)
|
||||
}
|
||||
|
||||
func getTimeline(for configuration: FollowersCountIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
|
||||
func getTimeline(for configuration: FollowersCountIntent, in context: Context, completion: @escaping (Timeline<FollowersEntry>) -> ()) {
|
||||
loadCurrentEntry(for: configuration, in: context) { entry in
|
||||
completion(Timeline(entries: [entry], policy: .after(.now)))
|
||||
}
|
||||
|
@ -51,69 +51,21 @@ struct FollowersEntry: TimelineEntry {
|
|||
}
|
||||
}
|
||||
|
||||
struct FollowersWidgetExtensionEntryView : View {
|
||||
var entry: FollowersProvider.Entry
|
||||
|
||||
var body: some View {
|
||||
if let account = entry.account {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
if let avatarImage = account.avatarImage {
|
||||
Image(uiImage: avatarImage)
|
||||
.resizable()
|
||||
.frame(width: 50, height: 50)
|
||||
.cornerRadius(12)
|
||||
.padding(.bottom, 8)
|
||||
}
|
||||
|
||||
Text(account.followersCount.asAbbreviatedCountString())
|
||||
.font(.largeTitle)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
|
||||
Text(account.displayNameWithFallback)
|
||||
.font(.system(size: 13))
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
|
||||
Text("@\(account.acct)")
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
}
|
||||
.padding(.leading, 20)
|
||||
.padding([.top, .bottom], 16)
|
||||
Spacer()
|
||||
}
|
||||
} else {
|
||||
Text("Please use the Widget settings to select an Account.")
|
||||
.multilineTextAlignment(.center)
|
||||
.font(.caption)
|
||||
.padding(.all, 20)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct FollowersWidgetExtension: Widget {
|
||||
private var availableFamilies: [WidgetFamily] {
|
||||
if #available(iOS 16, *) {
|
||||
return [.systemSmall, .accessoryRectangular, .accessoryCircular]
|
||||
}
|
||||
return [.systemSmall]
|
||||
}
|
||||
|
||||
var body: some WidgetConfiguration {
|
||||
IntentConfiguration(kind: "Followers", intent: FollowersCountIntent.self, provider: FollowersProvider()) { entry in
|
||||
FollowersWidgetExtensionEntryView(entry: entry)
|
||||
FollowCountWidgetView(entry: entry)
|
||||
}
|
||||
.configurationDisplayName("Followers")
|
||||
.description("Show number of followers.")
|
||||
.supportedFamilies([.systemSmall])
|
||||
}
|
||||
}
|
||||
|
||||
struct WidgetExtension_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
FollowersWidgetExtensionEntryView(entry: FollowersEntry(
|
||||
date: Date(),
|
||||
account: nil,
|
||||
configuration: FollowersCountIntent())
|
||||
)
|
||||
.previewContext(WidgetPreviewContext(family: .systemSmall))
|
||||
.supportedFamilies(availableFamilies)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,98 @@
|
|||
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
|
||||
|
||||
import SwiftUI
|
||||
import WidgetKit
|
||||
|
||||
struct FollowCountWidgetView: View {
|
||||
@Environment(\.widgetFamily) var family
|
||||
|
||||
var entry: FollowersProvider.Entry
|
||||
|
||||
var body: some View {
|
||||
if let account = entry.account {
|
||||
switch family {
|
||||
case .systemSmall:
|
||||
viewForSmallWidget(account)
|
||||
case .accessoryRectangular:
|
||||
viewForAccessoryRectangular(account)
|
||||
case .accessoryCircular:
|
||||
viewForAccessoryCircular(account)
|
||||
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(_ account: FollowersEntryAccountable) -> some View {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
if let avatarImage = account.avatarImage {
|
||||
Image(uiImage: avatarImage)
|
||||
.resizable()
|
||||
.frame(width: 50, height: 50)
|
||||
.cornerRadius(12)
|
||||
.padding(.bottom, 8)
|
||||
}
|
||||
|
||||
Text(account.followersCount.asAbbreviatedCountString())
|
||||
.font(.largeTitle)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
|
||||
Text(account.displayNameWithFallback)
|
||||
.font(.system(size: 13))
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
|
||||
Text("@\(account.acct)")
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
}
|
||||
.padding(.leading, 20)
|
||||
.padding([.top, .bottom], 16)
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
|
||||
private func viewForAccessoryRectangular(_ account :FollowersEntryAccountable) -> some View {
|
||||
HStack(spacing: 0) {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
HStack(alignment: .center) {
|
||||
Image("BrandIcon")
|
||||
Text("FOLLOWERS")
|
||||
.font(.system(size: 15, weight: .semibold))
|
||||
}
|
||||
.padding(.top, 6)
|
||||
|
||||
Text(account.followersCount.asAbbreviatedCountString())
|
||||
.font(.system(size: 43))
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
|
||||
private func viewForAccessoryCircular(_ account :FollowersEntryAccountable) -> some View {
|
||||
ZStack {
|
||||
if #available(iOS 16, *) {
|
||||
AccessoryWidgetBackground()
|
||||
}
|
||||
VStack {
|
||||
Image("BrandIcon")
|
||||
|
||||
Text(account.followersCount.asAbbreviatedCountString())
|
||||
.font(.system(size: 15))
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue