Merge pull request #915 from mastodon/IOS-37_Widgets

Feature: Followers, Multiple Followers, Latest Followers Widget
This commit is contained in:
Marcus Kida 2023-02-07 13:54:47 +01:00 committed by GitHub
commit a6dc97b22b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
62 changed files with 2827 additions and 2 deletions

3
.gitignore vendored
View File

@ -131,3 +131,6 @@ env/**/**
## Ruby ###
vendor/
.bundle/
## IntelliJ IDEA ##
.idea

View File

@ -816,5 +816,31 @@
"open_in": {
"invalid_link_error": "This doesn't seem to be a valid Mastodon link."
}
},
"widget": {
"common": {
"unsupported_widget_family": "Sorry but this Widget family is unsupported.",
"user_not_logged_in": "Please open Mastodon to log in to an Account."
},
"followers_count": {
"configuration_display_name": "Followers",
"configuration_description": "Show number of followers.",
"title": "FOLLOWERS",
"followers_today": "%s followers today"
},
"multiple_followers": {
"configuration_display_name": "Multiple followers",
"configuration_description": "Show number of followers for multiple accounts.",
"mock_user": {
"display_name": "Another follower",
"account_name": "another@follower.social"
}
},
"latest_followers": {
"configuration_display_name": "Latest followers",
"configuration_description": "Show latest followers.",
"title": "Latest followers",
"last_update": "Last update: %s"
}
}
}

View File

