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