From a24b476d06ae5a08749c24b074267dbff2b56760 Mon Sep 17 00:00:00 2001 From: automation Date: Wed, 10 Sep 2025 10:02:47 -0400 Subject: [PATCH] feat: initial commit with Chrome/Firefox builds --- build.sh | 44 ++++++++++ dist/ali-sharelink-chrome.zip | Bin 0 -> 2962 bytes dist/ali-sharelink-firefox.zip | Bin 0 -> 2962 bytes manifest.json | 26 ++++++ popup.html | 46 +++++++++++ popup.js | 141 +++++++++++++++++++++++++++++++++ 6 files changed, 257 insertions(+) create mode 100644 build.sh create mode 100644 dist/ali-sharelink-chrome.zip create mode 100644 dist/ali-sharelink-firefox.zip create mode 100644 manifest.json create mode 100644 popup.html create mode 100644 popup.js 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 0000000000000000000000000000000000000000..07c180319bb036b43368e8d450fba14d7cecfbbf GIT binary patch literal 2962 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 R8USG5AD{NuRNkMbe*tSzKX(8C literal 0 HcmV?d00001 diff --git a/dist/ali-sharelink-firefox.zip b/dist/ali-sharelink-firefox.zip new file mode 100644 index 0000000000000000000000000000000000000000..07c180319bb036b43368e8d450fba14d7cecfbbf GIT binary patch literal 2962 zcma);cTf}98pT5(v;+uEBdD}c7Z4>xP%uCO(hc3Eiii|LFA4$zQluy*L3%V&q=qIf zC?&2mL8OJMhyp^u07{T%dH8nby>(~y&6_)SZkf5~`{SPZ&JSzK&LIK-0Js6BSS82M z(w)~}P5=NY`19s4AizH$G(goS*w%&zz|M@PdHtNB*T4Waj!iZI;NLHc&%X?sMnc>6 z6ffM7L(3^mGAzY3h^PFP%ZSa!+FH+zIq-LH-Qs{j>noM=86zy(F5LMml*5B+K_p6j zTy6T1T1-sxvRwzMVspVRgyRm}ZDq;20>r|A$r>B00W9p{8$o?V&jySP0xk7Y8nJiC zL)+Kn*`=q_pX();xBI>*ThEsXm&#_EM>)j2R)F+Z1C}~w5u=#4vG^Kz=xzH8y}5P$ zO9uSXne&GC1LX7sJAQXzEr5CB7oL}NEi zmUS5w6v80mCZ90Q7-gliD`X<#YqL1des~dRh+MMKf^lY5`jw}E1G=P8R84TfbhP3q zN=&*@s%i-qW!J6VI8Q1xzG!}*ECPJe!Prd#RT(1f*|KFN(lu z2wKm_#R=U7+QC?rf%}#K0?)@tPa>2)h3Qoy;0OChq;54NKU5Ea93RDhnr)e#8fBG1 z`G~ARbBG5#lvTjSTQ`H$13jUG-%GSLif*p%)7$#Knv28jdMO$U9dcFz&M zoI>sMQ5%g+-Lq4TC?+YY6gfW9;!RHXxT%*PlD|M4QGF9I15m{oNrlbuqz*KH8_&Tn_u>ap-6Qx8AoTR8E}kaPnYu_ zb0u9jvrd;^Y{)&Kn?}dKt3vTnpPgvB6pEOyI;lzqk*Oa;Zd<#2;f#Q-uAVIXZ32fp z-mIIpX)?gy7-vL3_dD^$W3!wYkpx>$vZVGxFex_&siI0xSEHKf8|>=KXgYhO7gT6w zdEf_2C6E@4uga?X@Nd7dX1E(HSQE#;)OC^bvcR??VtSx5x${Lkpaad7^qYd5Cnczt zcZ}3a&9dJoO=d*A92U(Qo-Rah%1d@N^rgd!HN|T z1bE>a$#CC-{&+|Esb$SqC1;Q}UDaU@7PAzg`Zy=WbApLU^4+1)ER7aj@TKj+;QDb8 z_-V)6G2jKpTD;9Ia7zJ1N8kCj%s>jWzTdCC9-Z1nc(B1I zD8IfGKUDv!%$=;PlAAW_HXG^yePgo8?<|}d$hLL^g^jBnM((5pj;uDlaAUVgSskxx zUd6UPn2U8Wnv-=@Z*vo7l?_dwcMa){riGuP*pzIv6LBHSh`ES9EC1R)dLwPdD(3tnatx?ajv&@Cz)NAeaUD+5!BRsct(Qyf46ZW+msZ;7 z^}6?vM*R{pX|DlDwXAr?X`P&jdD&^P2+LPZ?hog*K%I^fZY~)!Dj)Ak2Bx{_q5Z>- zR+zTk@$+?b5K5f-+K|lVaM7XTz381Vljpc{U&K}|L15}fH}SXZX&w!BbJVq~b6n`J zer8h{N>#8Rm&74wt%5X@rj9Rp*Va>JAxgZ(x1m1o5;x}`w14O4d>6|E7mTRT!kDZ~ zq&Vh0&I!7O>^t)8LYKxzycMD*{JgKfoA9D|FvAyRSCI+8@KugUZ11ghZ+&ME#N|lp ze}SEhddH;h#$C;hDZ0$>5H!*FaF^K>+r*R8!T*52=!|dW0yaud#C5JxM!^k5zQ2tytQu$Ycb+Ci^ok&M+(c~zWbq5Q3Upq-Fzp9yx#9HM zVtyp+74rscKx^PuE}M@oP+y!8^?u0}`%9Q`pV6sG00ID?K>z^nf5OBo#25F!;)G;V z>o=_iee+G!9z^n)v=VAgRz9R~LbNAC(t0RXS}|3_KnQ;CxytUEPO_DU|6DNfB-wlDEIuwHH~&=o_eexK)+YgP2lfTg^hIy81x zx8bvs6w5%(^jq2AOG+niC_B z;sn~s`UTC>x9{#jUeAQjJf~!%&C>AN{CF+x&y)}AtWbV|mQj~iG{J?7-IUU?LFf}v zrJJnbr&SN_b7*uWMp!#n+o>ay`P;pMPnPT4J>uTJ6lts31%bP?KS*j_N;pCJarskZ z17UBC`>@dne5aWc4HNdsV+)FJEN)D5?04_*R)jfWO}ek)c!Vh{pH^h>S=QzrE3I}$ zVWTMXfdizKNW)GhQ>BaNjW5Mdv~RYcd>!L`g=APX1iXFWoQ86m=rzO}&{4YF{D+p` zBznp+orz8B&1;G?7P;7C^+6M@w@e$`Z0VidB}=)s-8N`D zERS4!?7F>g9Vf_jW*LO3BsVQ*5P(SSoV*Sc-jfBWjDm`%+$y0d_(7tXKPHo zcH`Z3YJ-R_iZzQp(5*TZP#+Ev-{yGpz4@a~0xi7Nxp0s!{I=kIq4Zgb+R^;tQmec% zB@0RN&31<@S3B z(Om0^vCX$sdOSK?Xu=v_gW{>iZd^x7p;7H8pMgW+hwdGFKFlbb?~3@s9$b5>xN=*d zrfHZr`J3nG)3)nw5_RPqn+im_aiHo_OrZ3@=^#Sn0BLe9W^K^f5_rVb-CZ%{=*1s` z#%jbiUT+qo{>FPle8MIr-%Fe8_T@yjNX!e{;%lOxs{(1gGAHVwN z3P#O9ls|=j^IRF(JT@W+GKI@9F{`U{n?5@k(T}|bc3^aBc{BiP$_6~j{!cIp+lN#D zVE-2YMX^73|3y>KpJ@6wEd9IMuifacYUcaE`={EUz3Knc`_&YG)jPlg_`5~^NfNYQ R8USG5AD{NuRNkMbe*tSzKX(8C literal 0 HcmV?d00001 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); +})(); + +