@ -24,6 +24,8 @@
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 */; };
2A33062D2987DBFA001D4C51 /* FollowersCountHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A33062C2987DBFA001D4C51 /* FollowersCountHistory.swift */; };
2A33AB662982C4AF008A7FB1 /* FollowersCountWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A33AB652982C4AF008A7FB1 /* FollowersCountWidgetView.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 */; };
@ -33,10 +35,29 @@
2A71F541296DBDA80049F54A /* Media.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2A71F53D296DBDA80049F54A /* Media.xcassets */; };
2A71F542296DBDA80049F54A /* Action.js in Resources */ = {isa = PBXBuildFile; fileRef = 2A71F53E296DBDA80049F54A /* Action.js */; };
2A71F543296DBDA80049F54A /* ActionRequestHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A71F53F296DBDA80049F54A /* ActionRequestHandler.swift */; };
2A728122297EA9D7004138C5 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2A728121297EA9D7004138C5 /* WidgetKit.framework */; };
2A728124297EA9D7004138C5 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2A728123297EA9D7004138C5 /* SwiftUI.framework */; };
2A728127297EA9D7004138C5 /* WidgetExtensionBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A728126297EA9D7004138C5 /* WidgetExtensionBundle.swift */; };
2A72812B297EA9D7004138C5 /* FollowersCountWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A72812A297EA9D7004138C5 /* FollowersCountWidget.swift */; };
2A72812E297EA9D8004138C5 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2A72812D297EA9D8004138C5 /* Assets.xcassets */; };
2A728134297EA9D8004138C5 /* WidgetExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 2A728120297EA9D7004138C5 /* WidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
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 */; };
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 */; };
2AB5011B2992322500346092 /* LightChart in Frameworks */ = {isa = PBXBuildFile; productRef = 2AB5011A2992322500346092 /* LightChart */; };
2AB5011C299243FB00346092 /* WidgetExtension.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 2AB50120299243FB00346092 /* WidgetExtension.intentdefinition */; };
2AB5011D299243FB00346092 /* WidgetExtension.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 2AB50120299243FB00346092 /* WidgetExtension.intentdefinition */; };
2AB5011E299243FB00346092 /* WidgetExtension.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 2AB50120299243FB00346092 /* WidgetExtension.intentdefinition */; };
2AE202AC297FE19100F66E55 /* FollowersCountIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AE202AB297FE19100F66E55 /* FollowersCountIntentHandler.swift */; };
2AE202AD297FE1CD00F66E55 /* WidgetExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A72813E297EC762004138C5 /* WidgetExtension.swift */; };
2AE244482927831100BDBF7C /* UIImage+SFSymbols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AE244472927831100BDBF7C /* UIImage+SFSymbols.swift */; };
2D198643261BF09500F0B013 /* SearchResultItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D198642261BF09500F0B013 /* SearchResultItem.swift */; };
2D198649261C0B8500F0B013 /* SearchResultSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D198648261C0B8500F0B013 /* SearchResultSection.swift */; };
@ -467,6 +488,13 @@
remoteGlobalIDString = 2A64515C29642A8A00CD8B8A;
remoteInfo = FollowActionExtension;
};
2A728132297EA9D8004138C5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = DB427DCA25BAA00100D1B89D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 2A72811F297EA9D7004138C5;
remoteInfo = WidgetExtensionExtension;
};
DB427DE925BAA00100D1B89D /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = DB427DCA25BAA00100D1B89D /* Project object */;
@ -505,6 +533,16 @@
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
2A72813D297EC6F7004138C5 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
2A90A159296EEE500026C155 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
@ -544,6 +582,7 @@
dstSubfolderSpec = 13;
files = (
DB8FABCE26AEC7B2008E5AF4 /* MastodonIntent.appex in Embed Foundation Extensions */,
2A728134297EA9D8004138C5 /* WidgetExtension.appex in Embed Foundation Extensions */,
2A64516929642A8B00CD8B8A /* OpenInActionExtension.appex in Embed Foundation Extensions */,
DBC6461C26A170AB00B0E31B /* ShareActionExtension.appex in Embed Foundation Extensions */,
DBF8AE1A263293E400C9C23C /* NotificationService.appex in Embed Foundation Extensions */,
@ -573,7 +612,9 @@
27D701F4292FC2D60031BCBB /* DataSourceFacade+URL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+URL.swift"; sourceTree = "<group>"; };
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>"; };
2A33062C2987DBFA001D4C51 /* FollowersCountHistory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowersCountHistory.swift; sourceTree = "<group>"; };
2A33625329759B4200481A90 /* OpenInActionExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = OpenInActionExtension.entitlements; sourceTree = "<group>"; };
2A33AB652982C4AF008A7FB1 /* FollowersCountWidgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowersCountWidgetView.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>"; };
@ -584,9 +625,51 @@
2A71F53E296DBDA80049F54A /* Action.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = Action.js; sourceTree = "<group>"; };
2A71F53F296DBDA80049F54A /* ActionRequestHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActionRequestHandler.swift; sourceTree = "<group>"; };
2A71F540296DBDA80049F54A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
2A728120297EA9D7004138C5 /* WidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
2A728121297EA9D7004138C5 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
2A728123297EA9D7004138C5 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; };
2A728126297EA9D7004138C5 /* WidgetExtensionBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetExtensionBundle.swift; sourceTree = "<group>"; };
2A72812A297EA9D7004138C5 /* FollowersCountWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowersCountWidget.swift; sourceTree = "<group>"; };
2A72812D297EA9D8004138C5 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
2A72812F297EA9D8004138C5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
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>"; };
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>"; };
2AB5011F299243FB00346092 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.intentdefinition; name = Base; path = Base.lproj/WidgetExtension.intentdefinition; sourceTree = "<group>"; };
2AB501222992440200346092 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/WidgetExtension.strings; sourceTree = "<group>"; };
2AB501242992443100346092 /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = vi; path = vi.lproj/WidgetExtension.strings; sourceTree = "<group>"; };
2AB501262992443100346092 /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/WidgetExtension.strings; sourceTree = "<group>"; };
2AB501282992443200346092 /* th */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = th; path = th.lproj/WidgetExtension.strings; sourceTree = "<group>"; };
2AB5012A2992443200346092 /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/WidgetExtension.strings; sourceTree = "<group>"; };
2AB5012C2992443300346092 /* es-AR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-AR"; path = "es-AR.lproj/WidgetExtension.strings"; sourceTree = "<group>"; };
2AB5012E2992443300346092 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/WidgetExtension.strings; sourceTree = "<group>"; };
2AB501302992443400346092 /* sl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sl; path = sl.lproj/WidgetExtension.strings; sourceTree = "<group>"; };
2AB501322992443400346092 /* gd */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = gd; path = gd.lproj/WidgetExtension.strings; sourceTree = "<group>"; };
2AB501342992443500346092 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/WidgetExtension.strings; sourceTree = "<group>"; };
2AB501362992443600346092 /* ckb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ckb; path = ckb.lproj/WidgetExtension.strings; sourceTree = "<group>"; };
2AB501382992443600346092 /* ku */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ku; path = ku.lproj/WidgetExtension.strings; sourceTree = "<group>"; };
2AB5013A2992443700346092 /* kab */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = kab; path = kab.lproj/WidgetExtension.strings; sourceTree = "<group>"; };
2AB5013C2992443700346092 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/WidgetExtension.strings; sourceTree = "<group>"; };
2AB5013E2992443800346092 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/WidgetExtension.strings; sourceTree = "<group>"; };
2AB501402992443800346092 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/WidgetExtension.strings; sourceTree = "<group>"; };
2AB501422992443900346092 /* gl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = gl; path = gl.lproj/WidgetExtension.strings; sourceTree = "<group>"; };
2AB501442992443900346092 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/WidgetExtension.strings; sourceTree = "<group>"; };
2AB501462992443900346092 /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/WidgetExtension.strings; sourceTree = "<group>"; };
2AB501482992443A00346092 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/WidgetExtension.strings; sourceTree = "<group>"; };
2AB5014A2992443A00346092 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/WidgetExtension.strings; sourceTree = "<group>"; };
2AB5014C2992443B00346092 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/WidgetExtension.strings"; sourceTree = "<group>"; };
2AB5014E2992443B00346092 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/WidgetExtension.strings"; sourceTree = "<group>"; };
2AB501502992443C00346092 /* ca */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ca; path = ca.lproj/WidgetExtension.strings; sourceTree = "<group>"; };
2AB501522992443C00346092 /* eu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = eu; path = eu.lproj/WidgetExtension.strings; sourceTree = "<group>"; };
2AB501542992443D00346092 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/WidgetExtension.strings; 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>"; };
2AE244472927831100BDBF7C /* UIImage+SFSymbols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+SFSymbols.swift"; sourceTree = "<group>"; };
2D198642261BF09500F0B013 /* SearchResultItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultItem.swift; sourceTree = "<group>"; };
2D198648261C0B8500F0B013 /* SearchResultSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultSection.swift; sourceTree = "<group>"; };
@ -1148,6 +1231,17 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
2A72811D297EA9D7004138C5 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
2AB5011B2992322500346092 /* LightChart in Frameworks */,
2A9D066F298D0FD100BF38CB /* MastodonSDKDynamic in Frameworks */,
2A728124297EA9D7004138C5 /* SwiftUI.framework in Frameworks */,
2A728122297EA9D7004138C5 /* WidgetKit.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
DB427DCF25BAA00100D1B89D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
@ -1343,6 +1437,58 @@
path = OpenInActionExtension;
sourceTree = "<group>";
};
2A728125297EA9D7004138C5 /* WidgetExtension */ = {
isa = PBXGroup;
children = (
2A86A14329892700007F1062 /* Variants */,
2AE202A9297FDDF500F66E55 /* WidgetExtension.entitlements */,
2A72813E297EC762004138C5 /* WidgetExtension.swift */,
2A728126297EA9D7004138C5 /* WidgetExtensionBundle.swift */,
2AB50120299243FB00346092 /* WidgetExtension.intentdefinition */,
2A72812D297EA9D8004138C5 /* Assets.xcassets */,
2A72812F297EA9D8004138C5 /* Info.plist */,
);
path = WidgetExtension;
sourceTree = "<group>";
};
2A86A14329892700007F1062 /* Variants */ = {
isa = PBXGroup;
children = (
2A86A14429892709007F1062 /* FollowersCount */,
2A86A14729892B1B007F1062 /* MultiFollowersCount */,
2A9D0662298C045000BF38CB /* LatestFollowers */,
);
path = Variants;
sourceTree = "<group>";
};
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>";
};
2A9D0662298C045000BF38CB /* LatestFollowers */ = {
isa = PBXGroup;
children = (
2A9D0663298C048800BF38CB /* LatestFollowersWidget.swift */,
2A9D0665298C05A800BF38CB /* LatestFollowersWidgetView.swift */,
);
path = LatestFollowers;
sourceTree = "<group>";
};
2D152A8A25C295B8009AA50C /* Content */ = {
isa = PBXGroup;
children = (
@ -1562,6 +1708,8 @@
DB8FAB9E26AEC3A2008E5AF4 /* Intents.framework */,
DB8FABA926AEC3A2008E5AF4 /* IntentsUI.framework */,
2A6451022964223800CD8B8A /* UniformTypeIdentifiers.framework */,
2A728121297EA9D7004138C5 /* WidgetKit.framework */,
2A728123297EA9D7004138C5 /* SwiftUI.framework */,
);
name = Frameworks;
sourceTree = "<group>";
@ -1880,6 +2028,7 @@
DBC6461326A170AB00B0E31B /* ShareActionExtension */,
DB8FABC826AEC7B2008E5AF4 /* MastodonIntent */,
2A71F53C296DBDA80049F54A /* OpenInActionExtension */,
2A728125297EA9D7004138C5 /* WidgetExtension */,
DB427DD325BAA00100D1B89D /* Products */,
1EBA4F56E920856A3FC84ACB /* Pods */,
3FE14AD363ED19AE7FF210A6 /* Frameworks */,
@ -1898,6 +2047,7 @@
DBC6461226A170AB00B0E31B /* ShareActionExtension.appex */,
DB8FABC626AEC7B2008E5AF4 /* MastodonIntent.appex */,
2A64515D29642A8A00CD8B8A /* OpenInActionExtension.appex */,
2A728120297EA9D7004138C5 /* WidgetExtension.appex */,
);
name = Products;
sourceTree = "<group>";
@ -2164,6 +2314,8 @@
isa = PBXGroup;
children = (
DBB8AB4526AECDE200F6D281 /* SendPostIntentHandler.swift */,
2AE202AB297FE19100F66E55 /* FollowersCountIntentHandler.swift */,
2A86A14529892944007F1062 /* MultiFollowersCountIntentHandler.swift */,
);
path = Handler;
sourceTree = "<group>";
@ -2860,6 +3012,28 @@
productReference = 2A64515D29642A8A00CD8B8A /* OpenInActionExtension.appex */;
productType = "com.apple.product-type.app-extension";
};
2A72811F297EA9D7004138C5 /* WidgetExtension */ = {
isa = PBXNativeTarget;
buildConfigurationList = 2A728139297EA9D8004138C5 /* Build configuration list for PBXNativeTarget "WidgetExtension" */;
buildPhases = (
2A72811C297EA9D7004138C5 /* Sources */,
2A72811D297EA9D7004138C5 /* Frameworks */,
2A72811E297EA9D7004138C5 /* Resources */,
2A72813D297EC6F7004138C5 /* Embed Frameworks */,
);
buildRules = (
);
dependencies = (
);
name = WidgetExtension;
packageProductDependencies = (
2A9D066E298D0FD100BF38CB /* MastodonSDKDynamic */,
2AB5011A2992322500346092 /* LightChart */,
);
productName = WidgetExtensionExtension;
productReference = 2A728120297EA9D7004138C5 /* WidgetExtension.appex */;
productType = "com.apple.product-type.app-extension";
};
DB427DD125BAA00100D1B89D /* Mastodon */ = {
isa = PBXNativeTarget;
buildConfigurationList = DB427DFC25BAA00100D1B89D /* Build configuration list for PBXNativeTarget "Mastodon" */;
@ -2882,6 +3056,7 @@
DBC6461B26A170AB00B0E31B /* PBXTargetDependency */,
DB8FABCD26AEC7B2008E5AF4 /* PBXTargetDependency */,
2A64516829642A8B00CD8B8A /* PBXTargetDependency */,
2A728133297EA9D8004138C5 /* PBXTargetDependency */,
);
name = Mastodon;
packageProductDependencies = (
@ -3006,6 +3181,9 @@
2A64515C29642A8A00CD8B8A = {
CreatedOnToolsVersion = 14.2;
};
2A72811F297EA9D7004138C5 = {
CreatedOnToolsVersion = 14.2;
};
DB427DD125BAA00100D1B89D = {
CreatedOnToolsVersion = 12.4;
LastSwiftMigration = 1300;
@ -3064,6 +3242,7 @@
);
mainGroup = DB427DC925BAA00100D1B89D;
packageReferences = (
2AB501192992322500346092 /* XCRemoteSwiftPackageReference "LightChart" */,
);
productRefGroup = DB427DD325BAA00100D1B89D /* Products */;
projectDirPath = "";
@ -3076,6 +3255,7 @@
DBC6461126A170AB00B0E31B /* ShareActionExtension */,
DB8FABC526AEC7B2008E5AF4 /* MastodonIntent */,
2A64515C29642A8A00CD8B8A /* OpenInActionExtension */,
2A72811F297EA9D7004138C5 /* WidgetExtension */,
);
};
/* End PBXProject section */
@ -3090,6 +3270,14 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
2A72811E297EA9D7004138C5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
2A72812E297EA9D8004138C5 /* Assets.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
DB427DD025BAA00100D1B89D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
@ -3316,6 +3504,23 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
2A72811C297EA9D7004138C5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
2A33062D2987DBFA001D4C51 /* FollowersCountHistory.swift in Sources */,
2A9D0666298C05A800BF38CB /* LatestFollowersWidgetView.swift in Sources */,
2AB5011E299243FB00346092 /* WidgetExtension.intentdefinition in Sources */,
2A86A14B2989326E007F1062 /* MultiFollowersCountWidgetView.swift in Sources */,
2A72813F297EC762004138C5 /* WidgetExtension.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 */,
);
runOnlyForDeploymentPostprocessing = 0;
};
DB427DCE25BAA00100D1B89D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
@ -3610,6 +3815,7 @@
DB98EB5627B0FF1B0082E365 /* ReportViewControllerAppearance.swift in Sources */,
DB3EA8E6281B79E200598866 /* DiscoveryCommunityViewController.swift in Sources */,
2D206B8625F5FB0900143C56 /* Double.swift in Sources */,
2AB5011C299243FB00346092 /* WidgetExtension.intentdefinition in Sources */,
DB9F58F126EF512300E7BBE9 /* AccountListTableViewCell.swift in Sources */,
2D76319F25C1521200929FB9 /* StatusSection.swift in Sources */,
DB938F0326240EA300E5B6C1 /* CachedThreadViewModel.swift in Sources */,
@ -3745,9 +3951,13 @@
buildActionMask = 2147483647;
files = (
DB0009A726AEE5DC009B9D2D /* Intents.intentdefinition in Sources */,
2AE202AD297FE1CD00F66E55 /* WidgetExtension.swift in Sources */,
DBB8AB4626AECDE200F6D281 /* SendPostIntentHandler.swift in Sources */,
2AB5011D299243FB00346092 /* WidgetExtension.intentdefinition in Sources */,
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;
@ -3781,6 +3991,11 @@
target = 2A64515C29642A8A00CD8B8A /* OpenInActionExtension */;
targetProxy = 2A64516729642A8B00CD8B8A /* PBXContainerItemProxy */;
};
2A728133297EA9D8004138C5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 2A72811F297EA9D7004138C5 /* WidgetExtension */;
targetProxy = 2A728132297EA9D8004138C5 /* PBXContainerItemProxy */;
};
DB427DEA25BAA00100D1B89D /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = DB427DD125BAA00100D1B89D /* Mastodon */;
@ -3809,6 +4024,40 @@
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
2AB50120299243FB00346092 /* WidgetExtension.intentdefinition */ = {
isa = PBXVariantGroup;
children = (
2AB5011F299243FB00346092 /* Base */,
2AB501222992440200346092 /* en */,
2AB501242992443100346092 /* vi */,
2AB501262992443100346092 /* tr */,
2AB501282992443200346092 /* th */,
2AB5012A2992443200346092 /* sv */,
2AB5012C2992443300346092 /* es-AR */,
2AB5012E2992443300346092 /* es */,
2AB501302992443400346092 /* sl */,
2AB501322992443400346092 /* gd */,
2AB501342992443500346092 /* ru */,
2AB501362992443600346092 /* ckb */,
2AB501382992443600346092 /* ku */,
2AB5013A2992443700346092 /* kab */,
2AB5013C2992443700346092 /* ja */,
2AB5013E2992443800346092 /* it */,
2AB501402992443800346092 /* de */,
2AB501422992443900346092 /* gl */,
2AB501442992443900346092 /* fr */,
2AB501462992443900346092 /* fi */,
2AB501482992443A00346092 /* nl */,
2AB5014A2992443A00346092 /* cs */,
2AB5014C2992443B00346092 /* zh-Hant */,
2AB5014E2992443B00346092 /* zh-Hans */,
2AB501502992443C00346092 /* ca */,
2AB501522992443C00346092 /* eu */,
2AB501542992443D00346092 /* ar */,
);
name = WidgetExtension.intentdefinition;
sourceTree = "<group>";
};
DB0009A926AEE5DC009B9D2D /* Intents.intentdefinition */ = {
isa = PBXVariantGroup;
children = (
@ -4057,6 +4306,131 @@
};
name = "Release Snapshot";
};
2A728135297EA9D8004138C5 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 5Z4GVSS33P;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = WidgetExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = WidgetExtension;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.WidgetExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
2A728136297EA9D8004138C5 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 5Z4GVSS33P;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = WidgetExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = WidgetExtension;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.WidgetExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Profile;
};
2A728137297EA9D8004138C5 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 5Z4GVSS33P;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = WidgetExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = WidgetExtension;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.WidgetExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
2A728138297EA9D8004138C5 /* Release Snapshot */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 5Z4GVSS33P;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = WidgetExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = WidgetExtension;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.WidgetExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = "Release Snapshot";
};
DB427DFA25BAA00100D1B89D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
@ -4890,6 +5264,17 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
2A728139297EA9D8004138C5 /* Build configuration list for PBXNativeTarget "WidgetExtension" */ = {
isa = XCConfigurationList;
buildConfigurations = (
2A728135297EA9D8004138C5 /* Debug */,
2A728136297EA9D8004138C5 /* Profile */,
2A728137297EA9D8004138C5 /* Release */,
2A728138297EA9D8004138C5 /* Release Snapshot */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
DB427DCD25BAA00100D1B89D /* Build configuration list for PBXProject "Mastodon" */ = {
isa = XCConfigurationList;
buildConfigurations = (
@ -4969,11 +5354,31 @@
};
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
2AB501192992322500346092 /* XCRemoteSwiftPackageReference "LightChart" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/Bearologics/LightChart.git";
requirement = {
branch = master;
kind = branch;
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
2A90A156296EEE500026C155 /* MastodonSDKDynamic */ = {
isa = XCSwiftPackageProductDependency;
productName = MastodonSDKDynamic;
};
2A9D066E298D0FD100BF38CB /* MastodonSDKDynamic */ = {
isa = XCSwiftPackageProductDependency;
productName = MastodonSDKDynamic;
};
2AB5011A2992322500346092 /* LightChart */ = {
isa = XCSwiftPackageProductDependency;
package = 2AB501192992322500346092 /* XCRemoteSwiftPackageReference "LightChart" */;
productName = LightChart;
};
357FEEAE29523D470021C9DC /* MastodonSDKDynamic */ = {
isa = XCSwiftPackageProductDependency;
productName = MastodonSDKDynamic;

View File

@ -73,6 +73,15 @@
"version": "4.2.2"
}
},
{
"package": "LightChart",
"repositoryURL": "https://github.com/Bearologics/LightChart.git",
"state": {
"branch": "master",
"revision": "a7e724e9ec3cdcaa2d0840b95780e66b870dbf1e",
"version": null
}
},
{
"package": "MetaTextKit",
"repositoryURL": "https://github.com/TwidereProject/MetaTextKit.git",

View File

@ -61,6 +61,9 @@
</dict>
<key>NSUserActivityTypes</key>
<array>
<string>FollowersCountIntent</string>
<string>MultiFollowersCountIntent</string>
<string>LatestFollowersIntent</string>
<string>SendPostIntent</string>
</array>
<key>UIApplicationSceneManifest</key>

View File

@ -0,0 +1,35 @@
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
import Foundation
import Intents
import MastodonCore
import MastodonSDK
import MastodonLocalization
class FollowersCountIntentHandler: INExtension, FollowersCountIntentHandling {
func resolveShowChart(for intent: FollowersCountIntent) async -> INBooleanResolutionResult {
return .success(with: intent.showChart?.boolValue ?? false)
}
func resolveAccount(for intent: FollowersCountIntent) async -> INStringResolutionResult {
.confirmationRequired(with: intent.account)
}
func provideAccountOptionsCollection(for intent: FollowersCountIntent, 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.acctWithDomainIfMissing(authenticationBox.domain) as NSString })
}
}

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, MultiFollowersCountIntentHandling {
func provideAccountsOptionsCollection(for intent: MultiFollowersCountIntent, 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.acctWithDomainIfMissing(authenticationBox.domain) as NSString })
}
}

