From aeaa3ea3ab3ca885b6076e2b60e5259944fddddb Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Thu, 26 Jan 2023 16:22:18 +0100 Subject: [PATCH] feat(Widget): Implement lockscreen widgets --- Mastodon.xcodeproj/project.pbxproj | 12 +++ .../BrandIcon.imageset/Contents.json | 56 ++++++++++ .../BrandIcon.imageset/Logo 1.png | Bin 0 -> 284 bytes .../BrandIcon.imageset/Logo.png | Bin 0 -> 286 bytes .../BrandIcon.imageset/Logo@2x 1.png | Bin 0 -> 484 bytes .../BrandIcon.imageset/Logo@2x.png | Bin 0 -> 444 bytes .../BrandIcon.imageset/Logo@3x 1.png | Bin 0 -> 694 bytes .../BrandIcon.imageset/Logo@3x.png | Bin 0 -> 636 bytes .../FollowersWidgetExtension.swift | 68 ++---------- .../WidgetViews/FollowCountWidgetView.swift | 98 ++++++++++++++++++ 10 files changed, 176 insertions(+), 58 deletions(-) create mode 100644 WidgetExtension/Assets.xcassets/BrandIcon.imageset/Contents.json create mode 100644 WidgetExtension/Assets.xcassets/BrandIcon.imageset/Logo 1.png create mode 100644 WidgetExtension/Assets.xcassets/BrandIcon.imageset/Logo.png create mode 100644 WidgetExtension/Assets.xcassets/BrandIcon.imageset/Logo@2x 1.png create mode 100644 WidgetExtension/Assets.xcassets/BrandIcon.imageset/Logo@2x.png create mode 100644 WidgetExtension/Assets.xcassets/BrandIcon.imageset/Logo@3x 1.png create mode 100644 WidgetExtension/Assets.xcassets/BrandIcon.imageset/Logo@3x.png create mode 100644 WidgetExtension/WidgetViews/FollowCountWidgetView.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 31a895c05..d1fb8b234 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -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 = ""; }; 2A1FE47D2938C11200784BF1 /* Collection+IsNotEmpty.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection+IsNotEmpty.swift"; sourceTree = ""; }; 2A33625329759B4200481A90 /* OpenInActionExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = OpenInActionExtension.entitlements; sourceTree = ""; }; + 2A33AB652982C4AF008A7FB1 /* FollowCountWidgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowCountWidgetView.swift; sourceTree = ""; }; 2A3F6FE2292ECB5E002E6DA7 /* FollowedTagsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowedTagsViewModel.swift; sourceTree = ""; }; 2A3F6FE4292F6E44002E6DA7 /* FollowedTagsTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowedTagsTableViewCell.swift; sourceTree = ""; }; 2A506CF3292CD85800059C37 /* FollowedTagsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowedTagsViewController.swift; sourceTree = ""; }; @@ -1372,6 +1374,14 @@ path = Pods; sourceTree = ""; }; + 2A33AB642982C4A3008A7FB1 /* WidgetViews */ = { + isa = PBXGroup; + children = ( + 2A33AB652982C4AF008A7FB1 /* FollowCountWidgetView.swift */, + ); + path = WidgetViews; + sourceTree = ""; + }; 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 */, ); diff --git a/WidgetExtension/Assets.xcassets/BrandIcon.imageset/Contents.json b/WidgetExtension/Assets.xcassets/BrandIcon.imageset/Contents.json new file mode 100644 index 000000000..e3acfa8de --- /dev/null +++ b/WidgetExtension/Assets.xcassets/BrandIcon.imageset/Contents.json @@ -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 + } +} diff --git a/WidgetExtension/Assets.xcassets/BrandIcon.imageset/Logo 1.png b/WidgetExtension/Assets.xcassets/BrandIcon.imageset/Logo 1.png new file mode 100644 index 0000000000000000000000000000000000000000..0cd6d0b8b956ff9e1f425e44d4124104ce7980ba GIT binary patch literal 284 zcmeAS@N?(olHy`uVBq!ia0vp^JRmj)8<3o<+3y6TI14-?iy0WWg+Z8+Vb&Z8pde#$ zkh>GZx^prwfgF}}M_)$E)e-c@N{EeP2jv*C{Qv(n39x~u55%_Q9Wp#}@UuYhvCB9TzlQ@#Di3EUrBuwaJlM4zV({9;DJ-j7f3SoNiM<6rRtXJ*?;%NKq) zd*9$ii3|T6&x)p~OwX5(w{5YUVpn6d|DM2x$_<;E%S`XB{GZJ6(9kRCTW8d%W5KnT ZrL|9_8vdE`ni1%D22WQ%mvv4FO#p`2XQ2Q9 literal 0 HcmV?d00001 diff --git a/WidgetExtension/Assets.xcassets/BrandIcon.imageset/Logo.png b/WidgetExtension/Assets.xcassets/BrandIcon.imageset/Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..12f3c23d34370c29fe24be9f1c5c5d5946453bd8 GIT binary patch literal 286 zcmeAS@N?(olHy`uVBq!ia0vp^JRmj)8<3o<+3y6TI14-?iy0WWg+Z8+Vb&Z8pde#$ zkh>GZx^prwfgF}}M_)$E)e-c@N{LP*&jv*C{Qzz~gJmkRRV(O50f>@+ zgU~t_uLZ&{8iX_swg~KGdGX-J=5RSh9_CqSwNbT`bYAdQhBfQ(Lwq@x1Ydq=Mit=2_?Fg!@J<_}csA{EPv`TCAFb~zZQR-y evaRaF+1gKw literal 0 HcmV?d00001 diff --git a/WidgetExtension/Assets.xcassets/BrandIcon.imageset/Logo@2x 1.png b/WidgetExtension/Assets.xcassets/BrandIcon.imageset/Logo@2x 1.png new file mode 100644 index 0000000000000000000000000000000000000000..496d567732aee87f2b2b2b3ac4a9a34d0d2cfa4a GIT binary patch literal 484 zcmV06OwPn4f zC{_y!V6CnAamUom(oJiU(R)wBJQPLIik9qHGmrNF=$N(SytB(<2j7Jg=n`r8E0D6= zEj!;woVUz_GsL^N_v|qD$~?Ocl0fZk`1a=Cy!ppqkMe{k*a(ptZ>+qrj*LUQ2{yDc zAkfo#Fl$F(wlj=NuY}9oO%O1dws?^h%|kB!Td%1`WA%VP=1Q$;)$Ej zHAQDy7REV$!4C+0CXB~5REH}&300000HV*BT$_x!n&gXdmyJ``}y`OF1MJ10&daI}iSN0@5k z*(E{H#|JiU6(8>;a1;hQEMbDk06FP<$oar2>FlAS2{YDU1(A!4mW}> m9*h4x@Q6wL7#y2xPJRKKoA^XREjLC00000<3V9i0_IhdksL|B#DCU(0sTX;?wRBy1`CCL z0A^-v%-k}a>{rT;_&OGjS)claL3d{vYs-qC289HRy-14~=qmIVV1ab@rtxgf9DxI* zx5}T+P1mnIK*Jyp_5PF6jyjb(V?ExvdDc$UekcdwYgA7~J%Y-=cIEY2KRYU#OIt-ml!MSr1frujbIpd-+8k?IjNXYx&P--qJbT&ky_!DNt3pt^=lr&sBXrFsS( z7^K6u)LX#@WiTLyW>DJ2E9;cO8T17A6XNZ+?k=ZhgHo`6g-z_CJF=m@vfQ8x42QGH zhbGcDXaax7m--4ddWSM}7y=x1OHs}QXv!MY?=pxb_wpe0Z1+9;RP4{au^>C;)4@f0`5;O5|*la;wXOZ{YREXs)6Qj@5}pg6-i|%Lf@jn=w6CMXd65)rqXU`LTbU cbeo@vA7dL)HB<7XWdHyG07*qoM6N<$g4tL;*8l(j literal 0 HcmV?d00001 diff --git a/WidgetExtension/Assets.xcassets/BrandIcon.imageset/Logo@3x.png b/WidgetExtension/Assets.xcassets/BrandIcon.imageset/Logo@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..02abd6055f1512d0798b57dc63e1482894f26d6f GIT binary patch literal 636 zcmV-?0)zdDP)K~#7F%~;`i zf-n$$_Ur$$fpLPeLFoo;pc~XqP&$EbP&d#GXeY2v08Aho*qnxvrpXZmmG^xQl5iw1 z6*}yWi*M{xS+j1)A}SdOJh(nUzaLYNJ3M`dSzDYF|bn6++g~5jUrymy2{~r9*C*;H zYY*xksb>>(fIz=TGZ}h90zXIh8RFg`aN71}X9%IrEYZ=2I7EuJ6IpZe+o*Kw9nBB2 W-0yoRL)mx$0000) -> ()) { + func getTimeline(for configuration: FollowersCountIntent, in context: Context, completion: @escaping (Timeline) -> ()) { 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) } } diff --git a/WidgetExtension/WidgetViews/FollowCountWidgetView.swift b/WidgetExtension/WidgetViews/FollowCountWidgetView.swift new file mode 100644 index 000000000..c040f8d75 --- /dev/null +++ b/WidgetExtension/WidgetViews/FollowCountWidgetView.swift @@ -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) + } + } + } +}