Merge pull request #915 from mastodon/IOS-37_Widgets
Feature: Followers, Multiple Followers, Latest Followers Widget
This commit is contained in:
commit
a6dc97b22b
|
@ -131,3 +131,6 @@ env/**/**
|
|||
## Ruby ###
|
||||
vendor/
|
||||
.bundle/
|
||||
|
||||
## IntelliJ IDEA ##
|
||||
.idea
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 })
|
||||
}
|
||||
}
|
|
@ -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 })
|
||||
}
|
||||
}
|
|
@ -30,6 +30,8 @@
|
|||
<array/>
|
||||
<key>IntentsSupported</key>
|
||||
<array>
|
||||
<string>FollowersCountIntent</string>
|
||||
<string>MultiFollowersCountIntent</string>
|
||||
<string>SendPostIntent</string>
|
||||
</array>
|
||||
</dict>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -518,3 +518,17 @@ You can’t 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: %@";
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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 |
|
@ -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 |
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Logo.svg",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -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 |
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -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 |
|
@ -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>
|
|
@ -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>
|
|
@ -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)"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -0,0 +1,9 @@
|
|||
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
|
||||
|
||||
import MastodonCore
|
||||
import MastodonSDK
|
||||
import MastodonLocalization
|
||||
|
||||
enum WidgetExtension {
|
||||
static let appContext = AppContext()
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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?";
|
||||
|
|
@ -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?";
|
||||
|
|
@ -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?";
|
||||
|
|
@ -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?";
|
||||
|
|
@ -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?";
|
||||
|
|
@ -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?";
|
||||
|
|
@ -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?";
|
||||
|
|
@ -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?";
|
||||
|
|
@ -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?";
|
||||
|
|
@ -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?";
|
||||
|
|
@ -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?";
|
||||
|
|
@ -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?";
|
||||
|
|
@ -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?";
|
||||
|
|
@ -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?";
|
||||
|
|
@ -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?";
|
||||
|
|
@ -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?";
|
||||
|
|
@ -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?";
|
||||
|
|
@ -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?";
|
||||
|
|
@ -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?";
|
||||
|
|
@ -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?";
|
||||
|
|
@ -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?";
|
||||
|
|
@ -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?";
|
||||
|
|
@ -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?";
|
||||
|
|
@ -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?";
|
||||
|
|
@ -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?";
|
||||
|
|
@ -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?";
|
||||
|
Loading…
Reference in New Issue