View File

@ -30,6 +30,8 @@
<array/>
<key>IntentsSupported</key>
<array>
<string>FollowersCountIntent</string>
<string>MultiFollowersCountIntent</string>
<string>SendPostIntent</string>
</array>
</dict>

View File

@ -15,6 +15,10 @@ class IntentHandler: INExtension {
switch intent {
case is SendPostIntent:
return SendPostIntentHandler()
case is FollowersCountIntent:
return FollowersCountIntentHandler()
case is MultiFollowersCountIntent:
return MultiFollowersCountIntentHandler()
default:
return self
}

View File

@ -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
}
}

View File

@ -1479,6 +1479,50 @@ public enum L10n {
public static let newInMastodon = L10n.tr("Localizable", "Scene.Wizard.NewInMastodon", fallback: "New in Mastodon")
}
}
public enum Widget {
public enum Common {
/// Sorry but this Widget family is unsupported.
public static let unsupportedWidgetFamily = L10n.tr("Localizable", "Widget.Common.UnsupportedWidgetFamily", fallback: "Sorry but this Widget family is unsupported.")
/// Please open Mastodon to log in to an Account.
public static let userNotLoggedIn = L10n.tr("Localizable", "Widget.Common.UserNotLoggedIn", fallback: "Please open Mastodon to log in to an Account.")
}
public enum FollowersCount {
/// Show number of followers.
public static let configurationDescription = L10n.tr("Localizable", "Widget.FollowersCount.ConfigurationDescription", fallback: "Show number of followers.")
/// Followers
public static let configurationDisplayName = L10n.tr("Localizable", "Widget.FollowersCount.ConfigurationDisplayName", fallback: "Followers")
/// %@ followers today
public static func followersToday(_ p1: Any) -> String {
return L10n.tr("Localizable", "Widget.FollowersCount.FollowersToday", String(describing: p1), fallback: "%@ followers today")
}
/// FOLLOWERS
public static let title = L10n.tr("Localizable", "Widget.FollowersCount.Title", fallback: "FOLLOWERS")
}
public enum LatestFollowers {
/// Show latest followers.
public static let configurationDescription = L10n.tr("Localizable", "Widget.LatestFollowers.ConfigurationDescription", fallback: "Show latest followers.")
/// Latest followers
public static let configurationDisplayName = L10n.tr("Localizable", "Widget.LatestFollowers.ConfigurationDisplayName", fallback: "Latest followers")
/// Last update: %@
public static func lastUpdate(_ p1: Any) -> String {
return L10n.tr("Localizable", "Widget.LatestFollowers.LastUpdate", String(describing: p1), fallback: "Last update: %@")
}
/// Latest followers
public static let title = L10n.tr("Localizable", "Widget.LatestFollowers.Title", fallback: "Latest followers")
}
public enum MultipleFollowers {
/// Show number of followers for multiple accounts.
public static let configurationDescription = L10n.tr("Localizable", "Widget.MultipleFollowers.ConfigurationDescription", fallback: "Show number of followers for multiple accounts.")
/// Multiple followers
public static let configurationDisplayName = L10n.tr("Localizable", "Widget.MultipleFollowers.ConfigurationDisplayName", fallback: "Multiple followers")
public enum MockUser {
/// another@follower.social
public static let accountName = L10n.tr("Localizable", "Widget.MultipleFollowers.MockUser.AccountName", fallback: "another@follower.social")
/// Another follower
public static let displayName = L10n.tr("Localizable", "Widget.MultipleFollowers.MockUser.DisplayName", fallback: "Another follower")
}
}
}
public enum A11y {
public enum Plural {
public enum Count {

View File

@ -518,3 +518,17 @@ You cant go wrong with any of our recommend servers, so regardless of which o
"Scene.Privacy.Policy.Ios" = "Privacy Policy - Mastodon for iOS";
"Scene.Privacy.Policy.Server" = "Privacy Policy - %@";
"Extension.OpenIn.InvalidLinkError" = "This doesn't seem to be a valid Mastodon link.";
"Widget.Common.UnsupportedWidgetFamily" = "Sorry but this Widget family is unsupported.";
"Widget.Common.UserNotLoggedIn" = "Please open Mastodon to log in to an Account.";
"Widget.FollowersCount.ConfigurationDisplayName" = "Followers";
"Widget.FollowersCount.ConfigurationDescription" = "Show number of followers.";
"Widget.FollowersCount.Title" = "FOLLOWERS";
"Widget.FollowersCount.FollowersToday" = "%@ followers today";
"Widget.MultipleFollowers.ConfigurationDisplayName" = "Multiple followers";
"Widget.MultipleFollowers.ConfigurationDescription" = "Show number of followers for multiple accounts.";
"Widget.MultipleFollowers.MockUser.DisplayName" = "Another follower";
"Widget.MultipleFollowers.MockUser.AccountName" = "another@follower.social";
"Widget.LatestFollowers.ConfigurationDisplayName" = "Latest followers";
"Widget.LatestFollowers.ConfigurationDescription" = "Show latest followers.";
"Widget.LatestFollowers.Title" = "Latest followers";
"Widget.LatestFollowers.LastUpdate" = "Last update: %@";

View File

@ -82,6 +82,13 @@ extension Mastodon.Entity {
case muteExpiresAt = "mute_expires_at"
}
}
}
extension Mastodon.Entity.Account {
public func acctWithDomainIfMissing(_ localDomain: String) -> String {
guard acct.contains("@") else {
return "\(acct)@\(localDomain)"
}
return acct
}
}

View File

@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,13 @@
{
"images" : [
{
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"filename" : "Logo.svg",
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"filename" : "Logo 1.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,3 @@
<svg width="12" height="13" viewBox="0 0 12 13" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.14984 0.167213C10.5693 0.380513 11.7667 1.48128 11.9516 2.87808C12.0321 3.70898 11.9922 4.95747 11.9729 5.56415C11.968 5.71503 11.9645 5.82622 11.9644 5.88141C11.9644 5.96295 11.9527 6.70743 11.9481 6.78602C11.8237 8.78194 10.5995 9.57015 9.31318 9.82126C9.29925 9.82551 9.28346 9.82863 9.2673 9.83182C9.26323 9.83262 9.25914 9.83343 9.25505 9.83426C8.43952 9.99616 7.56587 10.0393 6.73697 10.0629C6.53879 10.0682 6.34119 10.0682 6.14302 10.0682H6.14291C5.31876 10.0685 4.49749 9.96929 3.69633 9.77281C3.69208 9.77169 3.68763 9.7716 3.68335 9.77256C3.67906 9.77351 3.67506 9.77547 3.67166 9.7783C3.66826 9.78112 3.66557 9.78471 3.66379 9.78879C3.66202 9.79287 3.66122 9.79731 3.66145 9.80176C3.6841 10.0669 3.74077 10.3279 3.83002 10.5782C3.94104 10.8677 4.32875 11.5631 5.77031 11.5631C6.60796 11.5647 7.44286 11.4655 8.25759 11.2677C8.26173 11.2667 8.26602 11.2667 8.27017 11.2676C8.27432 11.2686 8.2782 11.2704 8.28155 11.2731C8.28489 11.2758 8.28761 11.2792 8.2895 11.283C8.29138 11.2869 8.2924 11.2911 8.29246 11.2955V12.2751C8.29231 12.2797 8.29113 12.2842 8.28902 12.2883C8.2869 12.2924 8.28391 12.296 8.28026 12.2987C8.02556 12.4864 7.68309 12.5967 7.38313 12.6934C7.36955 12.6978 7.35605 12.7021 7.34266 12.7064C7.20625 12.7502 7.0681 12.7886 6.92821 12.8216C5.65656 13.1161 4.32938 13.0449 3.09529 12.616C1.94262 12.2048 0.766121 11.1968 0.475483 9.98552C0.320274 9.32968 0.210915 8.6635 0.148226 7.99196C0.0823574 7.25761 0.0603482 6.522 0.0383084 5.78538C0.0300068 5.50792 0.0217009 5.23032 0.0110452 4.95258C-0.0162747 4.24473 -0.000580304 3.47307 0.146482 2.77704C0.452233 1.3637 1.71244 0.374605 3.09238 0.167213C3.12074 0.162945 3.15207 0.157343 3.18876 0.150782C3.46186 0.101943 4.03192 0 5.8854 0H5.9011C8.00299 0 8.91036 0.131171 9.14984 0.167213ZM9.91361 7.91751V4.44739C9.914 3.73797 9.73632 3.17468 9.38058 2.75754C9.01205 2.34098 8.53018 2.12709 7.93263 2.12709C7.24149 2.12709 6.71834 2.3977 6.36958 2.93834L6.03244 3.51266L5.69588 2.93834C5.34712 2.3977 4.82397 2.12709 4.13167 2.12709C3.53354 2.12709 3.05167 2.34098 2.6843 2.75754C2.32817 3.17508 2.15011 3.73836 2.15011 4.44739V7.91751H3.50215V4.54961C3.50215 3.84058 3.7957 3.47898 4.38337 3.47898C5.03323 3.47898 5.35932 3.90676 5.35932 4.75169V6.59517H6.70381V4.75169C6.70381 3.90676 7.02933 3.47898 7.67919 3.47898C8.27035 3.47898 8.56098 3.84058 8.56098 4.54961V7.91751H9.91361Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -0,0 +1,3 @@
<svg width="12" height="13" viewBox="0 0 12 13" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.14984 0.167213C10.5693 0.380513 11.7667 1.48128 11.9516 2.87808C12.0321 3.70898 11.9922 4.95747 11.9729 5.56415C11.968 5.71503 11.9645 5.82622 11.9644 5.88141C11.9644 5.96295 11.9527 6.70743 11.9481 6.78602C11.8237 8.78194 10.5995 9.57015 9.31318 9.82126C9.29925 9.82551 9.28346 9.82863 9.2673 9.83182C9.26323 9.83262 9.25914 9.83343 9.25505 9.83426C8.43952 9.99616 7.56587 10.0393 6.73697 10.0629C6.53879 10.0682 6.34119 10.0682 6.14302 10.0682H6.14291C5.31876 10.0685 4.49749 9.96929 3.69633 9.77281C3.69208 9.77169 3.68763 9.7716 3.68335 9.77256C3.67906 9.77351 3.67506 9.77547 3.67166 9.7783C3.66826 9.78112 3.66557 9.78471 3.66379 9.78879C3.66202 9.79287 3.66122 9.79731 3.66145 9.80176C3.6841 10.0669 3.74077 10.3279 3.83002 10.5782C3.94104 10.8677 4.32875 11.5631 5.77031 11.5631C6.60796 11.5647 7.44286 11.4655 8.25759 11.2677C8.26173 11.2667 8.26602 11.2667 8.27017 11.2676C8.27432 11.2686 8.2782 11.2704 8.28155 11.2731C8.28489 11.2758 8.28761 11.2792 8.2895 11.283C8.29138 11.2869 8.2924 11.2911 8.29246 11.2955V12.2751C8.29231 12.2797 8.29113 12.2842 8.28902 12.2883C8.2869 12.2924 8.28391 12.296 8.28026 12.2987C8.02556 12.4864 7.68309 12.5967 7.38313 12.6934C7.36955 12.6978 7.35605 12.7021 7.34266 12.7064C7.20625 12.7502 7.0681 12.7886 6.92821 12.8216C5.65656 13.1161 4.32938 13.0449 3.09529 12.616C1.94262 12.2048 0.766121 11.1968 0.475483 9.98552C0.320274 9.32968 0.210915 8.6635 0.148226 7.99196C0.0823574 7.25761 0.0603482 6.522 0.0383084 5.78538C0.0300068 5.50792 0.0217009 5.23032 0.0110452 4.95258C-0.0162747 4.24473 -0.000580304 3.47307 0.146482 2.77704C0.452233 1.3637 1.71244 0.374605 3.09238 0.167213C3.12074 0.162945 3.15207 0.157343 3.18876 0.150782C3.46186 0.101943 4.03192 0 5.8854 0H5.9011C8.00299 0 8.91036 0.131171 9.14984 0.167213ZM9.91361 7.91751V4.44739C9.914 3.73797 9.73632 3.17468 9.38058 2.75754C9.01205 2.34098 8.53018 2.12709 7.93263 2.12709C7.24149 2.12709 6.71834 2.3977 6.36958 2.93834L6.03244 3.51266L5.69588 2.93834C5.34712 2.3977 4.82397 2.12709 4.13167 2.12709C3.53354 2.12709 3.05167 2.34098 2.6843 2.75754C2.32817 3.17508 2.15011 3.73836 2.15011 4.44739V7.91751H3.50215V4.54961C3.50215 3.84058 3.7957 3.47898 4.38337 3.47898C5.03323 3.47898 5.35932 3.90676 5.35932 4.75169V6.59517H6.70381V4.75169C6.70381 3.90676 7.02933 3.47898 7.67919 3.47898C8.27035 3.47898 8.56098 3.84058 8.56098 4.54961V7.91751H9.91361Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "Logo.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,4 @@
<svg width="18" height="20" viewBox="0 0 18 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17.9274 4.42781C17.6501 2.2789 15.854 0.585405 13.7248 0.257251C13.3655 0.201801 12.0045 0 8.85165 0H8.82811C5.6744 0 4.9978 0.201801 4.63857 0.257251C2.56865 0.576315 0.678349 2.098 0.219723 4.27237C-0.000870456 5.34319 -0.0244121 6.53036 0.0165678 7.61936C0.0749859 9.18104 0.0863207 10.74 0.222339 12.2953C0.316372 13.3285 0.48041 14.3534 0.713225 15.3623C1.14918 17.2258 2.91393 18.7766 4.64293 19.4093C6.49407 20.0691 8.48484 20.1786 10.3923 19.7256C10.6022 19.6747 10.8094 19.6156 11.014 19.5484C11.4778 19.3947 12.021 19.2229 12.4204 18.9211C12.4259 18.9169 12.4304 18.9114 12.4335 18.9051C12.4367 18.8988 12.4385 18.8919 12.4387 18.8848V17.3776C12.4386 17.371 12.4371 17.3644 12.4342 17.3585C12.4314 17.3525 12.4273 17.3473 12.4223 17.3432C12.4173 17.3391 12.4115 17.3363 12.4053 17.3348C12.399 17.3334 12.3926 17.3334 12.3864 17.3349C11.1643 17.6392 9.91194 17.7918 8.65547 17.7894C6.49313 17.7894 5.91156 16.7195 5.74503 16.2741C5.61116 15.8891 5.52616 15.4876 5.49217 15.0796C5.49182 15.0728 5.49303 15.0659 5.49569 15.0597C5.49835 15.0534 5.5024 15.0479 5.50749 15.0435C5.51259 15.0392 5.5186 15.0362 5.52502 15.0347C5.53145 15.0332 5.53812 15.0334 5.54449 15.0351C6.74623 15.3374 7.97814 15.4899 9.21436 15.4896C9.51169 15.4896 9.80814 15.4896 10.1055 15.4814C11.3488 15.4451 12.6593 15.3787 13.8826 15.1296C13.9131 15.1233 13.9436 15.1178 13.9698 15.1096C15.8993 14.7233 17.7356 13.5107 17.9221 10.44C17.9291 10.3191 17.9466 9.17377 17.9466 9.04833C17.9474 8.622 18.0782 6.02404 17.9274 4.42781Z" fill="#6364FF"/>
<path d="M14.8705 6.84215V12.1808H12.8415V6.99941C12.8415 5.90859 12.4056 5.35228 11.5188 5.35228C10.544 5.35228 10.0558 6.0104 10.0558 7.31029V10.1464H8.03904V7.31029C8.03904 6.0104 7.5499 5.35228 6.5751 5.35228C5.6936 5.35228 5.25329 5.90859 5.25329 6.99941V12.1808H3.22522V6.84215C3.22522 5.75133 3.49232 4.88474 4.02651 4.24237C4.57755 3.60152 5.30037 3.27245 6.19757 3.27245C7.23601 3.27245 8.02073 3.68878 8.54388 4.52053L9.04872 5.40409L9.55443 4.52053C10.0776 3.68878 10.8623 3.27245 11.899 3.27245C12.7953 3.27245 13.5181 3.60152 14.0709 4.24237C14.6045 4.88414 14.8711 5.75073 14.8705 6.84215Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "missing.png",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -0,0 +1,382 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>INEnums</key>
<array/>
<key>INIntentDefinitionModelVersion</key>
<string>1.2</string>
<key>INIntentDefinitionNamespace</key>
<string>88xZPY</string>
<key>INIntentDefinitionSystemVersion</key>
<string>22D49</string>
<key>INIntentDefinitionToolsBuildVersion</key>
<string>14C18</string>
<key>INIntentDefinitionToolsVersion</key>
<string>14.2</string>
<key>INIntents</key>
<array>
<dict>
<key>INIntentCategory</key>
<string>information</string>
<key>INIntentDescription</key>
<string>Followers</string>
<key>INIntentDescriptionID</key>
<string>tVvJ9c</string>
<key>INIntentEligibleForWidgets</key>
<true/>
<key>INIntentIneligibleForSuggestions</key>
<true/>
<key>INIntentLastParameterTag</key>
<integer>7</integer>
<key>INIntentName</key>
<string>FollowersCount</string>
<key>INIntentParameters</key>
<array>
<dict>
<key>INIntentParameterConfigurable</key>
<true/>
<key>INIntentParameterDisplayName</key>
<string>Account</string>
<key>INIntentParameterDisplayNameID</key>
<string>OL6lkx</string>
<key>INIntentParameterDisplayPriority</key>
<integer>1</integer>
<key>INIntentParameterMetadata</key>
<dict>
<key>INIntentParameterMetadataCapitalization</key>
<string>Sentences</string>
<key>INIntentParameterMetadataDefaultValueID</key>
<string>2V4PKr</string>
</dict>
<key>INIntentParameterName</key>
<string>account</string>
<key>INIntentParameterPromptDialogs</key>
<array>
<dict>
<key>INIntentParameterPromptDialogCustom</key>
<true/>
<key>INIntentParameterPromptDialogFormatString</key>
<string>Enter follower Username</string>
<key>INIntentParameterPromptDialogFormatStringID</key>
<string>sOLUtG</string>
<key>INIntentParameterPromptDialogType</key>
<string>Configuration</string>
</dict>
<dict>
<key>INIntentParameterPromptDialogCustom</key>
<true/>
<key>INIntentParameterPromptDialogType</key>
<string>Primary</string>
</dict>
</array>
<key>INIntentParameterSupportsDynamicEnumeration</key>
<true/>
<key>INIntentParameterSupportsSearch</key>
<true/>
<key>INIntentParameterTag</key>
<integer>5</integer>
<key>INIntentParameterType</key>
<string>String</string>
</dict>
<dict>
<key>INIntentParameterConfigurable</key>
<true/>
<key>INIntentParameterDisplayName</key>
<string>Show chart</string>
<key>INIntentParameterDisplayNameID</key>
<string>xVtyec</string>
<key>INIntentParameterDisplayPriority</key>
<integer>2</integer>
<key>INIntentParameterMetadata</key>
<dict>
<key>INIntentParameterMetadataFalseDisplayName</key>
<string>No</string>
<key>INIntentParameterMetadataFalseDisplayNameID</key>
<string>jg9D5P</string>
<key>INIntentParameterMetadataTrueDisplayName</key>
<string>Yes</string>
<key>INIntentParameterMetadataTrueDisplayNameID</key>
<string>82L4Nj</string>
</dict>
<key>INIntentParameterName</key>
<string>showChart</string>
<key>INIntentParameterPromptDialogs</key>
<array>
<dict>
<key>INIntentParameterPromptDialogCustom</key>
<true/>
<key>INIntentParameterPromptDialogType</key>
<string>Configuration</string>
</dict>
<dict>
<key>INIntentParameterPromptDialogCustom</key>
<true/>
<key>INIntentParameterPromptDialogFormatString</key>
<string>Should the Widget show a chart?</string>
<key>INIntentParameterPromptDialogFormatStringID</key>
<string>zeJo4f</string>
<key>INIntentParameterPromptDialogType</key>
<string>Primary</string>
</dict>
</array>
<key>INIntentParameterSupportsResolution</key>
<true/>
<key>INIntentParameterTag</key>
<integer>7</integer>
<key>INIntentParameterType</key>
<string>Boolean</string>
</dict>
</array>
<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>
<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>BFppgH</string>
<key>INIntentResponseParameterDisplayPriority</key>
<integer>1</integer>
<key>INIntentResponseParameterName</key>
<string>username</string>
<key>INIntentResponseParameterTag</key>
<integer>4</integer>
<key>INIntentResponseParameterType</key>
<string>String</string>
</dict>
</array>
</dict>
<key>INIntentTitle</key>
<string>Followers Count</string>
<key>INIntentTitleID</key>
<string>gpCwrM</string>
<key>INIntentType</key>
<string>Custom</string>
<key>INIntentVerb</key>
<string>View</string>
</dict>
<dict>
<key>INIntentCategory</key>
<string>information</string>
<key>INIntentDescriptionID</key>
<string>B9KyhZ</string>
<key>INIntentEligibleForWidgets</key>
<true/>
<key>INIntentIneligibleForSuggestions</key>
<true/>
<key>INIntentLastParameterTag</key>
<integer>6</integer>
<key>INIntentName</key>
<string>MultiFollowersCount</string>
<key>INIntentParameters</key>
<array>
<dict>
<key>INIntentParameterArraySizes</key>
<array>
<dict>
<key>INIntentParameterArraySizeSize</key>
<integer>3</integer>
<key>INIntentParameterArraySizeSizeClass</key>
<string>Small</string>
</dict>
<dict>
<key>INIntentParameterArraySizeSize</key>
<integer>6</integer>
<key>INIntentParameterArraySizeSizeClass</key>
<string>Medium</string>
</dict>
<dict>
<key>INIntentParameterArraySizeSize</key>
<integer>6</integer>
<key>INIntentParameterArraySizeSizeClass</key>
<string>Large</string>
</dict>
<dict>
<key>INIntentParameterArraySizeSize</key>
<integer>6</integer>
<key>INIntentParameterArraySizeSizeClass</key>
<string>ExtraLarge</string>
</dict>
<dict>
<key>INIntentParameterArraySizeSize</key>
<integer>1</integer>
<key>INIntentParameterArraySizeSizeClass</key>
<string>AccessoryInline</string>
</dict>
<dict>
<key>INIntentParameterArraySizeSize</key>
<integer>1</integer>
<key>INIntentParameterArraySizeSizeClass</key>
<string>AccessoryCorner</string>
</dict>
<dict>
<key>INIntentParameterArraySizeSize</key>
<integer>1</integer>
<key>INIntentParameterArraySizeSizeClass</key>
<string>AccessoryCircular</string>
</dict>
<dict>
<key>INIntentParameterArraySizeSize</key>
<integer>1</integer>
<key>INIntentParameterArraySizeSizeClass</key>
<string>AccessoryRectangular</string>
</dict>
</array>
<key>INIntentParameterConfigurable</key>
<true/>
<key>INIntentParameterDisplayName</key>
<string>Accounts</string>
<key>INIntentParameterDisplayNameID</key>
<string>fovmPX</string>
<key>INIntentParameterDisplayPriority</key>
<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>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>
<dict>
<key>INIntentParameterPromptDialogCustom</key>
<true/>
<key>INIntentParameterPromptDialogType</key>
<string>Primary</string>
</dict>
</array>
<key>INIntentParameterSupportsDynamicEnumeration</key>
<true/>
<key>INIntentParameterSupportsMultipleValues</key>
<true/>
<key>INIntentParameterSupportsSearch</key>
<true/>
<key>INIntentParameterTag</key>
<integer>6</integer>
<key>INIntentParameterType</key>
<string>String</string>
</dict>
</array>
<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>
<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 Followers Count</string>
<key>INIntentTitleID</key>
<string>e0W2wo</string>
<key>INIntentType</key>
<string>Custom</string>
<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/>
</dict>
</plist>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.widgetkit-extension</string>
</dict>
</dict>
</plist>

View File

@ -0,0 +1,112 @@
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
import Foundation
import MastodonSDK
struct FollowersCountHistoryDay: Codable {
let dstring: String
let day: Int
let count: Int
func copy(count: Int) -> Self {
FollowersCountHistoryDay(dstring: dstring, day: day, count: count)
}
}
class FollowersCountHistory {
static let shared = FollowersCountHistory()
private let userDefaults = UserDefaults.standard
private let calendar = Calendar.current
private let followersCountCacheDateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "yyyyMMdd"
return formatter
}()
private func elapsedFollowersCountDateStrings() -> [String] {
(-7...0).map { elapsedDay in
let date = calendar.date(byAdding: .day, value: elapsedDay, to: .now)!
return followersCountCacheDateFormatter.string(from: date)
}
}
private func userDefaultsKey(for account: FollowersEntryAccountable) -> String {
if account.acct.contains("@") {
return account.acct
}
return "\(account.acct)@\(account.domain)"
}
private func emptyHistoricDataForToday(for account: FollowersEntryAccountable) -> [FollowersCountHistoryDay] {
elapsedFollowersCountDateStrings().enumerated().map { FollowersCountHistoryDay(dstring: $0.element, day: $0.offset, count: account.followersCount) }
}
private func followersHistorySorted(for account: FollowersEntryAccountable) -> [FollowersCountHistoryDay] {
guard
let jsonData = userDefaults.string(forKey: userDefaultsKey(for: account))?.data(using: .utf8),
let jsonObject = try? JSONDecoder().decode([FollowersCountHistoryDay].self, from: jsonData)
else {
return emptyHistoricDataForToday(for: account)
}
return jsonObject
}
func updateFollowersTodayCount(account: FollowersEntryAccountable, count: Int) {
let relevantDays = elapsedFollowersCountDateStrings()
let existingHistory = followersHistorySorted(for: account)
var newHistory = existingHistory
/// first we're going to update the existing day and remove legacy days (older than 7)
existingHistory.forEach { existingDay in
if !relevantDays.contains(where: { $0 == existingDay.dstring }) {
/// remove legacy data/
newHistory.removeAll(where: { $0.dstring == existingDay.dstring })
}
}
relevantDays.enumerated().forEach { index, day in
if !newHistory.contains(where: { $0.dstring == day }) {
newHistory.insert(
FollowersCountHistoryDay(dstring: day, day: index, count: account.followersCount),
at: index
)
}
}
/// then we're going to update the history dataset with new value, if this is the first encounter
if let last = newHistory.popLast()?.copy(count: count) {
newHistory.append(last)
}
if let jsonData = try? JSONEncoder().encode(newHistory), let jsonString = String(data: jsonData, encoding: .utf8) {
userDefaults.set(jsonString, forKey: userDefaultsKey(for: account))
}
}
func chartValues(for account: FollowersEntryAccountable) -> [Double] {
followersHistorySorted(for: account).map { Double($0.count) }
}
func increaseCountString(for account: FollowersEntryAccountable) -> String? {
let history = followersHistorySorted(for: account)
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[safe: history.count-2]?.count ?? account.followersCount
let followersChange = followersToday - followersYesterday
switch followersChange {
case ..<0:
return "\(followersChange)"
case 0:
return nil
default:
return "+\(followersChange)"
}
}
}

View File

@ -0,0 +1,153 @@
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
import WidgetKit
import SwiftUI
import Intents
import MastodonSDK
import MastodonLocalization
struct FollowersCountWidgetProvider: IntentTimelineProvider {
private let followersHistory = FollowersCountHistory.shared
func placeholder(in context: Context) -> FollowersCountEntry {
.placeholder
}
func getSnapshot(for configuration: FollowersCountIntent, in context: Context, completion: @escaping (FollowersCountEntry) -> ()) {
loadCurrentEntry(for: configuration, in: context, completion: completion)
}
func getTimeline(for configuration: FollowersCountIntent, in context: Context, completion: @escaping (Timeline<FollowersCountEntry>) -> ()) {
loadCurrentEntry(for: configuration, in: context) { entry in
completion(Timeline(entries: [entry], policy: .after(.now)))
}
}
}
struct FollowersCountEntry: TimelineEntry {
let date: Date
let account: FollowersEntryAccountable?
let configuration: FollowersCountIntent
static var placeholder: Self {
FollowersCountEntry(
date: .now,
account: FollowersEntryAccount(
followersCount: 99_900,
displayNameWithFallback: "Mastodon",
acct: "mastodon",
avatarImage: UIImage(named: "missingAvatar")!,
domain: "mastodon"
),
configuration: FollowersCountIntent()
)
}
static var unconfigured: Self {
FollowersCountEntry(
date: .now,
account: nil,
configuration: FollowersCountIntent()
)
}
}
struct FollowersCountWidget: 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: FollowersCountWidgetProvider()) { entry in
FollowersCountWidgetView(entry: entry)
}
.configurationDisplayName(L10n.Widget.FollowersCount.configurationDisplayName)
.description(L10n.Widget.FollowersCount.configurationDescription)
.supportedFamilies(availableFamilies)
}
}
private extension FollowersCountWidgetProvider {
func loadCurrentEntry(for configuration: FollowersCountIntent, in context: Context, completion: @escaping (FollowersCountEntry) -> Void) {
Task {
guard
let authBox = WidgetExtension.appContext
.authenticationService
.mastodonAuthenticationBoxes
.first
else {
guard !context.isPreview else {
return completion(.placeholder)
}
return completion(.unconfigured)
}
guard
let desiredAccount = configuration.account ?? authBox.authenticationRecord.object(
in: WidgetExtension.appContext.managedObjectContext
)?.user.acctWithDomain
else {
return completion(.unconfigured)
}
guard
let resultingAccount = try await WidgetExtension.appContext
.apiService
.search(query: .init(q: desiredAccount, type: .accounts), authenticationBox: authBox)
.value
.accounts
.first(where: { $0.acctWithDomainIfMissing(authBox.domain) == desiredAccount })
else {
return completion(.unconfigured)
}
let imageData = try await URLSession.shared.data(from: resultingAccount.avatarImageURLWithFallback(domain: authBox.domain)).0
let entry = FollowersCountEntry(
date: Date(),
account: FollowersEntryAccount.from(
mastodonAccount: resultingAccount,
domain: authBox.domain,
avatarImage: UIImage(data: imageData) ?? UIImage(named: "missingAvatar")!
),
configuration: configuration
)
followersHistory.updateFollowersTodayCount(
account: entry.account!,
count: resultingAccount.followersCount
)
completion(entry)
}
}
}
protocol FollowersEntryAccountable {
var followersCount: Int { get }
var displayNameWithFallback: String { get }
var acct: String { get }
var avatarImage: UIImage { get }
var domain: String { get }
}
struct FollowersEntryAccount: FollowersEntryAccountable {
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 {
FollowersEntryAccount(
followersCount: mastodonAccount.followersCount,
displayNameWithFallback: mastodonAccount.displayNameWithFallback,
acct: mastodonAccount.acct,
avatarImage: avatarImage,
domain: domain
)
}
}

