Initial import of LogDarioSeq baseline
Establishes version control for the personal Logseq DB fork before starting the server-client architecture migration and domain features described in docs/agent-guide and the approved implementation plan. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
912
ios/App/App.xcodeproj/project.pbxproj
Normal file
@@ -0,0 +1,912 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 77;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
2FAD9763203C412B000D30F8 /* config.xml in Resources */ = {isa = PBXBuildFile; fileRef = 2FAD9762203C412B000D30F8 /* config.xml */; };
|
||||
50379B232058CBB4000EE86E /* capacitor.config.json in Resources */ = {isa = PBXBuildFile; fileRef = 50379B222058CBB4000EE86E /* capacitor.config.json */; };
|
||||
504EC3081FED79650016851F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504EC3071FED79650016851F /* AppDelegate.swift */; };
|
||||
504EC30D1FED79650016851F /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30B1FED79650016851F /* Main.storyboard */; };
|
||||
504EC30F1FED79650016851F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30E1FED79650016851F /* Assets.xcassets */; };
|
||||
504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC3101FED79650016851F /* LaunchScreen.storyboard */; };
|
||||
50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; };
|
||||
5FF8632A283B5ADB0047731B /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FF86329283B5ADB0047731B /* Utils.swift */; };
|
||||
5FF8632C283B5BFD0047731B /* Utils.m in Sources */ = {isa = PBXBuildFile; fileRef = 5FF8632B283B5BFD0047731B /* Utils.m */; };
|
||||
5FFF7D6D27E343FA00B00DA8 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FFF7D6C27E343FA00B00DA8 /* ShareViewController.swift */; };
|
||||
5FFF7D7027E343FA00B00DA8 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5FFF7D6E27E343FA00B00DA8 /* MainInterface.storyboard */; };
|
||||
5FFF7D7427E343FA00B00DA8 /* ShareViewController.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 5FFF7D6A27E343FA00B00DA8 /* ShareViewController.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
7435D10C2704659F00AB88E0 /* FolderPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7435D10B2704659F00AB88E0 /* FolderPicker.swift */; };
|
||||
7435D10F2704660B00AB88E0 /* FolderPicker.m in Sources */ = {isa = PBXBuildFile; fileRef = 7435D10E2704660B00AB88E0 /* FolderPicker.m */; };
|
||||
A1B2C3D41E2F3A4B5C6D7E90 /* NativePageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D41E2F3A4B5C6D7E8F /* NativePageViewController.swift */; };
|
||||
A1B2C3D41E2F3A4B5C6D7E92 /* SharedWebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D41E2F3A4B5C6D7E91 /* SharedWebViewController.swift */; };
|
||||
ABCDEF0123456789000000AB /* NativeTopBarPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABCDEF0123456789000000AA /* NativeTopBarPlugin.swift */; };
|
||||
C3718FCEFAECFFB66E93FFC4 /* Pods_Logseq.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B2E26D73EA097D0B3B22942E /* Pods_Logseq.framework */; };
|
||||
CBF2D2DA2DE83CB8006338BE /* UILocalPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBF2D2D92DE83CB0006338BE /* UILocalPlugin.swift */; };
|
||||
CBF2D2E22DE95970006338BE /* AppViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBF2D2E12DE95970006338BE /* AppViewController.swift */; };
|
||||
D32752BE275496C60039291C /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D32752BD275496C60039291C /* CloudKit.framework */; };
|
||||
D3490CC62E7CE9EA00E796A6 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D3490CC52E7CE9EA00E796A6 /* WidgetKit.framework */; };
|
||||
D3490CC82E7CE9EA00E796A6 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D3490CC72E7CE9EA00E796A6 /* SwiftUI.framework */; };
|
||||
D3490CD52E7CE9EB00E796A6 /* shortcutsExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = D3490CC42E7CE9EA00E796A6 /* shortcutsExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
D3989CC22ECB0E5700D06615 /* LiquidTabsRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3989CBE2ECB0E5700D06615 /* LiquidTabsRootView.swift */; };
|
||||
D3989CC32ECB0E5700D06615 /* LiquidTabsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3989CBF2ECB0E5700D06615 /* LiquidTabsStore.swift */; };
|
||||
D3989CC42ECB0E5700D06615 /* LiquidTabsPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3989CBD2ECB0E5700D06615 /* LiquidTabsPlugin.swift */; };
|
||||
D3989CC52ECB0E5700D06615 /* NativeNavHost.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3989CC02ECB0E5700D06615 /* NativeNavHost.swift */; };
|
||||
D3989CC82ECB174A00D06615 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3989CC72ECB174A00D06615 /* SceneDelegate.swift */; };
|
||||
D39D1FE02E7DAFB000C903D1 /* LogseqIntents.swift in Sources */ = {isa = PBXBuildFile; fileRef = D39D1FDF2E7DAFB000C903D1 /* LogseqIntents.swift */; };
|
||||
D39D1FE12E7DAFB000C903D1 /* LogseqIntents.swift in Sources */ = {isa = PBXBuildFile; fileRef = D39D1FDF2E7DAFB000C903D1 /* LogseqIntents.swift */; };
|
||||
D3C620AA2ED4B9A80009CCDA /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3C620A92ED4B9A80009CCDA /* Theme.swift */; };
|
||||
D3D62A0A275C92880003FBDC /* FileContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3D62A09275C92880003FBDC /* FileContainer.swift */; };
|
||||
D3D62A0C275C928F0003FBDC /* FileContainer.m in Sources */ = {isa = PBXBuildFile; fileRef = D3D62A0B275C928F0003FBDC /* FileContainer.m */; };
|
||||
D3F4A5B62F1234567890ABCE /* NativeBottomSheetPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3F4A5B62F1234567890ABCD /* NativeBottomSheetPlugin.swift */; };
|
||||
D3F4A5B62F1234567890ABD4 /* NativeEditorToolbarPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3F4A5B62F1234567890ABD3 /* NativeEditorToolbarPlugin.swift */; };
|
||||
D3F4A5B62F1234567890ABD1 /* NativeSelectionActionBarPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3F4A5B62F1234567890ABCF /* NativeSelectionActionBarPlugin.swift */; };
|
||||
FE647FF427BDFEDE00F3206B /* FsWatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE647FF327BDFEDE00F3206B /* FsWatcher.swift */; };
|
||||
FE647FF627BDFEF500F3206B /* FsWatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = FE647FF527BDFEF500F3206B /* FsWatcher.m */; };
|
||||
FE96D6102A1B811A001ECE32 /* SharedData.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE96D60F2A1B811A001ECE32 /* SharedData.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
5FFF7D7227E343FA00B00DA8 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 504EC2FC1FED79650016851F /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 5FFF7D6927E343FA00B00DA8;
|
||||
remoteInfo = ShareViewController;
|
||||
};
|
||||
D3490CD32E7CE9EB00E796A6 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 504EC2FC1FED79650016851F /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = D3490CC32E7CE9EA00E796A6;
|
||||
remoteInfo = shortcutsExtension;
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
5FFF7D7527E343FA00B00DA8 /* Embed App Extensions */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 13;
|
||||
files = (
|
||||
D3490CD52E7CE9EB00E796A6 /* shortcutsExtension.appex in Embed App Extensions */,
|
||||
5FFF7D7427E343FA00B00DA8 /* ShareViewController.appex in Embed App Extensions */,
|
||||
);
|
||||
name = "Embed App Extensions";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
2FAD9762203C412B000D30F8 /* config.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = config.xml; sourceTree = "<group>"; };
|
||||
50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = "<group>"; };
|
||||
504EC3041FED79650016851F /* Logseq.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Logseq.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
504EC3071FED79650016851F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
504EC30C1FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||
504EC30E1FED79650016851F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
504EC3111FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
504EC3131FED79650016851F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = "<group>"; };
|
||||
5FF86329283B5ADB0047731B /* Utils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = "<group>"; };
|
||||
5FF8632B283B5BFD0047731B /* Utils.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = Utils.m; sourceTree = "<group>"; };
|
||||
5FFF7D6A27E343FA00B00DA8 /* ShareViewController.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ShareViewController.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
5FFF7D6C27E343FA00B00DA8 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = "<group>"; };
|
||||
5FFF7D6F27E343FA00B00DA8 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = "<group>"; };
|
||||
5FFF7D7127E343FA00B00DA8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
5FFF7D7927E4E70700B00DA8 /* ShareViewController.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ShareViewController.entitlements; sourceTree = "<group>"; };
|
||||
7435D10B2704659F00AB88E0 /* FolderPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FolderPicker.swift; sourceTree = "<group>"; };
|
||||
7435D10D2704660A00AB88E0 /* App-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "App-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
7435D10E2704660B00AB88E0 /* FolderPicker.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FolderPicker.m; sourceTree = "<group>"; };
|
||||
8A489CEC51E94726DDD58810 /* Pods-Logseq.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Logseq.release.xcconfig"; path = "Target Support Files/Pods-Logseq/Pods-Logseq.release.xcconfig"; sourceTree = "<group>"; };
|
||||
A1B2C3D41E2F3A4B5C6D7E8F /* NativePageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativePageViewController.swift; sourceTree = "<group>"; };
|
||||
A1B2C3D41E2F3A4B5C6D7E91 /* SharedWebViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedWebViewController.swift; sourceTree = "<group>"; };
|
||||
ABCDEF0123456789000000AA /* NativeTopBarPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeTopBarPlugin.swift; sourceTree = "<group>"; };
|
||||
B2E26D73EA097D0B3B22942E /* Pods_Logseq.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Logseq.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
CBF2D2D92DE83CB0006338BE /* UILocalPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UILocalPlugin.swift; sourceTree = "<group>"; };
|
||||
CBF2D2E12DE95970006338BE /* AppViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppViewController.swift; sourceTree = "<group>"; };
|
||||
D32752BC275496A60039291C /* App.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = App.entitlements; sourceTree = "<group>"; };
|
||||
D32752BD275496C60039291C /* CloudKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CloudKit.framework; path = System/Library/Frameworks/CloudKit.framework; sourceTree = SDKROOT; };
|
||||
D32752BF2754C5AB0039291C /* AppDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = AppDebug.entitlements; sourceTree = "<group>"; };
|
||||
D3490CC42E7CE9EA00E796A6 /* shortcutsExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = shortcutsExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
D3490CC52E7CE9EA00E796A6 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
|
||||
D3490CC72E7CE9EA00E796A6 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; };
|
||||
D3490CDA2E7CEA3900E796A6 /* shortcutsExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = shortcutsExtension.entitlements; sourceTree = "<group>"; };
|
||||
D3989CBD2ECB0E5700D06615 /* LiquidTabsPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiquidTabsPlugin.swift; sourceTree = "<group>"; };
|
||||
D3989CBE2ECB0E5700D06615 /* LiquidTabsRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiquidTabsRootView.swift; sourceTree = "<group>"; };
|
||||
D3989CBF2ECB0E5700D06615 /* LiquidTabsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiquidTabsStore.swift; sourceTree = "<group>"; };
|
||||
D3989CC02ECB0E5700D06615 /* NativeNavHost.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeNavHost.swift; sourceTree = "<group>"; };
|
||||
D3989CC72ECB174A00D06615 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
|
||||
D39D1FDF2E7DAFB000C903D1 /* LogseqIntents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogseqIntents.swift; sourceTree = "<group>"; };
|
||||
D3C620A92ED4B9A80009CCDA /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = "<group>"; };
|
||||
D3D62A09275C92880003FBDC /* FileContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileContainer.swift; sourceTree = "<group>"; };
|
||||
D3D62A0B275C928F0003FBDC /* FileContainer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FileContainer.m; sourceTree = "<group>"; };
|
||||
D3F4A5B62F1234567890ABCD /* NativeBottomSheetPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeBottomSheetPlugin.swift; sourceTree = "<group>"; };
|
||||
D3F4A5B62F1234567890ABD3 /* NativeEditorToolbarPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeEditorToolbarPlugin.swift; sourceTree = "<group>"; };
|
||||
D3F4A5B62F1234567890ABCF /* NativeSelectionActionBarPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeSelectionActionBarPlugin.swift; sourceTree = "<group>"; };
|
||||
DE5650F4AD4E2242AB9C012D /* Pods-Logseq.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Logseq.debug.xcconfig"; path = "Target Support Files/Pods-Logseq/Pods-Logseq.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
FE647FF327BDFEDE00F3206B /* FsWatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FsWatcher.swift; sourceTree = "<group>"; };
|
||||
FE647FF527BDFEF500F3206B /* FsWatcher.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FsWatcher.m; sourceTree = "<group>"; };
|
||||
FE96D60F2A1B811A001ECE32 /* SharedData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedData.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
D3490CD82E7CE9EB00E796A6 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {
|
||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||
membershipExceptions = (
|
||||
Info.plist,
|
||||
);
|
||||
target = D3490CC32E7CE9EA00E796A6 /* shortcutsExtension */;
|
||||
};
|
||||
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
D3490CC92E7CE9EA00E796A6 /* shortcuts */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (D3490CD82E7CE9EB00E796A6 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = shortcuts; sourceTree = "<group>"; };
|
||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
504EC3011FED79650016851F /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D32752BE275496C60039291C /* CloudKit.framework in Frameworks */,
|
||||
C3718FCEFAECFFB66E93FFC4 /* Pods_Logseq.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
5FFF7D6727E343FA00B00DA8 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
D3490CC12E7CE9EA00E796A6 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D3490CC82E7CE9EA00E796A6 /* SwiftUI.framework in Frameworks */,
|
||||
D3490CC62E7CE9EA00E796A6 /* WidgetKit.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
504EC2FB1FED79650016851F = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D3490CDA2E7CEA3900E796A6 /* shortcutsExtension.entitlements */,
|
||||
504EC3061FED79650016851F /* App */,
|
||||
5FFF7D6B27E343FA00B00DA8 /* ShareViewController */,
|
||||
D3490CC92E7CE9EA00E796A6 /* shortcuts */,
|
||||
504EC3051FED79650016851F /* Products */,
|
||||
D337740F89DEEAD18C87762B /* Pods */,
|
||||
9FC5AB18C7E7E43B09B33A61 /* Frameworks */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
504EC3051FED79650016851F /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
504EC3041FED79650016851F /* Logseq.app */,
|
||||
5FFF7D6A27E343FA00B00DA8 /* ShareViewController.appex */,
|
||||
D3490CC42E7CE9EA00E796A6 /* shortcutsExtension.appex */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
504EC3061FED79650016851F /* App */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D3C620A92ED4B9A80009CCDA /* Theme.swift */,
|
||||
D3989CC72ECB174A00D06615 /* SceneDelegate.swift */,
|
||||
D3989CBD2ECB0E5700D06615 /* LiquidTabsPlugin.swift */,
|
||||
D3989CBE2ECB0E5700D06615 /* LiquidTabsRootView.swift */,
|
||||
D3989CBF2ECB0E5700D06615 /* LiquidTabsStore.swift */,
|
||||
D3989CC02ECB0E5700D06615 /* NativeNavHost.swift */,
|
||||
D39D1FDF2E7DAFB000C903D1 /* LogseqIntents.swift */,
|
||||
A1B2C3D41E2F3A4B5C6D7E8F /* NativePageViewController.swift */,
|
||||
A1B2C3D41E2F3A4B5C6D7E91 /* SharedWebViewController.swift */,
|
||||
CBF2D2E12DE95970006338BE /* AppViewController.swift */,
|
||||
CBF2D2D92DE83CB0006338BE /* UILocalPlugin.swift */,
|
||||
ABCDEF0123456789000000AA /* NativeTopBarPlugin.swift */,
|
||||
D3F4A5B62F1234567890ABCD /* NativeBottomSheetPlugin.swift */,
|
||||
D3F4A5B62F1234567890ABD3 /* NativeEditorToolbarPlugin.swift */,
|
||||
D3F4A5B62F1234567890ABCF /* NativeSelectionActionBarPlugin.swift */,
|
||||
5FF86329283B5ADB0047731B /* Utils.swift */,
|
||||
5FF8632B283B5BFD0047731B /* Utils.m */,
|
||||
D32752BF2754C5AB0039291C /* AppDebug.entitlements */,
|
||||
D32752BC275496A60039291C /* App.entitlements */,
|
||||
50379B222058CBB4000EE86E /* capacitor.config.json */,
|
||||
504EC3071FED79650016851F /* AppDelegate.swift */,
|
||||
504EC30B1FED79650016851F /* Main.storyboard */,
|
||||
504EC30E1FED79650016851F /* Assets.xcassets */,
|
||||
504EC3101FED79650016851F /* LaunchScreen.storyboard */,
|
||||
504EC3131FED79650016851F /* Info.plist */,
|
||||
2FAD9762203C412B000D30F8 /* config.xml */,
|
||||
50B271D01FEDC1A000F3C39B /* public */,
|
||||
7435D10B2704659F00AB88E0 /* FolderPicker.swift */,
|
||||
FE647FF327BDFEDE00F3206B /* FsWatcher.swift */,
|
||||
FE647FF527BDFEF500F3206B /* FsWatcher.m */,
|
||||
7435D10E2704660B00AB88E0 /* FolderPicker.m */,
|
||||
D3D62A09275C92880003FBDC /* FileContainer.swift */,
|
||||
D3D62A0B275C928F0003FBDC /* FileContainer.m */,
|
||||
7435D10D2704660A00AB88E0 /* App-Bridging-Header.h */,
|
||||
);
|
||||
path = App;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
5FFF7D6B27E343FA00B00DA8 /* ShareViewController */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5FFF7D7927E4E70700B00DA8 /* ShareViewController.entitlements */,
|
||||
5FFF7D6C27E343FA00B00DA8 /* ShareViewController.swift */,
|
||||
FE96D60F2A1B811A001ECE32 /* SharedData.swift */,
|
||||
5FFF7D6E27E343FA00B00DA8 /* MainInterface.storyboard */,
|
||||
5FFF7D7127E343FA00B00DA8 /* Info.plist */,
|
||||
);
|
||||
path = ShareViewController;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9FC5AB18C7E7E43B09B33A61 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D32752BD275496C60039291C /* CloudKit.framework */,
|
||||
B2E26D73EA097D0B3B22942E /* Pods_Logseq.framework */,
|
||||
D3490CC52E7CE9EA00E796A6 /* WidgetKit.framework */,
|
||||
D3490CC72E7CE9EA00E796A6 /* SwiftUI.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D337740F89DEEAD18C87762B /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DE5650F4AD4E2242AB9C012D /* Pods-Logseq.debug.xcconfig */,
|
||||
8A489CEC51E94726DDD58810 /* Pods-Logseq.release.xcconfig */,
|
||||
);
|
||||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
504EC3031FED79650016851F /* Logseq */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "Logseq" */;
|
||||
buildPhases = (
|
||||
818545F0788D5DB466761623 /* [CP] Check Pods Manifest.lock */,
|
||||
504EC3001FED79650016851F /* Sources */,
|
||||
504EC3011FED79650016851F /* Frameworks */,
|
||||
504EC3021FED79650016851F /* Resources */,
|
||||
4BF32F1E9453A6AB603D7CD2 /* [CP] Embed Pods Frameworks */,
|
||||
5FFF7D7527E343FA00B00DA8 /* Embed App Extensions */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
5FFF7D7327E343FA00B00DA8 /* PBXTargetDependency */,
|
||||
D3490CD42E7CE9EB00E796A6 /* PBXTargetDependency */,
|
||||
);
|
||||
name = Logseq;
|
||||
productName = App;
|
||||
productReference = 504EC3041FED79650016851F /* Logseq.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
5FFF7D6927E343FA00B00DA8 /* ShareViewController */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 5FFF7D7827E343FA00B00DA8 /* Build configuration list for PBXNativeTarget "ShareViewController" */;
|
||||
buildPhases = (
|
||||
5FFF7D6627E343FA00B00DA8 /* Sources */,
|
||||
5FFF7D6727E343FA00B00DA8 /* Frameworks */,
|
||||
5FFF7D6827E343FA00B00DA8 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = ShareViewController;
|
||||
productName = ShareViewController;
|
||||
productReference = 5FFF7D6A27E343FA00B00DA8 /* ShareViewController.appex */;
|
||||
productType = "com.apple.product-type.app-extension";
|
||||
};
|
||||
D3490CC32E7CE9EA00E796A6 /* shortcutsExtension */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = D3490CD92E7CE9EB00E796A6 /* Build configuration list for PBXNativeTarget "shortcutsExtension" */;
|
||||
buildPhases = (
|
||||
D3490CC02E7CE9EA00E796A6 /* Sources */,
|
||||
D3490CC12E7CE9EA00E796A6 /* Frameworks */,
|
||||
D3490CC22E7CE9EA00E796A6 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
fileSystemSynchronizedGroups = (
|
||||
D3490CC92E7CE9EA00E796A6 /* shortcuts */,
|
||||
);
|
||||
name = shortcutsExtension;
|
||||
packageProductDependencies = (
|
||||
);
|
||||
productName = shortcutsExtension;
|
||||
productReference = D3490CC42E7CE9EA00E796A6 /* shortcutsExtension.appex */;
|
||||
productType = "com.apple.product-type.app-extension";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
504EC2FC1FED79650016851F /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 2600;
|
||||
LastUpgradeCheck = 1310;
|
||||
TargetAttributes = {
|
||||
504EC3031FED79650016851F = {
|
||||
CreatedOnToolsVersion = 9.2;
|
||||
LastSwiftMigration = 1250;
|
||||
ProvisioningStyle = Automatic;
|
||||
};
|
||||
5FFF7D6927E343FA00B00DA8 = {
|
||||
CreatedOnToolsVersion = 13.3;
|
||||
ProvisioningStyle = Automatic;
|
||||
};
|
||||
D3490CC32E7CE9EA00E796A6 = {
|
||||
CreatedOnToolsVersion = 26.0;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 504EC2FF1FED79650016851F /* Build configuration list for PBXProject "App" */;
|
||||
compatibilityVersion = "Xcode 8.0";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 504EC2FB1FED79650016851F;
|
||||
productRefGroup = 504EC3051FED79650016851F /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
504EC3031FED79650016851F /* Logseq */,
|
||||
5FFF7D6927E343FA00B00DA8 /* ShareViewController */,
|
||||
D3490CC32E7CE9EA00E796A6 /* shortcutsExtension */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
504EC3021FED79650016851F /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */,
|
||||
50B271D11FEDC1A000F3C39B /* public in Resources */,
|
||||
504EC30F1FED79650016851F /* Assets.xcassets in Resources */,
|
||||
50379B232058CBB4000EE86E /* capacitor.config.json in Resources */,
|
||||
504EC30D1FED79650016851F /* Main.storyboard in Resources */,
|
||||
2FAD9763203C412B000D30F8 /* config.xml in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
5FFF7D6827E343FA00B00DA8 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
5FFF7D7027E343FA00B00DA8 /* MainInterface.storyboard in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
D3490CC22E7CE9EA00E796A6 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
4BF32F1E9453A6AB603D7CD2 /* [CP] Embed Pods Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Logseq/Pods-Logseq-frameworks.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
818545F0788D5DB466761623 /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-Logseq-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
504EC3001FED79650016851F /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
504EC3081FED79650016851F /* AppDelegate.swift in Sources */,
|
||||
5FF8632C283B5BFD0047731B /* Utils.m in Sources */,
|
||||
FE647FF427BDFEDE00F3206B /* FsWatcher.swift in Sources */,
|
||||
D3989CC22ECB0E5700D06615 /* LiquidTabsRootView.swift in Sources */,
|
||||
D3989CC32ECB0E5700D06615 /* LiquidTabsStore.swift in Sources */,
|
||||
D3989CC42ECB0E5700D06615 /* LiquidTabsPlugin.swift in Sources */,
|
||||
D3989CC52ECB0E5700D06615 /* NativeNavHost.swift in Sources */,
|
||||
D3989CC82ECB174A00D06615 /* SceneDelegate.swift in Sources */,
|
||||
5FF8632A283B5ADB0047731B /* Utils.swift in Sources */,
|
||||
CBF2D2E22DE95970006338BE /* AppViewController.swift in Sources */,
|
||||
D3D62A0A275C92880003FBDC /* FileContainer.swift in Sources */,
|
||||
CBF2D2DA2DE83CB8006338BE /* UILocalPlugin.swift in Sources */,
|
||||
ABCDEF0123456789000000AB /* NativeTopBarPlugin.swift in Sources */,
|
||||
D3F4A5B62F1234567890ABCE /* NativeBottomSheetPlugin.swift in Sources */,
|
||||
D3F4A5B62F1234567890ABD4 /* NativeEditorToolbarPlugin.swift in Sources */,
|
||||
D3F4A5B62F1234567890ABD1 /* NativeSelectionActionBarPlugin.swift in Sources */,
|
||||
A1B2C3D41E2F3A4B5C6D7E90 /* NativePageViewController.swift in Sources */,
|
||||
D3C620AA2ED4B9A80009CCDA /* Theme.swift in Sources */,
|
||||
A1B2C3D41E2F3A4B5C6D7E92 /* SharedWebViewController.swift in Sources */,
|
||||
D3D62A0C275C928F0003FBDC /* FileContainer.m in Sources */,
|
||||
7435D10F2704660B00AB88E0 /* FolderPicker.m in Sources */,
|
||||
D39D1FE12E7DAFB000C903D1 /* LogseqIntents.swift in Sources */,
|
||||
7435D10C2704659F00AB88E0 /* FolderPicker.swift in Sources */,
|
||||
FE647FF627BDFEF500F3206B /* FsWatcher.m in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
5FFF7D6627E343FA00B00DA8 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
FE96D6102A1B811A001ECE32 /* SharedData.swift in Sources */,
|
||||
5FFF7D6D27E343FA00B00DA8 /* ShareViewController.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
D3490CC02E7CE9EA00E796A6 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D39D1FE02E7DAFB000C903D1 /* LogseqIntents.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
5FFF7D7327E343FA00B00DA8 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 5FFF7D6927E343FA00B00DA8 /* ShareViewController */;
|
||||
targetProxy = 5FFF7D7227E343FA00B00DA8 /* PBXContainerItemProxy */;
|
||||
};
|
||||
D3490CD42E7CE9EB00E796A6 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = D3490CC32E7CE9EA00E796A6 /* shortcutsExtension */;
|
||||
targetProxy = D3490CD32E7CE9EB00E796A6 /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
504EC30B1FED79650016851F /* Main.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
504EC30C1FED79650016851F /* Base */,
|
||||
);
|
||||
name = Main.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
504EC3101FED79650016851F /* LaunchScreen.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
504EC3111FED79650016851F /* Base */,
|
||||
);
|
||||
name = LaunchScreen.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
5FFF7D6E27E343FA00B00DA8 /* MainInterface.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
5FFF7D6F27E343FA00B00DA8 /* Base */,
|
||||
);
|
||||
name = MainInterface.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXVariantGroup section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
504EC3141FED79650016851F /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
504EC3151FED79650016851F /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
504EC3171FED79650016851F /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = DE5650F4AD4E2242AB9C012D /* Pods-Logseq.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = App/AppDebug.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = K378MFWK59;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = App/Info.plist;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.0.1;
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "App/App-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
504EC3181FED79650016851F /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 8A489CEC51E94726DDD58810 /* Pods-Logseq.release.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = App/App.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = K378MFWK59;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = App/Info.plist;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.0.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "App/App-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
5FFF7D7627E343FA00B00DA8 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = ShareViewController/ShareViewController.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = K378MFWK59;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = ShareViewController/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = ShareViewController;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.0.1;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq.ShareViewController;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
5FFF7D7727E343FA00B00DA8 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = ShareViewController/ShareViewController.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = K378MFWK59;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = ShareViewController/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = ShareViewController;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.0.1;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq.ShareViewController;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
D3490CD62E7CE9EB00E796A6 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = shortcutsExtension.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = K378MFWK59;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = shortcuts/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = shortcuts;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 2.0.1;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq.shortcuts;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
D3490CD72E7CE9EB00E796A6 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = shortcutsExtension.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = K378MFWK59;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = shortcuts/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = shortcuts;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 2.0.1;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.logseq.logseq.shortcuts;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
504EC2FF1FED79650016851F /* Build configuration list for PBXProject "App" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
504EC3141FED79650016851F /* Debug */,
|
||||
504EC3151FED79650016851F /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "Logseq" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
504EC3171FED79650016851F /* Debug */,
|
||||
504EC3181FED79650016851F /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
5FFF7D7827E343FA00B00DA8 /* Build configuration list for PBXNativeTarget "ShareViewController" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
5FFF7D7627E343FA00B00DA8 /* Debug */,
|
||||
5FFF7D7727E343FA00B00DA8 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
D3490CD92E7CE9EB00E796A6 /* Build configuration list for PBXNativeTarget "shortcutsExtension" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
D3490CD62E7CE9EB00E796A6 /* Debug */,
|
||||
D3490CD72E7CE9EB00E796A6 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 504EC2FC1FED79650016851F /* Project object */;
|
||||
}
|
||||
7
ios/App/App.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:App.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
78
ios/App/App.xcodeproj/xcshareddata/xcschemes/Logseq.xcscheme
Normal file
@@ -0,0 +1,78 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1400"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "504EC3031FED79650016851F"
|
||||
BuildableName = "Logseq.app"
|
||||
BlueprintName = "Logseq"
|
||||
ReferencedContainer = "container:App.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "504EC3031FED79650016851F"
|
||||
BuildableName = "Logseq.app"
|
||||
BlueprintName = "Logseq"
|
||||
ReferencedContainer = "container:App.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "504EC3031FED79650016851F"
|
||||
BuildableName = "Logseq.app"
|
||||
BlueprintName = "Logseq"
|
||||
ReferencedContainer = "container:App.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
10
ios/App/App.xcworkspace/contents.xcworkspacedata
generated
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:App.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Pods/Pods.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?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>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
4
ios/App/App/App-Bridging-Header.h
Normal file
@@ -0,0 +1,4 @@
|
||||
//
|
||||
// Use this file to import your target's public headers that you would like to expose to Swift.
|
||||
//
|
||||
|
||||
28
ios/App/App/App.entitlements
Normal file
@@ -0,0 +1,28 @@
|
||||
<?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>aps-environment</key>
|
||||
<string>development</string>
|
||||
<key>com.apple.developer.associated-domains</key>
|
||||
<array>
|
||||
<string>applinks:logseq.com</string>
|
||||
</array>
|
||||
<key>com.apple.developer.icloud-container-identifiers</key>
|
||||
<array>
|
||||
<string>iCloud.com.logseq.logseq</string>
|
||||
</array>
|
||||
<key>com.apple.developer.icloud-services</key>
|
||||
<array>
|
||||
<string>CloudDocuments</string>
|
||||
</array>
|
||||
<key>com.apple.developer.ubiquity-container-identifiers</key>
|
||||
<array>
|
||||
<string>iCloud.com.logseq.logseq</string>
|
||||
</array>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.com.logseq.logseq</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
30
ios/App/App/AppDebug.entitlements
Normal file
@@ -0,0 +1,30 @@
|
||||
<?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>aps-environment</key>
|
||||
<string>development</string>
|
||||
<key>com.apple.developer.associated-domains</key>
|
||||
<array>
|
||||
<string>applinks:logseq.com</string>
|
||||
</array>
|
||||
<key>com.apple.developer.icloud-container-environment</key>
|
||||
<string>Development</string>
|
||||
<key>com.apple.developer.icloud-container-identifiers</key>
|
||||
<array>
|
||||
<string>iCloud.com.logseq.logseq</string>
|
||||
</array>
|
||||
<key>com.apple.developer.icloud-services</key>
|
||||
<array>
|
||||
<string>CloudDocuments</string>
|
||||
</array>
|
||||
<key>com.apple.developer.ubiquity-container-identifiers</key>
|
||||
<array>
|
||||
<string>iCloud.com.logseq.logseq</string>
|
||||
</array>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.com.logseq.logseq</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
591
ios/App/App/AppDelegate.swift
Normal file
@@ -0,0 +1,591 @@
|
||||
import UIKit
|
||||
import Capacitor
|
||||
import Intents
|
||||
import SwiftUI
|
||||
|
||||
@UIApplicationMain
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate, UINavigationControllerDelegate {
|
||||
|
||||
var window: UIWindow?
|
||||
var navController: UINavigationController?
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// MARK: Multi-stack routing state
|
||||
// ---------------------------------------------------------
|
||||
|
||||
/// Currently active logical stack id (must match CLJS :stack, e.g. "home", "capture", "goto").
|
||||
private var activeStackId: String = "home"
|
||||
|
||||
/// Per-stack path stacks, including the active one.
|
||||
/// Example: ["home": ["/", "/page/A"], "capture": ["/__stack__/capture"]]
|
||||
private var stackPathStacks: [String: [String]] = [
|
||||
"home": ["/"]
|
||||
]
|
||||
|
||||
/// Mirror of the active stack's paths.
|
||||
private var pathStack: [String] = ["/"]
|
||||
|
||||
/// Used to ignore JS-driven pops when we're popping in response to a native gesture.
|
||||
private var ignoreRoutePopCount: Int = 0
|
||||
|
||||
/// Suppresses JS callbacks for native pops that already originated from JS history.
|
||||
private var suppressNextNativePopCallback: Bool = false
|
||||
|
||||
/// Temporary snapshot image for smooth pop transitions.
|
||||
private var popSnapshotView: UIView?
|
||||
|
||||
// Each stack has its own native VC stack, just like paths.
|
||||
private var stackViewControllerStacks: [String: [UIViewController]] = [:]
|
||||
|
||||
private var routeChangeObserver: NSObjectProtocol?
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// MARK: Helpers
|
||||
// ---------------------------------------------------------
|
||||
|
||||
private func normalizedPath(_ raw: String?) -> String {
|
||||
guard let raw = raw, !raw.isEmpty else { return "/" }
|
||||
return raw
|
||||
}
|
||||
|
||||
/// Returns the current native path stack for a given logical stack id,
|
||||
/// or initialises a sensible default if none exists yet.
|
||||
private func paths(for stackId: String) -> [String] {
|
||||
if let existing = stackPathStacks[stackId], !existing.isEmpty {
|
||||
return existing
|
||||
}
|
||||
|
||||
return [rootPath(for: stackId)]
|
||||
}
|
||||
|
||||
private func rootPath(for stackId: String) -> String {
|
||||
if stackId == "home" {
|
||||
return "/"
|
||||
} else {
|
||||
// Virtual stacks (e.g. capture, search, goto) default to a stack-root path.
|
||||
return "/__stack__/\(stackId)"
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the stored paths for a given stack id and keeps `pathStack`
|
||||
/// consistent if this is the active stack.
|
||||
private func setPaths(_ paths: [String], for stackId: String) {
|
||||
stackPathStacks[stackId] = paths
|
||||
if stackId == activeStackId {
|
||||
pathStack = paths
|
||||
}
|
||||
}
|
||||
|
||||
private func setViewControllers(_ vcs: [UIViewController], for stackId: String) {
|
||||
stackViewControllerStacks[stackId] = vcs
|
||||
}
|
||||
|
||||
private func viewControllers(for stackId: String) -> [UIViewController] {
|
||||
stackViewControllerStacks[stackId] ?? []
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// MARK: UIApplication lifecycle
|
||||
// ---------------------------------------------------------
|
||||
|
||||
func application(
|
||||
_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
|
||||
) -> Bool {
|
||||
if let shortcutItem = launchOptions?[.shortcutItem] as? UIApplicationShortcutItem {
|
||||
DispatchQueue.main.async {
|
||||
_ = self.handleShortcutItem(shortcutItem)
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication,
|
||||
performActionFor shortcutItem: UIApplicationShortcutItem,
|
||||
completionHandler: @escaping (Bool) -> Void) {
|
||||
let handled = handleShortcutItem(shortcutItem)
|
||||
completionHandler(handled)
|
||||
}
|
||||
|
||||
func application(
|
||||
_ application: UIApplication,
|
||||
configurationForConnecting connectingSceneSession: UISceneSession,
|
||||
options: UIScene.ConnectionOptions
|
||||
) -> UISceneConfiguration {
|
||||
let config = UISceneConfiguration(
|
||||
name: "Default Configuration",
|
||||
sessionRole: connectingSceneSession.role
|
||||
)
|
||||
config.delegateClass = SceneDelegate.self
|
||||
return config
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication,
|
||||
didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// MARK: Shortcuts
|
||||
// ---------------------------------------------------------
|
||||
|
||||
@discardableResult
|
||||
func handleShortcutItem(_ shortcutItem: UIApplicationShortcutItem) -> Bool {
|
||||
switch shortcutItem.type {
|
||||
case "logseq.quickadd":
|
||||
donateQuickAddShortcut()
|
||||
openURL("logseq://mobile/go/quick-add")
|
||||
return true
|
||||
|
||||
case "logseq.voice":
|
||||
donateAudioShortcut()
|
||||
openURL("logseq://mobile/go/audio")
|
||||
return true
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication,
|
||||
continue userActivity: NSUserActivity,
|
||||
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
|
||||
|
||||
if userActivity.activityType == "com.logseq.quickadd" {
|
||||
openURL("logseq://mobile/go/quick-add")
|
||||
return true
|
||||
}
|
||||
|
||||
if userActivity.activityType == "com.logseq.audio" {
|
||||
openURL("logseq://mobile/go/audio")
|
||||
return true
|
||||
}
|
||||
|
||||
if userActivity.activityType == NSUserActivityTypeBrowsingWeb,
|
||||
let url = userActivity.webpageURL {
|
||||
print("🌐 Universal link:", url)
|
||||
}
|
||||
|
||||
return ApplicationDelegateProxy.shared.application(
|
||||
application,
|
||||
continue: userActivity,
|
||||
restorationHandler: restorationHandler
|
||||
)
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication,
|
||||
open url: URL,
|
||||
options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
|
||||
print("🔥 opened with URL:", url)
|
||||
|
||||
return ApplicationDelegateProxy.shared.application(
|
||||
application,
|
||||
open: url,
|
||||
options: options
|
||||
)
|
||||
}
|
||||
|
||||
private func openURL(_ urlString: String) {
|
||||
if let url = URL(string: urlString) {
|
||||
UIApplication.shared.open(url, options: [:], completionHandler: nil)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// MARK: Navigation operations (within active stack)
|
||||
// ---------------------------------------------------------
|
||||
|
||||
private func emptyNavStack(path: String) {
|
||||
let path = normalizedPath(path)
|
||||
guard let nav = navController else { return }
|
||||
|
||||
ignoreRoutePopCount = 0
|
||||
popSnapshotView?.removeFromSuperview()
|
||||
popSnapshotView = nil
|
||||
|
||||
let vc = NativePageViewController(path: path, push: false)
|
||||
pathStack = [path]
|
||||
setPaths(pathStack, for: activeStackId)
|
||||
|
||||
nav.setViewControllers([vc], animated: false)
|
||||
SharedWebViewController.instance.clearPlaceholder()
|
||||
SharedWebViewController.instance.attach(to: vc)
|
||||
|
||||
}
|
||||
|
||||
private func pushIfNeeded(path: String, animated: Bool) {
|
||||
let path = normalizedPath(path)
|
||||
guard let nav = navController else { return }
|
||||
if pathStack.last == path { return }
|
||||
|
||||
if let fromVC = nav.topViewController as? NativePageViewController {
|
||||
SharedWebViewController.instance.storeSnapshot(for: fromVC)
|
||||
}
|
||||
|
||||
let vc = NativePageViewController(path: path, push: true)
|
||||
pathStack.append(path)
|
||||
setPaths(pathStack, for: activeStackId)
|
||||
|
||||
nav.pushViewController(vc, animated: animated)
|
||||
|
||||
}
|
||||
|
||||
private func replaceTop(path: String) {
|
||||
let path = normalizedPath(path)
|
||||
guard let nav = navController else { return }
|
||||
|
||||
_ = pathStack.popLast()
|
||||
pathStack.append(path)
|
||||
setPaths(pathStack, for: activeStackId)
|
||||
|
||||
let vc = NativePageViewController(path: path, push: false)
|
||||
var stack = nav.viewControllers
|
||||
if stack.isEmpty {
|
||||
stack = [vc]
|
||||
} else {
|
||||
stack[stack.count - 1] = vc
|
||||
}
|
||||
nav.setViewControllers(stack, animated: false)
|
||||
|
||||
}
|
||||
|
||||
private func popIfNeeded(animated: Bool) {
|
||||
guard let nav = navController else { return }
|
||||
|
||||
if nav.viewControllers.count > 1 {
|
||||
suppressNextNativePopCallback = true
|
||||
nav.popViewController(animated: animated)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// MARK: Navigation Delegate
|
||||
// ---------------------------------------------------------
|
||||
|
||||
func navigationController(
|
||||
_ navigationController: UINavigationController,
|
||||
willShow viewController: UIViewController,
|
||||
animated: Bool
|
||||
) {
|
||||
guard let toVC = viewController as? NativePageViewController else { return }
|
||||
guard let coordinator = navigationController.transitionCoordinator else { return }
|
||||
|
||||
let vcs = navigationController.viewControllers
|
||||
let toIndex = vcs.firstIndex(of: toVC)
|
||||
let fromVC = coordinator.viewController(forKey: .from) as? NativePageViewController
|
||||
let fromIndex = fromVC.flatMap { vcs.firstIndex(of: $0) }
|
||||
|
||||
let isPop = if let to = toIndex, let from = fromIndex {
|
||||
to < from
|
||||
} else {
|
||||
vcs.count < pathStack.count
|
||||
}
|
||||
|
||||
if isPop {
|
||||
// -----------------------------
|
||||
// POP — update per-stack pathStack, then notify JS.
|
||||
// -----------------------------
|
||||
let previousStack = pathStack
|
||||
|
||||
if let to = toIndex {
|
||||
pathStack = Array(pathStack.prefix(to + 1))
|
||||
} else if pathStack.count > 1 {
|
||||
_ = pathStack.popLast()
|
||||
}
|
||||
if pathStack.isEmpty {
|
||||
pathStack = [toVC.targetPath]
|
||||
}
|
||||
if let last = pathStack.last, last != toVC.targetPath {
|
||||
pathStack[pathStack.count - 1] = toVC.targetPath
|
||||
}
|
||||
setPaths(pathStack, for: activeStackId)
|
||||
|
||||
popSnapshotView?.removeFromSuperview()
|
||||
popSnapshotView = nil
|
||||
|
||||
if let snapshot = SharedWebViewController.instance.snapshot(for: toVC) {
|
||||
let iv = UIImageView(image: snapshot)
|
||||
iv.frame = toVC.view.bounds
|
||||
iv.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
toVC.view.addSubview(iv)
|
||||
popSnapshotView = iv
|
||||
}
|
||||
|
||||
coordinator.animate(alongsideTransition: nil) { [weak self] ctx in
|
||||
guard let self else { return }
|
||||
|
||||
guard !ctx.isCancelled else {
|
||||
self.pathStack = previousStack
|
||||
self.setPaths(previousStack, for: self.activeStackId)
|
||||
self.suppressNextNativePopCallback = false
|
||||
|
||||
if let fromVC {
|
||||
SharedWebViewController.instance.attach(to: fromVC)
|
||||
}
|
||||
SharedWebViewController.instance.clearPlaceholder()
|
||||
return
|
||||
}
|
||||
|
||||
// 🔑 DO NOT call webView.goBack().
|
||||
// Tell JS explicitly that native popped.
|
||||
let shouldNotifyNativePop = !self.suppressNextNativePopCallback
|
||||
self.suppressNextNativePopCallback = false
|
||||
|
||||
if shouldNotifyNativePop {
|
||||
self.ignoreRoutePopCount += 1
|
||||
|
||||
if let bridge = SharedWebViewController.instance.bridgeController.bridge {
|
||||
let js = "window.LogseqNative && window.LogseqNative.onNativePop && window.LogseqNative.onNativePop();"
|
||||
bridge.webView?.evaluateJavaScript(js, completionHandler: nil)
|
||||
}
|
||||
}
|
||||
|
||||
SharedWebViewController.instance.attach(
|
||||
to: toVC,
|
||||
leavePlaceholderInPreviousParent: fromVC != nil
|
||||
)
|
||||
|
||||
if let snapshot = self.popSnapshotView {
|
||||
toVC.view.bringSubviewToFront(snapshot)
|
||||
}
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||
SharedWebViewController.instance.clearPlaceholder()
|
||||
self.popSnapshotView?.removeFromSuperview()
|
||||
self.popSnapshotView = nil
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// -----------------------------
|
||||
// PUSH / RESET
|
||||
// -----------------------------
|
||||
SharedWebViewController.instance.attach(
|
||||
to: toVC,
|
||||
leavePlaceholderInPreviousParent: fromVC != nil
|
||||
)
|
||||
|
||||
coordinator.animate(alongsideTransition: nil) { ctx in
|
||||
if ctx.isCancelled, let fromVC {
|
||||
SharedWebViewController.instance.attach(to: fromVC)
|
||||
} else {
|
||||
SharedWebViewController.instance.clearPlaceholder()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func navigationController(
|
||||
_ navigationController: UINavigationController,
|
||||
didShow viewController: UIViewController,
|
||||
animated: Bool
|
||||
) {
|
||||
guard let current = viewController as? NativePageViewController else { return }
|
||||
|
||||
if !NativeBottomSheetPlugin.isPresentingSheet {
|
||||
SharedWebViewController.instance.clearPlaceholder()
|
||||
SharedWebViewController.instance.attach(to: current)
|
||||
}
|
||||
}
|
||||
|
||||
func navigationController(
|
||||
_ navigationController: UINavigationController,
|
||||
animationControllerFor operation: UINavigationController.Operation,
|
||||
from fromVC: UIViewController,
|
||||
to toVC: UIViewController
|
||||
) -> UIViewControllerAnimatedTransitioning? {
|
||||
// Sidebar animator removed → always return nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// MARK: Route Observation (JS -> Native)
|
||||
// ---------------------------------------------------------
|
||||
|
||||
private func observeRouteChanges() {
|
||||
if routeChangeObserver != nil {
|
||||
return
|
||||
}
|
||||
|
||||
routeChangeObserver = NotificationCenter.default.addObserver(
|
||||
forName: UILocalPlugin.routeChangeNotification,
|
||||
object: nil,
|
||||
queue: .main
|
||||
) { [weak self] notification in
|
||||
guard let self else { return }
|
||||
guard let nav = self.navController else { return }
|
||||
|
||||
let rawPath = notification.userInfo?["path"] as? String
|
||||
let path = self.normalizedPath(rawPath)
|
||||
let navigationType = (notification.userInfo?["navigationType"] as? String) ?? "push"
|
||||
let stackId = (notification.userInfo?["stack"] as? String) ?? "home"
|
||||
let previousStackId = self.activeStackId
|
||||
|
||||
// 🚫 Fast-path: ignore duplicate replace for same stack/path
|
||||
if stackId == self.activeStackId,
|
||||
navigationType == "replace",
|
||||
path == self.pathStack.last {
|
||||
return
|
||||
}
|
||||
|
||||
// Fast-path: leave SwiftUI search for the Home tab. During this
|
||||
// transition SwiftUI is already moving the shared nav controller
|
||||
// back into the Home tab, so rebuilding it here causes UIKit
|
||||
// transition conflicts.
|
||||
if previousStackId == "search",
|
||||
stackId == "home" {
|
||||
|
||||
self.setPaths(["/__stack__/search"], for: "search")
|
||||
self.activeStackId = "home"
|
||||
|
||||
var newPaths = self.paths(for: "home")
|
||||
if path == self.rootPath(for: "home") {
|
||||
newPaths = [path]
|
||||
} else if navigationType == "reset" {
|
||||
newPaths = [path]
|
||||
} else if newPaths.isEmpty {
|
||||
newPaths = [path]
|
||||
} else if navigationType == "push",
|
||||
let last = newPaths.last,
|
||||
last != path {
|
||||
newPaths.append(path)
|
||||
} else if let last = newPaths.last, last != path {
|
||||
newPaths[newPaths.count - 1] = path
|
||||
}
|
||||
|
||||
self.pathStack = newPaths
|
||||
self.setPaths(newPaths, for: "home")
|
||||
return
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 1️⃣ Stack switch: home ↔ search ↔ capture...
|
||||
// ============================================
|
||||
if stackId != self.activeStackId {
|
||||
self.setPaths(self.pathStack, for: previousStackId)
|
||||
|
||||
// Load saved paths for target stack
|
||||
var newPaths = self.paths(for: stackId)
|
||||
|
||||
// 🔑 Special rules for shaping the new stack
|
||||
if path == self.rootPath(for: stackId) {
|
||||
newPaths = [path]
|
||||
} else if navigationType == "reset" {
|
||||
newPaths = [path]
|
||||
} else if stackId == "home", path == "/" {
|
||||
// 👉 ALWAYS reset home to a single root VC.
|
||||
newPaths = ["/"]
|
||||
} else if newPaths.isEmpty {
|
||||
// First time visiting this stack
|
||||
newPaths = [path]
|
||||
} else if navigationType == "push",
|
||||
let last = newPaths.last,
|
||||
last != path {
|
||||
newPaths.append(path)
|
||||
} else if let last = newPaths.last, last != path {
|
||||
// Same history, but different top path → align the top.
|
||||
newPaths[newPaths.count - 1] = path
|
||||
}
|
||||
|
||||
self.activeStackId = stackId
|
||||
self.pathStack = newPaths
|
||||
self.setPaths(newPaths, for: stackId)
|
||||
|
||||
// Rebuild native stack for these paths
|
||||
var vcs: [UIViewController] = []
|
||||
for (idx, p) in newPaths.enumerated() {
|
||||
let vc = NativePageViewController(path: p, push: idx > 0)
|
||||
vcs.append(vc)
|
||||
}
|
||||
|
||||
nav.setViewControllers(vcs, animated: false)
|
||||
self.setViewControllers(vcs, for: stackId)
|
||||
|
||||
if let lastVC = vcs.last as? NativePageViewController {
|
||||
// Defer & avoid redundant attach.
|
||||
DispatchQueue.main.async {
|
||||
if let bridge = SharedWebViewController.instance.bridgeController.bridge,
|
||||
let webView = bridge.webView,
|
||||
webView.isDescendant(of: lastVC.view) {
|
||||
} else {
|
||||
SharedWebViewController.instance.attach(to: lastVC)
|
||||
}
|
||||
SharedWebViewController.instance.clearPlaceholder()
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 2️⃣ Navigation *within* active stack
|
||||
// ============================================
|
||||
switch navigationType {
|
||||
case "reset":
|
||||
self.emptyNavStack(path: path)
|
||||
|
||||
case "replace":
|
||||
self.replaceTop(path: path)
|
||||
|
||||
case "pop":
|
||||
if self.ignoreRoutePopCount > 0 {
|
||||
self.ignoreRoutePopCount -= 1
|
||||
return
|
||||
}
|
||||
if self.pathStack.count > 1 {
|
||||
self.popIfNeeded(animated: true)
|
||||
}
|
||||
|
||||
default:
|
||||
self.pushIfNeeded(path: path, animated: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// MARK: NSUserActivity utilities
|
||||
// ---------------------------------------------------------
|
||||
|
||||
extension NSUserActivity {
|
||||
static var quickAdd: NSUserActivity {
|
||||
let a = NSUserActivity(activityType: "com.logseq.quickadd")
|
||||
a.title = "Quick Add"
|
||||
a.isEligibleForSearch = true
|
||||
a.isEligibleForPrediction = true
|
||||
a.persistentIdentifier = NSUserActivityPersistentIdentifier("com.logseq.quickadd")
|
||||
a.suggestedInvocationPhrase = "Quick Add in Logseq"
|
||||
return a
|
||||
}
|
||||
|
||||
static var recordAudio: NSUserActivity {
|
||||
let a = NSUserActivity(activityType: "com.logseq.audio")
|
||||
a.title = "Record Audio"
|
||||
a.isEligibleForSearch = true
|
||||
a.isEligibleForPrediction = true
|
||||
a.persistentIdentifier = NSUserActivityPersistentIdentifier("com.logseq.audio")
|
||||
a.suggestedInvocationPhrase = "Record in Logseq"
|
||||
return a
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// MARK: Convenience
|
||||
// ---------------------------------------------------------
|
||||
|
||||
extension AppDelegate {
|
||||
func donateQuickAddShortcut() {
|
||||
let a = NSUserActivity.quickAdd
|
||||
window?.rootViewController?.userActivity = a
|
||||
a.becomeCurrent()
|
||||
}
|
||||
|
||||
func donateAudioShortcut() {
|
||||
let a = NSUserActivity.recordAudio
|
||||
window?.rootViewController?.userActivity = a
|
||||
a.becomeCurrent()
|
||||
}
|
||||
|
||||
func startRouteObservation() {
|
||||
observeRouteChanges()
|
||||
}
|
||||
}
|
||||
75
ios/App/App/AppViewController.swift
Normal file
@@ -0,0 +1,75 @@
|
||||
//
|
||||
// AppViewController.swift
|
||||
// Logseq
|
||||
//
|
||||
// Created by Charlie on 2025/5/30.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Capacitor
|
||||
import UIKit
|
||||
|
||||
@objc public class AppViewController: CAPBridgeViewController {
|
||||
override public func capacitorDidLoad() {
|
||||
bridge?.registerPluginInstance(UILocalPlugin())
|
||||
bridge?.registerPluginInstance(NativeTopBarPlugin())
|
||||
bridge?.registerPluginInstance(LiquidTabsPlugin())
|
||||
bridge?.registerPluginInstance(NativeBottomSheetPlugin())
|
||||
bridge?.registerPluginInstance(NativeEditorToolbarPlugin())
|
||||
bridge?.registerPluginInstance(NativeSelectionActionBarPlugin())
|
||||
bridge?.registerPluginInstance(Utils())
|
||||
}
|
||||
|
||||
public override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
// initial setup
|
||||
applyLogseqTheme()
|
||||
}
|
||||
|
||||
// MARK: - Theme application (background + tint)
|
||||
|
||||
private func applyLogseqTheme() {
|
||||
let bg = UIColor.logseqBackground
|
||||
let tint = UIColor.logseqTint
|
||||
|
||||
// Background
|
||||
view.backgroundColor = bg
|
||||
|
||||
if let webView = self.webView {
|
||||
webView.isOpaque = true
|
||||
webView.backgroundColor = bg
|
||||
webView.scrollView.backgroundColor = bg
|
||||
|
||||
// Sometimes WKWebView uses an internal subview for its background
|
||||
webView.scrollView.subviews.first?.backgroundColor = bg
|
||||
}
|
||||
|
||||
// Tint
|
||||
view.tintColor = tint
|
||||
webView?.tintColor = tint
|
||||
webView?.scrollView.tintColor = tint
|
||||
|
||||
// Propagate to container UI if possible
|
||||
navigationController?.view.tintColor = tint
|
||||
navigationController?.navigationBar.tintColor = tint
|
||||
navigationController?.tabBarController?.tabBar.tintColor = tint
|
||||
|
||||
// Global window tint (affects many UIKit + SwiftUI bits)
|
||||
if let window = view.window {
|
||||
window.tintColor = tint
|
||||
}
|
||||
}
|
||||
|
||||
public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
|
||||
guard let previousTraitCollection,
|
||||
traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) else {
|
||||
return
|
||||
}
|
||||
|
||||
// Re-apply dynamic colors when light/dark changes
|
||||
applyLogseqTheme()
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 39 KiB |
14
ios/App/App/Assets.xcassets/AppIcon.appiconset/Contents.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"images": [
|
||||
{
|
||||
"idiom": "universal",
|
||||
"size": "1024x1024",
|
||||
"filename": "AppIcon-512@2x.png",
|
||||
"platform": "ios"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
6
ios/App/App/Assets.xcassets/Contents.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
56
ios/App/App/Assets.xcassets/Splash.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"images": [
|
||||
{
|
||||
"idiom": "universal",
|
||||
"filename": "Default@1x~universal~anyany.png",
|
||||
"scale": "1x"
|
||||
},
|
||||
{
|
||||
"idiom": "universal",
|
||||
"filename": "Default@2x~universal~anyany.png",
|
||||
"scale": "2x"
|
||||
},
|
||||
{
|
||||
"idiom": "universal",
|
||||
"filename": "Default@3x~universal~anyany.png",
|
||||
"scale": "3x"
|
||||
},
|
||||
{
|
||||
"appearances": [
|
||||
{
|
||||
"appearance": "luminosity",
|
||||
"value": "dark"
|
||||
}
|
||||
],
|
||||
"idiom": "universal",
|
||||
"scale": "1x",
|
||||
"filename": "Default@1x~universal~anyany-dark.png"
|
||||
},
|
||||
{
|
||||
"appearances": [
|
||||
{
|
||||
"appearance": "luminosity",
|
||||
"value": "dark"
|
||||
}
|
||||
],
|
||||
"idiom": "universal",
|
||||
"scale": "2x",
|
||||
"filename": "Default@2x~universal~anyany-dark.png"
|
||||
},
|
||||
{
|
||||
"appearances": [
|
||||
{
|
||||
"appearance": "luminosity",
|
||||
"value": "dark"
|
||||
}
|
||||
],
|
||||
"idiom": "universal",
|
||||
"scale": "3x",
|
||||
"filename": "Default@3x~universal~anyany-dark.png"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"version": 1,
|
||||
"author": "xcode"
|
||||
}
|
||||
}
|
||||
BIN
ios/App/App/Assets.xcassets/Splash.imageset/Default@1x~universal~anyany-dark.png
vendored
Normal file
|
After Width: | Height: | Size: 244 KiB |
BIN
ios/App/App/Assets.xcassets/Splash.imageset/Default@1x~universal~anyany.png
vendored
Normal file
|
After Width: | Height: | Size: 244 KiB |
BIN
ios/App/App/Assets.xcassets/Splash.imageset/Default@2x~universal~anyany-dark.png
vendored
Normal file
|
After Width: | Height: | Size: 244 KiB |
BIN
ios/App/App/Assets.xcassets/Splash.imageset/Default@2x~universal~anyany.png
vendored
Normal file
|
After Width: | Height: | Size: 244 KiB |
BIN
ios/App/App/Assets.xcassets/Splash.imageset/Default@3x~universal~anyany-dark.png
vendored
Normal file
|
After Width: | Height: | Size: 244 KiB |
BIN
ios/App/App/Assets.xcassets/Splash.imageset/Default@3x~universal~anyany.png
vendored
Normal file
|
After Width: | Height: | Size: 244 KiB |
BIN
ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732-1.png
vendored
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732-2.png
vendored
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732.png
vendored
Normal file
|
After Width: | Height: | Size: 72 KiB |
12
ios/App/App/Assets.xcassets/brackets.symbolset/Contents.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"symbols" : [
|
||||
{
|
||||
"filename" : "brackets.symbols.svg",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--Generator: Apple Native CoreSVG 341-->
|
||||
<!DOCTYPE svg
|
||||
PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 3300 2200">
|
||||
<!--glyph: "", point size: 100.0, font version: "21.0d6e2", template writer version: "138.0.0"-->
|
||||
<style>.defaults {-sfsymbols-variable-value-mode:color;-sfsymbols-draw-reverses-motion-groups:true}
|
||||
|
||||
.monochrome-0 {-sfsymbols-motion-group:0;-sfsymbols-layer-tags:520029a6f9523332}
|
||||
|
||||
.multicolor-0:tintColor {-sfsymbols-motion-group:0;-sfsymbols-layer-tags:520029a6f9523332}
|
||||
|
||||
.hierarchical-0:primary {-sfsymbols-motion-group:0;-sfsymbols-layer-tags:520029a6f9523332}
|
||||
|
||||
.SFSymbolsPreviewWireframe {fill:none;opacity:1.0;stroke:black;stroke-width:0.5}
|
||||
</style>
|
||||
<g id="Notes">
|
||||
<rect height="2200" id="artboard" style="fill:white;opacity:1" width="3300" x="0" y="0"/>
|
||||
<line style="fill:none;stroke:black;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="292" y2="292"/>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;font-weight:bold;" transform="matrix(1 0 0 1 263 322)">Weight/Scale Variations</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 559.711 322)">Ultralight</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 856.422 322)">Thin</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 1153.13 322)">Light</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 1449.84 322)">Regular</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 1746.56 322)">Medium</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 2043.27 322)">Semibold</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 2339.98 322)">Bold</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 2636.69 322)">Heavy</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 2933.4 322)">Black</text>
|
||||
<line style="fill:none;stroke:black;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="1903" y2="1903"/>
|
||||
<g transform="matrix(0.2 0 0 0.2 263 1933)">
|
||||
<path d="m46.2402 4.15039c21.7773 0 39.4531-17.627 39.4531-39.4043s-17.6758-39.4043-39.4531-39.4043c-21.7285 0-39.4043 17.627-39.4043 39.4043s17.6758 39.4043 39.4043 39.4043Zm0-7.42188c-17.6758 0-31.9336-14.3066-31.9336-31.9824s14.2578-31.9824 31.9336-31.9824 31.9824 14.3066 31.9824 31.9824-14.3066 31.9824-31.9824 31.9824Zm3.61328-17.7734v-28.4668c0-2.24609-1.46484-3.75977-3.71094-3.75977-2.14844 0-3.61328 1.51367-3.61328 3.75977v28.4668c0 2.19727 1.46484 3.71094 3.61328 3.71094 2.24609 0 3.71094-1.51367 3.71094-3.71094Zm-17.8223-10.5957h28.418c2.19727 0 3.71094-1.46484 3.71094-3.61328 0-2.19727-1.51367-3.71094-3.71094-3.71094h-28.418c-2.24609 0-3.75977 1.51367-3.75977 3.71094 0 2.14844 1.51367 3.61328 3.75977 3.61328Z"/>
|
||||
</g>
|
||||
<g transform="matrix(0.2 0 0 0.2 281.506 1933)">
|
||||
<path d="m58.5449 14.5508c27.4902 0 49.8047-22.3145 49.8047-49.8047s-22.3145-49.8047-49.8047-49.8047-49.8047 22.3145-49.8047 49.8047 22.3145 49.8047 49.8047 49.8047Zm0-8.30078c-22.9492 0-41.5039-18.5547-41.5039-41.5039s18.5547-41.5039 41.5039-41.5039 41.5039 18.5547 41.5039 41.5039-18.5547 41.5039-41.5039 41.5039Zm4.05273-23.0957v-36.9141c0-2.49023-1.70898-4.19922-4.15039-4.19922-2.39258 0-4.05273 1.70898-4.05273 4.19922v36.9141c0 2.44141 1.66016 4.15039 4.05273 4.15039 2.44141 0 4.15039-1.66016 4.15039-4.15039Zm-22.5586-14.4043h36.9629c2.44141 0 4.15039-1.61133 4.15039-4.00391 0-2.44141-1.70898-4.15039-4.15039-4.15039h-36.9629c-2.49023 0-4.15039 1.70898-4.15039 4.15039 0 2.39258 1.66016 4.00391 4.15039 4.00391Z"/>
|
||||
</g>
|
||||
<g transform="matrix(0.2 0 0 0.2 304.924 1933)">
|
||||
<path d="m74.8535 28.3203c35.1074 0 63.623-28.4668 63.623-63.5742s-28.5156-63.623-63.623-63.623-63.5742 28.5156-63.5742 63.623 28.4668 63.5742 63.5742 63.5742Zm0-9.08203c-30.127 0-54.4922-24.3652-54.4922-54.4922s24.3652-54.4922 54.4922-54.4922 54.4922 24.3652 54.4922 54.4922-24.3652 54.4922-54.4922 54.4922Zm4.44336-30.3223v-48.4863c0-2.73438-1.85547-4.63867-4.54102-4.63867-2.58789 0-4.44336 1.9043-4.44336 4.63867v48.4863c0 2.68555 1.85547 4.58984 4.44336 4.58984 2.68555 0 4.54102-1.85547 4.54102-4.58984Zm-28.7109-19.7754h48.4863c2.68555 0 4.58984-1.80664 4.58984-4.39453 0-2.73438-1.85547-4.58984-4.58984-4.58984h-48.4863c-2.73438 0-4.58984 1.85547-4.58984 4.58984 0 2.58789 1.85547 4.39453 4.58984 4.39453Z"/>
|
||||
</g>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;font-weight:bold;" transform="matrix(1 0 0 1 263 1953)">Design Variations</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 1971)">Symbols are supported in up to nine weights and three scales.</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 1989)">For optimal layout with text and other symbols, vertically align</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 2007)">symbols with the adjacent text.</text>
|
||||
<line style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="776" x2="776" y1="1919" y2="1933"/>
|
||||
<g transform="matrix(0.2 0 0 0.2 776 1933)">
|
||||
<path d="m16.5527 0.78125c2.58789 0 3.85742-0.976562 4.78516-3.71094l20.5566-57.5195h0.244141l20.6055 57.5195c0.927734 2.73438 2.19727 3.71094 4.73633 3.71094 2.58789 0 4.24805-1.5625 4.24805-4.00391 0-0.830078-0.146484-1.61133-0.537109-2.63672l-22.9004-60.9863c-1.12305-2.97852-3.125-4.49219-6.25-4.49219-3.02734 0-5.07812 1.46484-6.15234 4.44336l-22.9004 61.084c-0.390625 1.02539-0.537109 1.80664-0.537109 2.63672 0 2.44141 1.5625 3.95508 4.10156 3.95508Zm10.2051-20.9473h30.6641c2.00195 0 3.66211-1.66016 3.66211-3.66211 0-2.05078-1.66016-3.66211-3.66211-3.66211h-30.6641c-2.00195 0-3.66211 1.61133-3.66211 3.66211 0 2.00195 1.66016 3.66211 3.66211 3.66211Z"/>
|
||||
</g>
|
||||
<line style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="792.836" x2="792.836" y1="1919" y2="1933"/>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;font-weight:bold;" transform="matrix(1 0 0 1 776 1953)">Margins</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 776 1971)">Leading and trailing margins on the left and right side of each symbol</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 776 1989)">can be adjusted by modifying the x-location of the margin guidelines.</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 776 2007)">Modifications are automatically applied proportionally to all</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 776 2025)">scales and weights.</text>
|
||||
<g transform="matrix(0.2 0 0 0.2 1289 1933)">
|
||||
<path d="m14.209 13.1348 7.86133 7.86133c4.29688 4.39453 9.32617 4.10156 13.8672-1.02539l60.6934-68.2129-4.88281-4.88281-60.2539 67.6758c-1.80664 1.95312-3.4668 2.44141-5.81055 0.0976562l-5.17578-5.12695c-2.29492-2.29492-1.80664-3.95508 0.195312-5.81055l67.4805-62.1582-4.88281-4.83398-68.0664 62.5977c-4.98047 4.58984-5.32227 9.47266-1.02539 13.8184Zm44.873-97.4609c-2.05078 2.00195-2.24609 4.88281-1.07422 6.78711 1.12305 1.80664 3.4668 3.02734 6.5918 2.24609 5.85938-1.66016 12.5977-2.39258 18.8965 0.927734l-2.68555 7.12891c-1.61133 4.00391-0.732422 6.88477 1.70898 9.42383l10.2539 10.3027c2.34375 2.39258 4.54102 2.44141 7.08008 1.95312l4.44336-0.732422 2.58789 2.53906-0.195312 2.24609c-0.0976562 2.29492 0.537109 4.29688 2.7832 6.49414l3.36914 3.32031c2.29492 2.29492 5.51758 2.49023 7.8125 0.195312l12.9883-13.0371c2.29492-2.34375 2.14844-5.37109-0.195312-7.66602l-3.41797-3.41797c-2.19727-2.19727-4.05273-3.02734-6.34766-2.88086l-2.34375 0.244141-2.44141-2.44141 1.02539-4.6875c0.634766-2.73438-0.244141-4.98047-2.88086-7.61719l-11.2793-11.1816c-12.9395-12.8418-35.5957-11.0352-46.6797-0.146484Zm7.08008 2.05078c8.78906-6.39648 25.9766-5.66406 33.6914 1.95312l12.3047 12.207c1.02539 1.02539 1.2207 1.80664 0.927734 3.32031l-1.46484 6.64062 6.73828 6.68945 4.39453-0.244141c1.12305-0.0488281 1.51367 0.0488281 2.34375 0.878906l2.53906 2.49023-10.8398 10.8398-2.49023-2.49023c-0.830078-0.878906-0.976562-1.2207-0.927734-2.39258l0.292969-4.3457-6.68945-6.73828-6.83594 1.17188c-1.41602 0.292969-2.05078 0.195312-3.17383-0.878906l-8.93555-8.88672c-1.07422-1.02539-1.17188-1.70898-0.488281-3.36914l4.58984-11.4746c-6.10352-6.34766-17.041-7.51953-25.5859-4.58984-0.683594 0.244141-0.927734-0.390625-0.390625-0.78125Z"/>
|
||||
</g>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;font-weight:bold;" transform="matrix(1 0 0 1 1289 1953)">Exporting</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 1289 1971)">Symbols should be outlined when exporting to ensure the</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 1289 1989)">design is preserved when submitting to Xcode.</text>
|
||||
<text id="template-version" style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:end;" transform="matrix(1 0 0 1 3036 1933)">Template v.7.0</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:end;" transform="matrix(1 0 0 1 3036 1951)">Requires Xcode 17 or greater</text>
|
||||
<text id="descriptive-name" style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:end;" transform="matrix(1 0 0 1 3036 1969)">Generated from brackets.symbols</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:end;" transform="matrix(1 0 0 1 3036 1987)">Typeset at 100.0 points</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 726)">Small</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 1156)">Medium</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 1586)">Large</text>
|
||||
</g>
|
||||
<g id="Guides">
|
||||
<g id="H-reference" style="fill:#27AAE1;stroke:none;" transform="matrix(1 0 0 1 339 696)">
|
||||
<path d="M0.993654 0L3.63775 0L29.3281-67.1323L30.0303-67.1323L30.0303-70.459L28.1226-70.459ZM11.6885-24.4799L46.9815-24.4799L46.2315-26.7285L12.4385-26.7285ZM55.1196 0L57.7637 0L30.6382-70.459L29.4326-70.459L29.4326-67.1323Z"/>
|
||||
</g>
|
||||
<line id="Baseline-S" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="696" y2="696"/>
|
||||
<line id="Capline-S" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="625.541" y2="625.541"/>
|
||||
<g id="H-reference" style="fill:#27AAE1;stroke:none;" transform="matrix(1 0 0 1 339 1126)">
|
||||
<path d="M0.993654 0L3.63775 0L29.3281-67.1323L30.0303-67.1323L30.0303-70.459L28.1226-70.459ZM11.6885-24.4799L46.9815-24.4799L46.2315-26.7285L12.4385-26.7285ZM55.1196 0L57.7637 0L30.6382-70.459L29.4326-70.459L29.4326-67.1323Z"/>
|
||||
</g>
|
||||
<line id="Baseline-M" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="1126" y2="1126"/>
|
||||
<line id="Capline-M" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="1055.54" y2="1055.54"/>
|
||||
<g id="H-reference" style="fill:#27AAE1;stroke:none;" transform="matrix(1 0 0 1 339 1556)">
|
||||
<path d="M0.993654 0L3.63775 0L29.3281-67.1323L30.0303-67.1323L30.0303-70.459L28.1226-70.459ZM11.6885-24.4799L46.9815-24.4799L46.2315-26.7285L12.4385-26.7285ZM55.1196 0L57.7637 0L30.6382-70.459L29.4326-70.459L29.4326-67.1323Z"/>
|
||||
</g>
|
||||
<line id="Baseline-L" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="1556" y2="1556"/>
|
||||
<line id="Capline-L" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="1485.54" y2="1485.54"/>
|
||||
<line id="right-margin-Black-S" style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="2977.69" x2="2977.69" y1="600.785" y2="720.121"/>
|
||||
<line id="left-margin-Black-S" style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="2889.11" x2="2889.11" y1="600.785" y2="720.121"/>
|
||||
<line id="right-margin-Regular-S" style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="1494.13" x2="1494.13" y1="600.785" y2="720.121"/>
|
||||
<line id="left-margin-Regular-S" style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="1405.56" x2="1405.56" y1="600.785" y2="720.121"/>
|
||||
<line id="right-margin-Ultralight-S" style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="604" x2="604" y1="600.785" y2="720.121"/>
|
||||
<line id="left-margin-Ultralight-S" style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="515.423" x2="515.423" y1="600.785" y2="720.121"/>
|
||||
</g>
|
||||
<g id="Symbols">
|
||||
<g id="Black-S" transform="matrix(1.00656 0 0 1.00656 2889.11 696)">
|
||||
<path class="monochrome-0 multicolor-0:tintColor hierarchical-0:primary SFSymbolsPreviewWireframe" d="M32.333-70L32.333-62.222L20.667-62.222L20.667-7.778L32.333-7.778L32.333 0L12.889 0L12.889-70L32.333-70ZM55.667-70L75.111-70L75.111 0L55.667 0L55.667-7.778L67.333-7.778L67.333-62.222L55.667-62.222L55.667-70Z"/>
|
||||
</g>
|
||||
<g id="Regular-S" transform="matrix(1.00656 0 0 1.00656 1405.56 696)">
|
||||
<path class="monochrome-0 multicolor-0:tintColor hierarchical-0:primary SFSymbolsPreviewWireframe" d="M32.333-70L32.333-62.222L20.667-62.222L20.667-7.778L32.333-7.778L32.333 0L12.889 0L12.889-70L32.333-70ZM55.667-70L75.111-70L75.111 0L55.667 0L55.667-7.778L67.333-7.778L67.333-62.222L55.667-62.222L55.667-70Z"/>
|
||||
</g>
|
||||
<g id="Ultralight-S" transform="matrix(1.00656 0 0 1.00656 515.423 696)">
|
||||
<path class="monochrome-0 multicolor-0:tintColor hierarchical-0:primary SFSymbolsPreviewWireframe" d="M32.333-70L32.333-62.222L20.667-62.222L20.667-7.778L32.333-7.778L32.333 0L12.889 0L12.889-70L32.333-70ZM55.667-70L75.111-70L75.111 0L55.667 0L55.667-7.778L67.333-7.778L67.333-62.222L55.667-62.222L55.667-70Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 14 KiB |
32
ios/App/App/Base.lproj/LaunchScreen.storyboard
Normal file
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17132" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17105"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="EHf-IW-A2E">
|
||||
<objects>
|
||||
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||
<imageView key="view" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Splash" id="snD-IY-ifK">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
</imageView>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="53" y="375"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="Splash" width="1366" height="1366"/>
|
||||
<systemColor name="systemBackgroundColor">
|
||||
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</systemColor>
|
||||
</resources>
|
||||
</document>
|
||||
18
ios/App/App/Base.lproj/Main.storyboard
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="23504" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
|
||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23506"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--App View Controller-->
|
||||
<scene sceneID="tne-QT-ifu">
|
||||
<objects>
|
||||
<viewController id="BYZ-38-t0r" customClass="AppViewController" customModule="Logseq" sceneMemberID="viewController"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="44" y="-2"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
12
ios/App/App/FileContainer.m
Normal file
@@ -0,0 +1,12 @@
|
||||
//
|
||||
// FileContainer.m
|
||||
// App
|
||||
//
|
||||
// Created by weihua on 9/29/21.
|
||||
//
|
||||
|
||||
#import <Capacitor/Capacitor.h>
|
||||
|
||||
CAP_PLUGIN(FileContainer, "FileContainer",
|
||||
CAP_PLUGIN_METHOD(ensureDocuments, CAPPluginReturnPromise);
|
||||
)
|
||||
62
ios/App/App/FileContainer.swift
Normal file
@@ -0,0 +1,62 @@
|
||||
//
|
||||
// FileContainer.swift
|
||||
// App
|
||||
//
|
||||
//
|
||||
|
||||
import Capacitor
|
||||
import Foundation
|
||||
import MobileCoreServices
|
||||
|
||||
@objc(FileContainer)
|
||||
public class FileContainer: CAPPlugin, UIDocumentPickerDelegate {
|
||||
|
||||
var iCloudContainerUrl: URL? {
|
||||
return FileManager.default.url(forUbiquityContainerIdentifier: nil)?.appendingPathComponent("Documents")
|
||||
}
|
||||
|
||||
var localContainerUrl: URL? {
|
||||
return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
|
||||
}
|
||||
|
||||
@objc func ensureDocuments(_ call: CAPPluginCall) {
|
||||
var data: [String: String] = [:]
|
||||
|
||||
if self.iCloudContainerUrl != nil {
|
||||
validateDocuments(at: self.iCloudContainerUrl!)
|
||||
data["iCloudContainerUrl"] = self.iCloudContainerUrl?.absoluteString
|
||||
}
|
||||
|
||||
if self.localContainerUrl != nil {
|
||||
validateDocuments(at: self.localContainerUrl!)
|
||||
data["localContainerUrl"] = self.localContainerUrl?.absoluteString
|
||||
}
|
||||
|
||||
call.resolve(data)
|
||||
}
|
||||
|
||||
func validateDocuments(at url: URL) {
|
||||
|
||||
if !FileManager.default.fileExists(atPath: url.path, isDirectory: nil) {
|
||||
do {
|
||||
print("the url = " + url.path)
|
||||
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true, attributes: nil)
|
||||
} catch {
|
||||
print("container doesn't exist")
|
||||
print(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
let str = ""
|
||||
let filename = url.appendingPathComponent(".logseq", isDirectory: false)
|
||||
|
||||
if !FileManager.default.fileExists(atPath: filename.path) {
|
||||
do {
|
||||
try str.write(to: filename, atomically: true, encoding: String.Encoding.utf8)
|
||||
} catch {
|
||||
print("write .logseq failed")
|
||||
print(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
45
ios/App/App/FileSync/Data+ChaChaPoly.swift
Normal file
@@ -0,0 +1,45 @@
|
||||
//
|
||||
// Data+ChaChaPoly.swift
|
||||
// Logseq
|
||||
//
|
||||
// Created by Mono Wang on 5/20/R4.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CryptoKit
|
||||
|
||||
extension Data {
|
||||
/**
|
||||
Encrypts current data using ChaChaPoly cipher.
|
||||
*/
|
||||
public func sealChaChaPoly(with passphrase: String) -> Data? {
|
||||
guard let symmetricKey = try? SymmetricKey(passwordString: passphrase) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let nonce = try? ChaChaPoly.Nonce(data: Data(repeating: 0x00, count: 12))
|
||||
if let encrypted = try? ChaChaPoly.seal(self, using: symmetricKey, nonce: nonce!).combined {
|
||||
var ret = Data(hexEncoded: "4c530031")
|
||||
ret?.append(encrypted)
|
||||
return ret
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/**
|
||||
Decrypts current combined ChaChaPoly Selead box data (nonce || ciphertext || tag) using ChaChaPoly cipher.
|
||||
*/
|
||||
public func openChaChaPoly(with passphrase: String) -> Data? {
|
||||
if self.count <= 4 {
|
||||
return nil
|
||||
}
|
||||
guard let symmetricKey = try? SymmetricKey(passwordString: passphrase) else {
|
||||
return nil
|
||||
}
|
||||
guard let chaChaPolySealedBox = try? ChaChaPoly.SealedBox(combined: self[4...]) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return try? ChaChaPoly.open(chaChaPolySealedBox, using: symmetricKey)
|
||||
}
|
||||
}
|
||||
12
ios/App/App/FolderPicker.m
Normal file
@@ -0,0 +1,12 @@
|
||||
//
|
||||
// FolderPicker.m
|
||||
// App
|
||||
//
|
||||
// Created by weihua on 9/29/21.
|
||||
//
|
||||
|
||||
#import <Capacitor/Capacitor.h>
|
||||
|
||||
CAP_PLUGIN(FolderPicker, "FolderPicker",
|
||||
CAP_PLUGIN_METHOD(pickFolder, CAPPluginReturnPromise);
|
||||
)
|
||||
69
ios/App/App/FolderPicker.swift
Normal file
@@ -0,0 +1,69 @@
|
||||
//
|
||||
// FolderPicker.swift
|
||||
// App
|
||||
//
|
||||
// Created by weihua on 9/29/21.
|
||||
//
|
||||
|
||||
import Capacitor
|
||||
import Foundation
|
||||
import MobileCoreServices
|
||||
|
||||
@objc(FolderPicker)
|
||||
public class FolderPicker: CAPPlugin, UIDocumentPickerDelegate {
|
||||
|
||||
public var _call: CAPPluginCall?
|
||||
|
||||
@objc func pickFolder(_ call: CAPPluginCall) {
|
||||
self._call = call
|
||||
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
|
||||
let documentPicker = UIDocumentPickerViewController(
|
||||
documentTypes: [String(kUTTypeFolder)],
|
||||
in: UIDocumentPickerMode.open
|
||||
)
|
||||
|
||||
// Set the initial directory.
|
||||
|
||||
if let path = call.getString("path") {
|
||||
guard let url = URL(string: path) else {
|
||||
call.reject("can not parse url")
|
||||
return
|
||||
}
|
||||
|
||||
print("picked folder url = " + url.path)
|
||||
|
||||
documentPicker.directoryURL = url
|
||||
}
|
||||
|
||||
documentPicker.allowsMultipleSelection = false
|
||||
documentPicker.delegate = self
|
||||
|
||||
documentPicker.modalPresentationStyle = UIModalPresentationStyle.fullScreen
|
||||
|
||||
self?.bridge?.viewController?.present(
|
||||
documentPicker,
|
||||
animated: true,
|
||||
completion: nil
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public func documentPicker(
|
||||
_ controller: UIDocumentPickerViewController,
|
||||
didPickDocumentsAt urls: [URL]
|
||||
) {
|
||||
var items: [String] = []
|
||||
let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
|
||||
|
||||
for url in urls {
|
||||
items.append(url.absoluteString)
|
||||
}
|
||||
|
||||
self._call?.resolve([
|
||||
"path": items.first as Any,
|
||||
"localDocumentsPath": documentsPath[0] as Any
|
||||
])
|
||||
}
|
||||
}
|
||||
13
ios/App/App/FsWatcher.m
Normal file
@@ -0,0 +1,13 @@
|
||||
//
|
||||
// FsWatcher.m
|
||||
// Logseq
|
||||
//
|
||||
// Created by Mono Wang on 2/17/R4.
|
||||
//
|
||||
|
||||
#import <Capacitor/Capacitor.h>
|
||||
|
||||
CAP_PLUGIN(FsWatcher, "FsWatcher",
|
||||
CAP_PLUGIN_METHOD(watch, CAPPluginReturnPromise);
|
||||
CAP_PLUGIN_METHOD(unwatch, CAPPluginReturnPromise);
|
||||
)
|
||||
305
ios/App/App/FsWatcher.swift
Normal file
@@ -0,0 +1,305 @@
|
||||
//
|
||||
// FsWatcher.swift
|
||||
// Logseq
|
||||
//
|
||||
// Created by Mono Wang on 2/17/R4.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Capacitor
|
||||
|
||||
// MARK: Watcher Plugin
|
||||
|
||||
@objc(FsWatcher)
|
||||
public class FsWatcher: CAPPlugin, PollingWatcherDelegate {
|
||||
private var watcher: PollingWatcher?
|
||||
private var baseUrl: URL?
|
||||
|
||||
override public func load() {
|
||||
print("debug FsWatcher iOS plugin loaded!")
|
||||
}
|
||||
|
||||
@objc func watch(_ call: CAPPluginCall) {
|
||||
if let path = call.getString("path") {
|
||||
guard let url = URL(string: path) else {
|
||||
call.reject("can not parse url")
|
||||
return
|
||||
}
|
||||
self.baseUrl = url
|
||||
self.watcher = PollingWatcher(at: url)
|
||||
self.watcher?.delegate = self
|
||||
self.watcher?.start()
|
||||
|
||||
call.resolve(["ok": true])
|
||||
|
||||
} else {
|
||||
call.reject("missing path string parameter")
|
||||
}
|
||||
}
|
||||
|
||||
@objc func unwatch(_ call: CAPPluginCall) {
|
||||
watcher?.stop()
|
||||
watcher = nil
|
||||
baseUrl = nil
|
||||
|
||||
call.resolve()
|
||||
}
|
||||
|
||||
public func receivedNotification(_ url: URL, _ event: PollingWatcherEvent, _ metadata: SimpleFileMetadata?) {
|
||||
guard let baseUrl = baseUrl else {
|
||||
// unwatch, ignore incoming
|
||||
return
|
||||
}
|
||||
// NOTE: Event in js {dir path content stat{mtime}}
|
||||
switch event {
|
||||
case .Unlink:
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
self.notifyListeners("watcher", data: ["event": "unlink",
|
||||
"dir": baseUrl.description as Any,
|
||||
"path": url.relativePath(from: baseUrl)?.precomposedStringWithCanonicalMapping as Any
|
||||
])
|
||||
}
|
||||
case .Add, .Change:
|
||||
var content: String?
|
||||
if url.shouldNotifyWithContent() {
|
||||
content = try? String(contentsOf: url, encoding: .utf8)
|
||||
}
|
||||
self.notifyListeners("watcher", data: ["event": event.description,
|
||||
"dir": baseUrl.description as Any,
|
||||
"path": url.relativePath(from: baseUrl)?.precomposedStringWithCanonicalMapping as Any,
|
||||
"content": content as Any,
|
||||
"stat": ["mtime": metadata?.contentModificationTimestamp ?? 0,
|
||||
"ctime": metadata?.creationTimestamp ?? 0,
|
||||
"size": metadata?.fileSize as Any]
|
||||
])
|
||||
|
||||
case .Error:
|
||||
// TODO: handle error?
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: URL extension
|
||||
|
||||
extension URL {
|
||||
func isSkipped() -> Bool {
|
||||
// skip hidden file
|
||||
if self.lastPathComponent.starts(with: ".") {
|
||||
return true
|
||||
}
|
||||
if self.absoluteString.contains("/logseq/bak/") || self.absoluteString.contains("/logseq/version-files/") {
|
||||
return true
|
||||
}
|
||||
if self.lastPathComponent == "graphs-txid.edn" || self.lastPathComponent == "broken-config.edn" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func shouldNotifyWithContent() -> Bool {
|
||||
let allowedPathExtensions: Set = ["md", "markdown", "org", "js", "edn", "css"]
|
||||
if allowedPathExtensions.contains(self.pathExtension.lowercased()) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isICloudPlaceholder() -> Bool {
|
||||
if self.lastPathComponent.starts(with: ".") && self.pathExtension.lowercased() == "icloud" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: PollingWatcher
|
||||
|
||||
public protocol PollingWatcherDelegate {
|
||||
func receivedNotification(_ url: URL, _ event: PollingWatcherEvent, _ metadata: SimpleFileMetadata?)
|
||||
}
|
||||
|
||||
public enum PollingWatcherEvent {
|
||||
case Add
|
||||
case Change
|
||||
case Unlink
|
||||
case Error
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .Add:
|
||||
return "add"
|
||||
case .Change:
|
||||
return "change"
|
||||
case .Unlink:
|
||||
return "unlink"
|
||||
case .Error:
|
||||
return "error"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct SimpleFileMetadata: CustomStringConvertible, Equatable {
|
||||
var contentModificationTimestamp: Double
|
||||
var creationTimestamp: Double
|
||||
var fileSize: Int
|
||||
|
||||
public init?(of fileURL: URL) {
|
||||
do {
|
||||
let fileAttributes = try fileURL.resourceValues(forKeys: [.isRegularFileKey, .fileSizeKey, .contentModificationDateKey, .creationDateKey])
|
||||
if fileAttributes.isRegularFile! {
|
||||
contentModificationTimestamp = fileAttributes.contentModificationDate?.timeIntervalSince1970 ?? 0.0
|
||||
creationTimestamp = fileAttributes.creationDate?.timeIntervalSince1970 ?? 0.0
|
||||
fileSize = fileAttributes.fileSize ?? 0
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public var description: String {
|
||||
return "Meta(size=\(self.fileSize), mtime=\(self.contentModificationTimestamp), ctime=\(self.creationTimestamp)"
|
||||
}
|
||||
}
|
||||
|
||||
public class PollingWatcher {
|
||||
private let url: URL
|
||||
private var timer: DispatchSourceTimer?
|
||||
public var delegate: PollingWatcherDelegate?
|
||||
private var metaDb: [URL: SimpleFileMetadata] = [:]
|
||||
|
||||
public init?(at: URL) {
|
||||
url = at
|
||||
}
|
||||
|
||||
public func start() {
|
||||
|
||||
self.tick(notify: false)
|
||||
|
||||
let queue = DispatchQueue(label: Bundle.main.bundleIdentifier! + ".timer")
|
||||
timer = DispatchSource.makeTimerSource(queue: queue)
|
||||
timer!.setEventHandler(qos: .background, flags: []) { [weak self] in
|
||||
self?.tick(notify: true)
|
||||
}
|
||||
timer!.schedule(deadline: .now())
|
||||
timer!.resume()
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.stop()
|
||||
}
|
||||
|
||||
public func stop() {
|
||||
timer?.cancel()
|
||||
timer = nil
|
||||
}
|
||||
|
||||
private func tick(notify: Bool) {
|
||||
// let startTime = DispatchTime.now()
|
||||
|
||||
if let enumerator = FileManager.default.enumerator(
|
||||
at: url,
|
||||
includingPropertiesForKeys: [.isRegularFileKey, .nameKey, .isDirectoryKey],
|
||||
// NOTE: icloud downloading requires non-skipsHiddenFiles
|
||||
options: [.skipsPackageDescendants]) {
|
||||
|
||||
var newMetaDb: [URL: SimpleFileMetadata] = [:]
|
||||
|
||||
for case let fileURL as URL in enumerator {
|
||||
guard let resourceValues = try? fileURL.resourceValues(forKeys: [.isRegularFileKey, .nameKey, .isDirectoryKey]),
|
||||
let isDirectory = resourceValues.isDirectory,
|
||||
let isRegularFile = resourceValues.isRegularFile,
|
||||
let name = resourceValues.name
|
||||
else {
|
||||
continue
|
||||
}
|
||||
|
||||
if isDirectory {
|
||||
// NOTE: URL.path won't end with a `/`
|
||||
if fileURL.path.hasSuffix("/logseq/bak") || fileURL.path.hasSuffix("/logseq/version-files") || name == ".recycle" || name.hasPrefix(".") || name == "node_modules" {
|
||||
enumerator.skipDescendants()
|
||||
}
|
||||
}
|
||||
|
||||
if isRegularFile && !fileURL.isSkipped() {
|
||||
if let meta = SimpleFileMetadata(of: fileURL) {
|
||||
newMetaDb[fileURL] = meta
|
||||
}
|
||||
} else if fileURL.isICloudPlaceholder() {
|
||||
try? FileManager.default.startDownloadingUbiquitousItem(at: fileURL)
|
||||
}
|
||||
}
|
||||
|
||||
if notify {
|
||||
self.updateMetaDb(with: newMetaDb)
|
||||
} else {
|
||||
self.metaDb = newMetaDb
|
||||
}
|
||||
}
|
||||
|
||||
// let elapsedNanoseconds = DispatchTime.now().uptimeNanoseconds - startTime.uptimeNanoseconds
|
||||
// let elapsedInMs = Double(elapsedNanoseconds) / 1_000_000
|
||||
// print("debug ticker elapsed=\(elapsedInMs)ms")
|
||||
|
||||
if #available(iOS 13.0, *) {
|
||||
timer?.schedule(deadline: .now().advanced(by: .seconds(2)), leeway: .milliseconds(100))
|
||||
} else {
|
||||
// Fallback on earlier versions
|
||||
timer?.schedule(deadline: .now() + 2.0, leeway: .milliseconds(100))
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: batch?
|
||||
private func updateMetaDb(with newMetaDb: [URL: SimpleFileMetadata]) {
|
||||
for (url, meta) in newMetaDb {
|
||||
if let idx = self.metaDb.index(forKey: url) {
|
||||
let (_, oldMeta) = self.metaDb.remove(at: idx)
|
||||
if oldMeta != meta {
|
||||
self.delegate?.receivedNotification(url, .Change, meta)
|
||||
}
|
||||
} else {
|
||||
self.delegate?.receivedNotification(url, .Add, meta)
|
||||
}
|
||||
}
|
||||
for url in self.metaDb.keys {
|
||||
self.delegate?.receivedNotification(url, .Unlink, nil)
|
||||
}
|
||||
self.metaDb = newMetaDb
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension URL {
|
||||
func relativePath(from base: URL) -> String? {
|
||||
// Ensure that both URLs represent files:
|
||||
guard self.isFileURL && base.isFileURL else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// NOTE: standardizedFileURL will remove `/private` prefix
|
||||
// If the file is not exist, it won't remove the prefix.
|
||||
|
||||
// Remove/replace "." and "..", make paths absolute:
|
||||
var destComponents = self.standardizedFileURL.pathComponents
|
||||
let baseComponents = base.standardizedFileURL.pathComponents
|
||||
|
||||
// replace "private" when needed
|
||||
if destComponents.count > 1 && destComponents[1] == "private" && baseComponents.count > 1 && baseComponents[1] != "private" {
|
||||
destComponents.remove(at: 1)
|
||||
}
|
||||
|
||||
// Find number of common path components:
|
||||
var i = 0
|
||||
while i < destComponents.count && i < baseComponents.count
|
||||
&& destComponents[i] == baseComponents[i] {
|
||||
i += 1
|
||||
}
|
||||
|
||||
// Build relative path:
|
||||
var relComponents = Array(repeating: "..", count: baseComponents.count - i)
|
||||
relComponents.append(contentsOf: destComponents[i...])
|
||||
return relComponents.joined(separator: "/")
|
||||
}
|
||||
}
|
||||
166
ios/App/App/Info.plist
Normal file
@@ -0,0 +1,166 @@
|
||||
<?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>APFiles</key>
|
||||
<dict>
|
||||
<key>APFileDescriptionKey</key>
|
||||
<string></string>
|
||||
<key>APFileDestinationPath</key>
|
||||
<string></string>
|
||||
<key>APFileName</key>
|
||||
<string></string>
|
||||
<key>APFileSourcePath</key>
|
||||
<string></string>
|
||||
</dict>
|
||||
<key>AppIntentsEnabled</key>
|
||||
<true/>
|
||||
<key>AppControlsEnabled</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Logseq</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleGetInfoString</key>
|
||||
<string></string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Viewer</string>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>com.logseq.logseq</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>logseq</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string></string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>We will access your camera when you take a photo, and embed it in your note.</string>
|
||||
<key>NSDocumentsFolderUsageDescription</key>
|
||||
<string></string>
|
||||
<key>NSDownloadsFolderUsageDescription</key>
|
||||
<string></string>
|
||||
<key>NSFileProviderDomainUsageDescription</key>
|
||||
<string></string>
|
||||
<key>NSFileProviderPresenceUsageDescription</key>
|
||||
<string></string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>We will access your microphone to record audio notes</string>
|
||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||
<string>We will access your album when you save a photo.</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>We will access your album when you choose a photo, and embed it in your note.</string>
|
||||
<key>NSSpeechRecognitionUsageDescription</key>
|
||||
<string>We need access to speech recognition to convert your voice to text.</string>
|
||||
<key>NSUbiquitousContainers</key>
|
||||
<dict>
|
||||
<key>iCloud.com.logseq.logseq</key>
|
||||
<dict>
|
||||
<key>NSUbiquitousContainerIsDocumentScopePublic</key>
|
||||
<true/>
|
||||
<key>NSUbiquitousContainerName</key>
|
||||
<string>Logseq</string>
|
||||
<key>NSUbiquitousContainerSupportedFolderLevels</key>
|
||||
<string>ANY</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>NSUserActivityTypes</key>
|
||||
<array>
|
||||
<string>com.logseq.quickadd</string>
|
||||
<string>com.logseq.audio</string>
|
||||
</array>
|
||||
<key>UIApplicationShortcutItems</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UIApplicationShortcutItemIconSymbolName</key>
|
||||
<string>waveform</string>
|
||||
<key>UIApplicationShortcutItemTitle</key>
|
||||
<string>Voice</string>
|
||||
<key>UIApplicationShortcutItemType</key>
|
||||
<string>logseq.voice</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>UIApplicationShortcutItemIconSymbolName</key>
|
||||
<string>plus</string>
|
||||
<key>UIApplicationShortcutItemTitle</key>
|
||||
<string>Quick Add</string>
|
||||
<key>UIApplicationShortcutItemType</key>
|
||||
<string>logseq.quickadd</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>audio</string>
|
||||
</array>
|
||||
<key>UIFileSharingEnabled</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>armv7</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
</array>
|
||||
<key>UISupportsDocumentBrowser</key>
|
||||
<true/>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<true/>
|
||||
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict>
|
||||
<key>UIApplicationSupportsMultipleScenes</key>
|
||||
<false/>
|
||||
<key>UISceneConfigurations</key>
|
||||
<dict>
|
||||
<key>UIWindowSceneSessionRoleApplication</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UISceneConfigurationName</key>
|
||||
<string>Default Configuration</string>
|
||||
<key>UISceneDelegateClassName</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
326
ios/App/App/LiquidTabsPlugin.swift
Normal file
@@ -0,0 +1,326 @@
|
||||
import Foundation
|
||||
import Capacitor
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
import WebKit
|
||||
|
||||
@objc(LiquidTabsPlugin)
|
||||
public class LiquidTabsPlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
// So SwiftUI can notify JS
|
||||
static weak var shared: LiquidTabsPlugin?
|
||||
private static let maxPhoneContentTabs = 4
|
||||
|
||||
private let store = LiquidTabsStore.shared
|
||||
private var keyboardHackScriptInstalled = false
|
||||
private let keyboardHackHandlerName = "keyboardHackKey"
|
||||
|
||||
public let identifier = "LiquidTabsPlugin"
|
||||
public let jsName = "LiquidTabsPlugin"
|
||||
public let pluginMethods: [CAPPluginMethod] = [
|
||||
CAPPluginMethod(name: "configureTabs", returnType: CAPPluginReturnPromise),
|
||||
CAPPluginMethod(name: "selectTab", returnType: CAPPluginReturnPromise),
|
||||
CAPPluginMethod(name: "updateNativeSearchResults", returnType: CAPPluginReturnPromise),
|
||||
CAPPluginMethod(name: "updateNativeGraphs", returnType: CAPPluginReturnPromise),
|
||||
CAPPluginMethod(name: "markTabContentReady", returnType: CAPPluginReturnPromise),
|
||||
]
|
||||
|
||||
public override func load() {
|
||||
super.load()
|
||||
LiquidTabsPlugin.shared = self
|
||||
installKeyboardHackScript()
|
||||
}
|
||||
|
||||
private static func visibleContentTabs(_ tabs: [LiquidTab]) -> [LiquidTab] {
|
||||
guard UIDevice.current.userInterfaceIdiom == .phone else {
|
||||
return tabs
|
||||
}
|
||||
|
||||
return Array(tabs.prefix(maxPhoneContentTabs))
|
||||
}
|
||||
|
||||
// MARK: - Methods from JS
|
||||
|
||||
/// Configure tabs from JS.
|
||||
/// Expected args:
|
||||
/// { tabs: [{ id, title, systemImage, role }] }
|
||||
@objc func configureTabs(_ call: CAPPluginCall) {
|
||||
guard let tabsArray = call.getArray("tabs") as? [[String: Any]] else {
|
||||
call.reject("Missing 'tabs'")
|
||||
return
|
||||
}
|
||||
|
||||
let tabs: [LiquidTab] = tabsArray.compactMap { dict in
|
||||
guard
|
||||
let id = dict["id"] as? String,
|
||||
let title = dict["title"] as? String
|
||||
else { return nil }
|
||||
|
||||
let rawSystemImage = dict["systemImage"] as? String ?? "square"
|
||||
|
||||
let systemImage: String = {
|
||||
if UIImage(systemName: rawSystemImage) != nil {
|
||||
return rawSystemImage
|
||||
} else {
|
||||
return "square"
|
||||
}
|
||||
}()
|
||||
|
||||
let roleStr = dict["role"] as? String ?? "normal"
|
||||
let role: LiquidTab.Role = (roleStr == "search") ? .search : .normal
|
||||
|
||||
return LiquidTab(id: id, title: title, systemImage: systemImage, role: role)
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
let selectedId = self.store.selectedId
|
||||
let visibleTabs = Self.visibleContentTabs(tabs)
|
||||
self.store.tabs = visibleTabs
|
||||
if let selectedId = selectedId,
|
||||
selectedId == "search" || visibleTabs.contains(where: { $0.id == selectedId }) {
|
||||
self.store.selectedId = selectedId
|
||||
} else {
|
||||
self.store.selectedId = visibleTabs.first?.id
|
||||
}
|
||||
}
|
||||
|
||||
call.resolve()
|
||||
}
|
||||
|
||||
/// Programmatically select a tab by id.
|
||||
/// { id: string }
|
||||
@objc func selectTab(_ call: CAPPluginCall) {
|
||||
guard let id = call.getString("id") else {
|
||||
call.reject("Missing 'id'")
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.store.selectedId = id
|
||||
}
|
||||
|
||||
call.resolve()
|
||||
}
|
||||
|
||||
/// Update native search results list from JS.
|
||||
/// { results: [{ id, title, subtitle? }] }
|
||||
@objc func updateNativeSearchResults(_ call: CAPPluginCall) {
|
||||
guard let resultDicts = call.getArray("results", JSObject.self) else {
|
||||
call.reject("Missing 'results'")
|
||||
return
|
||||
}
|
||||
|
||||
let mapped: [NativeSearchResult] = resultDicts.compactMap { dict in
|
||||
guard let id = dict["id"] as? String,
|
||||
let title = dict["title"] as? String else {
|
||||
return nil
|
||||
}
|
||||
let subtitle = dict["subtitle"] as? String
|
||||
return NativeSearchResult(id: id, title: title, subtitle: subtitle)
|
||||
}
|
||||
|
||||
store.updateSearchResults(mapped)
|
||||
call.resolve()
|
||||
}
|
||||
|
||||
// MARK: - Events to JS
|
||||
|
||||
func notifyTabSelected(id: String, reselected: Bool = false) {
|
||||
notifyListeners("tabSelected", data: ["id": id, "reselected": reselected])
|
||||
}
|
||||
|
||||
func notifySearchChanged(query: String) {
|
||||
notifyListeners("searchChanged", data: ["query": query])
|
||||
}
|
||||
|
||||
func notifyKeyboardHackKey(key: String) {
|
||||
notifyListeners("keyboardHackKey", data: ["key": key])
|
||||
}
|
||||
|
||||
func openResult(id: String, nativePush: Bool = true) {
|
||||
notifyListeners("openSearchResultBlock", data: [
|
||||
"id": id,
|
||||
"nativePush": nativePush
|
||||
])
|
||||
}
|
||||
|
||||
/// Update native graph list from JS.
|
||||
/// { sections: [{ id, title, refreshable?, graphs: [...] }], labels: { refresh, preparing, downloading }, refreshing? }
|
||||
@objc func updateNativeGraphs(_ call: CAPPluginCall) {
|
||||
guard let sectionDicts = call.getArray("sections", JSObject.self) else {
|
||||
call.reject("Missing 'sections'")
|
||||
return
|
||||
}
|
||||
|
||||
let labelDict = call.getObject("labels") ?? [:]
|
||||
let labels = NativeGraphLabels(
|
||||
refresh: labelDict["refresh"] as? String ?? "",
|
||||
preparing: labelDict["preparing"] as? String ?? "",
|
||||
downloading: labelDict["downloading"] as? String ?? ""
|
||||
)
|
||||
|
||||
let sections: [NativeGraphSection] = sectionDicts.compactMap { sectionDict in
|
||||
guard let id = sectionDict["id"] as? String,
|
||||
let title = sectionDict["title"] as? String else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let graphDicts = sectionDict["graphs"] as? [[String: Any]] ?? []
|
||||
let graphs: [NativeGraphItem] = graphDicts.compactMap { graphDict in
|
||||
guard let itemId = graphDict["id"] as? String,
|
||||
let displayName = graphDict["displayName"] as? String else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let actionDicts = graphDict["actions"] as? [[String: Any]] ?? []
|
||||
let actions: [NativeGraphAction] = actionDicts.compactMap { actionDict in
|
||||
guard let id = actionDict["id"] as? String,
|
||||
let title = actionDict["title"] as? String,
|
||||
let confirmTitle = actionDict["confirmTitle"] as? String,
|
||||
let confirmMessage = actionDict["confirmMessage"] as? String,
|
||||
let confirmButton = actionDict["confirmButton"] as? String,
|
||||
let cancelButton = actionDict["cancelButton"] as? String else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return NativeGraphAction(
|
||||
id: id,
|
||||
title: title,
|
||||
destructive: actionDict["destructive"] as? Bool ?? false,
|
||||
confirmTitle: confirmTitle,
|
||||
confirmMessage: confirmMessage,
|
||||
confirmButton: confirmButton,
|
||||
cancelButton: cancelButton
|
||||
)
|
||||
}
|
||||
|
||||
return NativeGraphItem(
|
||||
id: itemId,
|
||||
url: graphDict["url"] as? String,
|
||||
displayName: displayName,
|
||||
subtitle: graphDict["subtitle"] as? String,
|
||||
remote: graphDict["remote"] as? Bool ?? false,
|
||||
local: graphDict["local"] as? Bool ?? false,
|
||||
readyForUse: graphDict["readyForUse"] as? Bool ?? true,
|
||||
downloading: graphDict["downloading"] as? Bool ?? false,
|
||||
e2ee: graphDict["e2ee"] as? Bool ?? false,
|
||||
graphName: graphDict["graphName"] as? String,
|
||||
graphUUID: graphDict["graphUUID"] as? String,
|
||||
graphSchemaVersion: graphDict["graphSchemaVersion"] as? String,
|
||||
actions: actions
|
||||
)
|
||||
}
|
||||
|
||||
return NativeGraphSection(
|
||||
id: id,
|
||||
title: title,
|
||||
refreshable: sectionDict["refreshable"] as? Bool ?? false,
|
||||
graphs: graphs
|
||||
)
|
||||
}
|
||||
|
||||
store.updateGraphs(
|
||||
sections: sections,
|
||||
labels: labels,
|
||||
visible: call.getBool("visible"),
|
||||
refreshing: call.getBool("refreshing")
|
||||
)
|
||||
call.resolve()
|
||||
}
|
||||
|
||||
/// Clear the native transition cover after JS has rendered a WebView-backed tab.
|
||||
/// { id: string }
|
||||
@objc func markTabContentReady(_ call: CAPPluginCall) {
|
||||
guard let id = call.getString("id") else {
|
||||
call.reject("Missing 'id'")
|
||||
return
|
||||
}
|
||||
|
||||
store.markWebTabReady(id)
|
||||
call.resolve()
|
||||
}
|
||||
|
||||
func openGraph(_ graph: NativeGraphItem) {
|
||||
notifyListeners("nativeGraphAction", data: [
|
||||
"action": "open",
|
||||
"url": graph.url ?? ""
|
||||
])
|
||||
}
|
||||
|
||||
func downloadGraph(_ graph: NativeGraphItem) {
|
||||
notifyListeners("nativeGraphAction", data: [
|
||||
"action": "download",
|
||||
"graphName": graph.graphName ?? "",
|
||||
"graphUUID": graph.graphUUID ?? "",
|
||||
"graphSchemaVersion": graph.graphSchemaVersion ?? "",
|
||||
"graphE2ee": graph.e2ee
|
||||
])
|
||||
}
|
||||
|
||||
func refreshGraphs() {
|
||||
store.startGraphRefresh()
|
||||
notifyListeners("nativeGraphAction", data: ["action": "refresh"])
|
||||
}
|
||||
|
||||
func performGraphAction(_ action: NativeGraphAction, graph: NativeGraphItem) {
|
||||
notifyListeners("nativeGraphAction", data: [
|
||||
"action": action.id,
|
||||
"url": graph.url ?? "",
|
||||
"graphName": graph.graphName ?? "",
|
||||
"graphUUID": graph.graphUUID ?? "",
|
||||
"graphSchemaVersion": graph.graphSchemaVersion ?? "",
|
||||
"graphE2ee": graph.e2ee
|
||||
])
|
||||
}
|
||||
|
||||
private func installKeyboardHackScript() {
|
||||
guard !keyboardHackScriptInstalled,
|
||||
let controller = bridge?.webView?.configuration.userContentController else {
|
||||
return
|
||||
}
|
||||
|
||||
keyboardHackScriptInstalled = true
|
||||
controller.removeScriptMessageHandler(forName: keyboardHackHandlerName)
|
||||
controller.add(self, name: keyboardHackHandlerName)
|
||||
|
||||
let source = """
|
||||
(function() {
|
||||
if (window.__logseqKeyboardHackInstalled) return;
|
||||
window.__logseqKeyboardHackInstalled = true;
|
||||
window.addEventListener('keydown', function(e) {
|
||||
var k = null;
|
||||
switch (e.key) {
|
||||
case 'Backspace':
|
||||
k = 'backspace';
|
||||
break;
|
||||
case 'Enter':
|
||||
case 'Return':
|
||||
k = 'enter';
|
||||
break;
|
||||
default:
|
||||
if (e.keyCode === 8) k = 'backspace';
|
||||
else if (e.keyCode === 13) k = 'enter';
|
||||
break;
|
||||
}
|
||||
if (!k) return;
|
||||
try {
|
||||
window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.\(keyboardHackHandlerName).postMessage({ key: k });
|
||||
} catch (_) {}
|
||||
}, true);
|
||||
})();
|
||||
"""
|
||||
|
||||
let script = WKUserScript(source: source, injectionTime: .atDocumentStart, forMainFrameOnly: false)
|
||||
controller.addUserScript(script)
|
||||
}
|
||||
}
|
||||
|
||||
extension LiquidTabsPlugin: WKScriptMessageHandler {
|
||||
public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
|
||||
guard message.name == keyboardHackHandlerName else { return }
|
||||
|
||||
if let body = message.body as? [String: Any],
|
||||
let key = body["key"] as? String {
|
||||
notifyKeyboardHackKey(key: key)
|
||||
}
|
||||
}
|
||||
}
|
||||
1111
ios/App/App/LiquidTabsRootView.swift
Normal file
158
ios/App/App/LiquidTabsStore.swift
Normal file
@@ -0,0 +1,158 @@
|
||||
import SwiftUI
|
||||
import Combine
|
||||
|
||||
struct NativeSearchResult: Identifiable, Hashable {
|
||||
let id: String // page or block id from JS
|
||||
let title: String
|
||||
let subtitle: String? // optional: page path, snippet, etc.
|
||||
}
|
||||
|
||||
struct NativeGraphLabels: Equatable {
|
||||
var refresh: String = ""
|
||||
var preparing: String = ""
|
||||
var downloading: String = ""
|
||||
}
|
||||
|
||||
struct NativeGraphItem: Identifiable, Hashable {
|
||||
let id: String
|
||||
let url: String?
|
||||
let displayName: String
|
||||
let subtitle: String?
|
||||
let remote: Bool
|
||||
let local: Bool
|
||||
let readyForUse: Bool
|
||||
let downloading: Bool
|
||||
let e2ee: Bool
|
||||
let graphName: String?
|
||||
let graphUUID: String?
|
||||
let graphSchemaVersion: String?
|
||||
let actions: [NativeGraphAction]
|
||||
|
||||
var tappable: Bool {
|
||||
if local { return true }
|
||||
return remote && readyForUse && !downloading
|
||||
}
|
||||
}
|
||||
|
||||
struct NativeGraphAction: Identifiable, Hashable {
|
||||
let id: String
|
||||
let title: String
|
||||
let destructive: Bool
|
||||
let confirmTitle: String
|
||||
let confirmMessage: String
|
||||
let confirmButton: String
|
||||
let cancelButton: String
|
||||
}
|
||||
|
||||
struct NativeGraphSection: Identifiable, Hashable {
|
||||
let id: String
|
||||
let title: String
|
||||
let refreshable: Bool
|
||||
let graphs: [NativeGraphItem]
|
||||
}
|
||||
|
||||
struct LiquidTab: Identifiable, Equatable {
|
||||
let id: String
|
||||
let title: String
|
||||
let systemImage: String
|
||||
let role: Role
|
||||
|
||||
enum Role {
|
||||
case normal
|
||||
case search
|
||||
}
|
||||
}
|
||||
|
||||
final class LiquidTabsStore: ObservableObject {
|
||||
static let shared = LiquidTabsStore()
|
||||
|
||||
@Published var tabs: [LiquidTab] = []
|
||||
@Published var selectedId: String?
|
||||
@Published var searchText: String = ""
|
||||
|
||||
// Native-rendered search results supplied by JS.
|
||||
@Published var searchResults: [NativeSearchResult] = []
|
||||
var suppressSearchNotifications = false
|
||||
|
||||
// Native-rendered graph list supplied by JS.
|
||||
@Published var graphSections: [NativeGraphSection] = []
|
||||
@Published var graphLabels = NativeGraphLabels()
|
||||
@Published var nativeGraphsVisible = true
|
||||
@Published var graphsRefreshing = false
|
||||
@Published var pendingWebTabId: String?
|
||||
private var pendingWebTabRequestId = 0
|
||||
|
||||
// Helper to get a stable selection if JS forgets
|
||||
func effectiveSelectedId() -> String? {
|
||||
if let selectedId, tabs.contains(where: { $0.id == selectedId }) {
|
||||
return selectedId
|
||||
}
|
||||
return tabs.first?.id
|
||||
}
|
||||
|
||||
func tab(for id: String) -> LiquidTab? {
|
||||
tabs.first(where: { $0.id == id })
|
||||
}
|
||||
|
||||
func updateSearchResults(_ results: [NativeSearchResult]) {
|
||||
DispatchQueue.main.async {
|
||||
self.searchResults = results
|
||||
}
|
||||
}
|
||||
|
||||
func updateGraphs(sections: [NativeGraphSection], labels: NativeGraphLabels, visible: Bool?, refreshing: Bool?) {
|
||||
DispatchQueue.main.async {
|
||||
self.graphSections = sections
|
||||
self.graphLabels = labels
|
||||
if let visible {
|
||||
self.nativeGraphsVisible = visible
|
||||
}
|
||||
if let refreshing {
|
||||
self.graphsRefreshing = refreshing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func startGraphRefresh() {
|
||||
if Thread.isMainThread {
|
||||
self.graphsRefreshing = true
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
self.graphsRefreshing = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func updateOnMain(_ update: @escaping () -> Void) {
|
||||
if Thread.isMainThread {
|
||||
update()
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
update()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func beginWebTabTransitionIfNeeded(_ id: String?) {
|
||||
guard let id, id != "graphs", id != "search" else {
|
||||
return
|
||||
}
|
||||
|
||||
waitForWebTab(id)
|
||||
}
|
||||
|
||||
private func waitForWebTab(_ id: String) {
|
||||
updateOnMain {
|
||||
self.pendingWebTabRequestId += 1
|
||||
self.pendingWebTabId = id
|
||||
}
|
||||
}
|
||||
|
||||
func markWebTabReady(_ id: String) {
|
||||
updateOnMain {
|
||||
guard self.pendingWebTabId == id else { return }
|
||||
self.pendingWebTabRequestId += 1
|
||||
self.pendingWebTabId = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
59
ios/App/App/LogseqIntents.swift
Normal file
@@ -0,0 +1,59 @@
|
||||
import AppIntents
|
||||
import UIKit
|
||||
|
||||
import AppIntents
|
||||
import UIKit
|
||||
|
||||
@available(iOS 18.0, *)
|
||||
struct QuickAddIntent: AppIntent {
|
||||
static var title: LocalizedStringResource = "Quick Add"
|
||||
static var description = IntentDescription("Open Logseq Quick Add")
|
||||
|
||||
/// 🔑 ensure the app is launched when this intent runs
|
||||
static var openAppWhenRun: Bool = true
|
||||
|
||||
func perform() async throws -> some IntentResult & OpensIntent {
|
||||
let url = URL(string: "https://logseq.com/mobile/go/quick-add")!
|
||||
return .result(opensIntent: OpenURLIntent(url))
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 18.0, *)
|
||||
struct RecordAudioIntent: AppIntent {
|
||||
static var title: LocalizedStringResource = "Record Audio"
|
||||
static var description = IntentDescription("Open Logseq Record Audio")
|
||||
|
||||
static var openAppWhenRun: Bool = true
|
||||
|
||||
func perform() async throws -> some IntentResult & OpensIntent {
|
||||
let url = URL(string: "https://logseq.com/mobile/go/audio")!
|
||||
return .result(opensIntent: OpenURLIntent(url))
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 18.0, *)
|
||||
struct LogseqShortcuts: AppShortcutsProvider {
|
||||
static var appShortcuts: [AppShortcut] {
|
||||
return [
|
||||
AppShortcut(
|
||||
intent: QuickAddIntent(),
|
||||
phrases: [
|
||||
"Quick add in \(.applicationName)",
|
||||
"Add note in \(.applicationName)"
|
||||
],
|
||||
shortTitle: "Quick Add",
|
||||
systemImageName: "plus.circle"
|
||||
),
|
||||
|
||||
AppShortcut(
|
||||
intent: RecordAudioIntent(),
|
||||
phrases: [
|
||||
"Record audio in \(.applicationName)",
|
||||
"Start recording in \(.applicationName)"
|
||||
],
|
||||
shortTitle: "Record Audio",
|
||||
systemImageName: "waveform"
|
||||
)
|
||||
]
|
||||
}
|
||||
}
|
||||
263
ios/App/App/NativeBottomSheetPlugin.swift
Normal file
@@ -0,0 +1,263 @@
|
||||
import Capacitor
|
||||
import UIKit
|
||||
|
||||
@objc(NativeBottomSheetPlugin)
|
||||
public class NativeBottomSheetPlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
public let identifier = "NativeBottomSheetPlugin"
|
||||
public let jsName = "NativeBottomSheetPlugin"
|
||||
public let pluginMethods: [CAPPluginMethod] = [
|
||||
CAPPluginMethod(name: "present", returnType: CAPPluginReturnPromise),
|
||||
CAPPluginMethod(name: "dismiss", returnType: CAPPluginReturnPromise),
|
||||
CAPPluginMethod(name: "contentReady", returnType: CAPPluginReturnPromise)
|
||||
]
|
||||
|
||||
private weak var backgroundSnapshotView: UIView?
|
||||
private weak var previousParent: UIViewController?
|
||||
private var sheetController: NativeBottomSheetViewController?
|
||||
private var awaitingContentReady = false
|
||||
private var dismissalFallbackWorkItem: DispatchWorkItem?
|
||||
|
||||
/// Single source of truth: when true, the bottom sheet owns the shared webview.
|
||||
public static var isPresentingSheet: Bool = false
|
||||
|
||||
// MARK: - Public API
|
||||
|
||||
@objc func present(_ call: CAPPluginCall) {
|
||||
|
||||
DispatchQueue.main.async {
|
||||
// If a sheet is already visible, just resolve.
|
||||
if self.sheetController != nil {
|
||||
call.resolve()
|
||||
return
|
||||
}
|
||||
|
||||
guard let host = self.bridge?.viewController?.parent else {
|
||||
call.reject("Unable to locate host view controller")
|
||||
return
|
||||
}
|
||||
|
||||
let config = NativeBottomSheetConfiguration(
|
||||
defaultHeight: self.height(from: call, key: "defaultHeight"),
|
||||
allowFullHeight: call.getBool("allowFullHeight") ?? true
|
||||
)
|
||||
|
||||
let controller = NativeBottomSheetViewController(configuration: config)
|
||||
controller.onDismiss = { [weak self] in
|
||||
self?.handleSheetDismissed()
|
||||
}
|
||||
|
||||
self.previousParent = host
|
||||
|
||||
// Optional: snapshot for a nice frozen background (does not affect ownership)
|
||||
_ = self.showSnapshot(in: host)
|
||||
|
||||
// MARK: sheet takes ownership of the shared webview
|
||||
NativeBottomSheetPlugin.isPresentingSheet = true
|
||||
|
||||
SharedWebViewController.instance.attach(
|
||||
to: controller,
|
||||
// IMPORTANT: do not leave a live webview behind the sheet
|
||||
leavePlaceholderInPreviousParent: false
|
||||
)
|
||||
|
||||
self.notifyListeners("state", data: ["presenting": true])
|
||||
|
||||
host.present(controller, animated: true) {
|
||||
self.notifyListeners("state", data: ["presented": true])
|
||||
call.resolve()
|
||||
}
|
||||
|
||||
self.sheetController = controller
|
||||
}
|
||||
}
|
||||
|
||||
@objc func dismiss(_ call: CAPPluginCall) {
|
||||
DispatchQueue.main.async {
|
||||
guard let controller = self.sheetController else {
|
||||
call.resolve()
|
||||
return
|
||||
}
|
||||
controller.dismiss(animated: true) {
|
||||
// handleSheetDismissed will run via viewDidDisappear / delegate
|
||||
call.resolve()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc func contentReady(_ call: CAPPluginCall) {
|
||||
DispatchQueue.main.async {
|
||||
guard self.awaitingContentReady else {
|
||||
call.resolve()
|
||||
return
|
||||
}
|
||||
|
||||
self.finishDismissal()
|
||||
call.resolve()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Internal helpers
|
||||
|
||||
private func handleSheetDismissed() {
|
||||
guard sheetController != nil else { return }
|
||||
|
||||
let shared = SharedWebViewController.instance
|
||||
let previous = self.previousParent
|
||||
|
||||
DispatchQueue.main.async {
|
||||
guard let previous = previous else { return }
|
||||
|
||||
// We keep the snapshot/placeholder visible.
|
||||
// Hide the real webview container so its sheet content never flashes.
|
||||
if let webView = shared.bridgeController.bridge?.webView {
|
||||
webView.alpha = 0
|
||||
|
||||
// Attach immediately so JS updates run in the correct context
|
||||
shared.attach(to: previous)
|
||||
|
||||
// Sheet is logically gone now
|
||||
NativeBottomSheetPlugin.isPresentingSheet = false
|
||||
self.sheetController = nil
|
||||
self.previousParent = nil
|
||||
|
||||
self.awaitingContentReady = true
|
||||
|
||||
// JS listens to this to clear the popup. The snapshot stays up
|
||||
// until JS confirms the non-popup UI has rendered.
|
||||
self.notifyListeners("state", data: ["dismissing": true])
|
||||
self.scheduleDismissalFallback()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func finishDismissal() {
|
||||
dismissalFallbackWorkItem?.cancel()
|
||||
dismissalFallbackWorkItem = nil
|
||||
awaitingContentReady = false
|
||||
|
||||
if let webView = SharedWebViewController.instance.bridgeController.bridge?.webView {
|
||||
webView.alpha = 1
|
||||
}
|
||||
|
||||
clearSnapshot()
|
||||
SharedWebViewController.instance.clearPlaceholder()
|
||||
|
||||
notifyListeners("state", data: [
|
||||
"presented": false,
|
||||
"dismissing": false
|
||||
])
|
||||
}
|
||||
|
||||
private func scheduleDismissalFallback() {
|
||||
dismissalFallbackWorkItem?.cancel()
|
||||
|
||||
let workItem = DispatchWorkItem { [weak self] in
|
||||
guard let self, self.awaitingContentReady else { return }
|
||||
self.finishDismissal()
|
||||
}
|
||||
|
||||
dismissalFallbackWorkItem = workItem
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: workItem)
|
||||
}
|
||||
|
||||
private func showSnapshot(in host: UIViewController) -> Bool {
|
||||
clearSnapshot()
|
||||
|
||||
// Make sure layout is up-to-date (important after tab switches)
|
||||
host.view.layoutIfNeeded()
|
||||
|
||||
guard let snapshot = SharedWebViewController.instance.makeSnapshotView() else {
|
||||
return false
|
||||
}
|
||||
snapshot.frame = host.view.bounds
|
||||
snapshot.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
host.view.addSubview(snapshot)
|
||||
host.view.bringSubviewToFront(snapshot)
|
||||
backgroundSnapshotView = snapshot
|
||||
return true
|
||||
}
|
||||
|
||||
private func clearSnapshot() {
|
||||
backgroundSnapshotView?.removeFromSuperview()
|
||||
backgroundSnapshotView = nil
|
||||
}
|
||||
|
||||
private func height(from call: CAPPluginCall, key: String) -> CGFloat? {
|
||||
guard let value = call.getValue(key) else { return nil }
|
||||
if let number = value as? NSNumber {
|
||||
return CGFloat(truncating: number)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - View controller + configuration
|
||||
|
||||
private struct NativeBottomSheetConfiguration {
|
||||
let defaultHeight: CGFloat?
|
||||
let allowFullHeight: Bool
|
||||
}
|
||||
|
||||
private class NativeBottomSheetViewController: UIViewController, UISheetPresentationControllerDelegate {
|
||||
let configuration: NativeBottomSheetConfiguration
|
||||
var onDismiss: (() -> Void)?
|
||||
private var didNotifyDismiss = false
|
||||
|
||||
init(configuration: NativeBottomSheetConfiguration) {
|
||||
self.configuration = configuration
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
modalPresentationStyle = .pageSheet
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
view.backgroundColor = .logseqBackground
|
||||
configureSheet()
|
||||
}
|
||||
|
||||
override func viewDidDisappear(_ animated: Bool) {
|
||||
super.viewDidDisappear(animated)
|
||||
if isBeingDismissed || presentingViewController == nil {
|
||||
notifyDismissed()
|
||||
}
|
||||
}
|
||||
|
||||
func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
|
||||
notifyDismissed()
|
||||
}
|
||||
|
||||
private func configureSheet() {
|
||||
guard let sheet = sheetPresentationController else { return }
|
||||
sheet.delegate = self
|
||||
sheet.prefersGrabberVisible = true
|
||||
sheet.preferredCornerRadius = 18
|
||||
sheet.prefersScrollingExpandsWhenScrolledToEdge = true
|
||||
sheet.largestUndimmedDetentIdentifier = configuration.allowFullHeight ? .large : nil
|
||||
|
||||
if let height = configuration.defaultHeight {
|
||||
configureCustomDetent(sheet: sheet, height: height)
|
||||
} else {
|
||||
sheet.detents = [.medium(), .large()]
|
||||
sheet.selectedDetentIdentifier = .medium
|
||||
}
|
||||
}
|
||||
|
||||
private func configureCustomDetent(sheet: UISheetPresentationController, height: CGFloat) {
|
||||
let identifier = UISheetPresentationController.Detent.Identifier("logseq.custom")
|
||||
let custom = UISheetPresentationController.Detent.custom(identifier: identifier) { _ in
|
||||
height
|
||||
}
|
||||
sheet.detents = configuration.allowFullHeight ? [custom, .large()] : [custom]
|
||||
sheet.selectedDetentIdentifier = identifier
|
||||
}
|
||||
|
||||
private func notifyDismissed() {
|
||||
guard !didNotifyDismiss else { return }
|
||||
didNotifyDismiss = true
|
||||
onDismiss?()
|
||||
}
|
||||
}
|
||||
474
ios/App/App/NativeEditorToolbarPlugin.swift
Normal file
@@ -0,0 +1,474 @@
|
||||
import Capacitor
|
||||
import UIKit
|
||||
|
||||
private struct NativeEditorAction {
|
||||
let id: String
|
||||
let title: String
|
||||
let systemIcon: String?
|
||||
|
||||
init?(jsObject: JSObject) {
|
||||
guard let id = jsObject["id"] as? String else { return nil }
|
||||
self.id = id
|
||||
self.title = (jsObject["title"] as? String) ?? id
|
||||
self.systemIcon = jsObject["systemIcon"] as? String
|
||||
}
|
||||
}
|
||||
|
||||
private class NativeEditorToolbarView: UIView {
|
||||
/// Callback when any action is tapped.
|
||||
var onActionTapped: ((String) -> Void)?
|
||||
|
||||
/// Used to prevent an old dismiss animation from removing a newly-presented bar.
|
||||
private var dismissGeneration: Int = 0
|
||||
|
||||
/// Store actions so we can reconfigure when theme (light/dark) changes.
|
||||
private var storedActions: [NativeEditorAction] = []
|
||||
private var storedTrailingAction: NativeEditorAction?
|
||||
|
||||
private let blurView: UIVisualEffectView = {
|
||||
let effect = UIBlurEffect(style: .systemChromeMaterial)
|
||||
let view = UIVisualEffectView(effect: effect)
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.layer.cornerRadius = 18
|
||||
view.clipsToBounds = true
|
||||
view.isUserInteractionEnabled = true
|
||||
return view
|
||||
}()
|
||||
|
||||
private let rootStack: UIStackView = {
|
||||
let stack = UIStackView()
|
||||
stack.axis = .horizontal
|
||||
stack.alignment = .center
|
||||
stack.spacing = 6
|
||||
stack.isLayoutMarginsRelativeArrangement = true
|
||||
stack.layoutMargins = UIEdgeInsets(top: 7, left: 10, bottom: 7, right: 10)
|
||||
stack.translatesAutoresizingMaskIntoConstraints = false
|
||||
return stack
|
||||
}()
|
||||
|
||||
private let actionsScrollView: UIScrollView = {
|
||||
let view = UIScrollView()
|
||||
view.showsHorizontalScrollIndicator = false
|
||||
view.showsVerticalScrollIndicator = false
|
||||
view.alwaysBounceHorizontal = true
|
||||
view.contentInsetAdjustmentBehavior = .never
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
return view
|
||||
}()
|
||||
|
||||
private let actionsStack: UIStackView = {
|
||||
let stack = UIStackView()
|
||||
stack.axis = .horizontal
|
||||
stack.alignment = .center
|
||||
stack.spacing = 4
|
||||
stack.translatesAutoresizingMaskIntoConstraints = false
|
||||
stack.isLayoutMarginsRelativeArrangement = true
|
||||
stack.layoutMargins = UIEdgeInsets(top: 0, left: 2, bottom: 0, right: 2)
|
||||
return stack
|
||||
}()
|
||||
|
||||
private let trailingContainer: UIStackView = {
|
||||
let stack = UIStackView()
|
||||
stack.axis = .horizontal
|
||||
stack.alignment = .center
|
||||
stack.spacing = 6
|
||||
stack.translatesAutoresizingMaskIntoConstraints = false
|
||||
stack.isUserInteractionEnabled = true
|
||||
return stack
|
||||
}()
|
||||
|
||||
private let separator: UIView = {
|
||||
let view = UIView()
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.backgroundColor = UIColor.label.withAlphaComponent(0.08)
|
||||
view.widthAnchor.constraint(equalToConstant: 1 / UIScreen.main.scale).isActive = true
|
||||
view.heightAnchor.constraint(greaterThanOrEqualToConstant: 20).isActive = true
|
||||
return view
|
||||
}()
|
||||
|
||||
private let trailingButton: UIButton = {
|
||||
let button = UIButton(type: .system)
|
||||
button.translatesAutoresizingMaskIntoConstraints = false
|
||||
button.widthAnchor.constraint(greaterThanOrEqualToConstant: 30).isActive = true
|
||||
button.heightAnchor.constraint(greaterThanOrEqualToConstant: 30).isActive = true
|
||||
return button
|
||||
}()
|
||||
|
||||
private var trailingActionId: String?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
setupView()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
setupView()
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
func present(on host: UIView,
|
||||
actions: [NativeEditorAction],
|
||||
trailingAction: NativeEditorAction?,
|
||||
tintColor: UIColor?,
|
||||
backgroundColor: UIColor?) {
|
||||
// Bump generation to invalidate any previous dismiss completion
|
||||
dismissGeneration += 1
|
||||
layer.removeAllAnimations()
|
||||
|
||||
// Store actions so we can re-apply them when theme changes
|
||||
storedActions = actions
|
||||
storedTrailingAction = trailingAction
|
||||
|
||||
// We ignore tintColor/backgroundColor – they’re driven by theme.
|
||||
configure(actions: actions,
|
||||
trailingAction: trailingAction)
|
||||
attachIfNeeded(to: host)
|
||||
animateIn()
|
||||
}
|
||||
|
||||
func dismiss(animated: Bool = true) {
|
||||
dismissGeneration += 1
|
||||
let currentGen = dismissGeneration
|
||||
|
||||
layer.removeAllAnimations()
|
||||
|
||||
guard animated else {
|
||||
removeFromSuperview()
|
||||
transform = .identity
|
||||
alpha = 1
|
||||
return
|
||||
}
|
||||
|
||||
UIView.animate(withDuration: 0.16,
|
||||
delay: 0,
|
||||
options: [.curveEaseIn, .allowUserInteraction],
|
||||
animations: {
|
||||
self.alpha = 0
|
||||
self.transform = CGAffineTransform(translationX: 0, y: 8)
|
||||
}, completion: { _ in
|
||||
// Only remove if no newer present/dismiss has happened.
|
||||
if currentGen == self.dismissGeneration {
|
||||
self.removeFromSuperview()
|
||||
self.transform = .identity
|
||||
self.alpha = 1
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// MARK: - Theme / trait changes
|
||||
|
||||
/// Returns the theme-appropriate tint (light: black, dark: white).
|
||||
private func currentTintColor() -> UIColor {
|
||||
return traitCollection.userInterfaceStyle == .dark ? .white : .black
|
||||
}
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
|
||||
guard previousTraitCollection?.userInterfaceStyle != traitCollection.userInterfaceStyle else {
|
||||
return
|
||||
}
|
||||
|
||||
// Reconfigure with new tint when light/dark changes
|
||||
configure(actions: storedActions, trailingAction: storedTrailingAction)
|
||||
}
|
||||
|
||||
// MARK: - Private helpers
|
||||
|
||||
private func setupView() {
|
||||
backgroundColor = .clear
|
||||
isOpaque = false
|
||||
|
||||
layer.cornerRadius = 18
|
||||
layer.masksToBounds = false
|
||||
layer.shadowColor = UIColor.black.withAlphaComponent(0.25).cgColor
|
||||
layer.shadowOpacity = 0.25
|
||||
layer.shadowOffset = CGSize(width: 0, height: 8)
|
||||
layer.shadowRadius = 22
|
||||
|
||||
layer.borderColor = UIColor.label.withAlphaComponent(0.04).cgColor
|
||||
layer.borderWidth = 0.5
|
||||
|
||||
addSubview(blurView)
|
||||
NSLayoutConstraint.activate([
|
||||
blurView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
blurView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
blurView.topAnchor.constraint(equalTo: topAnchor),
|
||||
blurView.bottomAnchor.constraint(equalTo: bottomAnchor)
|
||||
])
|
||||
|
||||
blurView.contentView.addSubview(rootStack)
|
||||
NSLayoutConstraint.activate([
|
||||
rootStack.leadingAnchor.constraint(equalTo: blurView.contentView.leadingAnchor),
|
||||
rootStack.trailingAnchor.constraint(equalTo: blurView.contentView.trailingAnchor),
|
||||
rootStack.topAnchor.constraint(equalTo: blurView.contentView.topAnchor),
|
||||
rootStack.bottomAnchor.constraint(equalTo: blurView.contentView.bottomAnchor)
|
||||
])
|
||||
|
||||
actionsScrollView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||
actionsScrollView.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
||||
rootStack.addArrangedSubview(actionsScrollView)
|
||||
|
||||
trailingContainer.setContentHuggingPriority(.required, for: .horizontal)
|
||||
trailingContainer.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||
rootStack.addArrangedSubview(trailingContainer)
|
||||
|
||||
actionsScrollView.addSubview(actionsStack)
|
||||
NSLayoutConstraint.activate([
|
||||
actionsStack.leadingAnchor.constraint(equalTo: actionsScrollView.contentLayoutGuide.leadingAnchor),
|
||||
actionsStack.trailingAnchor.constraint(equalTo: actionsScrollView.contentLayoutGuide.trailingAnchor),
|
||||
actionsStack.topAnchor.constraint(equalTo: actionsScrollView.contentLayoutGuide.topAnchor),
|
||||
actionsStack.bottomAnchor.constraint(equalTo: actionsScrollView.contentLayoutGuide.bottomAnchor),
|
||||
actionsStack.heightAnchor.constraint(equalTo: actionsScrollView.frameLayoutGuide.heightAnchor)
|
||||
])
|
||||
|
||||
trailingContainer.isHidden = true
|
||||
trailingContainer.addArrangedSubview(separator)
|
||||
trailingContainer.addArrangedSubview(trailingButton)
|
||||
|
||||
trailingButton.addTarget(self, action: #selector(handleTrailingTap(_:)), for: .touchUpInside)
|
||||
}
|
||||
|
||||
private func configure(actions: [NativeEditorAction],
|
||||
trailingAction: NativeEditorAction?) {
|
||||
let tint = currentTintColor()
|
||||
// Always use Logseq background (theme-aware)
|
||||
let bgBase = UIColor.logseqBackground
|
||||
|
||||
blurView.backgroundColor = bgBase.withAlphaComponent(0.9)
|
||||
blurView.contentView.backgroundColor = .clear
|
||||
|
||||
// Main actions
|
||||
actionsStack.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
||||
actions.forEach { action in
|
||||
let button = makeButton(for: action, tintColor: tint)
|
||||
actionsStack.addArrangedSubview(button)
|
||||
}
|
||||
|
||||
// Trailing action (keyboard hide or audio)
|
||||
if let trailingAction {
|
||||
trailingContainer.isHidden = false
|
||||
trailingActionId = trailingAction.id
|
||||
separator.isHidden = false
|
||||
|
||||
var config = UIButton.Configuration.plain()
|
||||
config.baseForegroundColor = tint
|
||||
config.contentInsets = NSDirectionalEdgeInsets(top: 4, leading: 4, bottom: 4, trailing: 4)
|
||||
config.preferredSymbolConfigurationForImage =
|
||||
UIImage.SymbolConfiguration(pointSize: 17, weight: .regular)
|
||||
config.background = .clear()
|
||||
|
||||
let trailingSymbol = trailingAction.systemIcon ?? "keyboard.chevron.compact.down"
|
||||
config.image = UIImage(systemName: trailingSymbol) ?? UIImage(systemName: "keyboard.chevron.compact.down")
|
||||
|
||||
trailingButton.configuration = config
|
||||
trailingButton.tintColor = tint
|
||||
trailingButton.accessibilityIdentifier = trailingAction.id
|
||||
|
||||
trailingButton.configurationUpdateHandler = { button in
|
||||
var cfg = button.configuration
|
||||
if button.isHighlighted {
|
||||
cfg?.background.backgroundColor = tint.withAlphaComponent(0.18)
|
||||
} else {
|
||||
cfg?.background.backgroundColor = UIColor.clear
|
||||
}
|
||||
button.configuration = cfg
|
||||
}
|
||||
} else {
|
||||
trailingContainer.isHidden = true
|
||||
trailingActionId = nil
|
||||
separator.isHidden = true
|
||||
trailingButton.configuration = nil
|
||||
trailingButton.configurationUpdateHandler = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func attachIfNeeded(to host: UIView) {
|
||||
if superview !== host {
|
||||
removeFromSuperview()
|
||||
host.addSubview(self)
|
||||
host.bringSubviewToFront(self)
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
let leading = leadingAnchor.constraint(equalTo: host.leadingAnchor, constant: 16)
|
||||
let trailing = trailingAnchor.constraint(equalTo: host.trailingAnchor, constant: -16)
|
||||
let bottom: NSLayoutConstraint
|
||||
if #available(iOS 15.0, *) {
|
||||
bottom = bottomAnchor.constraint(equalTo: host.keyboardLayoutGuide.topAnchor, constant: -10)
|
||||
} else {
|
||||
bottom = bottomAnchor.constraint(equalTo: host.safeAreaLayoutGuide.bottomAnchor, constant: -16)
|
||||
}
|
||||
|
||||
NSLayoutConstraint.activate([leading, trailing, bottom])
|
||||
}
|
||||
}
|
||||
|
||||
private func animateIn() {
|
||||
alpha = 0
|
||||
transform = CGAffineTransform(translationX: 0, y: 10)
|
||||
UIView.animate(withDuration: 0.24,
|
||||
delay: 0,
|
||||
usingSpringWithDamping: 0.86,
|
||||
initialSpringVelocity: 0.4,
|
||||
options: [.curveEaseOut, .allowUserInteraction],
|
||||
animations: {
|
||||
self.alpha = 1
|
||||
self.transform = .identity
|
||||
}, completion: nil)
|
||||
}
|
||||
|
||||
private func makeButton(for action: NativeEditorAction, tintColor: UIColor) -> UIButton {
|
||||
var config = UIButton.Configuration.plain()
|
||||
config.baseForegroundColor = tintColor
|
||||
config.title = nil
|
||||
config.contentInsets = NSDirectionalEdgeInsets(top: 4, leading: 6, bottom: 4, trailing: 6)
|
||||
config.preferredSymbolConfigurationForImage =
|
||||
UIImage.SymbolConfiguration(pointSize: 17, weight: .regular)
|
||||
config.background = .clear()
|
||||
|
||||
let button = UIButton(configuration: config, primaryAction: nil)
|
||||
button.tintColor = tintColor
|
||||
|
||||
let symbolName = action.systemIcon ?? "circle"
|
||||
|
||||
// Try custom SF Symbol as systemName first, then fall back to asset by name.
|
||||
let image =
|
||||
UIImage(systemName: symbolName) ??
|
||||
UIImage(named: symbolName) ??
|
||||
UIImage(systemName: "circle")
|
||||
|
||||
button.setImage(image, for: .normal)
|
||||
|
||||
button.accessibilityIdentifier = action.id
|
||||
button.widthAnchor.constraint(greaterThanOrEqualToConstant: 30).isActive = true
|
||||
button.heightAnchor.constraint(greaterThanOrEqualToConstant: 30).isActive = true
|
||||
|
||||
button.configurationUpdateHandler = { btn in
|
||||
var cfg = btn.configuration
|
||||
if btn.isHighlighted {
|
||||
cfg?.background.backgroundColor = tintColor.withAlphaComponent(0.18)
|
||||
} else {
|
||||
cfg?.background.backgroundColor = .clear
|
||||
}
|
||||
btn.configuration = cfg
|
||||
}
|
||||
|
||||
button.addTarget(self, action: #selector(handleActionTap(_:)), for: .touchUpInside)
|
||||
return button
|
||||
}
|
||||
|
||||
@objc private func handleActionTap(_ sender: UIButton) {
|
||||
guard let id = sender.accessibilityIdentifier else { return }
|
||||
onActionTapped?(id)
|
||||
}
|
||||
|
||||
@objc private func handleTrailingTap(_ sender: UIButton) {
|
||||
guard let id = trailingActionId ?? sender.accessibilityIdentifier else { return }
|
||||
onActionTapped?(id)
|
||||
}
|
||||
}
|
||||
|
||||
@objc(NativeEditorToolbarPlugin)
|
||||
public class NativeEditorToolbarPlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
public let identifier = "NativeEditorToolbarPlugin"
|
||||
public let jsName = "NativeEditorToolbarPlugin"
|
||||
public let pluginMethods: [CAPPluginMethod] = [
|
||||
CAPPluginMethod(name: "present", returnType: CAPPluginReturnPromise),
|
||||
CAPPluginMethod(name: "dismiss", returnType: CAPPluginReturnPromise)
|
||||
]
|
||||
|
||||
private var toolbar: NativeEditorToolbarView?
|
||||
|
||||
@objc func present(_ call: CAPPluginCall) {
|
||||
let rawActions = call.getArray("actions", JSObject.self) ?? []
|
||||
let actions = rawActions.compactMap(NativeEditorAction.init(jsObject:))
|
||||
let trailingAction = call.getObject("trailingAction").flatMap(NativeEditorAction.init(jsObject:))
|
||||
|
||||
// We still read tintColor/backgroundColor for future flexibility,
|
||||
// but the toolbar currently ignores them and uses theme colors.
|
||||
_ = call.getString("tintColor")
|
||||
_ = call.getString("backgroundColor")
|
||||
|
||||
DispatchQueue.main.async {
|
||||
guard let host = self.hostView() else {
|
||||
call.reject("Host view not found")
|
||||
return
|
||||
}
|
||||
|
||||
// If there are no actions and no trailing action, dismiss and clear toolbar
|
||||
guard !actions.isEmpty || trailingAction != nil else {
|
||||
self.toolbar?.dismiss(animated: true)
|
||||
self.toolbar = nil
|
||||
call.resolve()
|
||||
return
|
||||
}
|
||||
|
||||
let bar = self.toolbar ?? NativeEditorToolbarView()
|
||||
bar.onActionTapped = { [weak self] id in
|
||||
guard let self = self else { return }
|
||||
self.notifyListeners("action", data: ["id": id])
|
||||
}
|
||||
|
||||
bar.present(on: host,
|
||||
actions: actions,
|
||||
trailingAction: trailingAction,
|
||||
tintColor: nil,
|
||||
backgroundColor: nil)
|
||||
|
||||
self.toolbar = bar
|
||||
call.resolve()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func dismiss(_ call: CAPPluginCall) {
|
||||
DispatchQueue.main.async {
|
||||
self.toolbar?.dismiss(animated: true)
|
||||
self.toolbar = nil
|
||||
call.resolve()
|
||||
}
|
||||
}
|
||||
|
||||
private func hostView() -> UIView? {
|
||||
if let parent = bridge?.viewController?.parent?.view {
|
||||
return parent
|
||||
}
|
||||
return bridge?.viewController?.view
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Color helper
|
||||
|
||||
private extension String {
|
||||
func toUIColor(defaultColor: UIColor) -> UIColor {
|
||||
var hexString = self.trimmingCharacters(in: .whitespacesAndNewlines).uppercased()
|
||||
if hexString.hasPrefix("#") {
|
||||
hexString.removeFirst()
|
||||
}
|
||||
|
||||
var rgbValue: UInt64 = 0
|
||||
guard Scanner(string: hexString).scanHexInt64(&rgbValue) else {
|
||||
return defaultColor
|
||||
}
|
||||
|
||||
switch hexString.count {
|
||||
case 6: // RRGGBB
|
||||
return UIColor(
|
||||
red: CGFloat((rgbValue & 0xFF0000) >> 16) / 255.0,
|
||||
green: CGFloat((rgbValue & 0x00FF00) >> 8) / 255.0,
|
||||
blue: CGFloat(rgbValue & 0x0000FF) / 255.0,
|
||||
alpha: 1.0
|
||||
)
|
||||
case 8: // RRGGBBAA
|
||||
return UIColor(
|
||||
red: CGFloat((rgbValue & 0xFF000000) >> 24) / 255.0,
|
||||
green: CGFloat((rgbValue & 0x00FF0000) >> 16) / 255.0,
|
||||
blue: CGFloat((rgbValue & 0x0000FF00) >> 8) / 255.0,
|
||||
alpha: CGFloat(rgbValue & 0x000000FF) / 255.0
|
||||
)
|
||||
default:
|
||||
return defaultColor
|
||||
}
|
||||
}
|
||||
}
|
||||
13
ios/App/App/NativeNavHost.swift
Normal file
@@ -0,0 +1,13 @@
|
||||
import SwiftUI
|
||||
|
||||
struct NativeNavHost: UIViewControllerRepresentable {
|
||||
let navController: UINavigationController
|
||||
|
||||
func makeUIViewController(context: Context) -> UINavigationController {
|
||||
navController
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: UINavigationController, context: Context) {
|
||||
// JS routing still drives pushes/pops; nothing to do here.
|
||||
}
|
||||
}
|
||||
60
ios/App/App/NativePageViewController.swift
Normal file
@@ -0,0 +1,60 @@
|
||||
//
|
||||
// NativePageViewController.swift
|
||||
// Logseq
|
||||
//
|
||||
// A lightweight UIViewController that hosts the shared WebView and
|
||||
// asks JS to render the desired path when shown.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class NativePageViewController: UIViewController, UIGestureRecognizerDelegate {
|
||||
let targetPath: String
|
||||
let push: Bool
|
||||
|
||||
init(path: String, push: Bool = true, title: String? = nil) {
|
||||
self.targetPath = path
|
||||
self.push = push
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
self.navigationItem.title = title
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
view.backgroundColor = .logseqBackground
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
navigationController?.interactivePopGestureRecognizer?.isEnabled = true
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
guard let nav = navigationController,
|
||||
let popGesture = nav.interactivePopGestureRecognizer else { return }
|
||||
|
||||
popGesture.delegate = self
|
||||
popGesture.isEnabled = nav.viewControllers.count > 1
|
||||
}
|
||||
|
||||
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
guard
|
||||
let nav = navigationController,
|
||||
gestureRecognizer === nav.interactivePopGestureRecognizer
|
||||
else { return true }
|
||||
|
||||
return nav.viewControllers.count > 1
|
||||
}
|
||||
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
|
||||
shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
guard let nav = navigationController else { return false }
|
||||
return gestureRecognizer === nav.interactivePopGestureRecognizer
|
||||
}
|
||||
}
|
||||
443
ios/App/App/NativeSelectionActionBarPlugin.swift
Normal file
@@ -0,0 +1,443 @@
|
||||
import Capacitor
|
||||
import UIKit
|
||||
|
||||
// MARK: - Model for a single selection action coming from JS
|
||||
|
||||
private struct NativeSelectionAction {
|
||||
let id: String
|
||||
let title: String
|
||||
let systemIcon: String?
|
||||
|
||||
init?(jsObject: JSObject) {
|
||||
guard
|
||||
let id = jsObject["id"] as? String,
|
||||
let title = jsObject["title"] as? String
|
||||
else { return nil }
|
||||
|
||||
self.id = id
|
||||
self.title = title
|
||||
self.systemIcon = jsObject["systemIcon"] as? String
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Native selection action bar UI
|
||||
|
||||
private class NativeSelectionActionBarView: UIView {
|
||||
/// Callback when an action is tapped. Sends the action id.
|
||||
var onActionTapped: ((String) -> Void)?
|
||||
|
||||
/// Blurred background container.
|
||||
private let blurView: UIVisualEffectView = {
|
||||
let view = UIVisualEffectView(effect: UIBlurEffect(style: .systemMaterial))
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.layer.cornerRadius = 16
|
||||
view.clipsToBounds = true
|
||||
view.isUserInteractionEnabled = true
|
||||
return view
|
||||
}()
|
||||
|
||||
/// Root horizontal stack that holds scrollable actions on the left and a fixed trailing action on the right.
|
||||
private let rootStack: UIStackView = {
|
||||
let stack = UIStackView()
|
||||
stack.axis = .horizontal
|
||||
stack.alignment = .center
|
||||
stack.spacing = 6
|
||||
stack.isLayoutMarginsRelativeArrangement = true
|
||||
stack.layoutMargins = UIEdgeInsets(top: 12, left: 12, bottom: 12, right: 12)
|
||||
stack.translatesAutoresizingMaskIntoConstraints = false
|
||||
stack.isUserInteractionEnabled = true
|
||||
return stack
|
||||
}()
|
||||
|
||||
/// Scroll view allowing the main actions to overflow horizontally.
|
||||
private let actionsScrollView: UIScrollView = {
|
||||
let view = UIScrollView()
|
||||
view.showsHorizontalScrollIndicator = false
|
||||
view.showsVerticalScrollIndicator = false
|
||||
view.alwaysBounceHorizontal = true
|
||||
view.contentInsetAdjustmentBehavior = .never
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
return view
|
||||
}()
|
||||
|
||||
/// Stack inside the scroll view for the leading actions.
|
||||
private let actionsStack: UIStackView = {
|
||||
let stack = UIStackView()
|
||||
stack.axis = .horizontal
|
||||
stack.alignment = .center
|
||||
stack.distribution = .fillEqually // equal widths for main actions
|
||||
stack.spacing = 4
|
||||
stack.translatesAutoresizingMaskIntoConstraints = false
|
||||
stack.isLayoutMarginsRelativeArrangement = true
|
||||
stack.layoutMargins = .zero
|
||||
return stack
|
||||
}()
|
||||
|
||||
/// Container for the fixed trailing action.
|
||||
private let trailingContainer: UIStackView = {
|
||||
let stack = UIStackView()
|
||||
stack.axis = .horizontal
|
||||
stack.alignment = .center
|
||||
stack.spacing = 6
|
||||
stack.translatesAutoresizingMaskIntoConstraints = false
|
||||
stack.isUserInteractionEnabled = true
|
||||
return stack
|
||||
}()
|
||||
|
||||
private let separator: UIView = {
|
||||
let view = UIView()
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.backgroundColor = UIColor.label.withAlphaComponent(0.1)
|
||||
view.widthAnchor.constraint(equalToConstant: 1 / UIScreen.main.scale).isActive = true
|
||||
view.heightAnchor.constraint(greaterThanOrEqualToConstant: 20).isActive = true
|
||||
return view
|
||||
}()
|
||||
|
||||
private var trailingButton: UIControl?
|
||||
private var actionsStackWidthConstraint: NSLayoutConstraint?
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
setupView()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
setupView()
|
||||
}
|
||||
|
||||
// MARK: - Public API
|
||||
|
||||
/// Present the bar on top of a host view with given actions and colors.
|
||||
func present(
|
||||
on host: UIView,
|
||||
actions: [NativeSelectionAction],
|
||||
tintColor: UIColor?,
|
||||
backgroundColor: UIColor?
|
||||
) {
|
||||
configure(actions: actions, tintColor: tintColor, backgroundColor: backgroundColor)
|
||||
attachIfNeeded(to: host)
|
||||
animateInIfNeeded()
|
||||
}
|
||||
|
||||
/// Dismiss with a small fade/transform animation.
|
||||
func dismiss() {
|
||||
UIView.animate(
|
||||
withDuration: 0.15,
|
||||
delay: 0,
|
||||
options: [.curveEaseIn],
|
||||
animations: {
|
||||
self.alpha = 0
|
||||
self.transform = CGAffineTransform(translationX: 0, y: 8)
|
||||
},
|
||||
completion: { _ in
|
||||
self.removeFromSuperview()
|
||||
self.transform = .identity
|
||||
self.alpha = 1
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Private helpers
|
||||
|
||||
/// Base visual setup: background, shadow, subview hierarchy and constraints.
|
||||
private func setupView() {
|
||||
backgroundColor = .clear
|
||||
isUserInteractionEnabled = true
|
||||
|
||||
layer.cornerRadius = 16
|
||||
layer.masksToBounds = false
|
||||
layer.shadowColor = UIColor.black.cgColor
|
||||
layer.shadowOpacity = 0.12
|
||||
layer.shadowOffset = CGSize(width: 0, height: 6)
|
||||
layer.shadowRadius = 14
|
||||
|
||||
addSubview(blurView)
|
||||
NSLayoutConstraint.activate([
|
||||
blurView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
blurView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
blurView.topAnchor.constraint(equalTo: topAnchor),
|
||||
blurView.bottomAnchor.constraint(equalTo: bottomAnchor)
|
||||
])
|
||||
|
||||
blurView.contentView.addSubview(rootStack)
|
||||
NSLayoutConstraint.activate([
|
||||
rootStack.leadingAnchor.constraint(equalTo: blurView.contentView.leadingAnchor),
|
||||
rootStack.trailingAnchor.constraint(equalTo: blurView.contentView.trailingAnchor),
|
||||
rootStack.topAnchor.constraint(equalTo: blurView.contentView.topAnchor),
|
||||
rootStack.bottomAnchor.constraint(equalTo: blurView.contentView.bottomAnchor)
|
||||
])
|
||||
|
||||
actionsScrollView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||
actionsScrollView.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
||||
rootStack.addArrangedSubview(actionsScrollView)
|
||||
|
||||
trailingContainer.setContentHuggingPriority(.required, for: .horizontal)
|
||||
trailingContainer.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||
rootStack.addArrangedSubview(trailingContainer)
|
||||
|
||||
actionsScrollView.addSubview(actionsStack)
|
||||
NSLayoutConstraint.activate([
|
||||
actionsStack.leadingAnchor.constraint(equalTo: actionsScrollView.contentLayoutGuide.leadingAnchor),
|
||||
actionsStack.trailingAnchor.constraint(equalTo: actionsScrollView.contentLayoutGuide.trailingAnchor),
|
||||
actionsStack.topAnchor.constraint(equalTo: actionsScrollView.contentLayoutGuide.topAnchor),
|
||||
actionsStack.bottomAnchor.constraint(equalTo: actionsScrollView.contentLayoutGuide.bottomAnchor),
|
||||
actionsStack.heightAnchor.constraint(equalTo: actionsScrollView.frameLayoutGuide.heightAnchor)
|
||||
])
|
||||
|
||||
actionsStackWidthConstraint = actionsStack.widthAnchor.constraint(
|
||||
greaterThanOrEqualTo: actionsScrollView.frameLayoutGuide.widthAnchor
|
||||
)
|
||||
actionsStackWidthConstraint?.priority = .defaultHigh
|
||||
actionsStackWidthConstraint?.isActive = true
|
||||
|
||||
trailingContainer.addArrangedSubview(separator)
|
||||
trailingContainer.isHidden = true
|
||||
}
|
||||
|
||||
/// Rebuilds the stack buttons for the current set of actions.
|
||||
private func configure(
|
||||
actions: [NativeSelectionAction],
|
||||
tintColor: UIColor?,
|
||||
backgroundColor: UIColor?
|
||||
) {
|
||||
let tint = tintColor ?? .label
|
||||
blurView.backgroundColor = backgroundColor ?? UIColor.logseqBackground.withAlphaComponent(0.94)
|
||||
blurView.contentView.backgroundColor = .clear
|
||||
|
||||
let mainActions = Array(actions.dropLast())
|
||||
let trailingAction = actions.last
|
||||
|
||||
// Clear existing main actions.
|
||||
actionsStack.arrangedSubviews.forEach { sub in
|
||||
actionsStack.removeArrangedSubview(sub)
|
||||
sub.removeFromSuperview()
|
||||
}
|
||||
actionsScrollView.isHidden = mainActions.isEmpty
|
||||
|
||||
// Rebuild main actions.
|
||||
mainActions.forEach { action in
|
||||
let button = makeButton(for: action, tintColor: tint)
|
||||
actionsStack.addArrangedSubview(button)
|
||||
}
|
||||
|
||||
configureTrailing(
|
||||
action: trailingAction,
|
||||
tintColor: tint,
|
||||
showSeparator: !mainActions.isEmpty
|
||||
)
|
||||
}
|
||||
|
||||
private func configureTrailing(
|
||||
action: NativeSelectionAction?,
|
||||
tintColor: UIColor,
|
||||
showSeparator: Bool
|
||||
) {
|
||||
if let existingButton = trailingButton {
|
||||
trailingContainer.removeArrangedSubview(existingButton)
|
||||
existingButton.removeFromSuperview()
|
||||
trailingButton = nil
|
||||
}
|
||||
|
||||
guard let action = action else {
|
||||
trailingContainer.isHidden = true
|
||||
separator.isHidden = true
|
||||
return
|
||||
}
|
||||
|
||||
trailingContainer.isHidden = false
|
||||
separator.isHidden = !showSeparator
|
||||
|
||||
let button = makeButton(for: action, tintColor: tintColor)
|
||||
button.setContentHuggingPriority(.required, for: .horizontal)
|
||||
trailingContainer.addArrangedSubview(button)
|
||||
trailingButton = button
|
||||
}
|
||||
|
||||
/// Attaches the bar to the given host view, pinned to the bottom with safe area.
|
||||
private func attachIfNeeded(to host: UIView) {
|
||||
guard superview !== host else { return }
|
||||
removeFromSuperview()
|
||||
|
||||
host.addSubview(self)
|
||||
host.bringSubviewToFront(self)
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
leadingAnchor.constraint(equalTo: host.leadingAnchor, constant: 12),
|
||||
trailingAnchor.constraint(equalTo: host.trailingAnchor, constant: -12),
|
||||
bottomAnchor.constraint(equalTo: host.safeAreaLayoutGuide.bottomAnchor, constant: -12)
|
||||
])
|
||||
}
|
||||
|
||||
/// Simple fade-in animation when the bar appears.
|
||||
private func animateInIfNeeded() {
|
||||
guard alpha == 1 else { return }
|
||||
|
||||
alpha = 0
|
||||
transform = CGAffineTransform(translationX: 0, y: 8)
|
||||
UIView.animate(
|
||||
withDuration: 0.2,
|
||||
delay: 0,
|
||||
options: [.curveEaseOut, .allowUserInteraction],
|
||||
animations: {
|
||||
self.alpha = 1
|
||||
self.transform = .identity
|
||||
},
|
||||
completion: nil
|
||||
)
|
||||
}
|
||||
|
||||
/// Creates a single button for an action (icon + label in a vertical stack).
|
||||
private func makeButton(for action: NativeSelectionAction, tintColor: UIColor) -> UIControl {
|
||||
let control = UIControl()
|
||||
control.accessibilityIdentifier = action.id
|
||||
control.translatesAutoresizingMaskIntoConstraints = false
|
||||
control.isUserInteractionEnabled = true
|
||||
|
||||
let iconView = UIImageView()
|
||||
iconView.contentMode = .scaleAspectFit
|
||||
iconView.tintColor = tintColor
|
||||
iconView.image = UIImage(systemName: action.systemIcon ?? "circle")
|
||||
iconView.translatesAutoresizingMaskIntoConstraints = false
|
||||
iconView.heightAnchor.constraint(equalToConstant: 22).isActive = true
|
||||
iconView.widthAnchor.constraint(equalToConstant: 22).isActive = true
|
||||
|
||||
let label = UILabel()
|
||||
label.text = action.title
|
||||
label.textAlignment = .center
|
||||
label.font = UIFont.systemFont(ofSize: 12, weight: .semibold)
|
||||
label.textColor = tintColor
|
||||
label.numberOfLines = 1
|
||||
|
||||
let column = UIStackView(arrangedSubviews: [iconView, label])
|
||||
column.axis = .vertical
|
||||
column.alignment = .center
|
||||
column.spacing = 6
|
||||
column.translatesAutoresizingMaskIntoConstraints = false
|
||||
column.isUserInteractionEnabled = false
|
||||
|
||||
control.addSubview(column)
|
||||
NSLayoutConstraint.activate([
|
||||
column.leadingAnchor.constraint(equalTo: control.leadingAnchor),
|
||||
column.trailingAnchor.constraint(equalTo: control.trailingAnchor),
|
||||
column.topAnchor.constraint(equalTo: control.topAnchor, constant: 4),
|
||||
column.bottomAnchor.constraint(equalTo: control.bottomAnchor, constant: -4)
|
||||
])
|
||||
|
||||
control.addTarget(self, action: #selector(handleTap(_:)), for: .touchUpInside)
|
||||
|
||||
return control
|
||||
}
|
||||
|
||||
// MARK: - Touch handling
|
||||
|
||||
@objc private func handleTap(_ sender: UIControl) {
|
||||
guard let id = sender.accessibilityIdentifier else { return }
|
||||
onActionTapped?(id)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Capacitor plugin
|
||||
|
||||
@objc(NativeSelectionActionBarPlugin)
|
||||
public class NativeSelectionActionBarPlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
public let identifier = "NativeSelectionActionBarPlugin"
|
||||
public let jsName = "NativeSelectionActionBarPlugin"
|
||||
public let pluginMethods: [CAPPluginMethod] = [
|
||||
CAPPluginMethod(name: "present", returnType: CAPPluginReturnPromise),
|
||||
CAPPluginMethod(name: "dismiss", returnType: CAPPluginReturnPromise)
|
||||
]
|
||||
|
||||
private var actionBar: NativeSelectionActionBarView?
|
||||
|
||||
@objc func present(_ call: CAPPluginCall) {
|
||||
let rawActions = call.getArray("actions", JSObject.self) ?? []
|
||||
let actions = rawActions.compactMap(NativeSelectionAction.init(jsObject:))
|
||||
let tintColor = call.getString("tintColor")?.toUIColor(defaultColor: .label)
|
||||
let backgroundColor = call.getString("backgroundColor")?.toUIColor(
|
||||
defaultColor: UIColor.logseqBackground.withAlphaComponent(0.94)
|
||||
)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
guard let host = self.hostView() else {
|
||||
call.reject("Host view not found")
|
||||
return
|
||||
}
|
||||
|
||||
guard !actions.isEmpty else {
|
||||
self.actionBar?.dismiss()
|
||||
self.actionBar = nil
|
||||
call.resolve()
|
||||
return
|
||||
}
|
||||
|
||||
let bar = self.actionBar ?? NativeSelectionActionBarView()
|
||||
bar.onActionTapped = { [weak self] id in
|
||||
print("action id", id)
|
||||
self?.notifyListeners("action", data: ["id": id])
|
||||
}
|
||||
bar.present(
|
||||
on: host,
|
||||
actions: actions,
|
||||
tintColor: tintColor,
|
||||
backgroundColor: backgroundColor
|
||||
)
|
||||
self.actionBar = bar
|
||||
|
||||
call.resolve()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func dismiss(_ call: CAPPluginCall) {
|
||||
DispatchQueue.main.async {
|
||||
self.actionBar?.dismiss()
|
||||
self.actionBar = nil
|
||||
call.resolve()
|
||||
}
|
||||
}
|
||||
|
||||
private func hostView() -> UIView? {
|
||||
if let parent = bridge?.viewController?.parent?.view {
|
||||
return parent
|
||||
}
|
||||
return bridge?.viewController?.view
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private extension String {
|
||||
func toUIColor(defaultColor: UIColor) -> UIColor {
|
||||
var hexString = self.trimmingCharacters(in: .whitespacesAndNewlines).uppercased()
|
||||
if hexString.hasPrefix("#") {
|
||||
hexString.removeFirst()
|
||||
}
|
||||
|
||||
var rgbValue: UInt64 = 0
|
||||
guard Scanner(string: hexString).scanHexInt64(&rgbValue) else {
|
||||
return defaultColor
|
||||
}
|
||||
|
||||
switch hexString.count {
|
||||
case 6:
|
||||
return UIColor(
|
||||
red: CGFloat((rgbValue & 0xFF0000) >> 16) / 255.0,
|
||||
green: CGFloat((rgbValue & 0x00FF00) >> 8) / 255.0,
|
||||
blue: CGFloat(rgbValue & 0x0000FF) / 255.0,
|
||||
alpha: 1.0
|
||||
)
|
||||
case 8:
|
||||
return UIColor(
|
||||
red: CGFloat((rgbValue & 0xFF000000) >> 24) / 255.0,
|
||||
green: CGFloat((rgbValue & 0x00FF0000) >> 16) / 255.0,
|
||||
blue: CGFloat((rgbValue & 0x0000FF00) >> 8) / 255.0,
|
||||
alpha: CGFloat(rgbValue & 0x000000FF) / 255.0
|
||||
)
|
||||
default:
|
||||
return defaultColor
|
||||
}
|
||||
}
|
||||
}
|
||||
241
ios/App/App/NativeTopBarPlugin.swift
Normal file
@@ -0,0 +1,241 @@
|
||||
import Capacitor
|
||||
import UIKit
|
||||
|
||||
@objc(NativeTopBarPlugin)
|
||||
public class NativeTopBarPlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
public let identifier = "NativeTopBarPlugin"
|
||||
public let jsName = "NativeTopBarPlugin"
|
||||
public let pluginMethods: [CAPPluginMethod] = [
|
||||
CAPPluginMethod(name: "configure", returnType: CAPPluginReturnPromise)
|
||||
]
|
||||
|
||||
private class NativeTopBarButton: UIButton {
|
||||
var buttonId: String = ""
|
||||
override var intrinsicContentSize: CGSize {
|
||||
// Keep a consistent tap target; icon size is controlled via SF Symbol configuration
|
||||
CGSize(width: 36, height: 32)
|
||||
}
|
||||
}
|
||||
|
||||
private func navigationController() -> UINavigationController? {
|
||||
if let nav = bridge?.viewController?.parent?.navigationController {
|
||||
return nav
|
||||
}
|
||||
if let nav = bridge?.viewController?.navigationController {
|
||||
return nav
|
||||
}
|
||||
if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
|
||||
return appDelegate.navController
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MARK: - UITabBar lookup
|
||||
|
||||
/// Recursively search for a UITabBar in the given view hierarchy.
|
||||
private func findTabBar(in view: UIView) -> UITabBar? {
|
||||
if let tabBar = view as? UITabBar {
|
||||
return tabBar
|
||||
}
|
||||
for subview in view.subviews {
|
||||
if let found = findTabBar(in: subview) {
|
||||
return found
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MARK: - Public API
|
||||
|
||||
@objc func configure(_ call: CAPPluginCall) {
|
||||
let title = call.getString("title")
|
||||
let leftButtons = call.getArray("leftButtons", JSObject.self) ?? []
|
||||
let rightButtons = call.getArray("rightButtons", JSObject.self) ?? []
|
||||
let backgroundColorHex = call.getString("backgroundColor")
|
||||
let tintColorHex = call.getString("tintColor")
|
||||
let hidden = call.getBool("hidden") ?? false
|
||||
let titleClickable = call.getBool("titleClickable") ?? false
|
||||
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
guard let nav = self.navigationController() else {
|
||||
call.reject("Navigation controller not found")
|
||||
return
|
||||
}
|
||||
|
||||
nav.setNavigationBarHidden(hidden, animated: false)
|
||||
guard !hidden else {
|
||||
call.resolve()
|
||||
return
|
||||
}
|
||||
|
||||
// Resolve dynamic colors from JS
|
||||
let resolvedBackgroundColor =
|
||||
backgroundColorHex?.toUIColor(defaultColor: .logseqBackground) ?? .logseqBackground
|
||||
let resolvedTintColor =
|
||||
tintColorHex?.toUIColor(defaultColor: .label) ?? .label
|
||||
|
||||
// --- NAVIGATION BAR (top) ---
|
||||
let appearance = UINavigationBarAppearance()
|
||||
appearance.configureWithTransparentBackground()
|
||||
appearance.shadowColor = .clear
|
||||
|
||||
appearance.titleTextAttributes = [
|
||||
.foregroundColor: resolvedTintColor
|
||||
]
|
||||
|
||||
nav.navigationBar.standardAppearance = appearance
|
||||
nav.navigationBar.scrollEdgeAppearance = appearance
|
||||
nav.navigationBar.compactAppearance = appearance
|
||||
nav.navigationBar.tintColor = resolvedTintColor
|
||||
|
||||
if let topVC = nav.topViewController {
|
||||
// Reset any previous custom title view
|
||||
topVC.navigationItem.titleView = nil
|
||||
|
||||
if titleClickable, let title {
|
||||
let button = NativeTopBarButton(type: .system)
|
||||
button.buttonId = "title"
|
||||
button.setTitle(title, for: .normal)
|
||||
button.titleLabel?.font = UIFont.systemFont(ofSize: 17, weight: .semibold)
|
||||
button.addTarget(self, action: #selector(self.titleTapped(_:)), for: .touchUpInside)
|
||||
|
||||
let baseColor = nav.navigationBar.tintColor ?? UIColor.label
|
||||
|
||||
// Use color with alpha instead of button.alpha
|
||||
button.setTitleColor(baseColor.withAlphaComponent(0.8), for: .normal)
|
||||
button.setTitleColor(baseColor.withAlphaComponent(1.0), for: .highlighted)
|
||||
button.setTitleColor(baseColor.withAlphaComponent(0.6), for: .disabled)
|
||||
|
||||
button.backgroundColor = .clear
|
||||
|
||||
topVC.navigationItem.titleView = button
|
||||
topVC.navigationItem.title = nil
|
||||
} else {
|
||||
let label = UILabel()
|
||||
label.text = title
|
||||
label.textAlignment = .center
|
||||
label.font = UIFont.systemFont(ofSize: 17, weight: .semibold)
|
||||
|
||||
let baseColor = nav.navigationBar.tintColor
|
||||
label.textColor = baseColor?.withAlphaComponent(0.8)
|
||||
|
||||
topVC.navigationItem.titleView = label
|
||||
topVC.navigationItem.title = nil
|
||||
|
||||
}
|
||||
|
||||
topVC.navigationItem.leftBarButtonItems = self.buildButtons(from: leftButtons)
|
||||
topVC.navigationItem.rightBarButtonItems = self.buildButtons(from: rightButtons)
|
||||
}
|
||||
|
||||
call.resolve()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Button building
|
||||
|
||||
private func buildButtons(from array: [JSObject]) -> [UIBarButtonItem] {
|
||||
return array.compactMap { obj in
|
||||
guard let id = obj["id"] as? String else { return nil }
|
||||
let systemIconName = (obj["systemIcon"] as? String) ?? "circle"
|
||||
|
||||
let button = NativeTopBarButton(type: .system)
|
||||
button.buttonId = id
|
||||
|
||||
// Size: small / medium / large -> SF Symbol pointSize
|
||||
let symbolConfig = symbolConfiguration(for: obj)
|
||||
if let image = UIImage(systemName: systemIconName, withConfiguration: symbolConfig) {
|
||||
button.setImage(image, for: .normal)
|
||||
}
|
||||
|
||||
// Per-button color: prefers "tintColor", then "color"
|
||||
button.tintColor = tintColor(for: obj)
|
||||
button.imageView?.contentMode = .scaleAspectFit
|
||||
button.addTarget(self, action: #selector(buttonTapped(_:)), for: .touchUpInside)
|
||||
|
||||
// Fixed tap target; icon itself is sized by SF Symbol config
|
||||
let container = UIView(frame: CGRect(x: 0, y: 0, width: 36, height: 32))
|
||||
button.frame = container.bounds
|
||||
button.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
container.addSubview(button)
|
||||
|
||||
let item = UIBarButtonItem(customView: container)
|
||||
|
||||
// iOS 26+ Liquid Glass: remove the pill/glass background
|
||||
if #available(iOS 26.0, *) {
|
||||
button.alpha = 0.8
|
||||
}
|
||||
|
||||
return item
|
||||
}
|
||||
}
|
||||
|
||||
private func tintColor(for obj: JSObject) -> UIColor {
|
||||
if let hex = (obj["tintColor"] as? String) ?? (obj["color"] as? String) {
|
||||
return hex.toUIColor(defaultColor: .label)
|
||||
}
|
||||
return .label
|
||||
}
|
||||
|
||||
private func symbolConfiguration(for obj: JSObject) -> UIImage.SymbolConfiguration {
|
||||
let sizeString = (obj["size"] as? String)?.lowercased()
|
||||
|
||||
let pointSize: CGFloat
|
||||
switch sizeString {
|
||||
case "small":
|
||||
pointSize = 8
|
||||
case "large":
|
||||
pointSize = 19
|
||||
default: // "medium" or nil
|
||||
pointSize = 15
|
||||
}
|
||||
|
||||
return UIImage.SymbolConfiguration(pointSize: pointSize, weight: .semibold)
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
@objc private func buttonTapped(_ sender: NativeTopBarButton) {
|
||||
notifyListeners("buttonTapped", data: ["id": sender.buttonId])
|
||||
}
|
||||
|
||||
@objc private func titleTapped(_ sender: NativeTopBarButton) {
|
||||
notifyListeners("buttonTapped", data: ["id": "title"])
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Color helpers
|
||||
|
||||
private extension String {
|
||||
func toUIColor(defaultColor: UIColor) -> UIColor {
|
||||
var hexString = self.trimmingCharacters(in: .whitespacesAndNewlines).uppercased()
|
||||
if hexString.hasPrefix("#") {
|
||||
hexString.removeFirst()
|
||||
}
|
||||
|
||||
var rgbValue: UInt64 = 0
|
||||
guard Scanner(string: hexString).scanHexInt64(&rgbValue) else {
|
||||
return defaultColor
|
||||
}
|
||||
|
||||
switch hexString.count {
|
||||
case 6: // RRGGBB
|
||||
return UIColor(
|
||||
red: CGFloat((rgbValue & 0xFF0000) >> 16) / 255.0,
|
||||
green: CGFloat((rgbValue & 0x00FF00) >> 8) / 255.0,
|
||||
blue: CGFloat(rgbValue & 0x0000FF) / 255.0,
|
||||
alpha: 1.0
|
||||
)
|
||||
case 8: // RRGGBBAA
|
||||
return UIColor(
|
||||
red: CGFloat((rgbValue & 0xFF000000) >> 24) / 255.0,
|
||||
green: CGFloat((rgbValue & 0x00FF0000) >> 16) / 255.0,
|
||||
blue: CGFloat((rgbValue & 0x0000FF00) >> 8) / 255.0,
|
||||
alpha: CGFloat(rgbValue & 0x000000FF) / 255.0
|
||||
)
|
||||
default:
|
||||
return defaultColor
|
||||
}
|
||||
}
|
||||
}
|
||||
142
ios/App/App/SceneDelegate.swift
Normal file
@@ -0,0 +1,142 @@
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
|
||||
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||
var window: UIWindow?
|
||||
|
||||
// If you still want a reference to nav:
|
||||
var navController: UINavigationController?
|
||||
|
||||
func scene(_ scene: UIScene,
|
||||
willConnectTo session: UISceneSession,
|
||||
options connectionOptions: UIScene.ConnectionOptions) {
|
||||
|
||||
guard let windowScene = scene as? UIWindowScene else { return }
|
||||
|
||||
// 1) Create your nav controller
|
||||
let nav = UINavigationController()
|
||||
nav.navigationBar.prefersLargeTitles = false
|
||||
|
||||
// hook the delegate on AppDelegate so all your existing
|
||||
// UINavigationControllerDelegate logic keeps working
|
||||
if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
|
||||
nav.delegate = appDelegate
|
||||
appDelegate.navController = nav
|
||||
}
|
||||
|
||||
let rootPath = "/"
|
||||
// Start with NO title so "Logseq" isn't shown during splash.
|
||||
let rootVC = NativePageViewController(
|
||||
path: rootPath,
|
||||
push: true,
|
||||
title: nil // or "" – plugin will set real title later
|
||||
)
|
||||
// Just to be extra safe:
|
||||
rootVC.navigationItem.title = ""
|
||||
|
||||
nav.setViewControllers([rootVC], animated: false)
|
||||
self.navController = nav
|
||||
|
||||
// 2) Wrap in SwiftUI root (LiquidTabsRootView)
|
||||
let rootView = LiquidTabsRootView(navController: nav)
|
||||
let hosting = UIHostingController(rootView: rootView)
|
||||
hosting.view.backgroundColor = UIColor.logseqBackground
|
||||
|
||||
let savedTheme = UserDefaults.standard.string(forKey: "logseqTheme") ?? "system"
|
||||
|
||||
let style: UIUserInterfaceStyle
|
||||
switch savedTheme {
|
||||
case "dark":
|
||||
style = .dark
|
||||
case "light":
|
||||
style = .light
|
||||
default:
|
||||
style = .unspecified
|
||||
}
|
||||
|
||||
// 3) Standard UIWindowScene setup
|
||||
let window = UIWindow(windowScene: windowScene)
|
||||
window.overrideUserInterfaceStyle = style
|
||||
window.rootViewController = hosting
|
||||
window.tintColor = UIColor.logseqTint
|
||||
self.window = window
|
||||
window.makeKeyAndVisible()
|
||||
|
||||
// 4) Start observing route changes (your existing logic)
|
||||
(UIApplication.shared.delegate as? AppDelegate)?.startRouteObservation()
|
||||
|
||||
handleInitialSceneOptions(connectionOptions)
|
||||
}
|
||||
|
||||
// Optional, but nice to have:
|
||||
func sceneDidDisconnect(_ scene: UIScene) { }
|
||||
func sceneDidBecomeActive(_ scene: UIScene) { }
|
||||
func sceneWillResignActive(_ scene: UIScene) { }
|
||||
func sceneWillEnterForeground(_ scene: UIScene) { }
|
||||
func sceneDidEnterBackground(_ scene: UIScene) { }
|
||||
|
||||
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
|
||||
forwardUserActivity(userActivity)
|
||||
}
|
||||
|
||||
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
|
||||
forwardURLContexts(URLContexts)
|
||||
}
|
||||
|
||||
func windowScene(_ windowScene: UIWindowScene,
|
||||
performActionFor shortcutItem: UIApplicationShortcutItem,
|
||||
completionHandler: @escaping (Bool) -> Void) {
|
||||
let handled = (UIApplication.shared.delegate as? AppDelegate)?
|
||||
.handleShortcutItem(shortcutItem) ?? false
|
||||
completionHandler(handled)
|
||||
}
|
||||
|
||||
private func handleInitialSceneOptions(_ options: UIScene.ConnectionOptions) {
|
||||
if let shortcut = options.shortcutItem {
|
||||
DispatchQueue.main.async {
|
||||
_ = (UIApplication.shared.delegate as? AppDelegate)?
|
||||
.handleShortcutItem(shortcut)
|
||||
}
|
||||
}
|
||||
|
||||
if let activity = options.userActivities.first {
|
||||
forwardUserActivity(activity)
|
||||
}
|
||||
|
||||
if !options.urlContexts.isEmpty {
|
||||
forwardURLContexts(options.urlContexts)
|
||||
}
|
||||
}
|
||||
|
||||
private func forwardUserActivity(_ userActivity: NSUserActivity) {
|
||||
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return }
|
||||
_ = appDelegate.application(
|
||||
UIApplication.shared,
|
||||
continue: userActivity,
|
||||
restorationHandler: { _ in }
|
||||
)
|
||||
}
|
||||
|
||||
private func forwardURLContexts(_ contexts: Set<UIOpenURLContext>) {
|
||||
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return }
|
||||
for context in contexts {
|
||||
let sceneOptions = context.options
|
||||
var appOptions: [UIApplication.OpenURLOptionsKey: Any] = [:]
|
||||
if let sourceApplication = sceneOptions.sourceApplication {
|
||||
appOptions[.sourceApplication] = sourceApplication
|
||||
}
|
||||
if let annotation = sceneOptions.annotation {
|
||||
appOptions[.annotation] = annotation
|
||||
}
|
||||
appOptions[.openInPlace] = sceneOptions.openInPlace
|
||||
if let eventAttribution = sceneOptions.eventAttribution {
|
||||
appOptions[.eventAttribution] = eventAttribution
|
||||
}
|
||||
_ = appDelegate.application(
|
||||
UIApplication.shared,
|
||||
open: context.url,
|
||||
options: appOptions
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
141
ios/App/App/SharedWebViewController.swift
Normal file
@@ -0,0 +1,141 @@
|
||||
//
|
||||
// SharedWebViewController.swift
|
||||
// Logseq
|
||||
//
|
||||
// Maintains a single shared AppViewController (Capacitor Bridge Host) and
|
||||
// re-attaches it to multiple UINavigationController-based pages.
|
||||
// This avoids reloading the WebView and enables native transitions.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Capacitor
|
||||
|
||||
@objc class SharedWebViewController: NSObject {
|
||||
static let instance = SharedWebViewController()
|
||||
|
||||
// The single shared CAPBridgeViewController (AppViewController)
|
||||
lazy var bridgeController: AppViewController = {
|
||||
let vc = AppViewController()
|
||||
|
||||
// Ensure the view is loaded so that vc.webView is created
|
||||
vc.loadViewIfNeeded()
|
||||
|
||||
return vc
|
||||
}()
|
||||
|
||||
private weak var currentParent: UIViewController?
|
||||
private weak var placeholderView: UIView?
|
||||
private var snapshots: [ObjectIdentifier: UIImage] = [:]
|
||||
|
||||
// MARK: - Attach / Detach
|
||||
|
||||
/// Attach the shared WebView host (`bridgeController`) to a new parent view controller.
|
||||
/// Optionally leave a snapshot placeholder in the previous parent for smoother transitions.
|
||||
func attach(to parent: UIViewController, leavePlaceholderInPreviousParent: Bool = false) {
|
||||
let vc = bridgeController
|
||||
guard currentParent !== parent else { return }
|
||||
|
||||
// 1) Snapshot current parent if requested
|
||||
if leavePlaceholderInPreviousParent,
|
||||
let previous = currentParent,
|
||||
placeholderView == nil,
|
||||
let snapshot = previous.view.snapshotView(afterScreenUpdates: true) {
|
||||
|
||||
snapshot.frame = previous.view.bounds
|
||||
snapshot.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
previous.view.addSubview(snapshot)
|
||||
placeholderView = snapshot
|
||||
}
|
||||
|
||||
// 2) Detach from previous parent
|
||||
if let previous = vc.parent {
|
||||
vc.willMove(toParent: nil)
|
||||
vc.view.removeFromSuperview()
|
||||
vc.removeFromParent()
|
||||
}
|
||||
|
||||
// 3) Attach to new parent
|
||||
currentParent = parent
|
||||
|
||||
parent.addChild(vc)
|
||||
|
||||
vc.view.frame = parent.view.bounds
|
||||
vc.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
|
||||
parent.view.addSubview(vc.view)
|
||||
vc.didMove(toParent: parent)
|
||||
}
|
||||
|
||||
/// Remove any placeholder snapshot left in the previous parent.
|
||||
func clearPlaceholder() {
|
||||
placeholderView?.removeFromSuperview()
|
||||
placeholderView = nil
|
||||
}
|
||||
|
||||
// MARK: - Live Snapshot View (for transitions)
|
||||
|
||||
/// Create a snapshot view of the current WebView for use during transitions.
|
||||
func makeSnapshotView() -> UIView? {
|
||||
let vc = bridgeController
|
||||
let bounds = vc.view.bounds
|
||||
guard bounds.width > 0, bounds.height > 0 else { return nil }
|
||||
|
||||
// Try UIView snapshot first
|
||||
if let snapshotView = vc.view.snapshotView(afterScreenUpdates: true) {
|
||||
snapshotView.frame = bounds
|
||||
snapshotView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
return snapshotView
|
||||
}
|
||||
|
||||
// Fallback: render manually as an image
|
||||
let format = UIGraphicsImageRendererFormat()
|
||||
format.scale = vc.view.window?.screen.scale ?? UIScreen.main.scale
|
||||
format.opaque = true // fully opaque with logseq background
|
||||
|
||||
let renderer = UIGraphicsImageRenderer(bounds: bounds, format: format)
|
||||
let image = renderer.image { ctx in
|
||||
UIColor.logseqBackground.setFill()
|
||||
ctx.fill(bounds)
|
||||
vc.view.drawHierarchy(in: bounds, afterScreenUpdates: true)
|
||||
}
|
||||
|
||||
let imageView = UIImageView(image: image)
|
||||
imageView.frame = bounds
|
||||
imageView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
imageView.contentMode = .scaleToFill
|
||||
imageView.backgroundColor = .logseqBackground
|
||||
|
||||
return imageView
|
||||
}
|
||||
|
||||
// MARK: - Cached Snapshots (per parent)
|
||||
|
||||
/// Store a static snapshot image for a given parent view controller.
|
||||
/// Used for caching between transitions.
|
||||
func storeSnapshot(for parent: UIViewController) {
|
||||
let vc = bridgeController
|
||||
guard currentParent === parent else { return }
|
||||
|
||||
let bounds = vc.view.bounds
|
||||
guard bounds.width > 0, bounds.height > 0 else { return }
|
||||
|
||||
let format = UIGraphicsImageRendererFormat()
|
||||
format.scale = vc.view.window?.screen.scale ?? UIScreen.main.scale
|
||||
format.opaque = true
|
||||
|
||||
let renderer = UIGraphicsImageRenderer(bounds: bounds, format: format)
|
||||
let image = renderer.image { ctx in
|
||||
UIColor.logseqBackground.setFill()
|
||||
ctx.fill(bounds)
|
||||
vc.view.drawHierarchy(in: bounds, afterScreenUpdates: true)
|
||||
}
|
||||
|
||||
snapshots[ObjectIdentifier(parent)] = image
|
||||
}
|
||||
|
||||
/// Retrieve a previously stored snapshot for a given parent.
|
||||
func snapshot(for parent: UIViewController) -> UIImage? {
|
||||
return snapshots[ObjectIdentifier(parent)]
|
||||
}
|
||||
}
|
||||
42
ios/App/App/Theme.swift
Normal file
@@ -0,0 +1,42 @@
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
|
||||
extension UIColor {
|
||||
// Base colors: never dynamic, never consult trait collections.
|
||||
static let logseqLight = UIColor(red: 0xfc/255, green: 0xfc/255, blue: 0xfc/255, alpha: 1)
|
||||
static let logseqDark = UIColor(red: 0x00/255, green: 0x2b/255, blue: 0x36/255, alpha: 1)
|
||||
|
||||
static let logseqTintLight = UIColor(
|
||||
red: 23/255,
|
||||
green: 129/255,
|
||||
blue: 225/255,
|
||||
alpha: 1.0
|
||||
)
|
||||
|
||||
static let logseqTintDark = UIColor(
|
||||
red: 245/255,
|
||||
green: 247/255,
|
||||
blue: 250/255,
|
||||
alpha: 1.0
|
||||
)
|
||||
|
||||
// Dynamic variants: **static let**, so the closure is created once,
|
||||
// and it *only* reads trait.userInterfaceStyle.
|
||||
static let logseqBackground: UIColor = {
|
||||
UIColor { trait in
|
||||
trait.userInterfaceStyle == .dark ? logseqDark : logseqLight
|
||||
}
|
||||
}()
|
||||
|
||||
static let logseqTint: UIColor = {
|
||||
UIColor { trait in
|
||||
trait.userInterfaceStyle == .dark ? logseqTintDark : logseqTintLight
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
extension Color {
|
||||
// SwiftUI uses the dynamic versions
|
||||
static let logseqBackground = Color(uiColor: .logseqBackground)
|
||||
static let logseqTint = Color(uiColor: .logseqTint)
|
||||
}
|
||||
672
ios/App/App/UILocalPlugin.swift
Normal file
@@ -0,0 +1,672 @@
|
||||
//
|
||||
// UILocal.swift
|
||||
// App
|
||||
//
|
||||
// Created by Charlie on 2025/5/29.
|
||||
//
|
||||
|
||||
import Capacitor
|
||||
import Foundation
|
||||
import Speech
|
||||
import NaturalLanguage
|
||||
import Drops
|
||||
|
||||
func isDarkMode() -> Bool {
|
||||
return UITraitCollection.current.userInterfaceStyle == .dark
|
||||
}
|
||||
|
||||
func isOnlyDayDifferentOrSame(date1: Foundation.Date, date2: Date) -> Bool {
|
||||
let calendar = Calendar.current
|
||||
let components1 = calendar.dateComponents([.year, .month, .day], from: date1)
|
||||
let components2 = calendar.dateComponents([.year, .month, .day], from: date2)
|
||||
|
||||
return components1.year == components2.year && components1.month == components2.month && (components1.day != components2.day || components1.day == components2.day)
|
||||
}
|
||||
|
||||
class DatePickerView: UIView {
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
isUserInteractionEnabled = true
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
isUserInteractionEnabled = true
|
||||
}
|
||||
|
||||
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
super.touchesBegan(touches, with: event)
|
||||
}
|
||||
}
|
||||
|
||||
class DatePickerDialogViewController: UIViewController {
|
||||
private let datePicker = UIDatePicker()
|
||||
private let dialogView = DatePickerView()
|
||||
|
||||
private var lastDate: Date?
|
||||
private var initialMonthLabel: UILabel?
|
||||
private var currentMonthText: String?
|
||||
private var didFinish = false
|
||||
|
||||
var onDateSelected: ((Date?) -> Void)?
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
lastDate = datePicker.date
|
||||
setupImplView()
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
|
||||
self?.settleMonthLabel()
|
||||
}
|
||||
}
|
||||
|
||||
private func settleMonthLabel() {
|
||||
initialMonthLabel = findMonthLabel(in: datePicker)
|
||||
if let label = initialMonthLabel {
|
||||
currentMonthText = label.text
|
||||
print("Initial month label: \(currentMonthText ?? "Unknown")")
|
||||
} else {
|
||||
print("Month label not found")
|
||||
}
|
||||
}
|
||||
|
||||
private func findMonthLabel(in view: UIView) -> UILabel? {
|
||||
for subview in view.subviews {
|
||||
if let label = subview as? UILabel, (label.text?.contains(" ")) == true {
|
||||
print(label.text as Any)
|
||||
return label
|
||||
}
|
||||
if let foundLabel = findMonthLabel(in: subview) {
|
||||
return foundLabel
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func inCalendarWheelPickerMode(in view: UIView) -> Bool? {
|
||||
for subview in view.subviews {
|
||||
if let label = subview as? UILabel, label.text?.contains("July") == true {
|
||||
print(label.text as Any)
|
||||
return true
|
||||
}
|
||||
|
||||
let found: Bool? = inCalendarWheelPickerMode(in: subview)
|
||||
|
||||
if found == true {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
private func finish(_ date: Date?) {
|
||||
guard !didFinish else { return }
|
||||
didFinish = true
|
||||
onDateSelected?(date)
|
||||
dismiss(animated: false, completion: nil)
|
||||
}
|
||||
|
||||
@objc private func confirmDate() {
|
||||
let label = findMonthLabel(in: datePicker)
|
||||
if isOnlyDayDifferentOrSame(date1: lastDate!, date2: datePicker.date) || (label != nil && label?.text != currentMonthText && (inCalendarWheelPickerMode(in: datePicker) != true)) {
|
||||
finish(datePicker.date)
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func dismissDialog() {
|
||||
finish(nil)
|
||||
}
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
|
||||
if traitCollection.userInterfaceStyle != previousTraitCollection?.userInterfaceStyle {
|
||||
dialogView.backgroundColor = .logseqBackground
|
||||
}
|
||||
}
|
||||
|
||||
func setupImplView() {
|
||||
datePicker.datePickerMode = .date
|
||||
datePicker.preferredDatePickerStyle = .inline
|
||||
datePicker.addTarget(
|
||||
self, action: #selector(confirmDate), for: .valueChanged)
|
||||
|
||||
// Create hosting view controller
|
||||
let view = self.view!
|
||||
|
||||
view.backgroundColor = .logseqBackground.withAlphaComponent(0.4)
|
||||
view.isUserInteractionEnabled = true
|
||||
|
||||
dialogView.backgroundColor = .logseqBackground
|
||||
|
||||
dialogView.layer.cornerRadius = 14
|
||||
dialogView.clipsToBounds = false // IMPORTANT: allow shadow
|
||||
|
||||
dialogView.layer.shadowColor = UIColor.black.cgColor
|
||||
dialogView.layer.shadowOpacity = isDarkMode() ? 0.4 : 0.15
|
||||
dialogView.layer.shadowOffset = CGSize(width: 0, height: 6)
|
||||
dialogView.layer.shadowRadius = 16
|
||||
dialogView.layer.masksToBounds = false
|
||||
|
||||
view.addSubview(dialogView)
|
||||
|
||||
dialogView.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
dialogView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
|
||||
dialogView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
|
||||
])
|
||||
|
||||
// Add sub views
|
||||
dialogView.addSubview(datePicker)
|
||||
|
||||
// Add date selector and toolbar to the view
|
||||
datePicker.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
datePicker.topAnchor.constraint(equalTo: dialogView.topAnchor),
|
||||
datePicker.bottomAnchor.constraint(equalTo: dialogView.bottomAnchor, constant: -8),
|
||||
datePicker.leadingAnchor.constraint(equalTo: dialogView.leadingAnchor, constant: 16),
|
||||
datePicker.trailingAnchor.constraint(equalTo: dialogView.trailingAnchor, constant: -16),
|
||||
])
|
||||
|
||||
datePicker.setContentHuggingPriority(.required, for: .horizontal)
|
||||
datePicker.setContentHuggingPriority(.required, for: .vertical)
|
||||
datePicker.setContentCompressionResistancePriority(
|
||||
.required, for: .horizontal)
|
||||
datePicker.setContentCompressionResistancePriority(
|
||||
.required, for: .vertical)
|
||||
}
|
||||
|
||||
|
||||
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
super.touchesBegan(touches, with: event)
|
||||
|
||||
if let touch = touches.first {
|
||||
let location = touch.location(in: view)
|
||||
if !dialogView.frame.contains(location) {
|
||||
finish(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc(UILocalPlugin)
|
||||
public class UILocalPlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
|
||||
public let identifier = "UILocalPlugin"
|
||||
public let jsName = "UILocal"
|
||||
public static let navigationNotification = Notification.Name("UILocalPluginNavigation")
|
||||
public static let routeChangeNotification = Notification.Name("UILocalPluginRouteChanged")
|
||||
private static var navigationReady = false
|
||||
private static var pendingRoutes: [[String: Any]] = []
|
||||
private static var pendingPaths: [(path: String, push: Bool)] = []
|
||||
private var navigationObserver: NSObjectProtocol?
|
||||
|
||||
private var call: CAPPluginCall?
|
||||
private var datepickerViewController: UIViewController?
|
||||
private var datepickerDialogView: UIView?
|
||||
|
||||
public let pluginMethods: [CAPPluginMethod] = [
|
||||
CAPPluginMethod(name: "showDatePicker", returnType: CAPPluginReturnPromise),
|
||||
CAPPluginMethod(name: "transcribeAudio2Text", returnType: CAPPluginReturnPromise),
|
||||
CAPPluginMethod(name: "routeDidChange", returnType: CAPPluginReturnPromise),
|
||||
CAPPluginMethod(name: "alert", returnType: CAPPluginReturnPromise),
|
||||
CAPPluginMethod(name: "hideAlert", returnType: CAPPluginReturnPromise)
|
||||
]
|
||||
|
||||
@objc func alert(_ call: CAPPluginCall) {
|
||||
guard let title = call.getString("title") ?? call.getString("message") else {
|
||||
call.reject("title is required")
|
||||
return
|
||||
}
|
||||
|
||||
let subtitle = call.getString("subtitle") ?? call.getString("description")
|
||||
let type = call.getString("type")?.lowercased()
|
||||
let iconName = call.getString("icon")
|
||||
let iconColorHex = call.getString("iconColor") ?? call.getString("tintColor")
|
||||
let position = (call.getString("position")?.lowercased() == "bottom") ? Drop.Position.bottom : Drop.Position.top
|
||||
let durationSeconds = call.getDouble("duration")
|
||||
let accessibilityMessage = call.getString("accessibility")
|
||||
|
||||
let drop = Drop(
|
||||
title: title,
|
||||
titleNumberOfLines: 0,
|
||||
subtitle: subtitle,
|
||||
subtitleNumberOfLines: 0,
|
||||
icon: buildIcon(type: type, iconName: iconName, hexColor: iconColorHex),
|
||||
action: nil,
|
||||
position: position,
|
||||
duration: durationSeconds.flatMap { Drop.Duration.seconds($0) } ?? .recommended,
|
||||
accessibility: accessibilityMessage.map { Drop.Accessibility(message: $0) }
|
||||
)
|
||||
|
||||
Drops.show(drop)
|
||||
call.resolve()
|
||||
}
|
||||
|
||||
@objc func hideAlert(_ call: CAPPluginCall) {
|
||||
Drops.hideAll()
|
||||
call.resolve()
|
||||
}
|
||||
|
||||
private func buildIcon(type: String?, iconName: String?, hexColor: String?) -> UIImage? {
|
||||
let tint = color(fromHex: hexColor) ?? color(for: type)
|
||||
|
||||
if let iconName, let image = UIImage(systemName: iconName) {
|
||||
guard let tint else { return image }
|
||||
return image.withTintColor(tint, renderingMode: .alwaysOriginal)
|
||||
}
|
||||
|
||||
guard let type else { return nil }
|
||||
|
||||
let symbolName: String
|
||||
switch type {
|
||||
case "success":
|
||||
symbolName = "checkmark.circle.fill"
|
||||
case "warning":
|
||||
symbolName = "exclamationmark.triangle.fill"
|
||||
case "error", "danger":
|
||||
symbolName = "xmark.octagon.fill"
|
||||
default:
|
||||
symbolName = "info.circle.fill"
|
||||
}
|
||||
|
||||
if let tint {
|
||||
return UIImage(systemName: symbolName)?
|
||||
.withTintColor(tint, renderingMode: .alwaysOriginal)
|
||||
}
|
||||
|
||||
return UIImage(systemName: symbolName)
|
||||
}
|
||||
|
||||
private func color(for type: String?) -> UIColor? {
|
||||
guard let type else { return nil }
|
||||
switch type {
|
||||
case "success":
|
||||
return .systemGreen
|
||||
case "warning":
|
||||
return .systemOrange
|
||||
case "error", "danger":
|
||||
return .systemRed
|
||||
default:
|
||||
return .systemBlue
|
||||
}
|
||||
}
|
||||
|
||||
private func color(fromHex hexString: String?) -> UIColor? {
|
||||
guard var hex = hexString?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
.uppercased(),
|
||||
!hex.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if hex.hasPrefix("#") {
|
||||
hex.removeFirst()
|
||||
}
|
||||
|
||||
var rgbValue: UInt64 = 0
|
||||
guard Scanner(string: hex).scanHexInt64(&rgbValue) else { return nil }
|
||||
|
||||
switch hex.count {
|
||||
case 6:
|
||||
return UIColor(
|
||||
red: CGFloat((rgbValue & 0xFF0000) >> 16) / 255.0,
|
||||
green: CGFloat((rgbValue & 0x00FF00) >> 8) / 255.0,
|
||||
blue: CGFloat(rgbValue & 0x0000FF) / 255.0,
|
||||
alpha: 1.0
|
||||
)
|
||||
case 8:
|
||||
return UIColor(
|
||||
red: CGFloat((rgbValue & 0xFF000000) >> 24) / 255.0,
|
||||
green: CGFloat((rgbValue & 0x00FF0000) >> 16) / 255.0,
|
||||
blue: CGFloat((rgbValue & 0x0000FF00) >> 8) / 255.0,
|
||||
alpha: CGFloat(rgbValue & 0x000000FF) / 255.0
|
||||
)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 26.0, *)
|
||||
func recognizeWithAutoLocale(from file: URL,
|
||||
completion: @escaping (String?, Error?) -> Void) {
|
||||
Task {
|
||||
do {
|
||||
// ---------- STEP 1: Gather candidate locales ----------
|
||||
let preferred = Array(Locale.preferredLanguages.prefix(3))
|
||||
var candidateIDs = preferred
|
||||
if !candidateIDs.contains(where: { $0.hasPrefix("en") }) {
|
||||
candidateIDs.append("en-US")
|
||||
}
|
||||
if !candidateIDs.contains(where: { $0.hasPrefix("zh") }) {
|
||||
candidateIDs.append("zh-CN")
|
||||
}
|
||||
|
||||
// ---------- STEP 2: Probe candidates in parallel ----------
|
||||
var results: [(Locale, String)] = []
|
||||
|
||||
await withTaskGroup(of: (Locale, String).self) { group in
|
||||
for id in candidateIDs {
|
||||
let candidate = Locale(identifier: id)
|
||||
if let supported = await SpeechTranscriber.supportedLocale(equivalentTo: candidate) {
|
||||
group.addTask {
|
||||
let text = (try? await self.quickSampleTranscription(file: file, locale: supported)) ?? ""
|
||||
return (supported, text)
|
||||
}
|
||||
}
|
||||
}
|
||||
for await (locale, text) in group {
|
||||
results.append((locale, text))
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- STEP 3: Score results ----------
|
||||
var bestLocale: Locale = Locale(identifier: "en-US")
|
||||
var bestScore = Int.min
|
||||
for (locale, text) in results {
|
||||
let score = scoreTranscript(text, locale: locale)
|
||||
print("📊 Candidate: \(locale.identifier), score: \(score), text: \(text)")
|
||||
if score > bestScore {
|
||||
bestScore = score
|
||||
bestLocale = locale
|
||||
}
|
||||
}
|
||||
|
||||
print("🎙 Running full transcription with locale: \(bestLocale.identifier)")
|
||||
|
||||
// ---------- STEP 4: Full transcription ----------
|
||||
let transcriber = SpeechTranscriber(locale: bestLocale, preset: .transcription)
|
||||
|
||||
if let req = try await AssetInventory.assetInstallationRequest(supporting: [transcriber]) {
|
||||
try await req.downloadAndInstall()
|
||||
print("✅ Model installed for \(bestLocale.identifier)")
|
||||
}
|
||||
|
||||
let collectFullTask = Task { () -> String in
|
||||
var full = ""
|
||||
do {
|
||||
for try await r in transcriber.results {
|
||||
full += String(r.text.characters) + " "
|
||||
}
|
||||
} catch {}
|
||||
return full
|
||||
}
|
||||
|
||||
let analyzer = SpeechAnalyzer(modules: [transcriber])
|
||||
let audio = try AVAudioFile(forReading: file)
|
||||
if let last = try await analyzer.analyzeSequence(from: audio) {
|
||||
try await analyzer.finalizeAndFinish(through: last)
|
||||
} else {
|
||||
try await analyzer.cancelAndFinishNow()
|
||||
}
|
||||
|
||||
let finalText = (await collectFullTask.value)
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
completion(finalText.isEmpty ? nil : finalText, nil)
|
||||
|
||||
} catch {
|
||||
completion(nil, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 26.0, *)
|
||||
private func quickSampleTranscription(file: URL, locale: Locale) async throws -> String {
|
||||
let transcriber = SpeechTranscriber(locale: locale, preset: .transcription)
|
||||
|
||||
// Install models if needed (you could cache this across runs)
|
||||
if let req = try await AssetInventory.assetInstallationRequest(supporting: [transcriber]) {
|
||||
try await req.downloadAndInstall()
|
||||
}
|
||||
|
||||
var sample = ""
|
||||
var count = 0
|
||||
let analyzer = SpeechAnalyzer(modules: [transcriber])
|
||||
|
||||
let collectTask = Task { () -> String in
|
||||
do {
|
||||
for try await r in transcriber.results {
|
||||
sample += String(r.text.characters) + " "
|
||||
count += 1
|
||||
if count >= 3 {
|
||||
// ✅ Early exit: stop once we have enough
|
||||
try? await analyzer.cancelAndFinishNow()
|
||||
break
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
return sample
|
||||
}
|
||||
|
||||
let audioFile = try AVAudioFile(forReading: file)
|
||||
if let last = try await analyzer.analyzeSequence(from: audioFile) {
|
||||
try await analyzer.finalizeAndFinish(through: last)
|
||||
} else {
|
||||
try await analyzer.cancelAndFinishNow()
|
||||
}
|
||||
|
||||
return await collectTask.value.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
}
|
||||
|
||||
private func scoreTranscript(_ text: String, locale: Locale) -> Int {
|
||||
// Normalize: keep only letters/digits/scripts (ignore punctuation)
|
||||
let normalized = text.unicodeScalars.filter {
|
||||
CharacterSet.letters.contains($0) ||
|
||||
CharacterSet.decimalDigits.contains($0) ||
|
||||
CharacterSet(charactersIn: "\u{4E00}"..."\u{9FFF}").contains($0) || // Han
|
||||
CharacterSet(charactersIn: "\u{3040}"..."\u{30FF}").contains($0) || // Kana
|
||||
CharacterSet(charactersIn: "\u{AC00}"..."\u{D7AF}").contains($0) || // Hangul
|
||||
CharacterSet(charactersIn: "\u{0400}"..."\u{04FF}").contains($0) || // Cyrillic
|
||||
CharacterSet(charactersIn: "\u{0600}"..."\u{06FF}").contains($0) || // Arabic
|
||||
CharacterSet(charactersIn: "\u{0590}"..."\u{05FF}").contains($0) || // Hebrew
|
||||
CharacterSet(charactersIn: "\u{0900}"..."\u{097F}").contains($0) // Devanagari
|
||||
}
|
||||
let coreText = String(String.UnicodeScalarView(normalized))
|
||||
var score = coreText.count
|
||||
|
||||
// Detect script presence
|
||||
let hasHan = coreText.range(of: #"\p{Han}"#, options: .regularExpression) != nil
|
||||
let hasKana = coreText.range(of: #"\u3040-\u30FF"#, options: .regularExpression) != nil
|
||||
let hasHangul = coreText.range(of: #"\uAC00-\uD7AF"#, options: .regularExpression) != nil
|
||||
let hasCyrillic = coreText.range(of: #"\u0400-\u04FF"#, options: .regularExpression) != nil
|
||||
let hasArabic = coreText.range(of: #"\u0600-\u06FF"#, options: .regularExpression) != nil
|
||||
let hasHebrew = coreText.range(of: #"\u0590-\u05FF"#, options: .regularExpression) != nil
|
||||
let hasDevanag = coreText.range(of: #"\u0900-\u097F"#, options: .regularExpression) != nil
|
||||
|
||||
// Latin ratio detection
|
||||
let latinLetters = coreText.filter { $0.isASCII && $0.isLetter }.count
|
||||
let latinRatio = coreText.isEmpty ? 0.0 : Double(latinLetters) / Double(coreText.count)
|
||||
|
||||
if latinRatio > 0.7 {
|
||||
if locale.identifier.hasPrefix("en") {
|
||||
score += 500
|
||||
} else if locale.identifier.hasPrefix("zh")
|
||||
|| locale.identifier.hasPrefix("ja")
|
||||
|| locale.identifier.hasPrefix("ko") {
|
||||
score -= 500
|
||||
}
|
||||
}
|
||||
|
||||
if hasHan { score += locale.identifier.hasPrefix("zh") || locale.identifier.hasPrefix("ja") ? 1000 : -500 }
|
||||
if hasKana { score += locale.identifier.hasPrefix("ja") ? 1000 : -500 }
|
||||
if hasHangul { score += locale.identifier.hasPrefix("ko") ? 1000 : -500 }
|
||||
if hasCyrillic { score += locale.identifier.hasPrefix("ru") ? 1000 : -500 }
|
||||
if hasArabic { score += locale.identifier.hasPrefix("ar") ? 1000 : -500 }
|
||||
if hasHebrew { score += locale.identifier.hasPrefix("he") ? 1000 : -500 }
|
||||
if hasDevanag { score += locale.identifier.hasPrefix("hi") ? 1000 : -500 }
|
||||
|
||||
// Bias toward user-preferred languages
|
||||
if Locale.preferredLanguages.contains(where: { locale.identifier.hasPrefix($0.prefix(2)) }) {
|
||||
score += 200
|
||||
}
|
||||
|
||||
return score
|
||||
}
|
||||
|
||||
|
||||
@available(iOS 26.0, *)
|
||||
@objc func transcribeAudio2Text(_ call: CAPPluginCall) {
|
||||
self.call = call
|
||||
|
||||
// audio arrayBuffer
|
||||
guard let audioArray = call.getArray("audioData", NSNumber.self) as? [UInt8] else {
|
||||
call.reject("invalid audioData")
|
||||
return
|
||||
}
|
||||
|
||||
let audioData = Data(audioArray)
|
||||
|
||||
let fileURL = FileManager.default.temporaryDirectory.appendingPathComponent("recordedAudio.m4a")
|
||||
|
||||
do {
|
||||
try audioData.write(to: fileURL)
|
||||
|
||||
let fileExists = FileManager.default.fileExists(atPath: fileURL.path)
|
||||
|
||||
print("file exists: \(fileExists), path: \(fileURL.path)")
|
||||
if !fileExists {
|
||||
call.reject("file save failed: file doesn't exist")
|
||||
return
|
||||
}
|
||||
|
||||
self.recognizeWithAutoLocale(from: fileURL) { result, error in
|
||||
if let result = result {
|
||||
call.resolve(["transcription": result])
|
||||
} else if let error = error {
|
||||
call.reject("failed to transcribe: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
call.reject("failed to transcribe: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
@objc func showDatePicker(_ call: CAPPluginCall) {
|
||||
self.call = call
|
||||
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
let viewController = DatePickerDialogViewController()
|
||||
|
||||
// Set view controller presentation
|
||||
viewController.modalPresentationStyle = .overFullScreen
|
||||
viewController.modalTransitionStyle = .crossDissolve
|
||||
viewController.isModalInPresentation = true // 禁止非按钮交互关闭
|
||||
|
||||
viewController.onDateSelected = self?.dateChanged
|
||||
|
||||
// Present View Controller
|
||||
guard let presentingViewController = self?.bridge?.viewController else {
|
||||
call.reject("Unable to present date picker")
|
||||
return
|
||||
}
|
||||
|
||||
presentingViewController.present(
|
||||
viewController, animated: false, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
private func dateChanged(_ date: Date?) {
|
||||
guard let call = self.call else { return }
|
||||
self.call = nil
|
||||
|
||||
if let date {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "yyyy-MM-dd"
|
||||
let dateString = formatter.string(from: date)
|
||||
let result: PluginCallResultData = ["value": dateString]
|
||||
call.resolve(result)
|
||||
} else {
|
||||
call.resolve([:])
|
||||
}
|
||||
}
|
||||
|
||||
override public func load() {
|
||||
super.load()
|
||||
print("🔅 UILocalPlugin loaded")
|
||||
navigationObserver = NotificationCenter.default.addObserver(
|
||||
forName: UILocalPlugin.navigationNotification,
|
||||
object: nil,
|
||||
queue: OperationQueue.main
|
||||
) { [weak self] notification in
|
||||
guard let strongSelf = self else { return }
|
||||
|
||||
if let route = notification.userInfo?["route"] as? [String: Any] {
|
||||
strongSelf.notifyListeners(
|
||||
"nativeNavigation",
|
||||
data: ["route": route]
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if let path = notification.userInfo?["path"] as? String {
|
||||
var data: [String: Any] = ["path": path]
|
||||
if let push = notification.userInfo?["push"] as? Bool {
|
||||
data["push"] = push
|
||||
}
|
||||
strongSelf.notifyListeners(
|
||||
"nativeNavigation",
|
||||
data: data
|
||||
)
|
||||
}
|
||||
}
|
||||
UILocalPlugin.navigationReady = true
|
||||
|
||||
// Flush any pending navigation requests that arrived before the plugin loaded.
|
||||
UILocalPlugin.pendingRoutes.forEach { route in
|
||||
NotificationCenter.default.post(
|
||||
name: UILocalPlugin.navigationNotification,
|
||||
object: nil,
|
||||
userInfo: ["route": route]
|
||||
)
|
||||
}
|
||||
UILocalPlugin.pendingRoutes.removeAll()
|
||||
|
||||
UILocalPlugin.pendingPaths.forEach { payload in
|
||||
NotificationCenter.default.post(
|
||||
name: UILocalPlugin.navigationNotification,
|
||||
object: nil,
|
||||
userInfo: ["path": payload.path, "push": payload.push]
|
||||
)
|
||||
}
|
||||
UILocalPlugin.pendingPaths.removeAll()
|
||||
}
|
||||
|
||||
deinit {
|
||||
if let navigationObserver {
|
||||
NotificationCenter.default.removeObserver(navigationObserver)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func routeDidChange(_ call: CAPPluginCall) {
|
||||
let navigationType = call.getString("navigationType") ?? "push"
|
||||
let push = call.getBool("push") ?? (navigationType == "push")
|
||||
let path = call.getString("path") ?? "/"
|
||||
|
||||
// ✅ read stack from JS, default to "home" only if missing
|
||||
let stack = call.getString("stack") ?? "home"
|
||||
|
||||
#if DEBUG
|
||||
print("📬 UILocal.routeDidChange call from JS")
|
||||
print(" navigationType=\(navigationType) push=\(push) stack=\(stack) path=\(path)")
|
||||
#endif
|
||||
|
||||
DispatchQueue.main.async {
|
||||
NotificationCenter.default.post(
|
||||
name: UILocalPlugin.routeChangeNotification,
|
||||
object: nil,
|
||||
userInfo: [
|
||||
"navigationType": navigationType,
|
||||
"push": push,
|
||||
"stack": stack, // 👈 forward it
|
||||
"path": path
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
call.resolve()
|
||||
}
|
||||
}
|
||||
15
ios/App/App/Utils.m
Normal file
@@ -0,0 +1,15 @@
|
||||
//
|
||||
// Utils.m
|
||||
// Logseq
|
||||
//
|
||||
// Created by leizhe on 2022/5/23.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <Capacitor/Capacitor.h>
|
||||
|
||||
CAP_PLUGIN(Utils, "Utils",
|
||||
CAP_PLUGIN_METHOD(isZoomed, CAPPluginReturnPromise);
|
||||
CAP_PLUGIN_METHOD(getDocumentRoot, CAPPluginReturnPromise);
|
||||
CAP_PLUGIN_METHOD(setInterfaceStyle, CAPPluginReturnPromise);
|
||||
)
|
||||
146
ios/App/App/Utils.swift
Normal file
@@ -0,0 +1,146 @@
|
||||
//
|
||||
// Utils.swift
|
||||
// Logseq
|
||||
//
|
||||
// Created by leizhe on 2022/5/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Capacitor
|
||||
import UIKit
|
||||
import WebKit
|
||||
|
||||
@objc(Utils)
|
||||
public class Utils: CAPPlugin {
|
||||
private var currentInterfaceStyle: UIUserInterfaceStyle = .unspecified
|
||||
private var contentSizeObserver: NSObjectProtocol?
|
||||
|
||||
public override func load() {
|
||||
super.load()
|
||||
|
||||
contentSizeObserver = NotificationCenter.default.addObserver(
|
||||
forName: UIContentSizeCategory.didChangeNotification,
|
||||
object: nil,
|
||||
queue: .main
|
||||
) { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
self.notifyListeners("contentSizeCategoryChanged", data: self.contentSizePayload())
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
if let observer = contentSizeObserver {
|
||||
NotificationCenter.default.removeObserver(observer)
|
||||
}
|
||||
}
|
||||
|
||||
private func contentSizePayload() -> [String: Any] {
|
||||
let category = UIApplication.shared.preferredContentSizeCategory
|
||||
let base: CGFloat = 17.0
|
||||
let scaled = UIFontMetrics(forTextStyle: .body).scaledValue(for: base)
|
||||
let scale = scaled / base
|
||||
|
||||
return [
|
||||
"category": category.rawValue,
|
||||
"scale": scale
|
||||
]
|
||||
}
|
||||
|
||||
@objc func isZoomed(_ call: CAPPluginCall) {
|
||||
|
||||
var isZoomed: Bool {
|
||||
UIScreen.main.scale < UIScreen.main.nativeScale
|
||||
}
|
||||
|
||||
call.resolve(["isZoomed": isZoomed])
|
||||
}
|
||||
|
||||
@objc func getContentSize(_ call: CAPPluginCall) {
|
||||
call.resolve(contentSizePayload())
|
||||
}
|
||||
|
||||
@objc func getDocumentRoot(_ call: CAPPluginCall) {
|
||||
let doc = FileManager.default.urls(
|
||||
for: .documentDirectory,
|
||||
in: .userDomainMask).first
|
||||
|
||||
if doc != nil {
|
||||
call.resolve(["documentRoot": doc!.path])
|
||||
} else {
|
||||
call.resolve(["documentRoot": ""])
|
||||
}
|
||||
}
|
||||
|
||||
@objc func setInterfaceStyle(_ call: CAPPluginCall) {
|
||||
let mode = call.getString("mode")?.lowercased() ?? "system"
|
||||
let followSystem = call.getBool("system") ?? (mode == "system")
|
||||
UserDefaults.standard.set(mode, forKey: "logseqTheme")
|
||||
UserDefaults.standard.synchronize()
|
||||
|
||||
let style: UIUserInterfaceStyle
|
||||
if followSystem {
|
||||
style = .unspecified
|
||||
} else {
|
||||
style = (mode == "dark") ? .dark : .light
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.applyInterfaceStyle(style)
|
||||
call.resolve()
|
||||
}
|
||||
}
|
||||
|
||||
private func applyInterfaceStyle(_ style: UIUserInterfaceStyle) {
|
||||
guard style != currentInterfaceStyle else { return }
|
||||
currentInterfaceStyle = style
|
||||
|
||||
let app = UIApplication.shared
|
||||
|
||||
let applyToWindow: (UIWindow) -> Void = { window in
|
||||
window.overrideUserInterfaceStyle = style
|
||||
window.rootViewController?.overrideUserInterfaceStyle = style
|
||||
}
|
||||
|
||||
// Propagate to all active windows (handles multi-scene).
|
||||
let targetScenes = app.connectedScenes
|
||||
.compactMap { $0 as? UIWindowScene }
|
||||
.filter { $0.activationState == .foregroundActive || $0.activationState == .foregroundInactive }
|
||||
|
||||
let windows = targetScenes.flatMap { $0.windows }
|
||||
if windows.isEmpty {
|
||||
app.windows.forEach(applyToWindow)
|
||||
} else {
|
||||
windows.forEach(applyToWindow)
|
||||
}
|
||||
|
||||
// Bridge VC + WKWebView
|
||||
bridge?.viewController?.overrideUserInterfaceStyle = style
|
||||
bridge?.webView?.overrideUserInterfaceStyle = style
|
||||
|
||||
// UINavigationController root (if available)
|
||||
if let nav = (app.delegate as? AppDelegate)?.navController {
|
||||
nav.overrideUserInterfaceStyle = style
|
||||
nav.viewControllers.forEach { $0.overrideUserInterfaceStyle = style }
|
||||
}
|
||||
}
|
||||
|
||||
public override func handleWKWebViewURLAuthenticationChallenge(
|
||||
_ challenge: URLAuthenticationChallenge,
|
||||
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
|
||||
) -> Bool {
|
||||
#if DEBUG
|
||||
guard let serverURL = bridge?.config.serverURL,
|
||||
serverURL.scheme == "https",
|
||||
challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
|
||||
challenge.protectionSpace.host == serverURL.host,
|
||||
let trust = challenge.protectionSpace.serverTrust else {
|
||||
return false
|
||||
}
|
||||
|
||||
completionHandler(.useCredential, URLCredential(trust: trust))
|
||||
return true
|
||||
#else
|
||||
return false
|
||||
#endif
|
||||
}
|
||||
}
|
||||
6
ios/App/App/config.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<widget version="1.0.0" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
|
||||
<access origin="*" />
|
||||
|
||||
|
||||
</widget>
|
||||
43
ios/App/Podfile
Normal file
@@ -0,0 +1,43 @@
|
||||
require_relative '../../node_modules/.pnpm/@capacitor+ios@8.3.1_@capacitor+core@8.3.1/node_modules/@capacitor/ios/scripts/pods_helpers'
|
||||
|
||||
platform :ios, '15.0'
|
||||
use_frameworks!
|
||||
|
||||
# workaround to avoid Xcode caching of Pods that requires
|
||||
# Product -> Clean Build Folder after new Cordova plugins installed
|
||||
# Requires CocoaPods 1.6 or newer
|
||||
install! 'cocoapods', :disable_input_output_paths => true
|
||||
|
||||
def capacitor_pods
|
||||
pod 'Capacitor', :path => '../../node_modules/.pnpm/@capacitor+ios@8.3.1_@capacitor+core@8.3.1/node_modules/@capacitor/ios'
|
||||
pod 'CapacitorCordova', :path => '../../node_modules/.pnpm/@capacitor+ios@8.3.1_@capacitor+core@8.3.1/node_modules/@capacitor/ios'
|
||||
pod 'AparajitaCapacitorSecureStorage', :path => '../../node_modules/.pnpm/@aparajita+capacitor-secure-storage@8.0.0/node_modules/@aparajita/capacitor-secure-storage'
|
||||
pod 'CapacitorCommunitySafeArea', :path => '../../node_modules/.pnpm/@capacitor-community+safe-area@8.0.1_@capacitor+core@8.3.1/node_modules/@capacitor-community/safe-area'
|
||||
pod 'CapacitorActionSheet', :path => '../../node_modules/.pnpm/@capacitor+action-sheet@8.1.1_@capacitor+core@8.3.1/node_modules/@capacitor/action-sheet'
|
||||
pod 'CapacitorApp', :path => '../../node_modules/.pnpm/@capacitor+app@8.1.0_@capacitor+core@8.3.1/node_modules/@capacitor/app'
|
||||
pod 'CapacitorCamera', :path => '../../node_modules/.pnpm/@capacitor+camera@8.2.0_@capacitor+core@8.3.1/node_modules/@capacitor/camera'
|
||||
pod 'CapacitorClipboard', :path => '../../node_modules/.pnpm/@capacitor+clipboard@8.0.1_@capacitor+core@8.3.1/node_modules/@capacitor/clipboard'
|
||||
pod 'CapacitorDevice', :path => '../../node_modules/.pnpm/@capacitor+device@8.0.2_@capacitor+core@8.3.1/node_modules/@capacitor/device'
|
||||
pod 'CapacitorDialog', :path => '../../node_modules/.pnpm/@capacitor+dialog@8.0.1_@capacitor+core@8.3.1/node_modules/@capacitor/dialog'
|
||||
pod 'CapacitorFilesystem', :path => '../../node_modules/.pnpm/@capacitor+filesystem@8.1.2_@capacitor+core@8.3.1/node_modules/@capacitor/filesystem'
|
||||
pod 'CapacitorHaptics', :path => '../../node_modules/.pnpm/@capacitor+haptics@8.0.2_@capacitor+core@8.3.1/node_modules/@capacitor/haptics'
|
||||
pod 'CapacitorKeyboard', :path => '../../node_modules/.pnpm/@capacitor+keyboard@8.0.3_@capacitor+core@8.3.1/node_modules/@capacitor/keyboard'
|
||||
pod 'CapacitorNetwork', :path => '../../node_modules/.pnpm/@capacitor+network@8.0.1_@capacitor+core@8.3.1/node_modules/@capacitor/network'
|
||||
pod 'CapacitorShare', :path => '../../node_modules/.pnpm/@capacitor+share@8.0.1_@capacitor+core@8.3.1/node_modules/@capacitor/share'
|
||||
pod 'CapacitorSplashScreen', :path => '../../node_modules/.pnpm/@capacitor+splash-screen@8.0.1_@capacitor+core@8.3.1/node_modules/@capacitor/splash-screen'
|
||||
pod 'CapacitorStatusBar', :path => '../../node_modules/.pnpm/@capacitor+status-bar@8.0.2_@capacitor+core@8.3.1/node_modules/@capacitor/status-bar'
|
||||
pod 'CapgoCapacitorNavigationBar', :path => '../../node_modules/.pnpm/@capgo+capacitor-navigation-bar@8.1.5_@capacitor+core@8.3.1/node_modules/@capgo/capacitor-navigation-bar'
|
||||
pod 'SendIntent', :path => '../../node_modules/.pnpm/send-intent@7.0.0_@capacitor+core@8.3.1/node_modules/send-intent'
|
||||
pod 'JcesarmobileSslSkip', :path => '../../node_modules/.pnpm/@jcesarmobile+ssl-skip@0.9.0/node_modules/@jcesarmobile/ssl-skip'
|
||||
end
|
||||
|
||||
target 'Logseq' do
|
||||
capacitor_pods
|
||||
# Add your Pods here
|
||||
pod 'Drops', :git => 'https://github.com/omaralbeik/Drops.git', :tag => '1.7.0'
|
||||
end
|
||||
|
||||
|
||||
post_install do |installer|
|
||||
assertDeploymentTarget(installer)
|
||||
end
|
||||
156
ios/App/Podfile.lock
Normal file
@@ -0,0 +1,156 @@
|
||||
PODS:
|
||||
- AparajitaCapacitorSecureStorage (8.0.0):
|
||||
- Capacitor
|
||||
- KeychainSwift (~> 21.0)
|
||||
- Capacitor (8.3.1):
|
||||
- CapacitorCordova
|
||||
- CapacitorActionSheet (8.1.1):
|
||||
- Capacitor
|
||||
- CapacitorApp (8.1.0):
|
||||
- Capacitor
|
||||
- CapacitorCamera (8.2.0):
|
||||
- Capacitor
|
||||
- IONCameraLib (~> 1.0.4)
|
||||
- CapacitorClipboard (8.0.1):
|
||||
- Capacitor
|
||||
- CapacitorCommunitySafeArea (8.0.1):
|
||||
- Capacitor
|
||||
- CapacitorCordova (8.3.1)
|
||||
- CapacitorDevice (8.0.2):
|
||||
- Capacitor
|
||||
- CapacitorDialog (8.0.1):
|
||||
- Capacitor
|
||||
- CapacitorFilesystem (8.1.2):
|
||||
- Capacitor
|
||||
- IONFilesystemLib (~> 1.1.1)
|
||||
- CapacitorHaptics (8.0.2):
|
||||
- Capacitor
|
||||
- CapacitorKeyboard (8.0.3):
|
||||
- Capacitor
|
||||
- CapacitorNetwork (8.0.1):
|
||||
- Capacitor
|
||||
- CapacitorShare (8.0.1):
|
||||
- Capacitor
|
||||
- CapacitorSplashScreen (8.0.1):
|
||||
- Capacitor
|
||||
- CapacitorStatusBar (8.0.2):
|
||||
- Capacitor
|
||||
- CapgoCapacitorNavigationBar (8.1.5):
|
||||
- Capacitor
|
||||
- Drops (1.7.0)
|
||||
- IONCameraLib (1.0.4)
|
||||
- IONFilesystemLib (1.1.2)
|
||||
- JcesarmobileSslSkip (0.9.0):
|
||||
- Capacitor
|
||||
- KeychainSwift (21.0.0)
|
||||
- SendIntent (7.0.0):
|
||||
- Capacitor
|
||||
|
||||
DEPENDENCIES:
|
||||
- "AparajitaCapacitorSecureStorage (from `../../node_modules/.pnpm/@aparajita+capacitor-secure-storage@8.0.0/node_modules/@aparajita/capacitor-secure-storage`)"
|
||||
- "Capacitor (from `../../node_modules/.pnpm/@capacitor+ios@8.3.1_@capacitor+core@8.3.1/node_modules/@capacitor/ios`)"
|
||||
- "CapacitorActionSheet (from `../../node_modules/.pnpm/@capacitor+action-sheet@8.1.1_@capacitor+core@8.3.1/node_modules/@capacitor/action-sheet`)"
|
||||
- "CapacitorApp (from `../../node_modules/.pnpm/@capacitor+app@8.1.0_@capacitor+core@8.3.1/node_modules/@capacitor/app`)"
|
||||
- "CapacitorCamera (from `../../node_modules/.pnpm/@capacitor+camera@8.2.0_@capacitor+core@8.3.1/node_modules/@capacitor/camera`)"
|
||||
- "CapacitorClipboard (from `../../node_modules/.pnpm/@capacitor+clipboard@8.0.1_@capacitor+core@8.3.1/node_modules/@capacitor/clipboard`)"
|
||||
- "CapacitorCommunitySafeArea (from `../../node_modules/.pnpm/@capacitor-community+safe-area@8.0.1_@capacitor+core@8.3.1/node_modules/@capacitor-community/safe-area`)"
|
||||
- "CapacitorCordova (from `../../node_modules/.pnpm/@capacitor+ios@8.3.1_@capacitor+core@8.3.1/node_modules/@capacitor/ios`)"
|
||||
- "CapacitorDevice (from `../../node_modules/.pnpm/@capacitor+device@8.0.2_@capacitor+core@8.3.1/node_modules/@capacitor/device`)"
|
||||
- "CapacitorDialog (from `../../node_modules/.pnpm/@capacitor+dialog@8.0.1_@capacitor+core@8.3.1/node_modules/@capacitor/dialog`)"
|
||||
- "CapacitorFilesystem (from `../../node_modules/.pnpm/@capacitor+filesystem@8.1.2_@capacitor+core@8.3.1/node_modules/@capacitor/filesystem`)"
|
||||
- "CapacitorHaptics (from `../../node_modules/.pnpm/@capacitor+haptics@8.0.2_@capacitor+core@8.3.1/node_modules/@capacitor/haptics`)"
|
||||
- "CapacitorKeyboard (from `../../node_modules/.pnpm/@capacitor+keyboard@8.0.3_@capacitor+core@8.3.1/node_modules/@capacitor/keyboard`)"
|
||||
- "CapacitorNetwork (from `../../node_modules/.pnpm/@capacitor+network@8.0.1_@capacitor+core@8.3.1/node_modules/@capacitor/network`)"
|
||||
- "CapacitorShare (from `../../node_modules/.pnpm/@capacitor+share@8.0.1_@capacitor+core@8.3.1/node_modules/@capacitor/share`)"
|
||||
- "CapacitorSplashScreen (from `../../node_modules/.pnpm/@capacitor+splash-screen@8.0.1_@capacitor+core@8.3.1/node_modules/@capacitor/splash-screen`)"
|
||||
- "CapacitorStatusBar (from `../../node_modules/.pnpm/@capacitor+status-bar@8.0.2_@capacitor+core@8.3.1/node_modules/@capacitor/status-bar`)"
|
||||
- "CapgoCapacitorNavigationBar (from `../../node_modules/.pnpm/@capgo+capacitor-navigation-bar@8.1.5_@capacitor+core@8.3.1/node_modules/@capgo/capacitor-navigation-bar`)"
|
||||
- Drops (from `https://github.com/omaralbeik/Drops.git`, tag `1.7.0`)
|
||||
- "JcesarmobileSslSkip (from `../../node_modules/.pnpm/@jcesarmobile+ssl-skip@0.9.0/node_modules/@jcesarmobile/ssl-skip`)"
|
||||
- "SendIntent (from `../../node_modules/.pnpm/send-intent@7.0.0_@capacitor+core@8.3.1/node_modules/send-intent`)"
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
- IONCameraLib
|
||||
- IONFilesystemLib
|
||||
- KeychainSwift
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
AparajitaCapacitorSecureStorage:
|
||||
:path: "../../node_modules/.pnpm/@aparajita+capacitor-secure-storage@8.0.0/node_modules/@aparajita/capacitor-secure-storage"
|
||||
Capacitor:
|
||||
:path: "../../node_modules/.pnpm/@capacitor+ios@8.3.1_@capacitor+core@8.3.1/node_modules/@capacitor/ios"
|
||||
CapacitorActionSheet:
|
||||
:path: "../../node_modules/.pnpm/@capacitor+action-sheet@8.1.1_@capacitor+core@8.3.1/node_modules/@capacitor/action-sheet"
|
||||
CapacitorApp:
|
||||
:path: "../../node_modules/.pnpm/@capacitor+app@8.1.0_@capacitor+core@8.3.1/node_modules/@capacitor/app"
|
||||
CapacitorCamera:
|
||||
:path: "../../node_modules/.pnpm/@capacitor+camera@8.2.0_@capacitor+core@8.3.1/node_modules/@capacitor/camera"
|
||||
CapacitorClipboard:
|
||||
:path: "../../node_modules/.pnpm/@capacitor+clipboard@8.0.1_@capacitor+core@8.3.1/node_modules/@capacitor/clipboard"
|
||||
CapacitorCommunitySafeArea:
|
||||
:path: "../../node_modules/.pnpm/@capacitor-community+safe-area@8.0.1_@capacitor+core@8.3.1/node_modules/@capacitor-community/safe-area"
|
||||
CapacitorCordova:
|
||||
:path: "../../node_modules/.pnpm/@capacitor+ios@8.3.1_@capacitor+core@8.3.1/node_modules/@capacitor/ios"
|
||||
CapacitorDevice:
|
||||
:path: "../../node_modules/.pnpm/@capacitor+device@8.0.2_@capacitor+core@8.3.1/node_modules/@capacitor/device"
|
||||
CapacitorDialog:
|
||||
:path: "../../node_modules/.pnpm/@capacitor+dialog@8.0.1_@capacitor+core@8.3.1/node_modules/@capacitor/dialog"
|
||||
CapacitorFilesystem:
|
||||
:path: "../../node_modules/.pnpm/@capacitor+filesystem@8.1.2_@capacitor+core@8.3.1/node_modules/@capacitor/filesystem"
|
||||
CapacitorHaptics:
|
||||
:path: "../../node_modules/.pnpm/@capacitor+haptics@8.0.2_@capacitor+core@8.3.1/node_modules/@capacitor/haptics"
|
||||
CapacitorKeyboard:
|
||||
:path: "../../node_modules/.pnpm/@capacitor+keyboard@8.0.3_@capacitor+core@8.3.1/node_modules/@capacitor/keyboard"
|
||||
CapacitorNetwork:
|
||||
:path: "../../node_modules/.pnpm/@capacitor+network@8.0.1_@capacitor+core@8.3.1/node_modules/@capacitor/network"
|
||||
CapacitorShare:
|
||||
:path: "../../node_modules/.pnpm/@capacitor+share@8.0.1_@capacitor+core@8.3.1/node_modules/@capacitor/share"
|
||||
CapacitorSplashScreen:
|
||||
:path: "../../node_modules/.pnpm/@capacitor+splash-screen@8.0.1_@capacitor+core@8.3.1/node_modules/@capacitor/splash-screen"
|
||||
CapacitorStatusBar:
|
||||
:path: "../../node_modules/.pnpm/@capacitor+status-bar@8.0.2_@capacitor+core@8.3.1/node_modules/@capacitor/status-bar"
|
||||
CapgoCapacitorNavigationBar:
|
||||
:path: "../../node_modules/.pnpm/@capgo+capacitor-navigation-bar@8.1.5_@capacitor+core@8.3.1/node_modules/@capgo/capacitor-navigation-bar"
|
||||
Drops:
|
||||
:git: https://github.com/omaralbeik/Drops.git
|
||||
:tag: 1.7.0
|
||||
JcesarmobileSslSkip:
|
||||
:path: "../../node_modules/.pnpm/@jcesarmobile+ssl-skip@0.9.0/node_modules/@jcesarmobile/ssl-skip"
|
||||
SendIntent:
|
||||
:path: "../../node_modules/.pnpm/send-intent@7.0.0_@capacitor+core@8.3.1/node_modules/send-intent"
|
||||
|
||||
CHECKOUT OPTIONS:
|
||||
Drops:
|
||||
:git: https://github.com/omaralbeik/Drops.git
|
||||
:tag: 1.7.0
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
AparajitaCapacitorSecureStorage: 8128d05cafcb13b00448e20fb388a0edccd44b12
|
||||
Capacitor: 46dbaa89c1d1cd121b9fc2dc839c1f39cc49e326
|
||||
CapacitorActionSheet: c3394c32eb29990e73e9c6c5677e3b31eb46ad3f
|
||||
CapacitorApp: 449ffe26375e96f8aaaee625ac6e01e5c57c8650
|
||||
CapacitorCamera: 8f6acf83664118321111e76acef86390211ccb08
|
||||
CapacitorClipboard: c75fdaa6e622111119f35e7914ecfc67b05efecb
|
||||
CapacitorCommunitySafeArea: 91738f4ae7f47d2d94e06e429e16fb73cd8fdc48
|
||||
CapacitorCordova: 9010447fe25e869705fcf525e5f490facde87608
|
||||
CapacitorDevice: 14cba6f88d1c3074cbf825fea977c8c526453ff8
|
||||
CapacitorDialog: 854c5dac405081cae697c177ca43bcaa95ae67bf
|
||||
CapacitorFilesystem: 2db285a1ac6e475cc27cae2e1bea999000f9e2be
|
||||
CapacitorHaptics: 296f771ecd89c7a1bd92a7b6826a7d268e2e70f5
|
||||
CapacitorKeyboard: 31c8285d92c9d1410071d52449f16d2c6673b6f4
|
||||
CapacitorNetwork: 8812ce60d11fb63d8f2e4ba51a49b2e59892ebe2
|
||||
CapacitorShare: 0c58305114538568059bfc07111f22dcb9cb2a82
|
||||
CapacitorSplashScreen: 4b46c72298db552b3c57ce021ef028b6dc7f1fa7
|
||||
CapacitorStatusBar: 01d5763b4ed720de5ce2edbc938de6a98f4c8f32
|
||||
CapgoCapacitorNavigationBar: fa8161edfef5a03b8fd551d962b6c5af45ad393e
|
||||
Drops: 5155b9ede54a2666b2129ac33f0734509b9f0784
|
||||
IONCameraLib: f2349804460bd9e3f892e42109b8d996ab3fa19d
|
||||
IONFilesystemLib: 21a63377696b2d8fab5632ecfb7d2ac67bddb68a
|
||||
JcesarmobileSslSkip: 0177378ba3aaa93bb02cb287f1363d5ca2c24fef
|
||||
KeychainSwift: 4a71a45c802fd9e73906457c2dcbdbdc06c9419d
|
||||
SendIntent: 8a6f646a4489f788d253ffbd1082a98ea388d870
|
||||
|
||||
PODFILE CHECKSUM: cbe0ea6549f3dd4fc86276229afae9a13b3fc09c
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="j1y-V4-xli">
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Share View Controller-->
|
||||
<scene sceneID="ceB-am-kn3">
|
||||
<objects>
|
||||
<viewController id="j1y-V4-xli" customClass="ShareViewController" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" opaque="NO" contentMode="scaleToFill" id="wbc-yd-nQP">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<viewLayoutGuide key="safeArea" id="1Xd-am-t49"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="CEy-Cv-SGf" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
35
ios/App/ShareViewController/Info.plist
Normal file
@@ -0,0 +1,35 @@
|
||||
<?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>NSExtensionAttributes</key>
|
||||
<dict>
|
||||
<key>NSExtensionActivationRule</key>
|
||||
<dict>
|
||||
<key>NSExtensionActivationDictionaryVersion</key>
|
||||
<integer>2</integer>
|
||||
<key>NSExtensionActivationSupportsFileWithMaxCount</key>
|
||||
<integer>5</integer>
|
||||
<key>NSExtensionActivationSupportsImageWithMaxCount</key>
|
||||
<integer>5</integer>
|
||||
<key>NSExtensionActivationSupportsMovieWithMaxCount</key>
|
||||
<integer>5</integer>
|
||||
<key>NSExtensionActivationSupportsText</key>
|
||||
<true/>
|
||||
<key>NSExtensionActivationSupportsWebPageWithMaxCount</key>
|
||||
<integer>3</integer>
|
||||
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
|
||||
<integer>3</integer>
|
||||
<key>NSExtensionActivationUsesStrictMatching</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>NSExtensionMainStoryboard</key>
|
||||
<string>MainInterface</string>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.share-services</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
10
ios/App/ShareViewController/ShareViewController.entitlements
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.com.logseq.logseq</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
266
ios/App/ShareViewController/ShareViewController.swift
Normal file
@@ -0,0 +1,266 @@
|
||||
//
|
||||
// ShareViewController.swift
|
||||
// ShareViewController
|
||||
//
|
||||
// Created by leizhe on 2022/3/17.
|
||||
//
|
||||
|
||||
|
||||
import MobileCoreServices
|
||||
import Social
|
||||
import UIKit
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
class ShareViewController: UIViewController {
|
||||
|
||||
private var sharedData: SharedData = SharedData.init(resources: [])
|
||||
|
||||
var groupContainerUrl: URL? {
|
||||
return FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.com.logseq.logseq")
|
||||
}
|
||||
|
||||
override public func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
|
||||
self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
|
||||
}
|
||||
}
|
||||
|
||||
private func sendData() {
|
||||
let encoder: JSONEncoder = JSONEncoder()
|
||||
let data = try? encoder.encode(self.sharedData)
|
||||
let queryPayload = String(decoding: data!, as: UTF8.self)
|
||||
|
||||
let queryItems =
|
||||
[
|
||||
URLQueryItem(
|
||||
name: "payload",
|
||||
value: queryPayload.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? ""),
|
||||
]
|
||||
var urlComps = URLComponents(string: "logseq://shared?")!
|
||||
urlComps.queryItems = queryItems
|
||||
openURL(urlComps.url!)
|
||||
}
|
||||
|
||||
fileprivate func createSharedFileUrl(_ url: URL?) -> URL? {
|
||||
let tempFilename = url!
|
||||
.lastPathComponent.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
|
||||
let copyFileUrl = groupContainerUrl!.appendingPathComponent(tempFilename)
|
||||
try? Data(contentsOf: url!).write(to: copyFileUrl)
|
||||
return copyFileUrl
|
||||
}
|
||||
|
||||
private func saveData(_ data: Data, fileExtension: String) -> URL? {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = "yyyy-MM-dd-HH-mm-ss"
|
||||
let filename = dateFormatter.string(from: Date()) + "." + fileExtension
|
||||
|
||||
let copyFileUrl = groupContainerUrl!.appendingPathComponent(filename)
|
||||
do {
|
||||
try data.write(to: copyFileUrl)
|
||||
return copyFileUrl
|
||||
} catch {
|
||||
print(error.localizedDescription)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Screenshots, shared images from some system App are passed as UIImage
|
||||
func saveUIImage(_ image: UIImage) -> URL? {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = "yyyy-MM-dd-HH-mm-ss"
|
||||
let filename = dateFormatter.string(from: Date()) + ".png"
|
||||
|
||||
let copyFileUrl = groupContainerUrl!.appendingPathComponent(filename)
|
||||
|
||||
do {
|
||||
try image.pngData()?.write(to: copyFileUrl)
|
||||
return copyFileUrl
|
||||
} catch {
|
||||
print(error.localizedDescription)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Can be a path or a web URL
|
||||
fileprivate func handleTypeUrl(_ attachment: NSItemProvider)
|
||||
async throws -> SharedResource
|
||||
{
|
||||
let results = try await attachment.loadItem(forTypeIdentifier: kUTTypeURL as String, options: nil)
|
||||
let url = results as! URL?
|
||||
|
||||
var res = SharedResource()
|
||||
|
||||
if url!.isFileURL {
|
||||
res.name = url!.lastPathComponent
|
||||
res.ext = url!.pathExtension
|
||||
res.type = url!.pathExtensionAsMimeType()
|
||||
res.url = createSharedFileUrl(url)
|
||||
} else {
|
||||
res.name = url!.absoluteString
|
||||
res.type = "text/plain"
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
// Some shares (notably system screenshots) can come through as a file URL item.
|
||||
fileprivate func handleTypeFileUrl(_ attachment: NSItemProvider)
|
||||
async throws -> SharedResource
|
||||
{
|
||||
let results = try await attachment.loadItem(forTypeIdentifier: kUTTypeFileURL as String, options: nil)
|
||||
let url = results as! URL?
|
||||
|
||||
var res = SharedResource()
|
||||
|
||||
if let url, url.isFileURL {
|
||||
res.name = url.lastPathComponent
|
||||
res.ext = url.pathExtension.lowercased()
|
||||
res.type = url.pathExtensionAsMimeType()
|
||||
res.url = createSharedFileUrl(url)
|
||||
} else if let url {
|
||||
res.name = url.absoluteString
|
||||
res.type = "text/plain"
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
fileprivate func handleTypeText(_ attachment: NSItemProvider)
|
||||
async throws -> SharedResource?
|
||||
{
|
||||
let item = try await attachment.loadItem(forTypeIdentifier: kUTTypeText as String, options: nil)
|
||||
self.sharedData.text = item as? String
|
||||
return nil
|
||||
}
|
||||
|
||||
fileprivate func handleTypeMovie(_ attachment: NSItemProvider)
|
||||
async throws -> SharedResource
|
||||
{
|
||||
let results = try await attachment.loadItem(forTypeIdentifier: kUTTypeMovie as String, options: nil)
|
||||
|
||||
let url = results as! URL?
|
||||
|
||||
let name = url!.lastPathComponent
|
||||
let ext = url!.pathExtension.lowercased()
|
||||
let type = url!.pathExtensionAsMimeType()
|
||||
let sharedUrl = createSharedFileUrl(url)
|
||||
|
||||
let res = SharedResource(name: name, ext: ext, type: type, url: sharedUrl)
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
fileprivate func handleTypeImage(_ attachment: NSItemProvider)
|
||||
async throws -> SharedResource
|
||||
{
|
||||
let data = try await attachment.loadItem(forTypeIdentifier: kUTTypeImage as String, options: nil)
|
||||
|
||||
var res = SharedResource()
|
||||
|
||||
switch data {
|
||||
case let image as UIImage:
|
||||
res.url = self.saveUIImage(image)
|
||||
res.ext = "png"
|
||||
res.name = res.url?.lastPathComponent
|
||||
res.type = res.url?.pathExtensionAsMimeType()
|
||||
case let data as Data:
|
||||
let ext: String
|
||||
if attachment.hasItemConformingToTypeIdentifier(UTType.png.identifier) {
|
||||
ext = "png"
|
||||
} else if attachment.hasItemConformingToTypeIdentifier(UTType.jpeg.identifier) {
|
||||
ext = "jpg"
|
||||
} else if attachment.hasItemConformingToTypeIdentifier(UTType.heic.identifier) {
|
||||
ext = "heic"
|
||||
} else {
|
||||
ext = "png"
|
||||
}
|
||||
|
||||
res.url = self.saveData(data, fileExtension: ext)
|
||||
res.ext = ext
|
||||
res.name = res.url?.lastPathComponent
|
||||
res.type = res.url?.pathExtensionAsMimeType()
|
||||
case let url as URL:
|
||||
res.name = url.lastPathComponent
|
||||
res.ext = url.pathExtension.lowercased()
|
||||
res.type = url.pathExtensionAsMimeType()
|
||||
res.url = self.createSharedFileUrl(url)
|
||||
default:
|
||||
print("Unexpected image data:", type(of: data))
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
|
||||
override public func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
sharedData.empty()
|
||||
let inputItems = extensionContext?.inputItems as! [NSExtensionItem]
|
||||
Task {
|
||||
try await withThrowingTaskGroup(
|
||||
of: SharedResource?.self,
|
||||
body: { taskGroup in
|
||||
for extensionItem in inputItems {
|
||||
for attachment in extensionItem.attachments! {
|
||||
if attachment.hasItemConformingToTypeIdentifier(kUTTypeURL as String) {
|
||||
taskGroup.addTask {
|
||||
return try await self.handleTypeUrl(attachment)
|
||||
}
|
||||
} else if attachment.hasItemConformingToTypeIdentifier(kUTTypeFileURL as String) {
|
||||
taskGroup.addTask {
|
||||
return try await self.handleTypeFileUrl(attachment)
|
||||
}
|
||||
} else if attachment.hasItemConformingToTypeIdentifier(kUTTypeText as String) {
|
||||
taskGroup.addTask {
|
||||
return try await self.handleTypeText(attachment)
|
||||
}
|
||||
} else if attachment.hasItemConformingToTypeIdentifier(kUTTypeMovie as String) {
|
||||
taskGroup.addTask {
|
||||
return try await self.handleTypeMovie(attachment)
|
||||
}
|
||||
} else if attachment.hasItemConformingToTypeIdentifier(kUTTypeImage as String) {
|
||||
taskGroup.addTask {
|
||||
return try await self.handleTypeImage(attachment)
|
||||
}
|
||||
} else {
|
||||
// Useful for diagnosing shares that don't match the legacy kUTType checks.
|
||||
print("Unhandled attachment types:", attachment.registeredTypeIdentifiers)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for try await item in taskGroup {
|
||||
if let item = item {
|
||||
self.sharedData.resources.append(item)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
self.sendData()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
@objc func openURL(_ url: URL) {
|
||||
var responder: UIResponder? = self
|
||||
while responder != nil {
|
||||
if let application = responder as? UIApplication {
|
||||
application.open(url, options: [:], completionHandler: nil)
|
||||
return
|
||||
}
|
||||
responder = responder?.next
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
extension URL {
|
||||
func pathExtensionAsMimeType() -> String? {
|
||||
let type = UTType(filenameExtension: self.pathExtension)
|
||||
return type?.preferredMIMEType
|
||||
}
|
||||
}
|
||||
25
ios/App/ShareViewController/SharedData.swift
Normal file
@@ -0,0 +1,25 @@
|
||||
//
|
||||
// SharedData.swift
|
||||
// ShareViewController
|
||||
//
|
||||
// Created by Mono Wang on 5/22/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct SharedResource: Decodable, Encodable {
|
||||
var name: String?
|
||||
var ext: String?
|
||||
var type: String?
|
||||
var url: URL?
|
||||
}
|
||||
|
||||
public struct SharedData: Decodable, Encodable {
|
||||
var text: String?
|
||||
var resources: [SharedResource]
|
||||
|
||||
mutating func empty() {
|
||||
text = nil
|
||||
resources = []
|
||||
}
|
||||
}
|
||||
8
ios/App/fastlane/Appfile
Normal file
@@ -0,0 +1,8 @@
|
||||
app_identifier("com.logseq.logseq") # The bundle identifier of your app
|
||||
apple_id("dev@logseq.com") # Your Apple Developer Portal username
|
||||
|
||||
itc_team_id("123783177") # App Store Connect Team ID
|
||||
team_id("K378MFWK59") # Developer Portal Team ID
|
||||
|
||||
# For more information about the Appfile, see:
|
||||
# https://docs.fastlane.tools/advanced/#appfile
|
||||
67
ios/App/fastlane/Fastfile
Normal file
@@ -0,0 +1,67 @@
|
||||
# This file contains the fastlane.tools configuration
|
||||
# You can find the documentation at https://docs.fastlane.tools
|
||||
#
|
||||
# For a list of all available actions, check out
|
||||
#
|
||||
# https://docs.fastlane.tools/actions
|
||||
#
|
||||
# For a list of all available plugins, check out
|
||||
#
|
||||
# https://docs.fastlane.tools/plugins/available-plugins
|
||||
#
|
||||
|
||||
# Uncomment the line if you want fastlane to automatically update itself
|
||||
# update_fastlane
|
||||
|
||||
default_platform(:ios)
|
||||
|
||||
platform :ios do
|
||||
desc "Push a new beta build to TestFlight"
|
||||
lane :beta do
|
||||
setup_ci
|
||||
|
||||
app_store_connect_api_key(
|
||||
key_id: ENV["APP_STORE_CONNECT_API_KEY_KEY_ID"],
|
||||
issuer_id: ENV["APP_STORE_CONNECT_API_KEY_ISSUER_ID"],
|
||||
key_filepath: ENV["APP_STORE_CONNECT_API_KEY_KEY_FILEPATH"],
|
||||
)
|
||||
|
||||
sync_code_signing(
|
||||
type: "appstore",
|
||||
readonly: false
|
||||
)
|
||||
|
||||
version_number = get_version_number(
|
||||
xcodeproj: "App.xcodeproj",
|
||||
target: "Logseq",
|
||||
)
|
||||
|
||||
latest_build_number = latest_testflight_build_number(
|
||||
version: version_number,
|
||||
initial_build_number: 1,
|
||||
)
|
||||
|
||||
build_number = increment_build_number(
|
||||
xcodeproj: "App.xcodeproj",
|
||||
build_number: latest_build_number + 1,
|
||||
)
|
||||
|
||||
# Ref: https://docs.fastlane.tools/advanced/fastlane/#directory-behavior
|
||||
sh("../../../scripts/patch-xcode-project.sh")
|
||||
|
||||
build_app(
|
||||
workspace: "App.xcworkspace",
|
||||
destination: "generic/platform=iOS",
|
||||
scheme: "Logseq",
|
||||
configuration: "Release",
|
||||
xcargs: "-allowProvisioningUpdates"
|
||||
)
|
||||
|
||||
upload_to_testflight(
|
||||
skip_submission: true,
|
||||
skip_waiting_for_build_processing: true,
|
||||
)
|
||||
|
||||
slack(message: "App Build (#{build_number}) successfully uploaded to TestFlight 🎉!")
|
||||
end
|
||||
end
|
||||
13
ios/App/fastlane/Matchfile
Normal file
@@ -0,0 +1,13 @@
|
||||
git_url("https://github.com/logseq/certificates.git")
|
||||
|
||||
storage_mode("git")
|
||||
|
||||
type("appstore") # The default type, can be: appstore, adhoc, enterprise or development
|
||||
|
||||
app_identifier(["com.logseq.logseq", "com.logseq.logseq.ShareViewController", "com.logseq.logseq.shortcuts"])
|
||||
# username("user@fastlane.tools") # Your Apple Developer Portal username
|
||||
|
||||
# For all available options run `fastlane match --help`
|
||||
# Remove the # in the beginning of the line to enable the other options
|
||||
|
||||
# The docs are available on https://docs.fastlane.tools/actions/match
|
||||
32
ios/App/fastlane/README.md
Normal file
@@ -0,0 +1,32 @@
|
||||
fastlane documentation
|
||||
----
|
||||
|
||||
# Installation
|
||||
|
||||
Make sure you have the latest version of the Xcode command line tools installed:
|
||||
|
||||
```sh
|
||||
xcode-select --install
|
||||
```
|
||||
|
||||
For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane)
|
||||
|
||||
# Available Actions
|
||||
|
||||
## iOS
|
||||
|
||||
### ios beta
|
||||
|
||||
```sh
|
||||
[bundle exec] fastlane ios beta
|
||||
```
|
||||
|
||||
Push a new beta build to TestFlight
|
||||
|
||||
----
|
||||
|
||||
This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run.
|
||||
|
||||
More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools).
|
||||
|
||||
The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools).
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "LogseqLogo.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "tinted"
|
||||
}
|
||||
],
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 39 KiB |
6
ios/App/shortcuts/Assets.xcassets/Contents.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
23
ios/App/shortcuts/Assets.xcassets/LogseqLogo.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "LogseqLogo.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "LogseqLogo 1.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "LogseqLogo 2.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
ios/App/shortcuts/Assets.xcassets/LogseqLogo.imageset/LogseqLogo 1.png
vendored
Normal file
|
After Width: | Height: | Size: 9.2 KiB |
BIN
ios/App/shortcuts/Assets.xcassets/LogseqLogo.imageset/LogseqLogo 2.png
vendored
Normal file
|
After Width: | Height: | Size: 9.2 KiB |
BIN
ios/App/shortcuts/Assets.xcassets/LogseqLogo.imageset/LogseqLogo.png
vendored
Normal file
|
After Width: | Height: | Size: 9.2 KiB |
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
11
ios/App/shortcuts/Info.plist
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.widgetkit-extension</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
158
ios/App/shortcuts/shortcuts.swift
Normal file
@@ -0,0 +1,158 @@
|
||||
import WidgetKit
|
||||
import SwiftUI
|
||||
|
||||
struct Provider: TimelineProvider {
|
||||
func placeholder(in context: Context) -> SimpleEntry {
|
||||
SimpleEntry(date: Date())
|
||||
}
|
||||
|
||||
func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
|
||||
let entry = SimpleEntry(date: Date())
|
||||
completion(entry)
|
||||
}
|
||||
|
||||
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
|
||||
let currentDate = Date()
|
||||
let entry = SimpleEntry(date: currentDate)
|
||||
let timeline = Timeline(entries: [entry], policy: .atEnd)
|
||||
completion(timeline)
|
||||
}
|
||||
}
|
||||
|
||||
struct SimpleEntry: TimelineEntry {
|
||||
let date: Date
|
||||
}
|
||||
|
||||
struct ShortcutsEntryView: View {
|
||||
var entry: Provider.Entry
|
||||
|
||||
// Format weekday name (e.g., "Monday")
|
||||
private var weekday: String {
|
||||
let formatter = DateFormatter()
|
||||
formatter.locale = Locale.current // ✅ respect user’s locale
|
||||
formatter.dateFormat = "EEEE" // full weekday name
|
||||
return formatter.string(from: Date())
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Link(destination: URL(string: "logseq://mobile/go/quick-add")!) {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
// Top heading
|
||||
Link(destination: URL(string: "logseq://mobile")!) {
|
||||
HStack(alignment: .top, spacing: 8) {
|
||||
Text(weekday)
|
||||
.font(.subheadline)
|
||||
.bold()
|
||||
.foregroundColor(.white.opacity(0.5))
|
||||
.padding(.top, 4)
|
||||
|
||||
Spacer()
|
||||
|
||||
Image("LogseqLogo")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 30, height: 30)
|
||||
}
|
||||
}
|
||||
|
||||
// Middle text
|
||||
Text("I have an idea...")
|
||||
.font(.headline)
|
||||
.bold()
|
||||
.foregroundColor(.white.opacity(0.8))
|
||||
|
||||
Spacer(minLength: 0)
|
||||
|
||||
// Bottom buttons row
|
||||
HStack(spacing: 8) {
|
||||
// Left button (audio waves)
|
||||
Link(destination: URL(string: "logseq://mobile/go/audio")!) {
|
||||
Image(systemName: "waveform")
|
||||
.font(.body)
|
||||
.foregroundColor(.white)
|
||||
.frame(maxWidth: .infinity, minHeight: 36)
|
||||
.background(Color.white.opacity(0.1))
|
||||
.clipShape(Capsule())
|
||||
.contentShape(Capsule())
|
||||
}
|
||||
|
||||
// Right button (quick add)
|
||||
Link(destination: URL(string: "logseq://mobile/go/quick-add")!) {
|
||||
Image(systemName: "plus")
|
||||
.font(.body)
|
||||
.foregroundColor(.white)
|
||||
.frame(maxWidth: .infinity, minHeight: 36)
|
||||
.background(Color.white.opacity(0.1))
|
||||
.clipShape(Capsule())
|
||||
.contentShape(Capsule())
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
||||
.padding(-8)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to support hex colors
|
||||
extension Color {
|
||||
init(hex: String) {
|
||||
let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
|
||||
var int: UInt64 = 0
|
||||
Scanner(string: hex).scanHexInt64(&int)
|
||||
let a, r, g, b: UInt64
|
||||
switch hex.count {
|
||||
case 3:
|
||||
(a, r, g, b) = (255,
|
||||
(int >> 8) * 17,
|
||||
(int >> 4 & 0xF) * 17,
|
||||
(int & 0xF) * 17)
|
||||
case 6:
|
||||
(a, r, g, b) = (255,
|
||||
int >> 16,
|
||||
int >> 8 & 0xFF,
|
||||
int & 0xFF)
|
||||
case 8:
|
||||
(a, r, g, b) = (int >> 24,
|
||||
int >> 16 & 0xFF,
|
||||
int >> 8 & 0xFF,
|
||||
int & 0xFF)
|
||||
default:
|
||||
(a, r, g, b) = (255, 0, 0, 0)
|
||||
}
|
||||
self.init(
|
||||
.sRGB,
|
||||
red: Double(r) / 255,
|
||||
green: Double(g) / 255,
|
||||
blue: Double(b) / 255,
|
||||
opacity: Double(a) / 255
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct Shortcuts: Widget {
|
||||
let kind: String = "shortcuts"
|
||||
|
||||
var body: some WidgetConfiguration {
|
||||
StaticConfiguration(kind: kind, provider: Provider()) { entry in
|
||||
if #available(iOS 17.0, *) {
|
||||
ShortcutsEntryView(entry: entry)
|
||||
.containerBackground(Color(hex: "#002b36"), for: .widget)
|
||||
} else {
|
||||
ZStack {
|
||||
Color(hex: "#002b36")
|
||||
ShortcutsEntryView(entry: entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
.configurationDisplayName("Logseq Shortcuts")
|
||||
.description("Quick actions for Logseq")
|
||||
}
|
||||
}
|
||||
|
||||
struct Shortcuts_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ShortcutsEntryView(entry: SimpleEntry(date: .now))
|
||||
.previewContext(WidgetPreviewContext(family: .systemSmall))
|
||||
}
|
||||
}
|
||||
19
ios/App/shortcuts/shortcutsBundle.swift
Normal file
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// shortcutsBundle.swift
|
||||
// shortcuts
|
||||
//
|
||||
// Created by Tienson Qin on 2025/9/19.
|
||||
//
|
||||
|
||||
import WidgetKit
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
@available(iOSApplicationExtension 18.0, *)
|
||||
struct shortcutsBundle: WidgetBundle {
|
||||
var body: some Widget {
|
||||
Shortcuts()
|
||||
QuickAddButton()
|
||||
RecordAudioButton()
|
||||
}
|
||||
}
|
||||
40
ios/App/shortcuts/shortcutsControl.swift
Normal file
@@ -0,0 +1,40 @@
|
||||
//
|
||||
// shortcutsControl.swift
|
||||
// shortcuts
|
||||
//
|
||||
// Created by Tienson Qin on 2025/9/19.
|
||||
//
|
||||
|
||||
import AppIntents
|
||||
import SwiftUI
|
||||
import WidgetKit
|
||||
|
||||
@available(iOS 18.0, *)
|
||||
struct QuickAddButton: ControlWidget {
|
||||
var body: some ControlWidgetConfiguration {
|
||||
StaticControlConfiguration(
|
||||
kind: "com.logseq.logseq.quickAddButton"
|
||||
) {
|
||||
ControlWidgetButton(action: QuickAddIntent()) {
|
||||
Label("Quick Add", systemImage: "plus.circle")
|
||||
}
|
||||
}
|
||||
.displayName("Quick Add")
|
||||
.description("Quick note.")
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 18.0, *)
|
||||
struct RecordAudioButton: ControlWidget {
|
||||
var body: some ControlWidgetConfiguration {
|
||||
StaticControlConfiguration(
|
||||
kind: "com.logseq.logseq.recordAudioButton"
|
||||
) {
|
||||
ControlWidgetButton(action: RecordAudioIntent()) {
|
||||
Label("Record Audio", systemImage: "waveform")
|
||||
}
|
||||
}
|
||||
.displayName("Record Audio")
|
||||
.description("Record Audio.")
|
||||
}
|
||||
}
|
||||
10
ios/App/shortcutsExtension.entitlements
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.com.logseq.logseq</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||