diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..b22cc5a --- /dev/null +++ b/build.sh @@ -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" + + diff --git a/dist/ali-sharelink-chrome.zip b/dist/ali-sharelink-chrome.zip new file mode 100644 index 0000000..07c1803 Binary files /dev/null and b/dist/ali-sharelink-chrome.zip differ diff --git a/dist/ali-sharelink-firefox.zip b/dist/ali-sharelink-firefox.zip new file mode 100644 index 0000000..07c1803 Binary files /dev/null and b/dist/ali-sharelink-firefox.zip differ diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..488c0ed --- /dev/null +++ b/manifest.json @@ -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" + } + } +} + diff --git a/popup.html b/popup.html new file mode 100644 index 0000000..8a239d8 --- /dev/null +++ b/popup.html @@ -0,0 +1,46 @@ + + + + + + Ali Sharelink + + + +

Ali Sharelink

+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + + + + + diff --git a/popup.js b/popup.js new file mode 100644 index 0000000..ea8be71 --- /dev/null +++ b/popup.js @@ -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); +})(); + +