View File

@ -0,0 +1,168 @@
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
import SwiftUI
import WidgetKit
import MastodonAsset
import MastodonLocalization
import LightChart
struct FollowersCountWidgetView: View {
private let followersHistory = FollowersCountHistory.shared
@Environment(\.widgetFamily) var family
var entry: FollowersCountWidgetProvider.Entry
var body: some View {
if let account = entry.account {
switch family {
case .systemSmall:
if let showChart = entry.configuration.showChart?.boolValue, showChart {
viewForSmallWidgetYesChart(account)
} else {
viewForSmallWidgetNoChart(account)
}
case .accessoryRectangular:
viewForAccessoryRectangular(account)
case .accessoryCircular:
viewForAccessoryCircular(account)
default:
Text(L10n.Widget.Common.unsupportedWidgetFamily)
}
} else {
Text(L10n.Widget.Common.userNotLoggedIn)
.multilineTextAlignment(.center)
.font(.caption)
.padding(.all, 20)
}
}
private func viewForSmallWidgetNoChart(_ 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: UIFontMetrics.default.scaledValue(for: 13)))
.lineLimit(1)
.truncationMode(.tail)
Text("@\(account.acct)")
.font(.caption2)
.foregroundColor(.secondary)
.lineLimit(1)
.truncationMode(.tail)
}
.padding(.leading, 20)
.padding(.vertical, 16)
Spacer()
}
}
private func viewForSmallWidgetYesChart(_ account: FollowersEntryAccountable) -> some View {
VStack(alignment: .leading, spacing: 0) {
HStack {
if let avatarImage = account.avatarImage {
Image(uiImage: avatarImage)
.resizable()
.frame(width: 23, height: 23)
.cornerRadius(5)
}
VStack(alignment: .leading) {
Text(account.displayNameWithFallback)
.font(.caption)
.lineLimit(1)
.truncationMode(.tail)
Text("@\(account.acct)")
.font(.caption2)
.foregroundColor(.secondary)
.lineLimit(1)
.truncationMode(.tail)
}
Spacer()
}
.padding(.leading, 20)
ZStack {
if let account = entry.account {
LightChartView(
data: followersHistory.chartValues(for: account),
type: .line,
visualType: .filled(color: Asset.Colors.Brand.blurple.swiftUIColor, lineWidth: 2),
offset: 0.8 /// this is the positive offset from the bottom edge of the graph (~80% above bottom level)
)
}
HStack {
VStack(alignment: .leading, spacing: 0) {
Spacer()
if let increaseCount = followersHistory.increaseCountString(for: account) {
Text(L10n.Widget.FollowersCount.followersToday(increaseCount))
.font(.system(size: UIFontMetrics.default.scaledValue(for: 12)))
.foregroundColor(.secondary)
.lineLimit(1)
.truncationMode(.tail)
}
Text(account.followersCount.asAbbreviatedCountString())
.font(.largeTitle)
.lineLimit(1)
.truncationMode(.tail)
}
Spacer()
}
.padding(.bottom, 16)
.padding(.leading, 20)
}
}
.padding(.top, 16)
}
private func viewForAccessoryRectangular(_ account :FollowersEntryAccountable) -> some View {
HStack(spacing: 0) {
VStack(alignment: .leading, spacing: 0) {
HStack(alignment: .center) {
Image("BrandIcon")
Text(L10n.Widget.FollowersCount.title)
.font(.system(size: UIFontMetrics.default.scaledValue(for: 15), weight: .semibold))
}
.padding(.top, 6)
Text(account.followersCount.asAbbreviatedCountString())
.font(.system(size: UIFontMetrics.default.scaledValue(for: 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: UIFontMetrics.default.scaledValue(for: 15)))
.lineLimit(1)
.truncationMode(.tail)
}
}
}
}

