From f9121cf0460163c1c8c3690ace28419c76854ad4 Mon Sep 17 00:00:00 2001 From: Mudkip Date: Fri, 22 Nov 2024 01:43:17 +0800 Subject: [PATCH] feat: add iOS 18 control widget --- MoeMemos.xcodeproj/project.pbxproj | 18 +++++++++-- MoeMemos/Helpers/Route.swift | 19 +++++++++++- MoeMemos/Info.plist | 19 ++++++++++++ MoeMemos/MoeMemosApp.swift | 14 ++++++++- MoeMemos/Views/ContentView.swift | 14 ++++----- MoeMemos/Views/MemoCard.swift | 8 ++--- MoeMemos/Views/MemosList.swift | 8 ++--- MoeMemosAppIntents/AppOpenIntent.swift | 23 ++++++++++++++ MoeMemosWidgets/MoeMemosWidgetsBundle.swift | 3 ++ MoeMemosWidgets/QuickMemoWidget.swift | 22 +++++++++++++ .../Sources/Account/AccountSectionView.swift | 3 +- .../Sources/Account/AccountViewModel.swift | 1 - .../Sources/Account/MemosAccountView.swift | 4 ++- Packages/Env/Package.swift | 2 ++ Packages/Env/Sources/Env/Route.swift | 31 +++++++++++++++++++ Packages/Models/Sources/Models/Memo.swift | 2 +- Packages/Models/Sources/Models/Resource.swift | 2 +- Packages/Models/Sources/Models/User.swift | 2 +- 18 files changed, 166 insertions(+), 29 deletions(-) create mode 100644 MoeMemosAppIntents/AppOpenIntent.swift create mode 100644 MoeMemosWidgets/QuickMemoWidget.swift diff --git a/MoeMemos.xcodeproj/project.pbxproj b/MoeMemos.xcodeproj/project.pbxproj index f2b3fe6..84addac 100644 --- a/MoeMemos.xcodeproj/project.pbxproj +++ b/MoeMemos.xcodeproj/project.pbxproj @@ -91,6 +91,9 @@ 19F31BF728C3E26D000C5207 /* MemosList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19F31BF628C3E26D000C5207 /* MemosList.swift */; }; 19F31BFB28C3E9B2000C5207 /* Memo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19F31BFA28C3E9B2000C5207 /* Memo.swift */; }; 19F31BFD28C3ED11000C5207 /* MemoCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19F31BFC28C3ED11000C5207 /* MemoCard.swift */; }; + 19F81CDE2CEDA6DB00728439 /* AppOpenIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19F81CDD2CEDA6D500728439 /* AppOpenIntent.swift */; }; + 19F81CE02CEDAE2800728439 /* QuickMemoWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19F81CDF2CEDAE2300728439 /* QuickMemoWidget.swift */; }; + 19F81CE12CEDAF2400728439 /* AppOpenIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19F81CDD2CEDA6D500728439 /* AppOpenIntent.swift */; }; 19FC1C7C290DA47C0078A7F2 /* Route.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19FC1C7B290DA47C0078A7F2 /* Route.swift */; }; 19FC1C7E290DA6E90078A7F2 /* Navigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19FC1C7D290DA6E90078A7F2 /* Navigation.swift */; }; 31EE785629B83D1B005F62D0 /* LazyImageProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31EE785529B83D1B005F62D0 /* LazyImageProvider.swift */; }; @@ -212,6 +215,8 @@ 19F31BF628C3E26D000C5207 /* MemosList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemosList.swift; sourceTree = ""; }; 19F31BFA28C3E9B2000C5207 /* Memo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Memo.swift; sourceTree = ""; }; 19F31BFC28C3ED11000C5207 /* MemoCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoCard.swift; sourceTree = ""; }; + 19F81CDD2CEDA6D500728439 /* AppOpenIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppOpenIntent.swift; sourceTree = ""; }; + 19F81CDF2CEDAE2300728439 /* QuickMemoWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickMemoWidget.swift; sourceTree = ""; }; 19FC1C7B290DA47C0078A7F2 /* Route.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Route.swift; sourceTree = ""; }; 19FC1C7D290DA6E90078A7F2 /* Navigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Navigation.swift; sourceTree = ""; }; 31EE785529B83D1B005F62D0 /* LazyImageProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyImageProvider.swift; sourceTree = ""; }; @@ -271,13 +276,14 @@ 191339B2291D7288001F2B2E /* MoeMemosWidgets */ = { isa = PBXGroup; children = ( - 191339B8291D7289001F2B2E /* Assets.xcassets */, + 191339D3291D7F13001F2B2E /* MoeMemosWidgetsExtension.entitlements */, 191339BA291D7289001F2B2E /* Info.plist */, 191339DB291D8F15001F2B2E /* MemoryWidget.swift */, 191339B5291D7288001F2B2E /* MemosGraphWidget.swift */, - 193F04E529CCBA9C0010E3BF /* MoeMemosWidgets.intentdefinition */, 191339B3291D7288001F2B2E /* MoeMemosWidgetsBundle.swift */, - 191339D3291D7F13001F2B2E /* MoeMemosWidgetsExtension.entitlements */, + 19F81CDF2CEDAE2300728439 /* QuickMemoWidget.swift */, + 191339B8291D7289001F2B2E /* Assets.xcassets */, + 193F04E529CCBA9C0010E3BF /* MoeMemosWidgets.intentdefinition */, ); path = MoeMemosWidgets; sourceTree = ""; @@ -382,6 +388,7 @@ 199009B72CE4F24E00578B04 /* MoeMemosAppIntents */ = { isa = PBXGroup; children = ( + 19F81CDD2CEDA6D500728439 /* AppOpenIntent.swift */, 197123DE2CEBC21700CEEECC /* AccountEntity.swift */, 197123022CEBB96B00CEEECC /* AppShortcuts.swift */, 199009B82CE4F43100578B04 /* SaveMemoIntent.swift */, @@ -625,10 +632,12 @@ 191339CA291D7675001F2B2E /* Usage.swift in Sources */, 191339D2291D7785001F2B2E /* Date.swift in Sources */, 191339CE291D76BD001F2B2E /* Constants.swift in Sources */, + 19F81CE02CEDAE2800728439 /* QuickMemoWidget.swift in Sources */, 191339DC291D8F15001F2B2E /* MemoryWidget.swift in Sources */, 191339CF291D7748001F2B2E /* Heatmap.swift in Sources */, 191339D0291D774B001F2B2E /* HeatmapStat.swift in Sources */, 191339D1291D7765001F2B2E /* Color.swift in Sources */, + 19F81CE12CEDAF2400728439 /* AppOpenIntent.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -657,6 +666,7 @@ 197123032CEBB96E00CEEECC /* AppShortcuts.swift in Sources */, 19709CFC297F2C1900F6A144 /* MemoInputResourceView.swift in Sources */, 197A4D3329599C300027427E /* MemoCardImageView.swift in Sources */, + 19F81CDE2CEDA6DB00728439 /* AppOpenIntent.swift in Sources */, 1919FBE329CF6BDD00FEF570 /* ArchivedMemoCard.swift in Sources */, 193AA8A928CDB27F00FF7EB6 /* ResourceCard.swift in Sources */, 1995E9CE28C46DC9004F2EDB /* Heatmap.swift in Sources */, @@ -955,6 +965,7 @@ INFOPLIST_FILE = MoeMemos/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = MoeMemos; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity"; + INFOPLIST_KEY_NSCameraUsageDescription = "Capture photos and attach to memos"; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; @@ -994,6 +1005,7 @@ INFOPLIST_FILE = MoeMemos/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = MoeMemos; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity"; + INFOPLIST_KEY_NSCameraUsageDescription = "Capture photos and attach to memos"; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; diff --git a/MoeMemos/Helpers/Route.swift b/MoeMemos/Helpers/Route.swift index 7e23a00..ad6657b 100644 --- a/MoeMemos/Helpers/Route.swift +++ b/MoeMemos/Helpers/Route.swift @@ -10,8 +10,9 @@ import Models import Env import Account +@MainActor extension Route { - @MainActor @ViewBuilder + @ViewBuilder func destination() -> some View { switch self { case .memos: @@ -31,3 +32,19 @@ extension Route { } } } + +@MainActor +extension View { + func withSheetDestinations(sheetDestinations: Binding) -> some View { + sheet(item: sheetDestinations) { destination in + switch destination { + case .newMemo: + MemoInput(memo: nil) + case .editMemo(let memo): + MemoInput(memo: memo) + case .addAccount: + AddAccountView() + } + } + } +} diff --git a/MoeMemos/Info.plist b/MoeMemos/Info.plist index 8bc6c2c..c2b601c 100644 --- a/MoeMemos/Info.plist +++ b/MoeMemos/Info.plist @@ -2,6 +2,25 @@ + CFBundleURLTypes + + + CFBundleTypeRole + Viewer + CFBundleURLName + me.mudkip.MoeMemos + CFBundleURLSchemes + + moememos + + + + INIntentsSupported + + AppOpenIntent + + ITSAppUsesNonExemptEncryption + NSAppTransportSecurity NSAllowsArbitraryLoads diff --git a/MoeMemos/MoeMemosApp.swift b/MoeMemos/MoeMemosApp.swift index 77c2dc1..6e33766 100644 --- a/MoeMemos/MoeMemosApp.swift +++ b/MoeMemos/MoeMemosApp.swift @@ -10,24 +10,36 @@ import Account import Models import Factory import AppIntents +import Env @main struct MoeMemosApp: App { @Injected(\.appInfo) private var appInfo @Injected(\.accountViewModel) private var userState @Injected(\.accountManager) private var accountManager - + @Injected(\.appPath) private var appPath + @State private var memosViewModel = MemosViewModel() + init() { AppDependencyManager.shared.add(dependency: Container.shared.accountManager()) AppDependencyManager.shared.add(dependency: Container.shared.accountViewModel()) + AppDependencyManager.shared.add(dependency: Container.shared.appPath()) + + AppShortcuts.updateAppShortcutParameters() } var body: some Scene { WindowGroup { ContentView() + .tint(.green) .environment(userState) .environment(accountManager) .environment(appInfo) + .environment(appPath) + .environment(memosViewModel) + .onOpenURL { url in + print(url) + } } } } diff --git a/MoeMemos/Views/ContentView.swift b/MoeMemos/Views/ContentView.swift index 10334c3..2551b0b 100644 --- a/MoeMemos/Views/ContentView.swift +++ b/MoeMemos/Views/ContentView.swift @@ -16,17 +16,18 @@ import Env struct ContentView: View { @Environment(AccountViewModel.self) private var accountViewModel: AccountViewModel @Environment(AccountManager.self) private var accountManager: AccountManager + @Environment(MemosViewModel.self) private var memosViewModel: MemosViewModel + @Environment(AppPath.self) private var appPath: AppPath @Injected(\.appInfo) private var appInfo @State private var selection: Route? = .memos - @State private var memosViewModel = MemosViewModel() @Environment(\.scenePhase) var scenePhase var body: some View { @Bindable var accountViewModel = accountViewModel + @Bindable var appPath = appPath Navigation(selection: $selection) - .tint(.green) - .environment(memosViewModel) + .environment(appPath) .onChange(of: scenePhase, initial: true, { _, newValue in if newValue == .active { Task { @@ -42,10 +43,7 @@ struct ContentView: View { try? await memosViewModel.loadTags() } .modelContext(appInfo.modelContext) - .sheet(isPresented: $accountViewModel.showingAddAccount) { - AddAccountView() - .tint(.green) - } + .withSheetDestinations(sheetDestinations: $appPath.presentedSheet) } private func loadCurrentUser() async { @@ -55,7 +53,7 @@ struct ContentView: View { } try await accountViewModel.reloadUsers() } catch MoeMemosError.notLogin { - accountViewModel.showingAddAccount = true + appPath.presentedSheet = .addAccount } catch { print(error) } diff --git a/MoeMemos/Views/MemoCard.swift b/MoeMemos/Views/MemoCard.swift index 8153e98..3393aa3 100644 --- a/MoeMemos/Views/MemoCard.swift +++ b/MoeMemos/Views/MemoCard.swift @@ -9,6 +9,7 @@ import SwiftUI import UniformTypeIdentifiers import MarkdownUI import Models +import Env @MainActor struct MemoCard: View { @@ -16,7 +17,7 @@ struct MemoCard: View { let defaultMemoVisilibity: MemoVisibility? @Environment(MemosViewModel.self) private var memosViewModel: MemosViewModel - @State private var showingEdit = false + @Environment(AppPath.self) private var appPath @State private var showingDeleteConfirmation = false init(_ memo: Memo, defaultMemoVisibility: MemoVisibility) { @@ -61,9 +62,6 @@ struct MemoCard: View { Label("memo.copy", systemImage: "doc.on.doc") } } - .sheet(isPresented: $showingEdit) { - MemoInput(memo: memo) - } .confirmationDialog("memo.delete.confirm", isPresented: $showingDeleteConfirmation, titleVisibility: .visible) { Button("memo.action.ok", role: .destructive) { Task { @@ -94,7 +92,7 @@ struct MemoCard: View { } } Button { - showingEdit = true + appPath.presentedSheet = .editMemo(memo) } label: { Label("memo.edit", systemImage: "pencil") } diff --git a/MoeMemos/Views/MemosList.swift b/MoeMemos/Views/MemosList.swift index 7df4c8c..f784a2c 100644 --- a/MoeMemos/Views/MemosList.swift +++ b/MoeMemos/Views/MemosList.swift @@ -8,12 +8,13 @@ import SwiftUI import Account import Models +import Env struct MemosList: View { let tag: Tag? @State private var searchString = "" - @State private var showingNewPost = false + @Environment(AppPath.self) private var appPath @Environment(AccountManager.self) private var accountManager: AccountManager @Environment(AccountViewModel.self) var userState: AccountViewModel @Environment(MemosViewModel.self) private var memosViewModel: MemosViewModel @@ -32,7 +33,7 @@ struct MemosList: View { if tag == nil { Button { - showingNewPost = true + appPath.presentedSheet = .newMemo } label: { Circle().overlay { Image(systemName: "plus") @@ -53,9 +54,6 @@ struct MemosList: View { }) .searchable(text: $searchString) .navigationTitle(tag?.name ?? NSLocalizedString("memo.memos", comment: "Memos")) - .sheet(isPresented: $showingNewPost) { - MemoInput(memo: nil) - } .onAppear { filteredMemoList = filterMemoList(memosViewModel.memoList, tag: tag, searchString: searchString) } diff --git a/MoeMemosAppIntents/AppOpenIntent.swift b/MoeMemosAppIntents/AppOpenIntent.swift new file mode 100644 index 0000000..0dee67f --- /dev/null +++ b/MoeMemosAppIntents/AppOpenIntent.swift @@ -0,0 +1,23 @@ +// +// OpenAppIntent.swift +// MoeMemos +// +// Created by Mudkip on 2024/11/20. +// + +import AppIntents +import Env + +struct AppOpenIntent: AppIntent { + static let title: LocalizedStringResource = "Launch Moe Memos" + + static let openAppWhenRun: Bool = true + + @Dependency + var appPath: AppPath + + func perform() async throws -> some IntentResult { + appPath.presentedSheet = .newMemo + return .result() + } +} diff --git a/MoeMemosWidgets/MoeMemosWidgetsBundle.swift b/MoeMemosWidgets/MoeMemosWidgetsBundle.swift index f7095a7..e57549f 100644 --- a/MoeMemosWidgets/MoeMemosWidgetsBundle.swift +++ b/MoeMemosWidgets/MoeMemosWidgetsBundle.swift @@ -13,5 +13,8 @@ struct MoeMemosWidgetsBundle: WidgetBundle { var body: some Widget { MemosGraphWidget() MemoryWidget() + if #available(iOS 18.0, *) { + QuickMemoWidget() + } } } diff --git a/MoeMemosWidgets/QuickMemoWidget.swift b/MoeMemosWidgets/QuickMemoWidget.swift new file mode 100644 index 0000000..d6f1dbb --- /dev/null +++ b/MoeMemosWidgets/QuickMemoWidget.swift @@ -0,0 +1,22 @@ +// +// QuickMemoWidget.swift +// MoeMemos +// +// Created by Mudkip on 2024/11/20. +// + +import WidgetKit +import SwiftUI + +@available(iOS 18.0, *) +struct QuickMemoWidget: ControlWidget { + var body: some ControlWidgetConfiguration { + StaticControlConfiguration(kind: "me.mudkip.MoeMemos.QuickMemoWidget") { + ControlWidgetButton(action: AppOpenIntent()) { + Label("Quick Memo", systemImage: "note.text.badge.plus") + } + } + .displayName("Quick Memo") + .description("Write a memo in Moe Memos.") + } +} diff --git a/Packages/Account/Sources/Account/AccountSectionView.swift b/Packages/Account/Sources/Account/AccountSectionView.swift index 00881e8..900e528 100644 --- a/Packages/Account/Sources/Account/AccountSectionView.swift +++ b/Packages/Account/Sources/Account/AccountSectionView.swift @@ -11,6 +11,7 @@ import Env public struct AccountSectionView: View { @Environment(AccountViewModel.self) private var accountViewModel @Environment(AccountManager.self) private var accountManager + @Environment(AppPath.self) private var appPath public init() {} @@ -41,7 +42,7 @@ public struct AccountSectionView: View { } } Button { - accountViewModel.showingAddAccount = true + appPath.presentedSheet = .addAccount } label: { Label("account.add-account", systemImage: "plus.circle") } diff --git a/Packages/Account/Sources/Account/AccountViewModel.swift b/Packages/Account/Sources/Account/AccountViewModel.swift index f23d740..e0632e0 100644 --- a/Packages/Account/Sources/Account/AccountViewModel.swift +++ b/Packages/Account/Sources/Account/AccountViewModel.swift @@ -15,7 +15,6 @@ import MemosV0Service @Observable public final class AccountViewModel: @unchecked Sendable { private var currentContext: ModelContext private var accountManager: AccountManager - public var showingAddAccount = false public init(currentContext: ModelContext, accountManager: AccountManager) { self.currentContext = currentContext diff --git a/Packages/Account/Sources/Account/MemosAccountView.swift b/Packages/Account/Sources/Account/MemosAccountView.swift index f427031..c38469b 100644 --- a/Packages/Account/Sources/Account/MemosAccountView.swift +++ b/Packages/Account/Sources/Account/MemosAccountView.swift @@ -8,6 +8,7 @@ import Foundation import SwiftUI import Models +import Env public struct MemosAccountView: View { @State var user: User? = nil @@ -15,6 +16,7 @@ public struct MemosAccountView: View { private let accountKey: String @Environment(AccountManager.self) private var accountManager @Environment(AccountViewModel.self) private var accountViewModel + @Environment(AppPath.self) private var appPath private var account: Account? { accountManager.accounts.first { $0.key == accountKey } } @Environment(\.presentationMode) var presentationMode @@ -77,7 +79,7 @@ public struct MemosAccountView: View { presentationMode.wrappedValue.dismiss() if accountManager.currentAccount == nil { - accountViewModel.showingAddAccount = true + appPath.presentedSheet = .addAccount } } } label: { diff --git a/Packages/Env/Package.swift b/Packages/Env/Package.swift index 9abff76..de3a70a 100644 --- a/Packages/Env/Package.swift +++ b/Packages/Env/Package.swift @@ -18,6 +18,7 @@ let package = Package( ], dependencies: [ .package(name: "Models", path: "../Models"), + .package(url: "https://github.com/hmlongco/Factory", from: "2.3.2") ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. @@ -26,6 +27,7 @@ let package = Package( name: "Env", dependencies: [ .product(name: "Models", package: "Models"), + .product(name: "Factory", package: "Factory") ] ), .testTarget( diff --git a/Packages/Env/Sources/Env/Route.swift b/Packages/Env/Sources/Env/Route.swift index 6062052..217c5fb 100644 --- a/Packages/Env/Sources/Env/Route.swift +++ b/Packages/Env/Sources/Env/Route.swift @@ -6,6 +6,8 @@ // import Models +import Observation +import Factory public enum Route: Hashable { case memos @@ -16,3 +18,32 @@ public enum Route: Hashable { case explore case memosAccount(String) } + +public enum SheetDestination: Identifiable, Hashable { + case newMemo + case editMemo(Memo) + case addAccount + + public var id: String { + switch self { + case .newMemo: + return "newMemo" + case .editMemo: + return "editMemo" + case .addAccount: + return "addAccount" + } + } +} + +@Observable public final class AppPath: Sendable { + public var presentedSheet: SheetDestination? + + public init() {} +} + +public extension Container { + var appPath: Factory { + self { AppPath() }.shared + } +} diff --git a/Packages/Models/Sources/Models/Memo.swift b/Packages/Models/Sources/Models/Memo.swift index b73111d..9fdf5a4 100644 --- a/Packages/Models/Sources/Models/Memo.swift +++ b/Packages/Models/Sources/Models/Memo.swift @@ -20,7 +20,7 @@ public enum MemoVisibility: Codable, Sendable { case direct } -public struct Memo: Equatable, Sendable { +public struct Memo: Equatable, Sendable, Hashable { public var user: RemoteUser? public var content: String public var pinned: Bool diff --git a/Packages/Models/Sources/Models/Resource.swift b/Packages/Models/Sources/Models/Resource.swift index 58fc15c..5bc144e 100644 --- a/Packages/Models/Sources/Models/Resource.swift +++ b/Packages/Models/Sources/Models/Resource.swift @@ -7,7 +7,7 @@ import Foundation -public struct Resource: Identifiable, Equatable, Sendable { +public struct Resource: Identifiable, Equatable, Sendable, Hashable { public var filename: String public var size: Int public var mimeType: String diff --git a/Packages/Models/Sources/Models/User.swift b/Packages/Models/Sources/Models/User.swift index b7293da..218a612 100644 --- a/Packages/Models/Sources/Models/User.swift +++ b/Packages/Models/Sources/Models/User.swift @@ -20,7 +20,7 @@ public protocol UserData: Equatable { var avatar: UserAvatar? { get } } -public struct RemoteUser: UserData, Sendable { +public struct RemoteUser: UserData, Sendable, Hashable { public var nickname: String public var defaultVisibility: MemoVisibility public var creationDate: Date