From 49a5c6a56a3342a445b64a13c1c19aede2ad9013 Mon Sep 17 00:00:00 2001 From: Thomas Ricouard Date: Thu, 2 May 2024 11:37:38 +0200 Subject: [PATCH] Add more shortcuts --- IceCubesApp.xcodeproj/project.pbxproj | 12 +++ IceCubesApp/App/AppRegistry.swift | 3 + IceCubesApp/App/Main/IceCubesApp+Scene.swift | 6 ++ IceCubesApp/App/Tabs/Tabs.swift | 1 + .../Localization/Localizable.xcstrings | 85 +++++++++++++++- IceCubesAppIntents/AppShortcuts.swift | 34 +++++++ IceCubesAppIntents/PostIntent.swift | 4 +- IceCubesAppIntents/PostPhotoIntent.swift | 23 +++++ IceCubesAppIntents/TabIntent.swift | 96 +++++++++++++++++++ Packages/Env/Sources/Env/Router.swift | 3 +- .../StatusKit/Editor/ViewModeMode.swift | 3 +- .../Sources/StatusKit/Editor/ViewModel.swift | 32 ++++++- 12 files changed, 295 insertions(+), 7 deletions(-) create mode 100644 IceCubesAppIntents/AppShortcuts.swift create mode 100644 IceCubesAppIntents/PostPhotoIntent.swift create mode 100644 IceCubesAppIntents/TabIntent.swift diff --git a/IceCubesApp.xcodeproj/project.pbxproj b/IceCubesApp.xcodeproj/project.pbxproj index 7e452529..35be5779 100644 --- a/IceCubesApp.xcodeproj/project.pbxproj +++ b/IceCubesApp.xcodeproj/project.pbxproj @@ -44,6 +44,9 @@ 9F35DB4C2952005C00B3281A /* MessagesTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F35DB4B2952005C00B3281A /* MessagesTab.swift */; }; 9F37BDDB2BE36E22007F28AD /* PostIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F37BDDA2BE36E22007F28AD /* PostIntent.swift */; }; 9F37BDDD2BE37193007F28AD /* AppIntentService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F37BDDC2BE37193007F28AD /* AppIntentService.swift */; }; + 9F37BDDF2BE37C35007F28AD /* TabIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F37BDDE2BE37C35007F28AD /* TabIntent.swift */; }; + 9F37BDE12BE38646007F28AD /* PostPhotoIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F37BDE02BE38646007F28AD /* PostPhotoIntent.swift */; }; + 9F37BDE32BE393A7007F28AD /* AppShortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F37BDE22BE393A7007F28AD /* AppShortcuts.swift */; }; 9F38A7332ACEA26100DBCD66 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 9F38A7322ACEA26100DBCD66 /* Localizable.xcstrings */; }; 9F38A7342ACEA26100DBCD66 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 9F38A7322ACEA26100DBCD66 /* Localizable.xcstrings */; }; 9F38A7352ACEA26100DBCD66 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 9F38A7322ACEA26100DBCD66 /* Localizable.xcstrings */; }; @@ -200,6 +203,9 @@ 9F35DB4B2952005C00B3281A /* MessagesTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagesTab.swift; sourceTree = ""; }; 9F37BDDA2BE36E22007F28AD /* PostIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostIntent.swift; sourceTree = ""; }; 9F37BDDC2BE37193007F28AD /* AppIntentService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIntentService.swift; sourceTree = ""; }; + 9F37BDDE2BE37C35007F28AD /* TabIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabIntent.swift; sourceTree = ""; }; + 9F37BDE02BE38646007F28AD /* PostPhotoIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostPhotoIntent.swift; sourceTree = ""; }; + 9F37BDE22BE393A7007F28AD /* AppShortcuts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppShortcuts.swift; sourceTree = ""; }; 9F38A7322ACEA26100DBCD66 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; 9F398AA32935F90100A889F2 /* Models */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Models; path = Packages/Models; sourceTree = ""; }; 9F398AA52935FE8A00A889F2 /* AppRegistry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRegistry.swift; sourceTree = ""; }; @@ -358,6 +364,9 @@ children = ( 9F37BDDA2BE36E22007F28AD /* PostIntent.swift */, 9F37BDDC2BE37193007F28AD /* AppIntentService.swift */, + 9F37BDDE2BE37C35007F28AD /* TabIntent.swift */, + 9F37BDE02BE38646007F28AD /* PostPhotoIntent.swift */, + 9F37BDE22BE393A7007F28AD /* AppShortcuts.swift */, ); path = IceCubesAppIntents; sourceTree = ""; @@ -844,6 +853,8 @@ 9F35DB4C2952005C00B3281A /* MessagesTab.swift in Sources */, 9F37BDDB2BE36E22007F28AD /* PostIntent.swift in Sources */, 9F37BDDD2BE37193007F28AD /* AppIntentService.swift in Sources */, + 9F37BDE12BE38646007F28AD /* PostPhotoIntent.swift in Sources */, + 9F37BDDF2BE37C35007F28AD /* TabIntent.swift in Sources */, 9FAD85CF2975B68900496AB1 /* SideBarView.swift in Sources */, 9FAE4ACB293783B000772766 /* SettingsTab.swift in Sources */, 9FC14EF42B494D940006CEE1 /* RemoteTimelinesSettingView.swift in Sources */, @@ -862,6 +873,7 @@ 9F2B92FA295DA7D700DE16D0 /* AddAccountsView.swift in Sources */, 639CDF9C296AC82F00C35E58 /* SafariRouter.swift in Sources */, 9F35DB4729506F6600B3281A /* NotificationTab.swift in Sources */, + 9F37BDE32BE393A7007F28AD /* AppShortcuts.swift in Sources */, 9F654BEF299AC45B00D27FA5 /* ReportView.swift in Sources */, D08A9C3529956CFA00204A4A /* SwipeActionsSettingsView.swift in Sources */, 9F7335F22967608F00AFF0BA /* AddRemoteTimelineView.swift in Sources */, diff --git a/IceCubesApp/App/AppRegistry.swift b/IceCubesApp/App/AppRegistry.swift index 4c8b6c8e..8de29b3f 100644 --- a/IceCubesApp/App/AppRegistry.swift +++ b/IceCubesApp/App/AppRegistry.swift @@ -90,6 +90,9 @@ extension View { case let .prefilledStatusEditor(text, visibility): StatusEditor.MainView(mode: .new(text: text, visibility: visibility)) .withEnvironments() + case let .imageURL(urls, visibility): + StatusEditor.MainView(mode: .imageURL(urls: urls, visibility: visibility)) + .withEnvironments() case let .editStatusEditor(status): StatusEditor.MainView(mode: .edit(status: status)) .withEnvironments() diff --git a/IceCubesApp/App/Main/IceCubesApp+Scene.swift b/IceCubesApp/App/Main/IceCubesApp+Scene.swift index 957d7c48..8b99bf0b 100644 --- a/IceCubesApp/App/Main/IceCubesApp+Scene.swift +++ b/IceCubesApp/App/Main/IceCubesApp+Scene.swift @@ -135,6 +135,12 @@ extension IceCubesApp { appRouterPath.presentedSheet = .prefilledStatusEditor(text: postIntent.content ?? "", visibility: userPreferences.postVisibility) #endif + } else if let tabIntent = appIntentService.handledIntent?.intent as? TabIntent { + selectedTab = tabIntent.tab.toAppTab + } else if let imageIntent = appIntentService.handledIntent?.intent as? PostPhotoIntent, + let urls = imageIntent.images?.compactMap({ $0.fileURL }) { + appRouterPath.presentedSheet = .imageURL(urls: urls, + visibility: userPreferences.postVisibility) } } } diff --git a/IceCubesApp/App/Tabs/Tabs.swift b/IceCubesApp/App/Tabs/Tabs.swift index 2a05600f..a7d450ac 100644 --- a/IceCubesApp/App/Tabs/Tabs.swift +++ b/IceCubesApp/App/Tabs/Tabs.swift @@ -4,6 +4,7 @@ import Explore import Foundation import StatusKit import SwiftUI +import AppIntents @MainActor enum Tab: Int, Identifiable, Hashable, CaseIterable, Codable { diff --git a/IceCubesApp/Resources/Localization/Localizable.xcstrings b/IceCubesApp/Resources/Localization/Localizable.xcstrings index 42d3c9c1..bff1953e 100644 --- a/IceCubesApp/Resources/Localization/Localizable.xcstrings +++ b/IceCubesApp/Resources/Localization/Localizable.xcstrings @@ -20513,6 +20513,9 @@ } } } + }, + "Bookmarks" : { + }, "conversations.action.delete" : { "comment" : "MARK: Package: Conversations", @@ -26422,6 +26425,9 @@ } } } + }, + "Explore & Trending" : { + }, "explore.navigation-title" : { "comment" : "MARK: Package: Explore", @@ -28446,6 +28452,12 @@ } } } + }, + "Favorites" : { + + }, + "Federated Timeline" : { + }, "filter.action.hide" : { "extractionState" : "manual", @@ -30708,6 +30720,18 @@ } } } + }, + "Followed Tags" : { + + }, + "Home Timeline" : { + + }, + "Image" : { + + }, + "Image to post on Mastodon" : { + }, "instance.info.domains" : { "comment" : "MARK: Instances", @@ -32957,6 +32981,9 @@ } } } + }, + "Lists" : { + }, "lists.add-remove-%@" : { "comment" : "MARK: Package: Lists", @@ -33910,6 +33937,12 @@ } } } + }, + "Local Timeline" : { + + }, + "Mentions" : { + }, "menu.font" : { "localizations" : { @@ -34619,6 +34652,12 @@ } } } + }, + "New post" : { + + }, + "Notifications" : { + }, "notifications-others-count %lld" : { "extractionState" : "manual", @@ -39700,6 +39739,15 @@ } } } + }, + "Open Ice Cubes" : { + + }, + "Open on a tab" : { + + }, + "Open the app on a specific tab" : { + }, "placeholder.loading.long" : { "localizations" : { @@ -39936,11 +39984,26 @@ } } } + }, + "Post a status" : { + + }, + "Post an image to Mastodon" : { + }, "Post content" : { }, - "Post to Mastodon" : { + "Post images" : { + + }, + "Post status to Mastodon" : { + + }, + "Private Messages" : { + + }, + "Profile" : { }, "report.action.send" : { @@ -40416,6 +40479,12 @@ } } } + }, + "Selected tab" : { + + }, + "Settings" : { + }, "settings.about.built-with" : { "localizations" : { @@ -74591,6 +74660,9 @@ } } } + }, + "Tab" : { + }, "tab.explore" : { "comment" : "MARK: Tabs", @@ -80113,6 +80185,12 @@ } } } + }, + "Trending Links" : { + + }, + "Trending Timeline" : { + }, "trending-tag-people-talking %lld" : { "extractionState" : "manual", @@ -80473,7 +80551,10 @@ } } }, - "Use Ice Cubes to post text to Mastodon" : { + "Use Ice Cubes to post a status to Mastodon" : { + + }, + "Use Ice Cubes to post a status with an image to Mastodon" : { } }, diff --git a/IceCubesAppIntents/AppShortcuts.swift b/IceCubesAppIntents/AppShortcuts.swift new file mode 100644 index 00000000..080a527d --- /dev/null +++ b/IceCubesAppIntents/AppShortcuts.swift @@ -0,0 +1,34 @@ +import AppIntents + +struct AppShortcuts: AppShortcutsProvider { + static var appShortcuts: [AppShortcut] { + AppShortcut( + intent: PostIntent(), + phrases: [ + "Post \(\.$content) in \(.applicationName)", + "Post a status on Mastodon with \(.applicationName)", + "Write a status in \(.applicationName)", + ], + shortTitle: "Post a status", + systemImageName: "square.and.pencil" + ) + AppShortcut( + intent: TabIntent(), + phrases: [ + "Open \(\.$tab) in \(.applicationName)", + "Open \(.applicationName)", + ], + shortTitle: "Open Ice Cubes", + systemImageName: "cube" + ) + AppShortcut( + intent: PostPhotoIntent(), + phrases: [ + "Post images \(\.$images) in \(.applicationName)", + "Send photos \(\.$images) with \(.applicationName)", + ], + shortTitle: "Post images", + systemImageName: "photo" + ) + } +} diff --git a/IceCubesAppIntents/PostIntent.swift b/IceCubesAppIntents/PostIntent.swift index e1b5e361..70677a12 100644 --- a/IceCubesAppIntents/PostIntent.swift +++ b/IceCubesAppIntents/PostIntent.swift @@ -2,10 +2,10 @@ import Foundation import AppIntents struct PostIntent: AppIntent { - static let title: LocalizedStringResource = "Post to Mastodon" + static let title: LocalizedStringResource = "Post status to Mastodon" static var description: IntentDescription { get { - "Use Ice Cubes to post text to Mastodon" + "Use Ice Cubes to post a status to Mastodon" } } static let openAppWhenRun: Bool = true diff --git a/IceCubesAppIntents/PostPhotoIntent.swift b/IceCubesAppIntents/PostPhotoIntent.swift new file mode 100644 index 00000000..030d7420 --- /dev/null +++ b/IceCubesAppIntents/PostPhotoIntent.swift @@ -0,0 +1,23 @@ +import Foundation +import AppIntents + +struct PostPhotoIntent: AppIntent { + static let title: LocalizedStringResource = "Post an image to Mastodon" + static var description: IntentDescription { + get { + "Use Ice Cubes to post a status with an image to Mastodon" + } + } + static let openAppWhenRun: Bool = true + + @Parameter(title: "Image", + description: "Image to post on Mastodon", + supportedTypeIdentifiers: ["public.image"], + inputConnectionBehavior: .connectToPreviousIntentResult) + var images: [IntentFile]? + + func perform() async throws -> some IntentResult { + AppIntentService.shared.handledIntent = .init(intent: self) + return .result() + } +} diff --git a/IceCubesAppIntents/TabIntent.swift b/IceCubesAppIntents/TabIntent.swift new file mode 100644 index 00000000..77efa0e3 --- /dev/null +++ b/IceCubesAppIntents/TabIntent.swift @@ -0,0 +1,96 @@ +import Foundation +import AppIntents + +enum TabEnum: String, AppEnum, Sendable { + case timeline, notifications, mentions, explore, messages, settings + case trending, federated, local + case profile + case bookmarks + case favorites + case post + case followedTags + case lists + case links + + static var typeDisplayName: LocalizedStringResource { + get { "Tab" } + } + + static let typeDisplayRepresentation: TypeDisplayRepresentation = "Tab" + + nonisolated static var caseDisplayRepresentations: [TabEnum : DisplayRepresentation] { + [.timeline: .init(title: "Home Timeline"), + .trending: .init(title: "Trending Timeline"), + .federated: .init(title: "Federated Timeline"), + .local: .init(title: "Local Timeline"), + .notifications: .init(title: "Notifications"), + .mentions: .init(title: "Mentions"), + .explore: .init(title: "Explore & Trending"), + .messages: .init(title: "Private Messages"), + .settings: .init(title: "Settings"), + .profile: .init(title: "Profile"), + .bookmarks: .init(title: "Bookmarks"), + .favorites: .init(title: "Favorites"), + .followedTags: .init(title: "Followed Tags"), + .lists: .init(title: "Lists"), + .links: .init(title: "Trending Links"), + .post: .init(title: "New post"), + ] + } + + var toAppTab: Tab { + switch self { + case .timeline: + .timeline + case .notifications: + .notifications + case .mentions: + .mentions + case .explore: + .explore + case .messages: + .messages + case .settings: + .settings + case .trending: + .trending + case .federated: + .federated + case .local: + .local + case .profile: + .profile + case .bookmarks: + .bookmarks + case .favorites: + .favorites + case .post: + .post + case .followedTags: + .followedTags + case .lists: + .lists + case .links: + .links + } + } +} + +struct TabIntent: AppIntent { + static let title: LocalizedStringResource = "Open on a tab" + static var description: IntentDescription { + get { + "Open the app on a specific tab" + } + } + static let openAppWhenRun: Bool = true + + @Parameter(title: "Selected tab") + var tab: TabEnum + + @MainActor + func perform() async throws -> some IntentResult { + AppIntentService.shared.handledIntent = .init(intent: self) + return .result() + } +} diff --git a/Packages/Env/Sources/Env/Router.swift b/Packages/Env/Sources/Env/Router.swift index 06cdcd25..c0cc64a1 100644 --- a/Packages/Env/Sources/Env/Router.swift +++ b/Packages/Env/Sources/Env/Router.swift @@ -54,6 +54,7 @@ public enum SheetDestination: Identifiable, Hashable { case newStatusEditor(visibility: Models.Visibility) case prefilledStatusEditor(text: String, visibility: Models.Visibility) + case imageURL(urls: [URL], visibility: Models.Visibility) case editStatusEditor(status: Status) case replyToStatusEditor(status: Status) case quoteStatusEditor(status: Status) @@ -80,7 +81,7 @@ public enum SheetDestination: Identifiable, Hashable { public var id: String { switch self { case .editStatusEditor, .newStatusEditor, .replyToStatusEditor, .quoteStatusEditor, - .mentionStatusEditor, .quoteLinkStatusEditor, .prefilledStatusEditor: + .mentionStatusEditor, .quoteLinkStatusEditor, .prefilledStatusEditor, .imageURL: "statusEditor" case .listCreate: "listCreate" diff --git a/Packages/StatusKit/Sources/StatusKit/Editor/ViewModeMode.swift b/Packages/StatusKit/Sources/StatusKit/Editor/ViewModeMode.swift index 2fe93ba3..e8d1f5d1 100644 --- a/Packages/StatusKit/Sources/StatusKit/Editor/ViewModeMode.swift +++ b/Packages/StatusKit/Sources/StatusKit/Editor/ViewModeMode.swift @@ -11,6 +11,7 @@ public extension StatusEditor.ViewModel { case quoteLink(link: URL) case mention(account: Account, visibility: Models.Visibility) case shareExtension(items: [NSItemProvider]) + case imageURL(urls: [URL], visibility: Models.Visibility) var isInShareExtension: Bool { switch self { @@ -41,7 +42,7 @@ public extension StatusEditor.ViewModel { var title: LocalizedStringKey { switch self { - case .new, .mention, .shareExtension, .quoteLink: + case .new, .mention, .shareExtension, .quoteLink, .imageURL: "status.editor.mode.new" case .edit: "status.editor.mode.edit" diff --git a/Packages/StatusKit/Sources/StatusKit/Editor/ViewModel.swift b/Packages/StatusKit/Sources/StatusKit/Editor/ViewModel.swift index 17638198..e854f8db 100644 --- a/Packages/StatusKit/Sources/StatusKit/Editor/ViewModel.swift +++ b/Packages/StatusKit/Sources/StatusKit/Editor/ViewModel.swift @@ -234,7 +234,7 @@ public extension StatusEditor { language: selectedLanguage, mediaAttributes: mediaAttributes) switch mode { - case .new, .replyTo, .quote, .mention, .shareExtension, .quoteLink: + case .new, .replyTo, .quote, .mention, .shareExtension, .quoteLink, .imageURL: postStatus = try await client.post(endpoint: Statuses.postStatus(json: data)) if let postStatus { StreamWatcher.shared.emmitPostEvent(for: postStatus) @@ -311,6 +311,13 @@ public extension StatusEditor { itemsProvider = items visibility = .pub processItemsProvider(items: items) + case let .imageURL(urls, visibility): + Task { + for container in await Self.makeImageContainer(from: urls) { + prepareToPost(for: container) + } + } + self.visibility = visibility case let .replyTo(status): var mentionString = "" if (status.reblog?.account.acct ?? status.account.acct) != currentAccount?.acct { @@ -739,6 +746,29 @@ public extension StatusEditor { error: nil ) } + + private static func makeImageContainer(from urls: [URL]) async -> [MediaContainer] { + var containers: [MediaContainer] = [] + + for url in urls { + let compressor = Compressor() + + if let compressedData = await compressor.compressImageFrom(url: url), + let image = UIImage(data: compressedData) { + + containers.append(MediaContainer( + id: UUID().uuidString, + image: image, + movieTransferable: nil, + gifTransferable: nil, + mediaAttachment: nil, + error: nil + )) + } + } + + return containers + } func upload(container: MediaContainer) async { if let index = indexOf(container: container) {