View File

@ -0,0 +1,149 @@
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
import WidgetKit
import SwiftUI
import Intents
import MastodonSDK
import MastodonLocalization
struct LatestFollowersWidgetProvider: IntentTimelineProvider {
func placeholder(in context: Context) -> LatestFollowersEntry {
.placeholder
}
func getSnapshot(for configuration: LatestFollowersIntent, in context: Context, completion: @escaping (LatestFollowersEntry) -> ()) {
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: nil,
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(L10n.Widget.LatestFollowers.configurationDisplayName)
.description(L10n.Widget.LatestFollowers.configurationDescription)
.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 {
guard !context.isPreview else {
return completion(.placeholder)
}
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)
}
}
}
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
)
}
}

View File

@ -0,0 +1,144 @@
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
import SwiftUI
import WidgetKit
import MastodonSDK
import MastodonAsset
import MastodonUI
import MastodonLocalization
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(L10n.Widget.Common.unsupportedWidgetFamily)
}
} else {
Text(L10n.Widget.Common.userNotLoggedIn)
.multilineTextAlignment(.center)
.font(.caption)
.padding(.all, 20)
}
}
private func viewForSmallWidget(_ accounts: [LatestFollowersEntryAccountable], lastUpdate: Date) -> some View {
VStack(alignment: .leading) {
Text(L10n.Widget.LatestFollowers.title)
.font(.system(size: UIFontMetrics.default.scaledValue(for: 16)))
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.displayNameWithFallback)
.font(.footnote.bold())
.lineLimit(1)
.truncationMode(.tail)
Text("@\(account.acct)")
.font(.caption2)
.foregroundColor(.secondary)
.lineLimit(1)
.truncationMode(.tail)
}
Spacer()
}
}
Spacer()
Text(L10n.Widget.LatestFollowers.lastUpdate(dateFormatter.string(from: lastUpdate)))
.font(.caption2)
.foregroundColor(.secondary)
}
.padding(.horizontal, 20)
.padding(.vertical, 16)
}
private func viewForMediumWidget(_ accounts: [LatestFollowersEntryAccountable], lastUpdate: Date) -> some View {
VStack(alignment: .leading) {
HStack {
Text(L10n.Widget.LatestFollowers.title)
.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())
.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(L10n.Widget.LatestFollowers.lastUpdate(dateFormatter.string(from: lastUpdate)))
.font(.caption2)
.foregroundColor(.secondary)
}
.padding(.horizontal, 20)
.padding(.vertical, 16)
}
}
/// This code is used to strip HTML tags from the bio description as the widgets currently dont support
/// rich text rendering due to the lack of SwiftUI-only components for this purpose.
/// todo: Implement rich text rendering for bio description and remove this code
/// https://github.com/mastodon/mastodon-ios/issues/921
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
}
}
}

View File

@ -0,0 +1,167 @@
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
import WidgetKit
import SwiftUI
import Intents
import MastodonSDK
import MastodonLocalization
struct MultiFollowersCountWidgetProvider: IntentTimelineProvider {
func placeholder(in context: Context) -> MultiFollowersCountEntry {
.placeholder
}
func getSnapshot(for configuration: MultiFollowersCountIntent, in context: Context, completion: @escaping (MultiFollowersCountEntry) -> ()) {
loadCurrentEntry(for: configuration, in: context, completion: completion)
}
func getTimeline(for configuration: MultiFollowersCountIntent, 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: [MultiFollowersEntryAccountable]?
let configuration: MultiFollowersCountIntent
static var placeholder: Self {
MultiFollowersCountEntry(
date: .now,
accounts: [
MultiFollowersEntryAccount(
followersCount: 99_900,
displayNameWithFallback: "Mastodon",
acct: "mastodon",
avatarImage: UIImage(named: "missingAvatar")!,
domain: "mastodon"
)
],
configuration: MultiFollowersCountIntent()
)
}
static var unconfigured: Self {
MultiFollowersCountEntry(
date: .now,
accounts: nil,
configuration: MultiFollowersCountIntent()
)
}
}
struct MultiFollowersCountWidget: Widget {
private var availableFamilies: [WidgetFamily] {
return [.systemSmall, .systemMedium]
}
var body: some WidgetConfiguration {
IntentConfiguration(kind: "Multiple followers", intent: MultiFollowersCountIntent.self, provider: MultiFollowersCountWidgetProvider()) { entry in
MultiFollowersCountWidgetView(entry: entry)
}
.configurationDisplayName(L10n.Widget.MultipleFollowers.configurationDisplayName)
.description(L10n.Widget.MultipleFollowers.configurationDescription)
.supportedFamilies(availableFamilies)
}
}
private extension MultiFollowersCountWidgetProvider {
func loadCurrentEntry(for configuration: MultiFollowersCountIntent, in context: Context, completion: @escaping (MultiFollowersCountEntry) -> Void) {
Task {
guard
let authBox = WidgetExtension.appContext
.authenticationService
.mastodonAuthenticationBoxes
.first
else {
guard !context.isPreview else {
return completion(.placeholder)
}
return completion(.unconfigured)
}
let desiredAccounts: [String]
if let configuredAccounts = configuration.accounts?.compactMap({ $0 }) {
desiredAccounts = configuredAccounts
} else if let currentlyLoggedInAccount = authBox.authenticationRecord.object(
in: WidgetExtension.appContext.managedObjectContext
)?.user.acctWithDomain {
desiredAccounts = [currentlyLoggedInAccount]
} else {
return completion(.unconfigured)
}
var accounts = [MultiFollowersEntryAccountable]()
for desiredAccount in desiredAccounts {
guard
let resultingAccount = try await WidgetExtension.appContext
.apiService
.search(query: .init(q: desiredAccount, type: .accounts), authenticationBox: authBox)
.value
.accounts
.first(where: { $0.acctWithDomainIfMissing(authBox.domain) == desiredAccount })
else {
continue
}
let imageData = try await URLSession.shared.data(from: resultingAccount.avatarImageURLWithFallback(domain: authBox.domain)).0
accounts.append(MultiFollowersEntryAccount.from(
mastodonAccount: resultingAccount,
domain: authBox.domain,
avatarImage: UIImage(data: imageData) ?? UIImage(named: "missingAvatar")!
))
}
if context.isPreview {
accounts.append(
MultiFollowersEntryAccount(
followersCount: 1_200,
displayNameWithFallback: L10n.Widget.MultipleFollowers.MockUser.displayName,
acct: L10n.Widget.MultipleFollowers.MockUser.accountName,
avatarImage: UIImage(named: "missingAvatar")!,
domain: authBox.domain
)
)
}
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,97 @@
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
import SwiftUI
import WidgetKit
import MastodonAsset
import MastodonLocalization
struct MultiFollowersCountWidgetView: View {
@Environment(\.widgetFamily) var family
var entry: MultiFollowersCountWidgetProvider.Entry
var body: some View {
if let accounts = entry.accounts {
switch family {
case .systemSmall:
viewForSmallWidget(accounts)
case .systemMedium:
viewForMediumWidget(accounts)
default:
Text(L10n.Widget.Common.unsupportedWidgetFamily)
}
} else {
Text(L10n.Widget.Common.userNotLoggedIn)
.multilineTextAlignment(.center)
.font(.caption)
.padding(.all, 20)
}
}
private func viewForSmallWidget(_ accounts: [MultiFollowersEntryAccountable]) -> 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)
}
private func viewForMediumWidget(_ accounts: [MultiFollowersEntryAccountable]) -> some View {
VStack(alignment: .leading, spacing: 0) {
LazyVGrid(columns: [
GridItem(.flexible()),
GridItem(.flexible())
]) {
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

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.org.joinmastodon.app</string>
</array>
</dict>
</plist>

View File

@ -0,0 +1,9 @@
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
import MastodonCore
import MastodonSDK
import MastodonLocalization
enum WidgetExtension {
static let appContext = AppContext()
}

View File

@ -0,0 +1,13 @@
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
import WidgetKit
import SwiftUI
@main
struct WidgetExtensionBundle: WidgetBundle {
var body: some Widget {
FollowersCountWidget()
MultiFollowersCountWidget()
LatestFollowersWidget()
}
}

View File

@ -0,0 +1,28 @@
"3d6HSO" = "Enter username";
"7DZrRA" = "Username";
"82L4Nj" = "Yes";
"BFppgH" = "Username";
"OL6lkx" = "Account";
"ZLZ6sg" = "Latest Followers";
"e0W2wo" = "Multi Followers Count";
"fovmPX" = "Accounts";
"gpCwrM" = "Followers Count";
"jg9D5P" = "No";
"sOLUtG" = "Enter follower Username";
"tVvJ9c" = "Followers";
"xVtyec" = "Show chart";
"zeJo4f" = "Should the Widget show a chart?";

View File

@ -0,0 +1,28 @@
"3d6HSO" = "Enter username";
"7DZrRA" = "Username";
"82L4Nj" = "Yes";
"BFppgH" = "Username";
"OL6lkx" = "Account";
"ZLZ6sg" = "Latest Followers";
"e0W2wo" = "Multi Followers Count";
"fovmPX" = "Accounts";
"gpCwrM" = "Followers Count";
"jg9D5P" = "No";
"sOLUtG" = "Enter follower Username";
"tVvJ9c" = "Followers";
"xVtyec" = "Show chart";
"zeJo4f" = "Should the Widget show a chart?";

View File

@ -0,0 +1,28 @@
"3d6HSO" = "Enter username";
"7DZrRA" = "Username";
"82L4Nj" = "Yes";
"BFppgH" = "Username";
"OL6lkx" = "Account";
"ZLZ6sg" = "Latest Followers";
"e0W2wo" = "Multi Followers Count";
"fovmPX" = "Accounts";
"gpCwrM" = "Followers Count";
"jg9D5P" = "No";
"sOLUtG" = "Enter follower Username";
"tVvJ9c" = "Followers";
"xVtyec" = "Show chart";
"zeJo4f" = "Should the Widget show a chart?";

View File

@ -0,0 +1,28 @@
"3d6HSO" = "Enter username";
"7DZrRA" = "Username";
"82L4Nj" = "Yes";
"BFppgH" = "Username";
"OL6lkx" = "Account";
"ZLZ6sg" = "Latest Followers";
"e0W2wo" = "Multi Followers Count";
"fovmPX" = "Accounts";
"gpCwrM" = "Followers Count";
"jg9D5P" = "No";
"sOLUtG" = "Enter follower Username";
"tVvJ9c" = "Followers";
"xVtyec" = "Show chart";
"zeJo4f" = "Should the Widget show a chart?";

View File

@ -0,0 +1,28 @@
"3d6HSO" = "Enter username";
"7DZrRA" = "Username";
"82L4Nj" = "Yes";
"BFppgH" = "Username";
"OL6lkx" = "Account";
"ZLZ6sg" = "Latest Followers";
"e0W2wo" = "Multi Followers Count";
"fovmPX" = "Accounts";
"gpCwrM" = "Followers Count";
"jg9D5P" = "No";
"sOLUtG" = "Enter follower Username";
"tVvJ9c" = "Followers";
"xVtyec" = "Show chart";
"zeJo4f" = "Should the Widget show a chart?";

View File

@ -0,0 +1,28 @@
"3d6HSO" = "Enter username";
"7DZrRA" = "Username";
"82L4Nj" = "Yes";
"BFppgH" = "Username";
"OL6lkx" = "Account";
"ZLZ6sg" = "Latest Followers";
"e0W2wo" = "Multi Followers Count";
"fovmPX" = "Accounts";
"gpCwrM" = "Followers Count";
"jg9D5P" = "No";
"sOLUtG" = "Enter follower Username";
"tVvJ9c" = "Followers";
"xVtyec" = "Show chart";
"zeJo4f" = "Should the Widget show a chart?";

View File

@ -0,0 +1,28 @@
"3d6HSO" = "Enter username";
"7DZrRA" = "Username";
"82L4Nj" = "Yes";
"BFppgH" = "Username";
"OL6lkx" = "Account";
"ZLZ6sg" = "Latest Followers";
"e0W2wo" = "Multi Followers Count";
"fovmPX" = "Accounts";
"gpCwrM" = "Followers Count";
"jg9D5P" = "No";
"sOLUtG" = "Enter follower Username";
"tVvJ9c" = "Followers";
"xVtyec" = "Show chart";
"zeJo4f" = "Should the Widget show a chart?";

View File

@ -0,0 +1,28 @@
"3d6HSO" = "Enter username";
"7DZrRA" = "Username";
"82L4Nj" = "Yes";
"BFppgH" = "Username";
"OL6lkx" = "Account";
"ZLZ6sg" = "Latest Followers";
"e0W2wo" = "Multi Followers Count";
"fovmPX" = "Accounts";
"gpCwrM" = "Followers Count";
"jg9D5P" = "No";
"sOLUtG" = "Enter follower Username";
"tVvJ9c" = "Followers";
"xVtyec" = "Show chart";
"zeJo4f" = "Should the Widget show a chart?";

View File

@ -0,0 +1,28 @@
"3d6HSO" = "Enter username";
"7DZrRA" = "Username";
"82L4Nj" = "Yes";
"BFppgH" = "Username";
"OL6lkx" = "Account";
"ZLZ6sg" = "Latest Followers";
"e0W2wo" = "Multi Followers Count";
"fovmPX" = "Accounts";
"gpCwrM" = "Followers Count";
"jg9D5P" = "No";
"sOLUtG" = "Enter follower Username";
"tVvJ9c" = "Followers";
"xVtyec" = "Show chart";
"zeJo4f" = "Should the Widget show a chart?";

View File

@ -0,0 +1,28 @@
"3d6HSO" = "Enter username";
"7DZrRA" = "Username";
"82L4Nj" = "Yes";
"BFppgH" = "Username";
"OL6lkx" = "Account";
"ZLZ6sg" = "Latest Followers";
"e0W2wo" = "Multi Followers Count";
"fovmPX" = "Accounts";
"gpCwrM" = "Followers Count";
"jg9D5P" = "No";
"sOLUtG" = "Enter follower Username";
"tVvJ9c" = "Followers";
"xVtyec" = "Show chart";
"zeJo4f" = "Should the Widget show a chart?";

View File

@ -0,0 +1,28 @@
"3d6HSO" = "Enter username";
"7DZrRA" = "Username";
"82L4Nj" = "Yes";
"BFppgH" = "Username";
"OL6lkx" = "Account";
"ZLZ6sg" = "Latest Followers";
"e0W2wo" = "Multi Followers Count";
"fovmPX" = "Accounts";
"gpCwrM" = "Followers Count";
"jg9D5P" = "No";
"sOLUtG" = "Enter follower Username";
"tVvJ9c" = "Followers";
"xVtyec" = "Show chart";
"zeJo4f" = "Should the Widget show a chart?";

View File

@ -0,0 +1,28 @@
"3d6HSO" = "Enter username";
"7DZrRA" = "Username";
"82L4Nj" = "Yes";
"BFppgH" = "Username";
"OL6lkx" = "Account";
"ZLZ6sg" = "Latest Followers";
"e0W2wo" = "Multi Followers Count";
"fovmPX" = "Accounts";
"gpCwrM" = "Followers Count";
"jg9D5P" = "No";
"sOLUtG" = "Enter follower Username";
"tVvJ9c" = "Followers";
"xVtyec" = "Show chart";
"zeJo4f" = "Should the Widget show a chart?";

View File

@ -0,0 +1,28 @@
"3d6HSO" = "Enter username";
"7DZrRA" = "Username";
"82L4Nj" = "Yes";
"BFppgH" = "Username";
"OL6lkx" = "Account";
"ZLZ6sg" = "Latest Followers";
"e0W2wo" = "Multi Followers Count";
"fovmPX" = "Accounts";
"gpCwrM" = "Followers Count";
"jg9D5P" = "No";
"sOLUtG" = "Enter follower Username";
"tVvJ9c" = "Followers";
"xVtyec" = "Show chart";
"zeJo4f" = "Should the Widget show a chart?";

View File

@ -0,0 +1,28 @@
"3d6HSO" = "Enter username";
"7DZrRA" = "Username";
"82L4Nj" = "Yes";
"BFppgH" = "Username";
"OL6lkx" = "Account";
"ZLZ6sg" = "Latest Followers";
"e0W2wo" = "Multi Followers Count";
"fovmPX" = "Accounts";
"gpCwrM" = "Followers Count";
"jg9D5P" = "No";
"sOLUtG" = "Enter follower Username";
"tVvJ9c" = "Followers";
"xVtyec" = "Show chart";
"zeJo4f" = "Should the Widget show a chart?";

View File

@ -0,0 +1,28 @@
"3d6HSO" = "Enter username";
"7DZrRA" = "Username";
"82L4Nj" = "Yes";
"BFppgH" = "Username";
"OL6lkx" = "Account";
"ZLZ6sg" = "Latest Followers";
"e0W2wo" = "Multi Followers Count";
"fovmPX" = "Accounts";
"gpCwrM" = "Followers Count";
"jg9D5P" = "No";
"sOLUtG" = "Enter follower Username";
"tVvJ9c" = "Followers";
"xVtyec" = "Show chart";
"zeJo4f" = "Should the Widget show a chart?";

View File

@ -0,0 +1,28 @@
"3d6HSO" = "Enter username";
"7DZrRA" = "Username";
"82L4Nj" = "Yes";
"BFppgH" = "Username";
"OL6lkx" = "Account";
"ZLZ6sg" = "Latest Followers";
"e0W2wo" = "Multi Followers Count";
"fovmPX" = "Accounts";
"gpCwrM" = "Followers Count";
"jg9D5P" = "No";
"sOLUtG" = "Enter follower Username";
"tVvJ9c" = "Followers";
"xVtyec" = "Show chart";
"zeJo4f" = "Should the Widget show a chart?";

View File

@ -0,0 +1,28 @@
"3d6HSO" = "Enter username";
"7DZrRA" = "Username";
"82L4Nj" = "Yes";
"BFppgH" = "Username";
"OL6lkx" = "Account";
"ZLZ6sg" = "Latest Followers";
"e0W2wo" = "Multi Followers Count";
"fovmPX" = "Accounts";
"gpCwrM" = "Followers Count";
"jg9D5P" = "No";
"sOLUtG" = "Enter follower Username";
"tVvJ9c" = "Followers";
"xVtyec" = "Show chart";
"zeJo4f" = "Should the Widget show a chart?";

View File

@ -0,0 +1,28 @@
"3d6HSO" = "Enter username";
"7DZrRA" = "Username";
"82L4Nj" = "Yes";
"BFppgH" = "Username";
"OL6lkx" = "Account";
"ZLZ6sg" = "Latest Followers";
"e0W2wo" = "Multi Followers Count";
"fovmPX" = "Accounts";
"gpCwrM" = "Followers Count";
"jg9D5P" = "No";
"sOLUtG" = "Enter follower Username";
"tVvJ9c" = "Followers";
"xVtyec" = "Show chart";
"zeJo4f" = "Should the Widget show a chart?";

View File

@ -0,0 +1,28 @@
"3d6HSO" = "Enter username";
"7DZrRA" = "Username";
"82L4Nj" = "Yes";
"BFppgH" = "Username";
"OL6lkx" = "Account";
"ZLZ6sg" = "Latest Followers";
"e0W2wo" = "Multi Followers Count";
"fovmPX" = "Accounts";
"gpCwrM" = "Followers Count";
"jg9D5P" = "No";
"sOLUtG" = "Enter follower Username";
"tVvJ9c" = "Followers";
"xVtyec" = "Show chart";
"zeJo4f" = "Should the Widget show a chart?";

View File

@ -0,0 +1,28 @@
"3d6HSO" = "Enter username";
"7DZrRA" = "Username";
"82L4Nj" = "Yes";
"BFppgH" = "Username";
"OL6lkx" = "Account";
"ZLZ6sg" = "Latest Followers";
"e0W2wo" = "Multi Followers Count";
"fovmPX" = "Accounts";
"gpCwrM" = "Followers Count";
"jg9D5P" = "No";
"sOLUtG" = "Enter follower Username";
"tVvJ9c" = "Followers";
"xVtyec" = "Show chart";
"zeJo4f" = "Should the Widget show a chart?";

View File

@ -0,0 +1,28 @@
"3d6HSO" = "Enter username";
"7DZrRA" = "Username";
"82L4Nj" = "Yes";
"BFppgH" = "Username";
"OL6lkx" = "Account";
"ZLZ6sg" = "Latest Followers";
"e0W2wo" = "Multi Followers Count";
"fovmPX" = "Accounts";
"gpCwrM" = "Followers Count";
"jg9D5P" = "No";
"sOLUtG" = "Enter follower Username";
"tVvJ9c" = "Followers";
"xVtyec" = "Show chart";
"zeJo4f" = "Should the Widget show a chart?";

View File

@ -0,0 +1,28 @@
"3d6HSO" = "Enter username";
"7DZrRA" = "Username";
"82L4Nj" = "Yes";
"BFppgH" = "Username";
"OL6lkx" = "Account";
"ZLZ6sg" = "Latest Followers";
"e0W2wo" = "Multi Followers Count";
"fovmPX" = "Accounts";
"gpCwrM" = "Followers Count";
"jg9D5P" = "No";
"sOLUtG" = "Enter follower Username";
"tVvJ9c" = "Followers";
"xVtyec" = "Show chart";
"zeJo4f" = "Should the Widget show a chart?";

View File

@ -0,0 +1,28 @@
"3d6HSO" = "Enter username";
"7DZrRA" = "Username";
"82L4Nj" = "Yes";
"BFppgH" = "Username";
"OL6lkx" = "Account";
"ZLZ6sg" = "Latest Followers";
"e0W2wo" = "Multi Followers Count";
"fovmPX" = "Accounts";
"gpCwrM" = "Followers Count";
"jg9D5P" = "No";
"sOLUtG" = "Enter follower Username";
"tVvJ9c" = "Followers";
"xVtyec" = "Show chart";
"zeJo4f" = "Should the Widget show a chart?";

View File

@ -0,0 +1,28 @@
"3d6HSO" = "Enter username";
"7DZrRA" = "Username";
"82L4Nj" = "Yes";
"BFppgH" = "Username";
"OL6lkx" = "Account";
"ZLZ6sg" = "Latest Followers";
"e0W2wo" = "Multi Followers Count";
"fovmPX" = "Accounts";
"gpCwrM" = "Followers Count";
"jg9D5P" = "No";
"sOLUtG" = "Enter follower Username";
"tVvJ9c" = "Followers";
"xVtyec" = "Show chart";
"zeJo4f" = "Should the Widget show a chart?";

View File

@ -0,0 +1,28 @@
"3d6HSO" = "Enter username";
"7DZrRA" = "Username";
"82L4Nj" = "Yes";
"BFppgH" = "Username";
"OL6lkx" = "Account";
"ZLZ6sg" = "Latest Followers";
"e0W2wo" = "Multi Followers Count";
"fovmPX" = "Accounts";
"gpCwrM" = "Followers Count";
"jg9D5P" = "No";
"sOLUtG" = "Enter follower Username";
"tVvJ9c" = "Followers";
"xVtyec" = "Show chart";
"zeJo4f" = "Should the Widget show a chart?";

View File

@ -0,0 +1,28 @@
"3d6HSO" = "Enter username";
"7DZrRA" = "Username";
"82L4Nj" = "Yes";
"BFppgH" = "Username";
"OL6lkx" = "Account";
"ZLZ6sg" = "Latest Followers";
"e0W2wo" = "Multi Followers Count";
"fovmPX" = "Accounts";
"gpCwrM" = "Followers Count";
"jg9D5P" = "No";
"sOLUtG" = "Enter follower Username";
"tVvJ9c" = "Followers";
"xVtyec" = "Show chart";
"zeJo4f" = "Should the Widget show a chart?";