167 lines
6.7 KiB
JavaScript
167 lines
6.7 KiB
JavaScript
import { mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
import path from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
import sharp from 'sharp'
|
|
import { decodeIco, isIco } from 'icojs'
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
const root = path.resolve(__dirname, '..')
|
|
const outDir = path.join(root, 'public', 'icons')
|
|
const linksPath = path.join(root, 'src', 'data', 'links.json')
|
|
const SIZE = 96
|
|
const WEBP_QUALITY = 72
|
|
|
|
const SOURCE = {
|
|
nomadro: 'https://nav.hackrobot.cn/assets/images/nomadro2.png',
|
|
nomadlist: 'https://nav.hackrobot.cn/assets/images/nomadlist.png',
|
|
nomadcna: 'https://nav.hackrobot.cn/assets/images/nomadcna.jpg',
|
|
chatgpt: 'https://nav.hackrobot.cn/assets/images/logos/chatgpt.png',
|
|
grok: 'https://nav.hackrobot.cn/assets/favicon_1759046289851.png',
|
|
'1psim': 'https://nav.hackrobot.cn/assets/images/1psim.png',
|
|
yesim: 'https://nav.hackrobot.cn/assets/images/yesim.png',
|
|
tello: 'https://nav.hackrobot.cn/assets/images/tello.png',
|
|
'9esim': 'https://nav.hackrobot.cn/assets/images/9esim.png',
|
|
wise: 'https://nav.hackrobot.cn/assets/images/logos/wise.png',
|
|
okx: 'https://nav.hackrobot.cn/assets/images/logos/okx.png',
|
|
safepal: 'https://nav.hackrobot.cn/assets/images/logos/safepal.png',
|
|
paypal: 'https://nav.hackrobot.cn/assets/images/paypal.png',
|
|
bmc: 'https://nav.hackrobot.cn/assets/images/buymecoffee.png',
|
|
jms: 'https://nav.hackrobot.cn/assets/images/logos/proxy.png',
|
|
ykk: 'https://nav.hackrobot.cn/assets/images/logos/ykk.jpg',
|
|
vultr: 'https://nav.hackrobot.cn/assets/images/logos/vultr.png',
|
|
bwh: 'https://nav.hackrobot.cn/assets/images/logos/bandwagonhost.jpg',
|
|
racknerd: 'https://nav.hackrobot.cn/assets/images/logos/RackNerd.jpg',
|
|
qcloud: 'https://nav.hackrobot.cn/assets/images/tengxunyun.png',
|
|
aliyun: 'https://nav.hackrobot.cn/assets/images/aliyun.jpg',
|
|
colocrossing: 'https://nav.hackrobot.cn/assets/images/logos/colocrossing.png',
|
|
iproyal: 'https://nav.hackrobot.cn/assets/images/logos/iproyal.jpg',
|
|
'proxy-cheap': 'https://nav.hackrobot.cn/assets/images/logos/proxy-cheap.jpg',
|
|
smartproxy: 'https://nav.hackrobot.cn/assets/images/logos/smartproxy.jpg',
|
|
lisa: 'https://nav.hackrobot.cn/assets/images/lisa.png',
|
|
voyracloud: 'https://nav.hackrobot.cn/assets/images/voyracloud.jpg',
|
|
rpi: 'https://nav.hackrobot.cn/assets/images/logos/raspberrypi.png',
|
|
'mini-pc': 'https://nav.hackrobot.cn/assets/images/logos/lingke.jpg',
|
|
gpu: 'https://nav.hackrobot.cn/assets/images/logos/nvidia.png',
|
|
'3x-ui': 'https://nav.hackrobot.cn/assets/images/logos/3x.jpg',
|
|
mihomo: 'https://nav.hackrobot.cn/assets/images/mihomo.jpg',
|
|
frp: 'https://nav.hackrobot.cn/assets/images/logos/frp.png',
|
|
wireguard: 'https://nav.hackrobot.cn/assets/images/wireguard.png',
|
|
lineageos: 'https://nav.hackrobot.cn/assets/images/logos/lineageos.webp',
|
|
'ai-glasses': 'https://nav.hackrobot.cn/assets/images/logos/glass.jpg',
|
|
'action-cam': 'https://nav.hackrobot.cn/assets/images/logos/action.jpg',
|
|
drone: 'https://nav.hackrobot.cn/assets/images/logos/neo2.jpg',
|
|
tiktok: 'https://nav.hackrobot.cn/assets/images/logos/tiktok.png',
|
|
'cloud-phone': 'https://nav.hackrobot.cn/assets/images/logos/phone.png',
|
|
vcam: 'https://nav.hackrobot.cn/assets/images/vcam.jpg',
|
|
youtube: 'https://nav.hackrobot.cn/assets/images/logos/youtube.png',
|
|
smmfollows: 'https://nav.hackrobot.cn/assets/images/logos/qqtube.png',
|
|
'yt-auto-dismiss': 'https://nav.hackrobot.cn/assets/images/greasyfork.png',
|
|
namesilo: 'https://nav.hackrobot.cn/assets/images/logos/namesilo.jpg',
|
|
aapanel: 'https://nav.hackrobot.cn/assets/images/logos/aapanel.png',
|
|
cloudflare: 'https://nav.hackrobot.cn/assets/images/logos/cloudflar.ico',
|
|
ga: 'https://nav.hackrobot.cn/assets/images/logos/analytics.ico',
|
|
adsense: 'https://nav.hackrobot.cn/assets/images/logos/gads.ico',
|
|
obs: 'https://nav.hackrobot.cn/assets/images/obs.jpg',
|
|
ffmpeg: 'https://nav.hackrobot.cn/assets/images/ffmpeg.jpg',
|
|
srs: 'https://nav.hackrobot.cn/assets/images/srs.png',
|
|
nowpayments: 'https://nav.hackrobot.cn/assets/images/nowpayments.ico',
|
|
creem: 'https://nav.hackrobot.cn/assets/images/creem.ico',
|
|
stripe: 'https://nav.hackrobot.cn/assets/images/stripe.ico',
|
|
zpay: 'https://nav.hackrobot.cn/assets/images/zpay.ico',
|
|
xorpay: 'https://nav.hackrobot.cn/assets/images/xorpay.ico',
|
|
yidu: 'https://nav.hackrobot.cn/assets/images/blog.jpg',
|
|
dsx: 'https://nav.hackrobot.cn/assets/images/dsx.jpg',
|
|
_brand: 'https://nav.hackrobot.cn/assets/images/nomadro.png',
|
|
}
|
|
|
|
async function fetchBuffer(url) {
|
|
const res = await fetch(url)
|
|
if (!res.ok) throw new Error(`${res.status} ${url}`)
|
|
return Buffer.from(await res.arrayBuffer())
|
|
}
|
|
|
|
async function normalizeInput(buf) {
|
|
if (isIco(buf)) {
|
|
const images = await decodeIco(buf)
|
|
let best = images[0]
|
|
let bestArea = 0
|
|
for (const img of images) {
|
|
const meta = await sharp(Buffer.from(img.buffer), { failOn: 'none' }).metadata()
|
|
const area = (meta.width || 0) * (meta.height || 0)
|
|
if (area >= bestArea) {
|
|
bestArea = area
|
|
best = img
|
|
}
|
|
}
|
|
return Buffer.from(best.buffer)
|
|
}
|
|
return buf
|
|
}
|
|
|
|
async function toWebp(buf) {
|
|
const input = await normalizeInput(buf)
|
|
return sharp(input, { animated: false, failOn: 'none' })
|
|
.rotate()
|
|
.resize(SIZE, SIZE, {
|
|
fit: 'cover',
|
|
withoutEnlargement: false,
|
|
background: { r: 255, g: 255, b: 255, alpha: 0 },
|
|
})
|
|
.webp({ quality: WEBP_QUALITY, effort: 6 })
|
|
.toBuffer()
|
|
}
|
|
|
|
async function main() {
|
|
await mkdir(outDir, { recursive: true })
|
|
const data = JSON.parse(await readFile(linksPath, 'utf8'))
|
|
|
|
const ids = new Set(['_brand'])
|
|
for (const cat of data.categories) {
|
|
for (const group of cat.groups) {
|
|
for (const link of group.links) ids.add(link.id)
|
|
}
|
|
}
|
|
|
|
let ok = 0
|
|
let fail = 0
|
|
let bytes = 0
|
|
|
|
for (const id of ids) {
|
|
const url = SOURCE[id]
|
|
if (!url) {
|
|
console.error(`✗ ${id}: missing source URL`)
|
|
fail += 1
|
|
continue
|
|
}
|
|
try {
|
|
const raw = await fetchBuffer(url)
|
|
const webp = await toWebp(raw)
|
|
await writeFile(path.join(outDir, `${id}.webp`), webp)
|
|
bytes += webp.length
|
|
ok += 1
|
|
console.log(`✓ ${id}.webp ${(webp.length / 1024).toFixed(1)}KB`)
|
|
} catch (err) {
|
|
fail += 1
|
|
console.error(`✗ ${id}: ${err.message}`)
|
|
}
|
|
}
|
|
|
|
for (const cat of data.categories) {
|
|
for (const group of cat.groups) {
|
|
for (const link of group.links) {
|
|
link.icon = `/icons/${link.id}.webp`
|
|
}
|
|
}
|
|
}
|
|
await writeFile(linksPath, `${JSON.stringify(data, null, 2)}\n`, 'utf8')
|
|
|
|
console.log(
|
|
`\nDone: ${ok} ok, ${fail} fail, total ${(bytes / 1024).toFixed(1)}KB → public/icons/`,
|
|
)
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error(err)
|
|
process.exit(1)
|
|
})
|