feat: initial commit with Chrome/Firefox builds
This commit is contained in:
parent
57799f3bf5
commit
a24b476d06
|
@ -0,0 +1,44 @@
|
||||||
|
#!/bin/zsh
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
DIST_DIR="$ROOT_DIR/dist"
|
||||||
|
|
||||||
|
mkdir -p "$DIST_DIR"
|
||||||
|
|
||||||
|
# Clean old artifacts
|
||||||
|
rm -f "$DIST_DIR"/*.zip 2>/dev/null || true
|
||||||
|
|
||||||
|
# Copy source to temp dirs for packaging
|
||||||
|
WORK_CHROME=$(mktemp -d)
|
||||||
|
WORK_FIREFOX=$(mktemp -d)
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
rm -rf "$WORK_CHROME" "$WORK_FIREFOX" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
# Common files
|
||||||
|
for f in manifest.json popup.html popup.js; do
|
||||||
|
cp "$ROOT_DIR/$f" "$WORK_CHROME/"
|
||||||
|
cp "$ROOT_DIR/$f" "$WORK_FIREFOX/"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Ensure Firefox manifest has browser_specific_settings with a stable id
|
||||||
|
# If already present, keep as-is.
|
||||||
|
|
||||||
|
# Nothing to change for Chrome; MV3 is fine.
|
||||||
|
|
||||||
|
pushd "$WORK_CHROME" >/dev/null
|
||||||
|
zip -r "$DIST_DIR/ali-sharelink-chrome.zip" . >/dev/null
|
||||||
|
popd >/dev/null
|
||||||
|
|
||||||
|
pushd "$WORK_FIREFOX" >/dev/null
|
||||||
|
zip -r "$DIST_DIR/ali-sharelink-firefox.zip" . >/dev/null
|
||||||
|
popd >/dev/null
|
||||||
|
|
||||||
|
echo "Build complete:"
|
||||||
|
echo " Chrome: $DIST_DIR/ali-sharelink-chrome.zip"
|
||||||
|
echo " Firefox: $DIST_DIR/ali-sharelink-firefox.zip"
|
||||||
|
|
||||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"manifest_version": 3,
|
||||||
|
"name": "Ali Sharelink",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Get a clean, canonical share link for AliExpress/Alibaba products, with optional shortlink.",
|
||||||
|
"action": {
|
||||||
|
"default_title": "Ali Sharelink",
|
||||||
|
"default_popup": "popup.html"
|
||||||
|
},
|
||||||
|
"permissions": [
|
||||||
|
"activeTab",
|
||||||
|
"tabs",
|
||||||
|
"clipboardWrite"
|
||||||
|
],
|
||||||
|
"host_permissions": [
|
||||||
|
"https://is.gd/*"
|
||||||
|
],
|
||||||
|
"icons": {},
|
||||||
|
"browser_specific_settings": {
|
||||||
|
"gecko": {
|
||||||
|
"id": "alisharelink@local",
|
||||||
|
"strict_min_version": "109.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Ali Sharelink</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; margin: 12px; width: 360px; }
|
||||||
|
h1 { font-size: 16px; margin: 0 0 8px; }
|
||||||
|
.row { margin: 8px 0; }
|
||||||
|
input[readonly] { width: 100%; padding: 6px 8px; border: 1px solid #ccc; border-radius: 6px; font-size: 12px; }
|
||||||
|
button { padding: 6px 10px; font-size: 12px; border-radius: 6px; border: 1px solid #999; background: #fff; cursor: pointer; }
|
||||||
|
button.primary { background: #0a84ff; border-color: #0a84ff; color: #fff; }
|
||||||
|
.actions { display: flex; gap: 8px; }
|
||||||
|
.muted { color: #666; font-size: 12px; }
|
||||||
|
.small { font-size: 11px; }
|
||||||
|
.error { color: #b00020; }
|
||||||
|
.ok { color: #007a3d; }
|
||||||
|
label { display: block; font-size: 12px; margin-bottom: 4px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Ali Sharelink</h1>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<label>Current URL</label>
|
||||||
|
<input id="currentUrl" type="text" readonly>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<label>Clean Link</label>
|
||||||
|
<input id="cleanUrl" type="text" readonly>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row actions">
|
||||||
|
<button id="copyClean" class="primary">Copy Clean</button>
|
||||||
|
<button id="shortenCopy">Shorten + Copy</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row small muted" id="status"></div>
|
||||||
|
|
||||||
|
<script src="popup.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,141 @@
|
||||||
|
/* global chrome, browser */
|
||||||
|
(function () {
|
||||||
|
const $ = (id) => document.getElementById(id);
|
||||||
|
const statusEl = $("status");
|
||||||
|
|
||||||
|
function setStatus(message, kind) {
|
||||||
|
statusEl.textContent = message || "";
|
||||||
|
statusEl.className = `row small ${kind === "error" ? "error" : kind === "ok" ? "ok" : "muted"}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getApi() {
|
||||||
|
if (typeof chrome !== "undefined" && chrome.tabs) return chrome;
|
||||||
|
if (typeof browser !== "undefined" && browser.tabs) return browser;
|
||||||
|
throw new Error("Unsupported browser API");
|
||||||
|
}
|
||||||
|
|
||||||
|
function tabsQuery(api, queryInfo) {
|
||||||
|
// chrome: callback-based, firefox: promise-based
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
const maybePromise = api.tabs.query(queryInfo, (tabs) => {
|
||||||
|
if (chrome && chrome.runtime && chrome.runtime.lastError) {
|
||||||
|
reject(chrome.runtime.lastError);
|
||||||
|
} else if (tabs) {
|
||||||
|
resolve(tabs);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (maybePromise && typeof maybePromise.then === "function") {
|
||||||
|
maybePromise.then(resolve, reject);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyToClipboard(text) {
|
||||||
|
return navigator.clipboard.writeText(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
function canonicalizeAliExpress(urlString) {
|
||||||
|
const url = new URL(urlString);
|
||||||
|
const host = url.hostname;
|
||||||
|
const path = url.pathname;
|
||||||
|
|
||||||
|
// Prefer global .com canonical
|
||||||
|
const canonicalHost = "www.aliexpress.com";
|
||||||
|
let productId = null;
|
||||||
|
|
||||||
|
const itemMatch = path.match(/\/(item|i)\/(\d+)\.html/i);
|
||||||
|
if (itemMatch) {
|
||||||
|
productId = itemMatch[2];
|
||||||
|
}
|
||||||
|
if (!productId) {
|
||||||
|
const xObjectId = url.searchParams.get("x_object_id") || url.searchParams.get("item_id");
|
||||||
|
if (xObjectId && /^\d+$/.test(xObjectId)) {
|
||||||
|
productId = xObjectId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (productId) {
|
||||||
|
return `https://${canonicalHost}/item/${productId}.html`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: strip params and hash
|
||||||
|
return `https://${canonicalHost}${path}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function canonicalizeAlibaba(urlString) {
|
||||||
|
const url = new URL(urlString);
|
||||||
|
// Keep origin + pathname only
|
||||||
|
return `${url.origin}${url.pathname}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function canonicalize(urlString) {
|
||||||
|
const url = new URL(urlString);
|
||||||
|
const hostname = url.hostname.toLowerCase();
|
||||||
|
|
||||||
|
if (hostname.includes("aliexpress.")) {
|
||||||
|
return { vendor: "AliExpress", url: canonicalizeAliExpress(urlString) };
|
||||||
|
}
|
||||||
|
if (hostname.includes("alibaba.")) {
|
||||||
|
return { vendor: "Alibaba", url: canonicalizeAlibaba(urlString) };
|
||||||
|
}
|
||||||
|
// Unknown: just strip params/hash
|
||||||
|
return { vendor: "Unknown", url: `${url.origin}${url.pathname}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function shortenWithIsGd(longUrl) {
|
||||||
|
const endpoint = `https://is.gd/create.php?format=simple&url=${encodeURIComponent(longUrl)}`;
|
||||||
|
const res = await fetch(endpoint, { method: "GET" });
|
||||||
|
const text = await res.text();
|
||||||
|
if (!res.ok) throw new Error(text || `Shorten failed (${res.status})`);
|
||||||
|
if (!/^https?:\/\//i.test(text)) throw new Error(text || "Shorten failed");
|
||||||
|
return text.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
setStatus("Reading current tab…");
|
||||||
|
const api = getApi();
|
||||||
|
try {
|
||||||
|
const tabs = await tabsQuery(api, { active: true, currentWindow: true });
|
||||||
|
const active = tabs && tabs[0];
|
||||||
|
if (!active || !active.url) {
|
||||||
|
setStatus("No active tab URL found", "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$("currentUrl").value = active.url;
|
||||||
|
|
||||||
|
const { vendor, url } = canonicalize(active.url);
|
||||||
|
$("cleanUrl").value = url;
|
||||||
|
setStatus(`Detected ${vendor}. Clean link ready.`, "ok");
|
||||||
|
|
||||||
|
$("copyClean").addEventListener("click", async () => {
|
||||||
|
try {
|
||||||
|
await copyToClipboard($("cleanUrl").value);
|
||||||
|
setStatus("Clean link copied.", "ok");
|
||||||
|
} catch (e) {
|
||||||
|
setStatus(`Copy failed: ${e.message}`, "error");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$("shortenCopy").addEventListener("click", async () => {
|
||||||
|
setStatus("Shortening with is.gd…");
|
||||||
|
try {
|
||||||
|
const shortUrl = await shortenWithIsGd($("cleanUrl").value);
|
||||||
|
await copyToClipboard(shortUrl);
|
||||||
|
setStatus("Short link copied.", "ok");
|
||||||
|
} catch (e) {
|
||||||
|
setStatus(`Shorten failed: ${e.message}`, "error");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
setStatus(`Error: ${e.message}`, "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", init);
|
||||||
|
})();
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue