mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2026-06-28 15:20:20 +00:00
f7ca93d12c
* feat: Add basic PWA support and service worker for offline caching * feat: Vite PWA implementation WIP * feat: Improve PWA icons generation * feat: Add PWA workbox to server routes * feat: Include `version.json` in static assets * feat: Add HTTP cache headers for PWA static assets * feat: Update app name for `apple-mobile-web-app-title` * feat: Implement PWA versioning and automatic update detection * chore: Update `.gitignore` files * feat: Splash Screens * feat: Add dark mode favicon support * refactor: Cleanup * fix: Use dark logo for dark splash screens * refactor: Simplify favicons SVG code * fix: Adjust caching and polling for reliable service worker updates * fix: Add missing favicon entry * fix: Align PWA service worker configuration with SvelteKit build structure * fix: Replace hashed bundle paths with versioned static paths * test: Add PWA tests * ci: Add build output for unit tests * refactor: Cleanup * fix: Server build & release versioning * chore: Update package-lock.json * chore: Increase PWA cache size * chore: Update packages * feat: Update favicons * refactor: Post-merge fix * feat: support explicit build version for PWA cache busting * fix: CI * feat: Improve PWA Refresh Alert UI * feat: Add toggleable build version display * refactor: Cleanup * feat: Add version mismatch detection and manual app reload * refactor: replace dynamic imports with static * refactor: Cleanup * feat: Add safe space for `pwa-<size>.png` rendered icons * fix: use relative paths for PWA assets to support base path deployment * feat: add PWA mode detection via URL query parameter * feat: Use ?cache=true for SW-cached PWA assets * refactor: Build process cleanup * refactor: Decouple PWA versioning and remove ?cache=true workaround * chore: Update README logo * feat: Include PWA Assets generation in build script * refactor: `usePwa` hook for core layout * fix: Relativize base vite plugin * fix: remove unnecessary backslash escapes in test regexes * test: update static asset paths for API Key test * refactor: Move SvelteKit PWA Options config to constants * ui: fix update notification never appearing Keep the PWA hook object intact instead of destructuring needRefreshByStorage, which freezes the reactive getter. Also exclude loading.html from PWA precache to prevent 404 errors and broken SW installation.
138 lines
4.2 KiB
JavaScript
138 lines
4.2 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
/**
|
|
* Apply circular mask to pwa-*.png icons.
|
|
* Uses the maskable icon as source (white bg, full logo) to avoid
|
|
* the small-colormap pwa icons looking bad when cropped to a circle.
|
|
*
|
|
* Usage: node scripts/make-icons-circular.js [--padding-pct <0-50>] [--scale-pct <50-100>]
|
|
*
|
|
* - padding-pct: percentage of icon size kept as padding around the circle (default: 25)
|
|
* - scale-pct: scale down the source image before cropping (default: 85)
|
|
*
|
|
* maskable-icon and apple-touch-icon are left untouched.
|
|
*/
|
|
|
|
import sharp from 'sharp';
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
import { fileURLToPath } from 'url';
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = path.dirname(__filename);
|
|
|
|
const STATIC_DIR = path.resolve(__dirname, '..', 'static');
|
|
|
|
const paddingPct = process.argv.reduce((acc, arg, i, args) => {
|
|
if (arg === '--padding-pct' && args[i + 1]) return parseFloat(args[i + 1]);
|
|
return acc;
|
|
}, 0);
|
|
|
|
// Scale down the source image before cropping to circle
|
|
const scalePct = process.argv.reduce((acc, arg, i, args) => {
|
|
if (arg === '--scale-pct' && args[i + 1]) return parseFloat(args[i + 1]);
|
|
return acc;
|
|
}, 85); // default 85% - icon fills 85% of the circular area
|
|
|
|
// Source for circular icons: the maskable icon (white bg, full logo)
|
|
const sourceIcon = 'maskable-icon-512x512.png';
|
|
const targetIcons = ['pwa-64x64.png', 'pwa-192x192.png', 'pwa-512x512.png'];
|
|
|
|
// maskable-icon and apple-touch-icon stay square
|
|
const untouchedIcons = ['maskable-icon-512x512.png', 'apple-touch-icon-180x180.png'];
|
|
|
|
async function makeCircle(targetFilename) {
|
|
const targetPath = path.join(STATIC_DIR, targetFilename);
|
|
const sourcePath = path.join(STATIC_DIR, sourceIcon);
|
|
|
|
if (!fs.existsSync(sourcePath)) {
|
|
console.log(`⏭️ ${sourceIcon} not found, skipping`);
|
|
return;
|
|
}
|
|
if (!fs.existsSync(targetPath)) {
|
|
console.log(`⏭️ ${targetFilename} not found, skipping`);
|
|
return;
|
|
}
|
|
|
|
const metadata = await sharp(targetPath).metadata();
|
|
const size = Math.max(metadata.width, metadata.height);
|
|
const radius = Math.floor((size * (1 - paddingPct / 100)) / 2);
|
|
const center = Math.floor(size / 2);
|
|
|
|
// Build circular mask as RGBA buffer: white opaque circle on transparent bg
|
|
const maskBuf = Buffer.alloc(size * size * 4, 0);
|
|
for (let y = 0; y < size; y++) {
|
|
for (let x = 0; x < size; x++) {
|
|
const dx = x - center;
|
|
const dy = y - center;
|
|
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
if (dist < radius) {
|
|
const i = (y * size + x) * 4;
|
|
maskBuf[i] = 255;
|
|
maskBuf[i + 1] = 255;
|
|
maskBuf[i + 2] = 255;
|
|
maskBuf[i + 3] = 255;
|
|
}
|
|
}
|
|
}
|
|
|
|
const tmpMask = path.join(STATIC_DIR, '.mask-tmp.png');
|
|
await sharp(maskBuf, {
|
|
raw: { width: size, height: size, channels: 4 }
|
|
})
|
|
.png()
|
|
.toFile(tmpMask);
|
|
|
|
// Step 1: Scale source relative to circle diameter (not full icon), composite centered onto white canvas of full size
|
|
const circleDiameter = Math.floor(size * (1 - paddingPct / 100));
|
|
const scaledSize = Math.floor((circleDiameter * scalePct) / 100);
|
|
const offset = Math.floor((size - scaledSize) / 2);
|
|
|
|
const scaledBuf = await sharp(sourcePath)
|
|
.resize(scaledSize, scaledSize, {
|
|
fit: 'cover',
|
|
background: { r: 255, g: 255, b: 255, alpha: 1 }
|
|
})
|
|
.ensureAlpha()
|
|
.png()
|
|
.toBuffer();
|
|
|
|
// Step 2: Composite scaled image onto white background, then apply circular mask
|
|
const output = await sharp({
|
|
create: {
|
|
width: size,
|
|
height: size,
|
|
channels: 4,
|
|
background: { r: 255, g: 255, b: 255, alpha: 1 }
|
|
}
|
|
})
|
|
.composite([
|
|
{ input: scaledBuf, top: offset, left: offset },
|
|
{ input: tmpMask, top: 0, left: 0, blend: 'dest-in' }
|
|
])
|
|
.png()
|
|
.toBuffer();
|
|
|
|
fs.writeFileSync(targetPath, output);
|
|
fs.unlinkSync(tmpMask);
|
|
|
|
console.log(
|
|
`✓ ${targetFilename} → circle from ${sourceIcon}, ${paddingPct}% padding (size=${size}, r=${radius}, scale=${scalePct}%, circleDiameter=${circleDiameter})`
|
|
);
|
|
}
|
|
|
|
async function main() {
|
|
console.log(`Circular mask: ${paddingPct}% padding, ${scalePct}% scale, source=${sourceIcon}\n`);
|
|
for (const icon of targetIcons) {
|
|
await makeCircle(icon);
|
|
}
|
|
|
|
console.log('\nUnchanged:');
|
|
for (const icon of untouchedIcons) {
|
|
const fp = path.join(STATIC_DIR, icon);
|
|
console.log(` ${icon} (${fs.existsSync(fp) ? fs.statSync(fp).size + ' bytes' : 'missing'})`);
|
|
}
|
|
}
|
|
|
|
main();
|