feat: add support for safari

This commit is contained in:
daniel31x13
2026-04-23 23:39:57 -04:00
parent 47b0e39fa7
commit d2c9c2d4a3
35 changed files with 1424 additions and 86 deletions
+6
View File
@@ -10,6 +10,7 @@ lerna-debug.log*
node_modules node_modules
dist dist
dist-ssr dist-ssr
dist-safari
*.local *.local
dist.crx dist.crx
dist.pem dist.pem
@@ -26,3 +27,8 @@ dist.pem
*.sw? *.sw?
linkwarden.zip linkwarden.zip
# Safari / Xcode
**/build/
**/DerivedData/
*.xcuserdata
+47
View File
@@ -0,0 +1,47 @@
{
"manifest_version": 3,
"name": "Linkwarden",
"description": "The browser extension for Linkwarden.",
"homepage_url": "https://linkwarden.app/",
"version": "1.5.3",
"action": {
"default_popup": "index.html",
"default_icon": {
"16": "16.png",
"32": "32.png",
"48": "48.png",
"128": "128.png"
},
"default_title": "Linkwarden"
},
"options_ui": {
"page": "src/pages/Options/options.html",
"browser_style": false
},
"icons": { "16": "16.png", "32": "32.png", "48": "48.png", "128": "128.png" },
"permissions": [
"storage",
"scripting",
"activeTab",
"tabs",
"contextMenus"
],
"host_permissions": ["<all_urls>"],
"background": {
"scripts": ["background.js"],
"type": "module"
},
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self'; connect-src 'self' http: https:;"
},
"commands": {
"_execute_action": {
"suggested_key": { "default": "Ctrl+Shift+F", "mac": "Command+Shift+Y" }
}
},
"browser_specific_settings": {
"safari": {
"strict_min_version": "15.4"
}
}
}
+2
View File
@@ -9,6 +9,8 @@
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "tsc && vite build && cp ./manifest.json dist/manifest.json", "build": "tsc && vite build && cp ./manifest.json dist/manifest.json",
"build:safari": "tsc && vite build --outDir dist-safari && cp ./manifest.safari.json dist-safari/manifest.json && cp public/*.png dist-safari/",
"convert:safari": "xcrun safari-web-extension-converter dist-safari/ --project-location safari/ --app-name Linkwarden --bundle-identifier app.linkwarden.safari-extension --swift --macos-only --no-prompt --no-open",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview" "preview": "vite preview"
}, },
@@ -0,0 +1,13 @@
<?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.Safari.web-extension</string>
<key>NSExtensionPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).SafariWebExtensionHandler</string>
</dict>
</dict>
</plist>
@@ -0,0 +1,42 @@
//
// SafariWebExtensionHandler.swift
// Linkwarden Extension
//
// Created by Daniel on 2026-04-23.
//
import SafariServices
import os.log
class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling {
func beginRequest(with context: NSExtensionContext) {
let request = context.inputItems.first as? NSExtensionItem
let profile: UUID?
if #available(iOS 17.0, macOS 14.0, *) {
profile = request?.userInfo?[SFExtensionProfileKey] as? UUID
} else {
profile = request?.userInfo?["profile"] as? UUID
}
let message: Any?
if #available(iOS 15.0, macOS 11.0, *) {
message = request?.userInfo?[SFExtensionMessageKey]
} else {
message = request?.userInfo?["message"]
}
os_log(.default, "Received message from browser.runtime.sendNativeMessage: %@ (profile: %@)", String(describing: message), profile?.uuidString ?? "none")
let response = NSExtensionItem()
if #available(iOS 15.0, macOS 11.0, *) {
response.userInfo = [ SFExtensionMessageKey: [ "echo": message ] ]
} else {
response.userInfo = [ "message": [ "echo": message ] ]
}
context.completeRequest(returningItems: [ response ], completionHandler: nil)
}
}
@@ -0,0 +1,647 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 77;
objects = {
/* Begin PBXBuildFile section */
65E051CD2F9AFEE100628D48 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65E051CC2F9AFEE100628D48 /* AppDelegate.swift */; };
65E051D12F9AFEE200628D48 /* Main.html in Resources */ = {isa = PBXBuildFile; fileRef = 65E051CF2F9AFEE100628D48 /* Main.html */; };
65E051D32F9AFEE200628D48 /* Icon.png in Resources */ = {isa = PBXBuildFile; fileRef = 65E051D22F9AFEE200628D48 /* Icon.png */; };
65E051D52F9AFEE200628D48 /* Style.css in Resources */ = {isa = PBXBuildFile; fileRef = 65E051D42F9AFEE200628D48 /* Style.css */; };
65E051D72F9AFEE200628D48 /* Script.js in Resources */ = {isa = PBXBuildFile; fileRef = 65E051D62F9AFEE200628D48 /* Script.js */; };
65E051D92F9AFEE200628D48 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65E051D82F9AFEE200628D48 /* ViewController.swift */; };
65E051DC2F9AFEE200628D48 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 65E051DA2F9AFEE200628D48 /* Main.storyboard */; };
65E051DE2F9AFEE300628D48 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 65E051DD2F9AFEE300628D48 /* Assets.xcassets */; };
65E051E52F9AFEE300628D48 /* Linkwarden Extension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 65E051E42F9AFEE300628D48 /* Linkwarden Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
65E051EA2F9AFEE300628D48 /* SafariWebExtensionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65E051E92F9AFEE300628D48 /* SafariWebExtensionHandler.swift */; };
65E052012F9AFEE300628D48 /* 48.png in Resources */ = {isa = PBXBuildFile; fileRef = 65E051F62F9AFEE300628D48 /* 48.png */; };
65E052022F9AFEE300628D48 /* index.html in Resources */ = {isa = PBXBuildFile; fileRef = 65E051F72F9AFEE300628D48 /* index.html */; };
65E052032F9AFEE300628D48 /* background.js in Resources */ = {isa = PBXBuildFile; fileRef = 65E051F82F9AFEE300628D48 /* background.js */; };
65E052042F9AFEE300628D48 /* 128.png in Resources */ = {isa = PBXBuildFile; fileRef = 65E051F92F9AFEE300628D48 /* 128.png */; };
65E052052F9AFEE300628D48 /* options.js in Resources */ = {isa = PBXBuildFile; fileRef = 65E051FA2F9AFEE300628D48 /* options.js */; };
65E052062F9AFEE300628D48 /* 16.png in Resources */ = {isa = PBXBuildFile; fileRef = 65E051FB2F9AFEE300628D48 /* 16.png */; };
65E052072F9AFEE300628D48 /* main.js in Resources */ = {isa = PBXBuildFile; fileRef = 65E051FC2F9AFEE300628D48 /* main.js */; };
65E052082F9AFEE300628D48 /* 32.png in Resources */ = {isa = PBXBuildFile; fileRef = 65E051FD2F9AFEE300628D48 /* 32.png */; };
65E052092F9AFEE300628D48 /* manifest.json in Resources */ = {isa = PBXBuildFile; fileRef = 65E051FE2F9AFEE300628D48 /* manifest.json */; };
65E0520A2F9AFEE300628D48 /* assets in Resources */ = {isa = PBXBuildFile; fileRef = 65E051FF2F9AFEE300628D48 /* assets */; };
65E0520B2F9AFEE300628D48 /* src in Resources */ = {isa = PBXBuildFile; fileRef = 65E052002F9AFEE300628D48 /* src */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
65E051E62F9AFEE300628D48 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 65E051C12F9AFEE100628D48 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 65E051E32F9AFEE300628D48;
remoteInfo = "Linkwarden Extension";
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
65E051F12F9AFEE300628D48 /* Embed Foundation Extensions */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 13;
files = (
65E051E52F9AFEE300628D48 /* Linkwarden Extension.appex in Embed Foundation Extensions */,
);
name = "Embed Foundation Extensions";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
65E051C92F9AFEE100628D48 /* Linkwarden.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Linkwarden.app; sourceTree = BUILT_PRODUCTS_DIR; };
65E051CC2F9AFEE100628D48 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
65E051D02F9AFEE100628D48 /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.html; name = Base; path = Base.lproj/Main.html; sourceTree = "<group>"; };
65E051D22F9AFEE200628D48 /* Icon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = Icon.png; sourceTree = "<group>"; };
65E051D42F9AFEE200628D48 /* Style.css */ = {isa = PBXFileReference; lastKnownFileType = text.css; path = Style.css; sourceTree = "<group>"; };
65E051D62F9AFEE200628D48 /* Script.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = Script.js; sourceTree = "<group>"; };
65E051D82F9AFEE200628D48 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
65E051DB2F9AFEE200628D48 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
65E051DD2F9AFEE300628D48 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
65E051DF2F9AFEE300628D48 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
65E051E42F9AFEE300628D48 /* Linkwarden Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Linkwarden Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
65E051E92F9AFEE300628D48 /* SafariWebExtensionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariWebExtensionHandler.swift; sourceTree = "<group>"; };
65E051EB2F9AFEE300628D48 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
65E051F62F9AFEE300628D48 /* 48.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = 48.png; path = "../../../dist-safari/48.png"; sourceTree = "<group>"; };
65E051F72F9AFEE300628D48 /* index.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; name = index.html; path = "../../../dist-safari/index.html"; sourceTree = "<group>"; };
65E051F82F9AFEE300628D48 /* background.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; name = background.js; path = "../../../dist-safari/background.js"; sourceTree = "<group>"; };
65E051F92F9AFEE300628D48 /* 128.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = 128.png; path = "../../../dist-safari/128.png"; sourceTree = "<group>"; };
65E051FA2F9AFEE300628D48 /* options.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; name = options.js; path = "../../../dist-safari/options.js"; sourceTree = "<group>"; };
65E051FB2F9AFEE300628D48 /* 16.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = 16.png; path = "../../../dist-safari/16.png"; sourceTree = "<group>"; };
65E051FC2F9AFEE300628D48 /* main.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; name = main.js; path = "../../../dist-safari/main.js"; sourceTree = "<group>"; };
65E051FD2F9AFEE300628D48 /* 32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = 32.png; path = "../../../dist-safari/32.png"; sourceTree = "<group>"; };
65E051FE2F9AFEE300628D48 /* manifest.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; name = manifest.json; path = "../../../dist-safari/manifest.json"; sourceTree = "<group>"; };
65E051FF2F9AFEE300628D48 /* assets */ = {isa = PBXFileReference; lastKnownFileType = folder; name = assets; path = "../../../dist-safari/assets"; sourceTree = "<group>"; };
65E052002F9AFEE300628D48 /* src */ = {isa = PBXFileReference; lastKnownFileType = folder; name = src; path = "../../../dist-safari/src"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
65E051C62F9AFEE100628D48 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
65E051E12F9AFEE300628D48 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
65E051C02F9AFEE100628D48 = {
isa = PBXGroup;
children = (
65E051CB2F9AFEE100628D48 /* Linkwarden */,
65E051E82F9AFEE300628D48 /* Linkwarden Extension */,
65E051CA2F9AFEE100628D48 /* Products */,
);
sourceTree = "<group>";
};
65E051CA2F9AFEE100628D48 /* Products */ = {
isa = PBXGroup;
children = (
65E051C92F9AFEE100628D48 /* Linkwarden.app */,
65E051E42F9AFEE300628D48 /* Linkwarden Extension.appex */,
);
name = Products;
sourceTree = "<group>";
};
65E051CB2F9AFEE100628D48 /* Linkwarden */ = {
isa = PBXGroup;
children = (
65E051CC2F9AFEE100628D48 /* AppDelegate.swift */,
65E051D82F9AFEE200628D48 /* ViewController.swift */,
65E051DA2F9AFEE200628D48 /* Main.storyboard */,
65E051DD2F9AFEE300628D48 /* Assets.xcassets */,
65E051DF2F9AFEE300628D48 /* Info.plist */,
65E051CE2F9AFEE100628D48 /* Resources */,
);
path = Linkwarden;
sourceTree = "<group>";
};
65E051CE2F9AFEE100628D48 /* Resources */ = {
isa = PBXGroup;
children = (
65E051CF2F9AFEE100628D48 /* Main.html */,
65E051D22F9AFEE200628D48 /* Icon.png */,
65E051D42F9AFEE200628D48 /* Style.css */,
65E051D62F9AFEE200628D48 /* Script.js */,
);
path = Resources;
sourceTree = "<group>";
};
65E051E82F9AFEE300628D48 /* Linkwarden Extension */ = {
isa = PBXGroup;
children = (
65E051F52F9AFEE300628D48 /* Resources */,
65E051E92F9AFEE300628D48 /* SafariWebExtensionHandler.swift */,
65E051EB2F9AFEE300628D48 /* Info.plist */,
);
path = "Linkwarden Extension";
sourceTree = "<group>";
};
65E051F52F9AFEE300628D48 /* Resources */ = {
isa = PBXGroup;
children = (
65E051F62F9AFEE300628D48 /* 48.png */,
65E051F72F9AFEE300628D48 /* index.html */,
65E051F82F9AFEE300628D48 /* background.js */,
65E051F92F9AFEE300628D48 /* 128.png */,
65E051FA2F9AFEE300628D48 /* options.js */,
65E051FB2F9AFEE300628D48 /* 16.png */,
65E051FC2F9AFEE300628D48 /* main.js */,
65E051FD2F9AFEE300628D48 /* 32.png */,
65E051FE2F9AFEE300628D48 /* manifest.json */,
65E051FF2F9AFEE300628D48 /* assets */,
65E052002F9AFEE300628D48 /* src */,
);
name = Resources;
path = "Linkwarden Extension";
sourceTree = SOURCE_ROOT;
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
65E051C82F9AFEE100628D48 /* Linkwarden */ = {
isa = PBXNativeTarget;
buildConfigurationList = 65E051F22F9AFEE300628D48 /* Build configuration list for PBXNativeTarget "Linkwarden" */;
buildPhases = (
65E051C52F9AFEE100628D48 /* Sources */,
65E051C62F9AFEE100628D48 /* Frameworks */,
65E051C72F9AFEE100628D48 /* Resources */,
65E051F12F9AFEE300628D48 /* Embed Foundation Extensions */,
);
buildRules = (
);
dependencies = (
65E051E72F9AFEE300628D48 /* PBXTargetDependency */,
);
name = Linkwarden;
packageProductDependencies = (
);
productName = Linkwarden;
productReference = 65E051C92F9AFEE100628D48 /* Linkwarden.app */;
productType = "com.apple.product-type.application";
};
65E051E32F9AFEE300628D48 /* Linkwarden Extension */ = {
isa = PBXNativeTarget;
buildConfigurationList = 65E051EE2F9AFEE300628D48 /* Build configuration list for PBXNativeTarget "Linkwarden Extension" */;
buildPhases = (
65E051E02F9AFEE300628D48 /* Sources */,
65E051E12F9AFEE300628D48 /* Frameworks */,
65E051E22F9AFEE300628D48 /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = "Linkwarden Extension";
packageProductDependencies = (
);
productName = "Linkwarden Extension";
productReference = 65E051E42F9AFEE300628D48 /* Linkwarden Extension.appex */;
productType = "com.apple.product-type.app-extension";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
65E051C12F9AFEE100628D48 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 2610;
LastUpgradeCheck = 2610;
TargetAttributes = {
65E051C82F9AFEE100628D48 = {
CreatedOnToolsVersion = 26.1;
};
65E051E32F9AFEE300628D48 = {
CreatedOnToolsVersion = 26.1;
};
};
};
buildConfigurationList = 65E051C42F9AFEE100628D48 /* Build configuration list for PBXProject "Linkwarden" */;
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 65E051C02F9AFEE100628D48;
minimizedProjectReferenceProxies = 1;
preferredProjectObjectVersion = 77;
productRefGroup = 65E051CA2F9AFEE100628D48 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
65E051C82F9AFEE100628D48 /* Linkwarden */,
65E051E32F9AFEE300628D48 /* Linkwarden Extension */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
65E051C72F9AFEE100628D48 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
65E051D32F9AFEE200628D48 /* Icon.png in Resources */,
65E051DC2F9AFEE200628D48 /* Main.storyboard in Resources */,
65E051D72F9AFEE200628D48 /* Script.js in Resources */,
65E051D12F9AFEE200628D48 /* Main.html in Resources */,
65E051DE2F9AFEE300628D48 /* Assets.xcassets in Resources */,
65E051D52F9AFEE200628D48 /* Style.css in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
65E051E22F9AFEE300628D48 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
65E052012F9AFEE300628D48 /* 48.png in Resources */,
65E052042F9AFEE300628D48 /* 128.png in Resources */,
65E052062F9AFEE300628D48 /* 16.png in Resources */,
65E052032F9AFEE300628D48 /* background.js in Resources */,
65E052092F9AFEE300628D48 /* manifest.json in Resources */,
65E0520A2F9AFEE300628D48 /* assets in Resources */,
65E052072F9AFEE300628D48 /* main.js in Resources */,
65E0520B2F9AFEE300628D48 /* src in Resources */,
65E052022F9AFEE300628D48 /* index.html in Resources */,
65E052082F9AFEE300628D48 /* 32.png in Resources */,
65E052052F9AFEE300628D48 /* options.js in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
65E051C52F9AFEE100628D48 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
65E051D92F9AFEE200628D48 /* ViewController.swift in Sources */,
65E051CD2F9AFEE100628D48 /* AppDelegate.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
65E051E02F9AFEE300628D48 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
65E051EA2F9AFEE300628D48 /* SafariWebExtensionHandler.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
65E051E72F9AFEE300628D48 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 65E051E32F9AFEE300628D48 /* Linkwarden Extension */;
targetProxy = 65E051E62F9AFEE300628D48 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
65E051CF2F9AFEE100628D48 /* Main.html */ = {
isa = PBXVariantGroup;
children = (
65E051D02F9AFEE100628D48 /* Base */,
);
name = Main.html;
sourceTree = "<group>";
};
65E051DA2F9AFEE200628D48 /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
65E051DB2F9AFEE200628D48 /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
65E051EC2F9AFEE300628D48 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = 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;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
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;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MACOSX_DEPLOYMENT_TARGET = 26.1;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
65E051ED2F9AFEE300628D48 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = 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;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
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;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MACOSX_DEPLOYMENT_TARGET = 26.1;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
};
name = Release;
};
65E051EF2F9AFEE300628D48 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = FZ44882Y36;
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_USER_SELECTED_FILES = readonly;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Linkwarden Extension/Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = "Linkwarden Extension";
INFOPLIST_KEY_NSHumanReadableCopyright = "";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
"@executable_path/../../../../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.5.3;
OTHER_LDFLAGS = (
"-framework",
SafariServices,
);
PRODUCT_BUNDLE_IDENTIFIER = app.linkwarden.extension.safari;
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;
};
name = Debug;
};
65E051F02F9AFEE300628D48 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = FZ44882Y36;
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_USER_SELECTED_FILES = readonly;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Linkwarden Extension/Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = "Linkwarden Extension";
INFOPLIST_KEY_NSHumanReadableCopyright = "";
"INFOPLIST_KEY_NSHumanReadableCopyright[sdk=*]" = "Copyright © 2026 Linkwarden. All rights reserved.";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
"@executable_path/../../../../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.5.3;
OTHER_LDFLAGS = (
"-framework",
SafariServices,
);
PRODUCT_BUNDLE_IDENTIFIER = app.linkwarden.extension.safari;
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;
};
name = Release;
};
65E051F32F9AFEE300628D48 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = FZ44882Y36;
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
ENABLE_USER_SELECTED_FILES = readonly;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = Linkwarden/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Linkwarden;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity";
INFOPLIST_KEY_NSHumanReadableCopyright = "";
INFOPLIST_KEY_NSMainStoryboardFile = Main;
INFOPLIST_KEY_NSPrincipalClass = NSApplication;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MARKETING_VERSION = 1.5.3;
OTHER_LDFLAGS = (
"-framework",
SafariServices,
"-framework",
WebKit,
);
PRODUCT_BUNDLE_IDENTIFIER = app.linkwarden.extension;
PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
};
name = Debug;
};
65E051F42F9AFEE300628D48 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = FZ44882Y36;
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
ENABLE_USER_SELECTED_FILES = readonly;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = Linkwarden/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Linkwarden;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity";
INFOPLIST_KEY_NSHumanReadableCopyright = "";
"INFOPLIST_KEY_NSHumanReadableCopyright[sdk=*]" = "Copyright © 2026 Linkwarden. All rights reserved.";
INFOPLIST_KEY_NSMainStoryboardFile = Main;
INFOPLIST_KEY_NSPrincipalClass = NSApplication;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MARKETING_VERSION = 1.5.3;
OTHER_LDFLAGS = (
"-framework",
SafariServices,
"-framework",
WebKit,
);
PRODUCT_BUNDLE_IDENTIFIER = app.linkwarden.extension;
PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
65E051C42F9AFEE100628D48 /* Build configuration list for PBXProject "Linkwarden" */ = {
isa = XCConfigurationList;
buildConfigurations = (
65E051EC2F9AFEE300628D48 /* Debug */,
65E051ED2F9AFEE300628D48 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
65E051EE2F9AFEE300628D48 /* Build configuration list for PBXNativeTarget "Linkwarden Extension" */ = {
isa = XCConfigurationList;
buildConfigurations = (
65E051EF2F9AFEE300628D48 /* Debug */,
65E051F02F9AFEE300628D48 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
65E051F22F9AFEE300628D48 /* Build configuration list for PBXNativeTarget "Linkwarden" */ = {
isa = XCConfigurationList;
buildConfigurations = (
65E051F32F9AFEE300628D48 /* Debug */,
65E051F42F9AFEE300628D48 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 65E051C12F9AFEE100628D48 /* Project object */;
}
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>
@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "2610"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "65E051C82F9AFEE100628D48"
BuildableName = "Linkwarden.app"
BlueprintName = "Linkwarden"
ReferencedContainer = "container:Linkwarden.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Release"
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 = "65E051C82F9AFEE100628D48"
BuildableName = "Linkwarden.app"
BlueprintName = "Linkwarden"
ReferencedContainer = "container:Linkwarden.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "65E051C82F9AFEE100628D48"
BuildableName = "Linkwarden.app"
BlueprintName = "Linkwarden"
ReferencedContainer = "container:Linkwarden.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
@@ -0,0 +1,22 @@
<?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>SchemeUserState</key>
<dict>
<key>Linkwarden.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>0</integer>
</dict>
</dict>
<key>SuppressBuildableAutocreation</key>
<dict>
<key>65E051C82F9AFEE100628D48</key>
<dict>
<key>primary</key>
<true/>
</dict>
</dict>
</dict>
</plist>
@@ -0,0 +1,21 @@
//
// AppDelegate.swift
// Linkwarden
//
// Created by Daniel on 2026-04-23.
//
import Cocoa
@main
class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_ notification: Notification) {
// Override point for customization after application launch.
}
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return true
}
}
@@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,68 @@
{
"images" : [
{
"filename" : "mac-icon-16@1x.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "16x16"
},
{
"filename" : "mac-icon-16@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "16x16"
},
{
"filename" : "mac-icon-32@1x.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "32x32"
},
{
"filename" : "mac-icon-32@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "32x32"
},
{
"filename" : "mac-icon-128@1x.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "128x128"
},
{
"filename" : "mac-icon-128@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "128x128"
},
{
"filename" : "mac-icon-256@1x.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "256x256"
},
{
"filename" : "mac-icon-256@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "256x256"
},
{
"filename" : "mac-icon-512@1x.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "512x512"
},
{
"filename" : "mac-icon-512@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "512x512"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 769 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 KiB

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,20 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,124 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="19085" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="19085"/>
<plugIn identifier="com.apple.WebKit2IBPlugin" version="19085"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Application-->
<scene sceneID="JPo-4y-FX3">
<objects>
<application id="hnw-xV-0zn" sceneMemberID="viewController">
<menu key="mainMenu" title="Main Menu" systemMenu="main" id="AYu-sK-qS6">
<items>
<menuItem title="Linkwarden" id="1Xt-HY-uBw">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Linkwarden" systemMenu="apple" id="uQy-DD-JDr">
<items>
<menuItem title="About Linkwarden" id="5kV-Vb-QxS">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="orderFrontStandardAboutPanel:" target="Ady-hI-5gd" id="Exp-CZ-Vem"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="VOq-y0-SEH"/>
<menuItem title="Hide Linkwarden" keyEquivalent="h" id="Olw-nP-bQN">
<connections>
<action selector="hide:" target="Ady-hI-5gd" id="PnN-Uc-m68"/>
</connections>
</menuItem>
<menuItem title="Hide Others" keyEquivalent="h" id="Vdr-fp-XzO">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="hideOtherApplications:" target="Ady-hI-5gd" id="VT4-aY-XCT"/>
</connections>
</menuItem>
<menuItem title="Show All" id="Kd2-mp-pUS">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="unhideAllApplications:" target="Ady-hI-5gd" id="Dhg-Le-xox"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="kCx-OE-vgT"/>
<menuItem title="Quit Linkwarden" keyEquivalent="q" id="4sb-4s-VLi">
<connections>
<action selector="terminate:" target="Ady-hI-5gd" id="Te7-pn-YzF"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Help" id="wpr-3q-Mcd">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Help" systemMenu="help" id="F2S-fz-NVQ">
<items>
<menuItem title="Linkwarden Help" keyEquivalent="?" id="FKE-Sm-Kum">
<connections>
<action selector="showHelp:" target="Ady-hI-5gd" id="y7X-2Q-9no"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
</items>
</menu>
<connections>
<outlet property="delegate" destination="Voe-Tx-rLC" id="PrD-fu-P6m"/>
</connections>
</application>
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModuleProvider="target"/>
<customObject id="YLy-65-1bz" customClass="NSFontManager"/>
<customObject id="Ady-hI-5gd" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="76" y="-134"/>
</scene>
<!--Window Controller-->
<scene sceneID="R2V-B0-nI4">
<objects>
<windowController showSeguePresentationStyle="single" id="B8D-0N-5wS" sceneMemberID="viewController">
<window key="window" title="Linkwarden" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" restorable="NO" releasedWhenClosed="NO" animationBehavior="default" id="IQv-IB-iLA">
<windowStyleMask key="styleMask" titled="YES" closable="YES"/>
<windowCollectionBehavior key="collectionBehavior" fullScreenNone="YES"/>
<rect key="contentRect" x="196" y="240" width="425" height="325"/>
<rect key="screenRect" x="0.0" y="0.0" width="1680" height="1027"/>
<connections>
<outlet property="delegate" destination="B8D-0N-5wS" id="98r-iN-zZc"/>
</connections>
</window>
<connections>
<segue destination="XfG-lQ-9wD" kind="relationship" relationship="window.shadowedContentViewController" id="cq2-FE-JQM"/>
</connections>
</windowController>
<customObject id="Oky-zY-oP4" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="75" y="250"/>
</scene>
<!--View Controller-->
<scene sceneID="hIz-AP-VOD">
<objects>
<viewController id="XfG-lQ-9wD" customClass="ViewController" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" id="m2S-Jp-Qdl">
<rect key="frame" x="0.0" y="0.0" width="425" height="325"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<wkWebView wantsLayer="YES" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="eOr-cG-IQY">
<rect key="frame" x="0.0" y="0.0" width="425" height="325"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<wkWebViewConfiguration key="configuration">
<audiovisualMediaTypes key="mediaTypesRequiringUserActionForPlayback" none="YES"/>
<wkPreferences key="preferences"/>
</wkWebViewConfiguration>
</wkWebView>
</subviews>
</view>
<connections>
<outlet property="webView" destination="eOr-cG-IQY" id="GFe-mU-dBY"/>
</connections>
</viewController>
<customObject id="rPt-NT-nkU" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="75" y="655"/>
</scene>
</scenes>
</document>
+8
View File
@@ -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>SFSafariWebExtensionConverterVersion</key>
<string>26.1</string>
</dict>
</plist>
@@ -0,0 +1,19 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<link rel="stylesheet" href="../Style.css">
<script src="../Script.js" defer></script>
</head>
<body>
<img src="../Icon.png" width="128" height="128" alt="Linkwarden Icon">
<p class="state-unknown">You can turn on Linkwardens extension in Safari Extensions preferences.</p>
<p class="state-on">Linkwardens extension is currently on. You can turn it off in Safari Extensions preferences.</p>
<p class="state-off">Linkwardens extension is currently off. You can turn it on in Safari Extensions preferences.</p>
<button class="open-preferences">Quit and Open Safari Extensions Preferences…</button>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

@@ -0,0 +1,22 @@
function show(enabled, useSettingsInsteadOfPreferences) {
if (useSettingsInsteadOfPreferences) {
document.getElementsByClassName('state-on')[0].innerText = "Linkwardens extension is currently on. You can turn it off in the Extensions section of Safari Settings.";
document.getElementsByClassName('state-off')[0].innerText = "Linkwardens extension is currently off. You can turn it on in the Extensions section of Safari Settings.";
document.getElementsByClassName('state-unknown')[0].innerText = "You can turn on Linkwardens extension in the Extensions section of Safari Settings.";
document.getElementsByClassName('open-preferences')[0].innerText = "Quit and Open Safari Settings…";
}
if (typeof enabled === "boolean") {
document.body.classList.toggle(`state-on`, enabled);
document.body.classList.toggle(`state-off`, !enabled);
} else {
document.body.classList.remove(`state-on`);
document.body.classList.remove(`state-off`);
}
}
function openPreferences() {
webkit.messageHandlers.controller.postMessage("open-preferences");
}
document.querySelector("button.open-preferences").addEventListener("click", openPreferences);
@@ -0,0 +1,45 @@
* {
-webkit-user-select: none;
-webkit-user-drag: none;
cursor: default;
}
:root {
color-scheme: light dark;
--spacing: 20px;
}
html {
height: 100%;
}
body {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
gap: var(--spacing);
margin: 0 calc(var(--spacing) * 2);
height: 100%;
font: -apple-system-short-body;
text-align: center;
}
body:not(.state-on, .state-off) :is(.state-on, .state-off) {
display: none;
}
body.state-on :is(.state-off, .state-unknown) {
display: none;
}
body.state-off :is(.state-on, .state-unknown) {
display: none;
}
button {
font-size: 1em;
}
@@ -0,0 +1,57 @@
//
// ViewController.swift
// Linkwarden
//
// Created by Daniel on 2026-04-23.
//
import Cocoa
import SafariServices
import WebKit
let extensionBundleIdentifier = "app.linkwarden.safari-extension.Extension"
class ViewController: NSViewController, WKNavigationDelegate, WKScriptMessageHandler {
@IBOutlet var webView: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
self.webView.navigationDelegate = self
self.webView.configuration.userContentController.add(self, name: "controller")
self.webView.loadFileURL(Bundle.main.url(forResource: "Main", withExtension: "html")!, allowingReadAccessTo: Bundle.main.resourceURL!)
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
SFSafariExtensionManager.getStateOfSafariExtension(withIdentifier: extensionBundleIdentifier) { (state, error) in
guard let state = state, error == nil else {
// Insert code to inform the user that something went wrong.
return
}
DispatchQueue.main.async {
if #available(macOS 13, *) {
webView.evaluateJavaScript("show(\(state.isEnabled), true)")
} else {
webView.evaluateJavaScript("show(\(state.isEnabled), false)")
}
}
}
}
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
if (message.body as! String != "open-preferences") {
return;
}
SFSafariApplication.showPreferencesForExtension(withIdentifier: extensionBundleIdentifier) { error in
DispatchQueue.main.async {
NSApplication.shared.terminate(nil)
}
}
}
}
+69 -12
View File
@@ -72,19 +72,58 @@ const drawImagesOnCanvas = async (
}; };
async function executeScript(tabId: number, func: any, args: any[] = []) { async function executeScript(tabId: number, func: any, args: any[] = []) {
if (typeof chrome.scripting !== 'undefined') { if (
typeof chrome !== 'undefined' &&
typeof chrome.scripting !== 'undefined'
) {
const results = await chrome.scripting.executeScript({ const results = await chrome.scripting.executeScript({
target: { tabId }, target: { tabId },
func, func,
args, args,
}); });
return results[0]?.result; return results[0]?.result;
} else {
const results = await browser.tabs.executeScript(tabId, {
code: `(${func})(${args.map((arg) => JSON.stringify(arg)).join(',')})`,
});
return results[0];
} }
const results = await browser.tabs.executeScript(tabId, {
code: `(${func})(${args.map((arg) => JSON.stringify(arg)).join(',')})`,
});
return results[0];
}
async function safeExecuteScript(tabId: number, func: any, args: any[] = []) {
try {
await executeScript(tabId, func, args);
} catch {
// Best effort cleanup for browsers that reject script execution.
}
}
function dataUrlToBlob(dataUrl: string): Blob {
const [meta = '', encodedData = ''] = dataUrl.split(',', 2);
const mimeType = meta.match(/^data:(.*?)(;base64)?$/)?.[1] ?? 'image/png';
if (meta.includes(';base64')) {
const binary = atob(encodedData);
const bytes = new Uint8Array(binary.length);
for (let index = 0; index < binary.length; index++) {
bytes[index] = binary.charCodeAt(index);
}
return new Blob([bytes], { type: mimeType });
}
return new Blob([decodeURIComponent(encodedData)], { type: mimeType });
}
async function captureVisibleTabScreenshot(): Promise<Blob> {
const tabs = await browser.tabs.query({ active: true, currentWindow: true });
const tab = tabs[0];
if (!tab || !tab.id) {
throw new Error('Unable to get the current tab.');
}
const dataUrl = await browser.tabs.captureVisibleTab(tab.windowId!, {
format: 'png',
});
return dataUrlToBlob(dataUrl);
} }
async function captureFullPageScreenshot(): Promise<Blob> { async function captureFullPageScreenshot(): Promise<Blob> {
@@ -218,8 +257,7 @@ async function captureFullPageScreenshot(): Promise<Blob> {
const dataUrl = await browser.tabs.captureVisibleTab(tab.windowId!, { const dataUrl = await browser.tabs.captureVisibleTab(tab.windowId!, {
format: 'png', format: 'png',
}); });
const blob = await fetch(dataUrl).then((res) => res.blob()); blobs.push(dataUrlToBlob(dataUrl));
blobs.push(blob);
} }
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
@@ -235,11 +273,30 @@ async function captureFullPageScreenshot(): Promise<Blob> {
dpr dpr
); );
await executeScript(tab.id, removeHideScrollbarClass); await safeExecuteScript(tab.id, removeHideScrollbarClass);
await executeScript(tab.id, restoreFixedElements, [originalStyles]); await safeExecuteScript(tab.id, restoreFixedElements, [originalStyles]);
await executeScript(tab.id, removeDisableSmoothScrollbarClass); await safeExecuteScript(tab.id, removeDisableSmoothScrollbarClass);
return resultBlob; return resultBlob;
} }
export default captureFullPageScreenshot; async function captureScreenshot(): Promise<Blob> {
try {
return await captureFullPageScreenshot();
} catch (fullPageError) {
console.warn(
'Full-page screenshot failed, falling back to visible tab capture.',
fullPageError
);
try {
return await captureVisibleTabScreenshot();
} catch (visibleTabError) {
console.error('Visible tab screenshot capture failed.', visibleTabError);
throw new Error(
'Screenshot capture is not available for this page.'
);
}
}
}
export default captureScreenshot;
+18
View File
@@ -57,6 +57,24 @@ export function openOptions() {
getBrowser().runtime.openOptionsPage(); getBrowser().runtime.openOptionsPage();
} }
export function isSafari(): boolean {
try {
return /^safari-web-extension:/.test(getBrowser().runtime.getURL(''));
} catch {
return false;
}
}
export function hasAPI(api: string): boolean {
const b = getBrowser();
let obj: any = b;
for (const part of api.split('.')) {
if (!obj || typeof obj[part] === 'undefined') return false;
obj = obj[part];
}
return true;
}
export async function updateBadge(tabId: number | undefined) { export async function updateBadge(tabId: number | undefined) {
if (!tabId) return; if (!tabId) return;
+72 -74
View File
@@ -1,6 +1,8 @@
import { import {
getBrowser, getBrowser,
getCurrentTabInfo, getCurrentTabInfo,
hasAPI,
isSafari,
updateBadge, updateBadge,
} from '../../@/lib/utils.ts'; } from '../../@/lib/utils.ts';
// import BookmarkTreeNode = chrome.bookmarks.BookmarkTreeNode; // import BookmarkTreeNode = chrome.bookmarks.BookmarkTreeNode;
@@ -240,7 +242,7 @@ async function genericOnClick(
} }
default: default:
// Handle cases where sync is enabled or not // Handle cases where sync is enabled or not
if (syncBookmarks) { if (syncBookmarks && hasAPI('bookmarks.create')) {
browser.bookmarks.create({ browser.bookmarks.create({
parentId: '1', parentId: '1',
title: tab.title, title: tab.title,
@@ -277,15 +279,9 @@ async function genericOnClick(
} }
browser.runtime.onInstalled.addListener(async function () { browser.runtime.onInstalled.addListener(async function () {
// Create one test item for each context type. // Create one test item for each context type.
const contexts: ContextType[] = [ const contexts: ContextType[] = isSafari()
'page', ? ['page', 'selection', 'link']
'selection', : ['page', 'selection', 'link', 'editable', 'image', 'video', 'audio'];
'link',
'editable',
'image',
'video',
'audio',
];
for (const context of contexts) { for (const context of contexts) {
const title: string = 'Add link to Linkwarden'; const title: string = 'Add link to Linkwarden';
browser.contextMenus.create({ browser.contextMenus.create({
@@ -346,79 +342,81 @@ browser.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
} }
})(); })();
// Omnibox implementation // Omnibox implementation (not available in Safari)
browser.omnibox.onInputStarted.addListener(async () => { if (hasAPI('omnibox.onInputStarted')) {
const configured = await isConfigured(); browser.omnibox.onInputStarted.addListener(async () => {
const description = configured
? 'Search links in linkwarden'
: 'Please configure the extension first';
browser.omnibox.setDefaultSuggestion({
description: description,
});
});
browser.omnibox.onInputChanged.addListener(
async (
text: string,
suggest: (arg0: { content: string; description: string }[]) => void
) => {
const configured = await isConfigured(); const configured = await isConfigured();
const description = configured
? 'Search links in linkwarden'
: 'Please configure the extension first';
if (!configured) { browser.omnibox.setDefaultSuggestion({
return; description: description,
}
const currentBookmarks = await getBookmarksMetadata();
const searchedBookmarks = currentBookmarks.filter((bookmark) => {
return bookmark.name?.includes(text) || bookmark.url.includes(text);
}); });
});
const bookmarkSuggestions = searchedBookmarks.map((bookmark) => { browser.omnibox.onInputChanged.addListener(
return { async (
content: bookmark.url, text: string,
description: bookmark.name || bookmark.url, suggest: (arg0: { content: string; description: string }[]) => void
}; ) => {
}); const configured = await isConfigured();
suggest(bookmarkSuggestions);
}
);
// This part was taken https://github.com/sissbruecker/linkding-extension/blob/master/src/background.js Thanks to @sissbruecker if (!configured) {
return;
}
browser.omnibox.onInputEntered.addListener( const currentBookmarks = await getBookmarksMetadata();
async (content: string, disposition: OnInputEnteredDisposition) => {
if (!(await isConfigured()) || !content) { const searchedBookmarks = currentBookmarks.filter((bookmark) => {
return; return bookmark.name?.includes(text) || bookmark.url.includes(text);
});
const bookmarkSuggestions = searchedBookmarks.map((bookmark) => {
return {
content: bookmark.url,
description: bookmark.name || bookmark.url,
};
});
suggest(bookmarkSuggestions);
} }
);
const isUrl = /^http(s)?:\/\//.test(content); // This part was taken https://github.com/sissbruecker/linkding-extension/blob/master/src/background.js Thanks to @sissbruecker
const url = isUrl ? content : `lk`;
// Edge doesn't allow updating the New Tab Page (tested with version 117). browser.omnibox.onInputEntered.addListener(
// Trying to do so will throw: "Error: Cannot update NTP tab." async (content: string, disposition: OnInputEnteredDisposition) => {
// As a workaround, open a new tab instead. if (!(await isConfigured()) || !content) {
if (disposition === 'currentTab') { return;
const tabInfo = await getCurrentTabInfo(); }
if (tabInfo.url === 'edge://newtab/') {
disposition = 'newForegroundTab'; const isUrl = /^http(s)?:\/\//.test(content);
const url = isUrl ? content : `lk`;
// Edge doesn't allow updating the New Tab Page (tested with version 117).
// Trying to do so will throw: "Error: Cannot update NTP tab."
// As a workaround, open a new tab instead.
if (disposition === 'currentTab') {
const tabInfo = await getCurrentTabInfo();
if (tabInfo.url === 'edge://newtab/') {
disposition = 'newForegroundTab';
}
}
switch (disposition) {
case 'currentTab':
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
await browser.tabs.update({ url });
break;
case 'newForegroundTab':
await browser.tabs.create({ url });
break;
case 'newBackgroundTab':
await browser.tabs.create({ url, active: false });
break;
} }
} }
);
switch (disposition) { }
case 'currentTab':
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
await browser.tabs.update({ url });
break;
case 'newForegroundTab':
await browser.tabs.create({ url });
break;
case 'newBackgroundTab':
await browser.tabs.create({ url, active: false });
break;
}
}
);