feat: initial commit with Chrome/Firefox builds

This commit is contained in:
automation 2025-09-10 10:02:47 -04:00
parent 57799f3bf5
commit a24b476d06
6 changed files with 257 additions and 0 deletions

44
build.sh Normal file
View File

@ -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"

BIN
dist/ali-sharelink-chrome.zip vendored Normal file

Binary file not shown.

BIN
dist/ali-sharelink-firefox.zip vendored Normal file

Binary file not shown.

26
manifest.json Normal file
View File

@ -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"
}
}
}

46
popup.html Normal file
View File

@ -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>

141
popup.js Normal file
View File

@ -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);
})();