Merge pull request #144 from linkwarden/dev

v1.5.4
This commit is contained in:
Daniel
2026-05-03 12:42:08 -04:00
committed by GitHub
39 changed files with 1731 additions and 130 deletions
+6
View File
@@ -10,6 +10,7 @@ lerna-debug.log*
node_modules
dist
dist-ssr
dist-safari
*.local
dist.crx
dist.pem
@@ -26,3 +27,8 @@ dist.pem
*.sw?
linkwarden.zip
# Safari / Xcode
**/build/
**/DerivedData/
*.xcuserdata
+1 -1
View File
@@ -4,7 +4,7 @@
"name": "Linkwarden",
"description": "The browser extension for Linkwarden.",
"homepage_url": "https://linkwarden.app/",
"version": "1.5.1",
"version": "1.5.4",
"action": {
"default_popup": "index.html",
"default_icon": {
+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.4",
"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": {
"dev": "vite",
"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",
"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)
}
}
}
}
+51 -17
View File
@@ -17,15 +17,15 @@ import { Button } from './ui/Button.tsx';
import { TagInput } from './TagInput.tsx';
import { Textarea } from './ui/Textarea.tsx';
import { getCurrentTabInfo, updateBadge } from '../lib/utils.ts';
import { useEffect, useState } from 'react';
import { useMutation, useQuery } from '@tanstack/react-query';
import { useEffect, useMemo, useState } from 'react';
import { useInfiniteQuery, useMutation, useQuery } from '@tanstack/react-query';
import { getConfig, isConfigured as getIsConfigured } from '../lib/config.ts';
import { checkLinkExists, postLink } from '../lib/actions/links.ts';
import { AxiosError } from 'axios';
import { toast } from '../../hooks/use-toast.ts';
import { Toaster } from './ui/Toaster.tsx';
import { getCollections } from '../lib/actions/collections.ts';
import { getTags } from '../lib/actions/tags.ts';
import { getShouldUseTagSearch, getTags } from '../lib/actions/tags.ts';
import { ExternalLink, X } from 'lucide-react';
import { Popover, PopoverContent, PopoverTrigger } from './ui/Popover.tsx';
import { CaretSortIcon } from '@radix-ui/react-icons';
@@ -44,6 +44,7 @@ const BookmarkForm = () => {
const [openCollections, setOpenCollections] = useState<boolean>(false);
const [uploadImage, setUploadImage] = useState<boolean>(false);
const [state, setState] = useState<'capturing' | 'uploading' | null>(null);
const [tagSearch, setTagSearch] = useState<string>('');
const [isConfigured, setIsConfigured] = useState(false);
const [isDuplicate, setIsDuplicate] = useState(false);
@@ -195,24 +196,46 @@ const BookmarkForm = () => {
enabled: isConfigured,
});
const {
isLoading: loadingTags,
data: tags,
error: tagsError,
} = useQuery({
queryKey: ['tags'],
queryFn: async () => {
const response = await getTags(
const { data: shouldUseTagSearch = false } = useQuery({
queryKey: ['tag-search-support', config?.baseUrl, config?.apiKey],
queryFn: async () =>
await getShouldUseTagSearch(
config?.baseUrl as string,
config?.apiKey as string
),
enabled: isConfigured && openOptions,
});
const effectiveTagSearch = shouldUseTagSearch ? tagSearch : '';
const {
isLoading: loadingTags,
data: tagsData,
error: tagsError,
hasNextPage,
isFetchingNextPage,
fetchNextPage,
} = useInfiniteQuery(
['tags', config?.baseUrl, config?.apiKey, effectiveTagSearch],
async ({ pageParam = 0 }) => {
return await getTags(
config?.baseUrl as string,
config?.apiKey as string,
pageParam,
effectiveTagSearch
);
},
{
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
enabled: isConfigured && openOptions,
}
);
return response.data.response.sort((a, b) => {
return a.name.localeCompare(b.name);
});
},
enabled: isConfigured,
});
const tags = useMemo(() => {
return (
tagsData?.pages
.flatMap((page) => page.tags)
.sort((a, b) => a.name.localeCompare(b.name)) ?? []
);
}, [tagsData]);
return (
<div>
@@ -427,18 +450,29 @@ const BookmarkForm = () => {
onChange={field.onChange}
value={[{ name: 'Loading tags...' }]}
tags={[{ id: 1, name: 'Loading tags...' }]}
hasNextPage={false}
isFetchingNextPage={false}
/>
) : tagsError ? (
<TagInput
onChange={field.onChange}
value={[{ name: 'Not found' }]}
tags={[{ id: 1, name: 'Not found' }]}
hasNextPage={false}
isFetchingNextPage={false}
/>
) : (
<TagInput
onChange={field.onChange}
value={field.value ?? []}
tags={tags}
hasNextPage={hasNextPage}
isFetchingNextPage={isFetchingNextPage}
onSearchChange={setTagSearch}
onReachEnd={() => {
if (!hasNextPage || isFetchingNextPage) return;
void fetchNextPage();
}}
/>
)}
<FormMessage />
+61 -13
View File
@@ -1,4 +1,4 @@
import { FC, useState } from 'react';
import { FC, UIEvent, useMemo, useState } from 'react';
import { Button } from './ui/Button.tsx';
import { Popover, PopoverContent, PopoverTrigger } from './ui/Popover.tsx';
import { Check, ChevronsUpDown } from 'lucide-react';
@@ -8,18 +8,53 @@ import {
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from './ui/Command.tsx';
import { cn } from '../lib/utils.ts';
import { ResponseTags } from '../lib/actions/tags.ts';
interface TagInputProps {
onChange: (tags: { name: string }[]) => void;
value: { name: string; id?: number }[];
tags: { id: number; name: string }[] | undefined;
tags: Pick<ResponseTags, 'id' | 'name'>[] | undefined;
hasNextPage?: boolean;
isFetchingNextPage?: boolean;
onReachEnd?: () => void;
onSearchChange?: (value: string) => void;
}
export const TagInput: FC<TagInputProps> = ({ value, onChange, tags }) => {
export const TagInput: FC<TagInputProps> = ({
value,
onChange,
tags,
hasNextPage,
isFetchingNextPage,
onReachEnd,
onSearchChange,
}) => {
const [open, setOpen] = useState<boolean>(false);
const [inputValue, setInputValue] = useState<string>('');
const filteredTags = useMemo(() => {
if (!Array.isArray(tags)) return [];
const normalizedInputValue = inputValue.trim().toLowerCase();
if (!normalizedInputValue) return tags;
return tags.filter((tag) =>
tag.name.toLowerCase().includes(normalizedInputValue)
);
}, [inputValue, tags]);
const handleListScroll = (event: UIEvent<HTMLDivElement>) => {
if (!hasNextPage || isFetchingNextPage || !onReachEnd) return;
const target = event.currentTarget;
const reachedBottom =
target.scrollTop + target.clientHeight >= target.scrollHeight - 16;
if (reachedBottom) onReachEnd();
};
function handleAddTag() {
if (inputValue && value.some((tagObj) => tagObj.name === inputValue))
@@ -27,6 +62,7 @@ export const TagInput: FC<TagInputProps> = ({ value, onChange, tags }) => {
if (inputValue) {
const newTags = [...value, { name: inputValue }];
setInputValue('');
onSearchChange?.('');
onChange(newTags);
}
}
@@ -53,32 +89,34 @@ export const TagInput: FC<TagInputProps> = ({ value, onChange, tags }) => {
</Button>
</PopoverTrigger>
<div className="min-w-full inset-x-0">
<PopoverContent className="min-w-full p-0 overflow-y-auto max-h-[200px]">
<Command className="flex-grow min-w-full">
<PopoverContent className="min-w-full p-0">
<Command className="flex-grow min-w-full" shouldFilter={false}>
<CommandInput
className="min-w-[280px]"
placeholder="Search tag or add tag (Enter)"
value={inputValue}
onValueChange={setInputValue}
onValueChange={(value) => {
setInputValue(value);
onSearchChange?.(value);
}}
onKeyDown={(e) => {
if (e.key === 'Enter') {
handleAddTag();
}
}}
/>
<CommandList
className="max-h-[200px]"
onScroll={handleListScroll}
>
<CommandEmpty>No tag found.</CommandEmpty>
{Array.isArray(tags) && (
<CommandGroup className="w-full">
{tags
.filter((tag) =>
tag.name
.toLowerCase()
.includes(inputValue.trim().toLowerCase())
)
.map((tag: { name: string }) => (
{filteredTags.map((tag: { name: string }) => (
<CommandItem
className="w-full"
key={tag.name}
value={tag.name}
onSelect={() => {
if (Array.isArray(value)) {
if (value.some((v) => v.name === tag.name)) {
@@ -102,8 +140,18 @@ export const TagInput: FC<TagInputProps> = ({ value, onChange, tags }) => {
{tag.name}
</CommandItem>
))}
{isFetchingNextPage ? (
<CommandItem
className="w-full"
value="Loading more tags..."
disabled
>
Loading more tags...
</CommandItem>
) : null}
</CommandGroup>
)}
</CommandList>
</Command>
</PopoverContent>
</div>
+185 -4
View File
@@ -1,6 +1,6 @@
import axios from 'axios';
interface ResponseTags {
export interface ResponseTags {
id: number;
name: string;
ownerId: number;
@@ -11,11 +11,192 @@ interface ResponseTags {
};
}
export async function getTags(baseUrl: string, apiKey: string) {
const url = `${baseUrl}/api/v1/tags`;
return await axios.get<{ response: ResponseTags[] }>(url, {
type ConfigResponse = {
response: {
INSTANCE_VERSION?: string | null;
};
};
type LegacyTagsResponse = {
response:
| ResponseTags[]
| { tags: ResponseTags[]; nextCursor?: number | null };
};
type PaginatedTagsResponse = {
data: {
tags: ResponseTags[];
nextCursor?: number | null;
};
};
const MIN_TAG_PAGINATION_VERSION = '2.14.0';
const MIN_TAG_SEARCH_VERSION = '2.14.1';
const TAG_SORT_NAME_ASC = 2;
const tagFeatureSupportCache = new Map<
string,
{
shouldUsePagination: boolean;
shouldUseSearch: boolean;
}
>();
export type TagsPage = {
tags: ResponseTags[];
nextCursor: number | null;
};
const normalizeVersion = (version?: string | null) => {
if (!version) return null;
return version
.replace(/^v/i, '')
.split('-')[0]
.split('.')
.map((part) => Number(part.replace(/\D/g, '')) || 0);
};
const isAtLeastInstanceVersion = (
version?: string | null,
minimumVersion?: string | null
) => {
const normalizedVersion = normalizeVersion(version);
const normalizedMinimumVersion = normalizeVersion(minimumVersion);
if (!normalizedVersion || !normalizedMinimumVersion) return false;
const length = Math.max(
normalizedVersion.length,
normalizedMinimumVersion.length
);
for (let index = 0; index < length; index++) {
const left = normalizedVersion[index] ?? 0;
const right = normalizedMinimumVersion[index] ?? 0;
if (left > right) return true;
if (left < right) return false;
}
return true;
};
const extractTagsPayload = (
data: LegacyTagsResponse | PaginatedTagsResponse
): { tags: ResponseTags[]; nextCursor: number | null } => {
if (Array.isArray((data as LegacyTagsResponse).response)) {
return {
tags: (data as LegacyTagsResponse).response as ResponseTags[],
nextCursor: null,
};
}
if (
(data as LegacyTagsResponse).response &&
!Array.isArray((data as LegacyTagsResponse).response)
) {
const response = (data as LegacyTagsResponse).response as {
tags: ResponseTags[];
nextCursor?: number | null;
};
return {
tags: response.tags,
nextCursor: response.nextCursor ?? null,
};
}
const response = (data as PaginatedTagsResponse).data;
return {
tags: response.tags,
nextCursor: response.nextCursor ?? null,
};
};
const getInstanceVersion = async (baseUrl: string, apiKey: string) => {
try {
const response = await axios.get<ConfigResponse>(
`${baseUrl}/api/v1/config`,
{
headers: {
Authorization: `Bearer ${apiKey}`,
},
}
);
return response.data.response.INSTANCE_VERSION ?? null;
} catch (_error) {
return null;
}
};
const getTagFeatureSupportCacheKey = (baseUrl: string, apiKey: string) =>
`${baseUrl}::${apiKey}`;
const getTagFeatures = async (baseUrl: string, apiKey: string) => {
const cacheKey = getTagFeatureSupportCacheKey(baseUrl, apiKey);
const cachedValue = tagFeatureSupportCache.get(cacheKey);
if (cachedValue !== undefined) return cachedValue;
const instanceVersion = await getInstanceVersion(baseUrl, apiKey);
const nextValue = {
shouldUsePagination: isAtLeastInstanceVersion(
instanceVersion,
MIN_TAG_PAGINATION_VERSION
),
shouldUseSearch: isAtLeastInstanceVersion(
instanceVersion,
MIN_TAG_SEARCH_VERSION
),
};
tagFeatureSupportCache.set(cacheKey, nextValue);
return nextValue;
};
export const getShouldUseTagSearch = async (baseUrl: string, apiKey: string) =>
(await getTagFeatures(baseUrl, apiKey)).shouldUseSearch;
export async function getTags(
baseUrl: string,
apiKey: string,
cursor = 0,
search = ''
): Promise<TagsPage> {
const { shouldUsePagination, shouldUseSearch } = await getTagFeatures(
baseUrl,
apiKey
);
const headers = {
Authorization: `Bearer ${apiKey}`,
};
const searchParams = new URLSearchParams();
const normalizedSearch = search.trim();
searchParams.set('sort', String(TAG_SORT_NAME_ASC));
if (shouldUsePagination) {
searchParams.set('cursor', String(cursor));
}
if (shouldUseSearch && normalizedSearch) {
searchParams.set('search', normalizedSearch);
}
const initialResponse = await axios.get<
LegacyTagsResponse | PaginatedTagsResponse
>(`${baseUrl}/api/v1/tags?${searchParams.toString()}`, {
headers,
});
const payload = extractTagsPayload(initialResponse.data);
return {
tags: payload.tags,
nextCursor: shouldUsePagination ? payload.nextCursor : null,
};
}
+65 -8
View File
@@ -72,21 +72,60 @@ const drawImagesOnCanvas = async (
};
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({
target: { tabId },
func,
args,
});
return results[0]?.result;
} else {
}
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> {
const tabs = await browser.tabs.query({ active: true, currentWindow: true });
const tab = tabs[0];
@@ -218,8 +257,7 @@ async function captureFullPageScreenshot(): Promise<Blob> {
const dataUrl = await browser.tabs.captureVisibleTab(tab.windowId!, {
format: 'png',
});
const blob = await fetch(dataUrl).then((res) => res.blob());
blobs.push(blob);
blobs.push(dataUrlToBlob(dataUrl));
}
const canvas = document.createElement('canvas');
@@ -235,11 +273,30 @@ async function captureFullPageScreenshot(): Promise<Blob> {
dpr
);
await executeScript(tab.id, removeHideScrollbarClass);
await executeScript(tab.id, restoreFixedElements, [originalStyles]);
await executeScript(tab.id, removeDisableSmoothScrollbarClass);
await safeExecuteScript(tab.id, removeHideScrollbarClass);
await safeExecuteScript(tab.id, restoreFixedElements, [originalStyles]);
await safeExecuteScript(tab.id, removeDisableSmoothScrollbarClass);
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();
}
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) {
if (!tabId) return;
+16 -18
View File
@@ -1,6 +1,8 @@
import {
getBrowser,
getCurrentTabInfo,
hasAPI,
isSafari,
updateBadge,
} from '../../@/lib/utils.ts';
// import BookmarkTreeNode = chrome.bookmarks.BookmarkTreeNode;
@@ -240,7 +242,7 @@ async function genericOnClick(
}
default:
// Handle cases where sync is enabled or not
if (syncBookmarks) {
if (syncBookmarks && hasAPI('bookmarks.create')) {
browser.bookmarks.create({
parentId: '1',
title: tab.title,
@@ -277,15 +279,9 @@ async function genericOnClick(
}
browser.runtime.onInstalled.addListener(async function () {
// Create one test item for each context type.
const contexts: ContextType[] = [
'page',
'selection',
'link',
'editable',
'image',
'video',
'audio',
];
const contexts: ContextType[] = isSafari()
? ['page', 'selection', 'link']
: ['page', 'selection', 'link', 'editable', 'image', 'video', 'audio'];
for (const context of contexts) {
const title: string = 'Add link to Linkwarden';
browser.contextMenus.create({
@@ -346,9 +342,10 @@ 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')) {
browser.omnibox.onInputStarted.addListener(async () => {
const configured = await isConfigured();
const description = configured
? 'Search links in linkwarden'
@@ -357,9 +354,9 @@ browser.omnibox.onInputStarted.addListener(async () => {
browser.omnibox.setDefaultSuggestion({
description: description,
});
});
});
browser.omnibox.onInputChanged.addListener(
browser.omnibox.onInputChanged.addListener(
async (
text: string,
suggest: (arg0: { content: string; description: string }[]) => void
@@ -384,11 +381,11 @@ browser.omnibox.onInputChanged.addListener(
});
suggest(bookmarkSuggestions);
}
);
);
// This part was taken https://github.com/sissbruecker/linkding-extension/blob/master/src/background.js Thanks to @sissbruecker
// This part was taken https://github.com/sissbruecker/linkding-extension/blob/master/src/background.js Thanks to @sissbruecker
browser.omnibox.onInputEntered.addListener(
browser.omnibox.onInputEntered.addListener(
async (content: string, disposition: OnInputEnteredDisposition) => {
if (!(await isConfigured()) || !content) {
return;
@@ -421,4 +418,5 @@ browser.omnibox.onInputEntered.addListener(
break;
}
}
);
);
}