diff --git a/Note.xcodeproj/project.pbxproj b/Note.xcodeproj/project.pbxproj index ae3ad6b..599e859 100644 --- a/Note.xcodeproj/project.pbxproj +++ b/Note.xcodeproj/project.pbxproj @@ -6,8 +6,37 @@ objectVersion = 77; objects = { +/* Begin PBXBuildFile section */ + CFD40B353003C283009C9F2A /* NoteShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = CFD40B283003C283009C9F2A /* NoteShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + CFD40B313003C283009C9F2A /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = CFD40B083003C283009C9F2A /* Project object */; + proxyType = 1; + remoteGlobalIDString = CFD40B293003C283009C9F2A; + remoteInfo = NoteShareExtension; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + CFD40B2F3003C283009C9F2A /* Embed Foundation Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 16; + files = ( + CFD40B353003C283009C9F2A /* NoteShareExtension.appex in Embed Foundation Extensions */, + ); + name = "Embed Foundation Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + /* Begin PBXFileReference section */ CFD40B103003C283009C9F2A /* Note.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Note.app; sourceTree = BUILT_PRODUCTS_DIR; }; + CFD40B283003C283009C9F2A /* NoteShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = NoteShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ @@ -18,6 +47,13 @@ ); target = CFD40B0F3003C283009C9F2A /* Note */; }; + CFD40B2E3003C283009C9F2A /* Exceptions for "NoteShareExtension" folder in "NoteShareExtension" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = CFD40B293003C283009C9F2A /* NoteShareExtension */; + }; /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ @@ -29,6 +65,14 @@ path = Note; sourceTree = ""; }; + CFD40B2D3003C283009C9F2A /* NoteShareExtension */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + CFD40B2E3003C283009C9F2A /* Exceptions for "NoteShareExtension" folder in "NoteShareExtension" target */, + ); + path = NoteShareExtension; + sourceTree = ""; + }; /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ @@ -39,6 +83,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + CFD40B323003C283009C9F2A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -46,6 +97,7 @@ isa = PBXGroup; children = ( CFD40B123003C283009C9F2A /* Note */, + CFD40B2D3003C283009C9F2A /* NoteShareExtension */, CFD40B113003C283009C9F2A /* Products */, ); sourceTree = ""; @@ -54,6 +106,7 @@ isa = PBXGroup; children = ( CFD40B103003C283009C9F2A /* Note.app */, + CFD40B283003C283009C9F2A /* NoteShareExtension.appex */, ); name = Products; sourceTree = ""; @@ -68,10 +121,12 @@ CFD40B0C3003C283009C9F2A /* Sources */, CFD40B0D3003C283009C9F2A /* Frameworks */, CFD40B0E3003C283009C9F2A /* Resources */, + CFD40B2F3003C283009C9F2A /* Embed Foundation Extensions */, ); buildRules = ( ); dependencies = ( + CFD40B303003C283009C9F2A /* PBXTargetDependency */, ); fileSystemSynchronizedGroups = ( CFD40B123003C283009C9F2A /* Note */, @@ -83,6 +138,28 @@ productReference = CFD40B103003C283009C9F2A /* Note.app */; productType = "com.apple.product-type.application"; }; + CFD40B293003C283009C9F2A /* NoteShareExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = CFD40B2A3003C283009C9F2A /* Build configuration list for PBXNativeTarget "NoteShareExtension" */; + buildPhases = ( + CFD40B333003C283009C9F2A /* Sources */, + CFD40B343003C283009C9F2A /* Resources */, + CFD40B323003C283009C9F2A /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + CFD40B2D3003C283009C9F2A /* NoteShareExtension */, + ); + name = NoteShareExtension; + packageProductDependencies = ( + ); + productName = NoteShareExtension; + productReference = CFD40B283003C283009C9F2A /* NoteShareExtension.appex */; + productType = "com.apple.product-type.app-extension"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -96,6 +173,9 @@ CFD40B0F3003C283009C9F2A = { CreatedOnToolsVersion = 26.3; }; + CFD40B293003C283009C9F2A = { + CreatedOnToolsVersion = 26.3; + }; }; }; buildConfigurationList = CFD40B0B3003C283009C9F2A /* Build configuration list for PBXProject "Note" */; @@ -113,6 +193,7 @@ projectRoot = ""; targets = ( CFD40B0F3003C283009C9F2A /* Note */, + CFD40B293003C283009C9F2A /* NoteShareExtension */, ); }; /* End PBXProject section */ @@ -125,6 +206,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + CFD40B343003C283009C9F2A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -135,14 +223,30 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + CFD40B333003C283009C9F2A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + CFD40B303003C283009C9F2A /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = CFD40B293003C283009C9F2A /* NoteShareExtension */; + targetProxy = CFD40B313003C283009C9F2A /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin XCBuildConfiguration section */ CFD40B243003C284009C9F2A /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = Note/Note.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 37HGUEC495; @@ -176,6 +280,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = Note/Note.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 37HGUEC495; @@ -325,6 +430,68 @@ }; name = Release; }; + CFD40B2B3003C283009C9F2A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CODE_SIGN_ENTITLEMENTS = NoteShareExtension/NoteShareExtension.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 37HGUEC495; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = NoteShareExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "备忘录格式化助手"; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0.0; + PRODUCT_BUNDLE_IDENTIFIER = com.fishestlife.note.shareextension; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + CFD40B2C3003C283009C9F2A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CODE_SIGN_ENTITLEMENTS = NoteShareExtension/NoteShareExtension.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 37HGUEC495; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = NoteShareExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "备忘录格式化助手"; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0.0; + PRODUCT_BUNDLE_IDENTIFIER = com.fishestlife.note.shareextension; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -346,6 +513,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + CFD40B2A3003C283009C9F2A /* Build configuration list for PBXNativeTarget "NoteShareExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + CFD40B2B3003C283009C9F2A /* Debug */, + CFD40B2C3003C283009C9F2A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ }; rootObject = CFD40B083003C283009C9F2A /* Project object */; diff --git a/Note.xcodeproj/xcshareddata/xcschemes/Note.xcscheme b/Note.xcodeproj/xcshareddata/xcschemes/Note.xcscheme new file mode 100644 index 0000000..654da1f --- /dev/null +++ b/Note.xcodeproj/xcshareddata/xcschemes/Note.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Note.xcodeproj/xcshareddata/xcschemes/NoteShareExtension.xcscheme b/Note.xcodeproj/xcshareddata/xcschemes/NoteShareExtension.xcscheme new file mode 100644 index 0000000..d582e16 --- /dev/null +++ b/Note.xcodeproj/xcshareddata/xcschemes/NoteShareExtension.xcscheme @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Note/Info.plist b/Note/Info.plist index 0a30ee5..27ca862 100644 --- a/Note/Info.plist +++ b/Note/Info.plist @@ -8,6 +8,18 @@ mobilenotes shareddocuments + noteapp + + CFBundleURLTypes + + + CFBundleURLName + com.fishestlife.note + CFBundleURLSchemes + + noteapp + + CFBundleDocumentTypes @@ -17,26 +29,73 @@ CFBundleTypeRole Editor LSHandlerRank - Alternate + Owner LSItemContentTypes net.daringfireball.markdown + public.markdown public.text public.plain-text public.utf8-plain-text + + CFBundleTypeExtensions + + md + markdown + mdown + mkd + mkdn + + CFBundleTypeMIMETypes + + text/markdown + text/x-markdown + + + + CFBundleTypeName + Plain Text File + CFBundleTypeRole + Editor + LSHandlerRank + Alternate + LSItemContentTypes + + public.plain-text + public.utf8-plain-text + public.text + + CFBundleTypeExtensions + + txt + text + + + + CFBundleTypeName + Any Text or Data File + CFBundleTypeRole + Viewer + LSHandlerRank + Alternate + LSItemContentTypes + public.data + public.content + public.item CFBundleTypeExtensions md markdown txt + * LSSupportsOpeningDocumentsInPlace - UTImportedTypeDeclarations + UTExportedTypeDeclarations UTTypeConformsTo @@ -54,6 +113,45 @@ md markdown + mdown + mkd + mkdn + + public.mime-type + + text/markdown + text/x-markdown + + + + + UTImportedTypeDeclarations + + + UTTypeConformsTo + + public.text + public.plain-text + public.data + + UTTypeDescription + Markdown File + UTTypeIdentifier + net.daringfireball.markdown + UTTypeTagSpecification + + public.filename-extension + + md + markdown + mdown + mkd + mkdn + + public.mime-type + + text/markdown + text/x-markdown @@ -71,7 +169,6 @@ Default Configuration UISceneDelegateClassName $(PRODUCT_MODULE_NAME).SceneDelegate - diff --git a/Note/Note.entitlements b/Note/Note.entitlements new file mode 100644 index 0000000..d7c5784 --- /dev/null +++ b/Note/Note.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.com.fishestlife.note + + + diff --git a/Note/SceneDelegate.swift b/Note/SceneDelegate.swift index 7183a4f..c254a32 100644 --- a/Note/SceneDelegate.swift +++ b/Note/SceneDelegate.swift @@ -60,11 +60,17 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { } private func handleSharedFile(_ url: URL) { - guard url.isFileURL, - let navigationController = window?.rootViewController as? UINavigationController else { + guard let navigationController = window?.rootViewController as? UINavigationController else { return } + if url.scheme == "noteapp", url.host == "share" { + handleShareURL(url, navigationController: navigationController) + return + } + + guard url.isFileURL else { return } + let shouldStopAccessing = url.startAccessingSecurityScopedResource() defer { if shouldStopAccessing { @@ -86,6 +92,40 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { } } + private func handleShareURL(_ url: URL, navigationController: UINavigationController) { + guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false), + let queryItems = components.queryItems else { return } + + let fileManager = FileManager.default + let containerURL = fileManager.containerURL(forSecurityApplicationGroupIdentifier: "group.com.fishestlife.note") + let sharedDir = containerURL?.appendingPathComponent("SharedFiles", isDirectory: true) + + var contents: [(text: String, fileName: String)] = [] + + for item in queryItems { + if item.name.hasPrefix("text"), let text = item.value, !text.isEmpty { + contents.append((text: text, fileName: "shared.md")) + } else if item.name.hasPrefix("file"), let fileName = item.value, !fileName.isEmpty { + if let sharedDir = sharedDir { + let fileURL = sharedDir.appendingPathComponent(fileName) + if let text = try? String(contentsOf: fileURL, encoding: .utf8) { + contents.append((text: text, fileName: fileName)) + } + } + } + } + + guard let first = contents.first else { return } + + DispatchQueue.main.async { + let viewController = PreviewViewController() + viewController.initialText = first.text + viewController.fileName = first.fileName + viewController.title = first.fileName + navigationController.pushViewController(viewController, animated: true) + } + } + } diff --git a/NoteShareExtension/Info.plist b/NoteShareExtension/Info.plist new file mode 100644 index 0000000..ae2410a --- /dev/null +++ b/NoteShareExtension/Info.plist @@ -0,0 +1,41 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + 备忘录格式化助手 + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0.0 + CFBundleVersion + 1 + NSExtension + + NSExtensionAttributes + + NSExtensionActivationRule + + NSExtensionActivationSupportsFileWithMaxCount + 10 + NSExtensionActivationSupportsText + + + + NSExtensionPointIdentifier + com.apple.share-services + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).ShareViewController + + + diff --git a/NoteShareExtension/NoteShareExtension.entitlements b/NoteShareExtension/NoteShareExtension.entitlements new file mode 100644 index 0000000..d7c5784 --- /dev/null +++ b/NoteShareExtension/NoteShareExtension.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.com.fishestlife.note + + + diff --git a/NoteShareExtension/ShareViewController.swift b/NoteShareExtension/ShareViewController.swift new file mode 100644 index 0000000..109fd31 --- /dev/null +++ b/NoteShareExtension/ShareViewController.swift @@ -0,0 +1,103 @@ +import UIKit +import UniformTypeIdentifiers + +class ShareViewController: UIViewController { + override func viewDidLoad() { + super.viewDidLoad() + handleSharedItems() + } + + private func handleSharedItems() { + guard let items = extensionContext?.inputItems as? [NSExtensionItem] else { + closeExtension() + return + } + + let fileManager = FileManager.default + guard let containerURL = fileManager.containerURL(forSecurityApplicationGroupIdentifier: "group.com.fishestlife.note") else { + closeExtension() + return + } + + let sharedDir = containerURL.appendingPathComponent("SharedFiles", isDirectory: true) + try? fileManager.createDirectory(at: sharedDir, withIntermediateDirectories: true) + + var sharedTexts: [String] = [] + var sharedFileNames: [String] = [] + let group = DispatchGroup() + + for item in items { + guard let attachments = item.attachments else { continue } + + for attachment in attachments { + group.enter() + + if attachment.hasItemConformingToTypeIdentifier(UTType.plainText.identifier) { + attachment.loadItem(forTypeIdentifier: UTType.plainText.identifier, options: nil) { data, _ in + defer { group.leave() } + if let text = data as? String { + sharedTexts.append(text) + } else if let url = data as? URL, let text = try? String(contentsOf: url, encoding: .utf8) { + sharedTexts.append(text) + } + } + } else if attachment.hasItemConformingToTypeIdentifier(UTType.url.identifier) { + attachment.loadItem(forTypeIdentifier: UTType.url.identifier, options: nil) { data, _ in + defer { group.leave() } + if let url = data as? URL, let text = try? String(contentsOf: url, encoding: .utf8) { + sharedTexts.append(text) + } + } + } else { + attachment.loadItem(forTypeIdentifier: UTType.data.identifier, options: nil) { data, _ in + defer { group.leave() } + if let url = data as? URL { + let destURL = sharedDir.appendingPathComponent(url.lastPathComponent) + try? fileManager.removeItem(at: destURL) + if (try? fileManager.copyItem(at: url, to: destURL)) != nil { + sharedFileNames.append(url.lastPathComponent) + } + } else if let fileData = data as? Data { + let fileName = "shared-\(UUID().uuidString).md" + let destURL = sharedDir.appendingPathComponent(fileName) + if (try? fileData.write(to: destURL)) != nil { + sharedFileNames.append(fileName) + } + } + } + } + } + } + + group.notify(queue: .main) { [weak self] in + self?.openMainApp(texts: sharedTexts, fileNames: sharedFileNames) + } + } + + private func openMainApp(texts: [String], fileNames: [String]) { + var components = URLComponents(string: "noteapp://share")! + var queryItems: [URLQueryItem] = [] + + for (index, text) in texts.prefix(5).enumerated() { + queryItems.append(URLQueryItem(name: "text\(index)", value: text)) + } + for (index, fileName) in fileNames.prefix(5).enumerated() { + queryItems.append(URLQueryItem(name: "file\(index)", value: fileName)) + } + + components.queryItems = queryItems.isEmpty ? nil : queryItems + + guard let url = components.url else { + closeExtension() + return + } + + extensionContext?.open(url) { [weak self] _ in + self?.closeExtension() + } + } + + private func closeExtension() { + extensionContext?.completeRequest(returningItems: nil, completionHandler: nil) + } +}