Rename project to trailhead-path-bookmarker
- npm name: trailhead-path-bookmarker (package.json + lockfile) - electron-builder appId: com.dbeuttel.trailhead-path-bookmarker - AppUserModelId in main.js matches the new appId - README title updated; userData path now %APPDATA%\Trailhead Path Bookmarker Icon generator now emits both sizes: - assets/tray-icon.png (32x32) for the system tray - assets/app-icon.png (256x256) for electron-builder's win.icon, which rejects PNGs smaller than 256
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
# Folder Bookmark Tray
|
||||
# Trailhead Path Bookmarker
|
||||
|
||||
A Windows system-tray app for bookmarking folder paths with quick-action buttons (Explorer, Terminal, Claude, Visual Studio).
|
||||
|
||||
@@ -41,7 +41,7 @@ npm run icon # regenerate assets/tray-icon.png
|
||||
Persisted to `app.getPath('userData')/config.json`. On Windows that's:
|
||||
|
||||
```
|
||||
%APPDATA%\Folder Bookmark Tray\config.json
|
||||
%APPDATA%\Trailhead Path Bookmarker\config.json
|
||||
```
|
||||
|
||||
Schema:
|
||||
@@ -78,7 +78,7 @@ assets/tray-icon.png orange folder pictogram, 32×32
|
||||
|
||||
## Stack
|
||||
|
||||
Electron 33, React 18, Vite 5. NSIS installer via `electron-builder` (`appId: com.dbeuttel.folder-bookmark-tray`). No native deps.
|
||||
Electron 33, React 18, Vite 5. NSIS installer via `electron-builder` (`appId: com.dbeuttel.trailhead-path-bookmarker`). No native deps.
|
||||
|
||||
## Non-goals
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 157 B After Width: | Height: | Size: 159 B |
+1
-1
@@ -750,7 +750,7 @@ ipcMain.handle('set-popup-width', (_event, w) => {
|
||||
|
||||
app.whenReady().then(() => {
|
||||
if (process.platform === 'win32') {
|
||||
app.setAppUserModelId('com.dbeuttel.folder-bookmark-tray');
|
||||
app.setAppUserModelId('com.dbeuttel.trailhead-path-bookmarker');
|
||||
}
|
||||
migrateConfigIfNeeded();
|
||||
pinned = !!readConfig().pinned;
|
||||
|
||||
Generated
+2
-2
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "folder-bookmark-tray",
|
||||
"name": "trailhead-path-bookmarker",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "folder-bookmark-tray",
|
||||
"name": "trailhead-path-bookmarker",
|
||||
"version": "0.1.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "folder-bookmark-tray",
|
||||
"name": "trailhead-path-bookmarker",
|
||||
"version": "0.1.0",
|
||||
"description": "Windows system tray app for bookmarking folders with quick-action buttons (Explorer, Terminal, Claude, Visual Studio).",
|
||||
"main": "electron/main.js",
|
||||
@@ -28,7 +28,7 @@
|
||||
"wait-on": "^8.0.1"
|
||||
},
|
||||
"build": {
|
||||
"appId": "com.dbeuttel.folder-bookmark-tray",
|
||||
"appId": "com.dbeuttel.trailhead-path-bookmarker",
|
||||
"productName": "Trailhead Path Bookmarker",
|
||||
"files": [
|
||||
"electron/**/*",
|
||||
@@ -37,7 +37,7 @@
|
||||
],
|
||||
"win": {
|
||||
"target": "nsis",
|
||||
"icon": "assets/tray-icon.png"
|
||||
"icon": "assets/app-icon.png"
|
||||
},
|
||||
"nsis": {
|
||||
"oneClick": false,
|
||||
|
||||
+76
-50
@@ -1,66 +1,85 @@
|
||||
// Generates assets/tray-icon.png — Trailhead branding: a bookmark ribbon
|
||||
// (vertical rectangle with a V-cut at the bottom — the universal "save"
|
||||
// glyph) carrying a mountain peak motif inside it. The ribbon silhouette
|
||||
// communicates the app's *function* (bookmarking); the inner peak ties to
|
||||
// the Trailhead theme.
|
||||
// Strokes are kept >=2 source pixels wide so the silhouette survives the
|
||||
// downscale to 16-24px Windows tray sizes.
|
||||
// Generates two PNGs from the same procedural Trailhead glyph:
|
||||
// assets/tray-icon.png — 32x32 (Windows tray)
|
||||
// assets/app-icon.png — 256x256 (electron-builder requires >=256 for the
|
||||
// win.icon used in the installer + .exe resource)
|
||||
//
|
||||
// Glyph: a vertical bookmark ribbon (rectangle with a V-cut bottom — the
|
||||
// universal "save" mark) carrying a cream mountain peak inside it.
|
||||
// Geometry is expressed as fractions of the canvas so a single set of rules
|
||||
// renders cleanly at both sizes.
|
||||
// No external image deps; raw PNG bytes via zlib.
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const zlib = require('zlib');
|
||||
|
||||
const SIZE = 32;
|
||||
const OUT = path.join(__dirname, '..', 'assets', 'tray-icon.png');
|
||||
|
||||
const ORANGE = [217, 119, 87, 255]; // #d97757 — ribbon fill
|
||||
const ORANGE_DARK = [165, 82, 56, 255]; // shadow on right edge
|
||||
const CREAM = [250, 230, 215, 255]; // mountain motif inside the ribbon
|
||||
const TRANSPARENT = [0, 0, 0, 0];
|
||||
|
||||
// Ribbon spans 14px wide (x=9..22) and 25px tall (y=3..27). The bottom 5
|
||||
// rows form a V-cut, narrowing the ribbon into two prongs.
|
||||
function inRibbon(x, y) {
|
||||
if (x < 9 || x > 22) return false;
|
||||
if (y < 3 || y > 27) return false;
|
||||
if (y >= 23) {
|
||||
const depth = y - 23; // 0..4
|
||||
const cutLeft = 15 - depth;
|
||||
const cutRight = 16 + depth;
|
||||
if (x >= cutLeft && x <= cutRight) return false;
|
||||
}
|
||||
return true;
|
||||
// Geometry, in 0..1 fractions of canvas.
|
||||
const G = {
|
||||
ribbonLeft: 9 / 32,
|
||||
ribbonRight: 23 / 32, // exclusive
|
||||
ribbonTop: 3 / 32,
|
||||
ribbonBottom: 28 / 32, // exclusive
|
||||
vCutTop: 23 / 32, // V notch starts here, points up into the ribbon
|
||||
mountainTop: 7 / 32,
|
||||
mountainBottom: 15 / 32, // exclusive
|
||||
mountainBaseHalfW: 4.5 / 32,
|
||||
shadowWidth: 1 / 32,
|
||||
};
|
||||
|
||||
function makeColorAt(size) {
|
||||
const rL = G.ribbonLeft * size;
|
||||
const rR = G.ribbonRight * size;
|
||||
const rT = G.ribbonTop * size;
|
||||
const rB = G.ribbonBottom * size;
|
||||
const vT = G.vCutTop * size;
|
||||
const mT = G.mountainTop * size;
|
||||
const mB = G.mountainBottom * size;
|
||||
const sw = G.shadowWidth * size;
|
||||
const cx = (rL + rR) / 2;
|
||||
const baseHalf = G.mountainBaseHalfW * size;
|
||||
|
||||
return function colorAt(x, y) {
|
||||
// Sample at pixel center so geometry edges land cleanly.
|
||||
const px = x + 0.5;
|
||||
const py = y + 0.5;
|
||||
|
||||
if (px < rL || px >= rR) return TRANSPARENT;
|
||||
if (py < rT || py >= rB) return TRANSPARENT;
|
||||
|
||||
// V-cut: triangular notch carved from the bottom of the ribbon.
|
||||
if (py >= vT) {
|
||||
const depth = (py - vT) / (rB - vT); // 0..1
|
||||
const cutHalfW = depth * ((rR - rL) / 2);
|
||||
if (Math.abs(px - cx) < cutHalfW) return TRANSPARENT;
|
||||
}
|
||||
|
||||
// Mountain peak motif inside the upper half of the ribbon. Peak at y=7,
|
||||
// base at y=14, max half-width 4 (so base spans 8 source pixels).
|
||||
function inMountain(x, y) {
|
||||
if (y < 7 || y > 14) return false;
|
||||
const halfW = Math.floor((y - 7) / 2) + 1;
|
||||
return x >= 15 - halfW + 1 && x <= 15 + halfW;
|
||||
// Mountain peak silhouette inside the upper half of the ribbon.
|
||||
if (py >= mT && py < mB) {
|
||||
const t = (py - mT) / (mB - mT); // 0..1 (top..base)
|
||||
const halfW = t * baseHalf;
|
||||
if (Math.abs(px - cx) <= halfW) return CREAM;
|
||||
}
|
||||
|
||||
function colorAt(x, y) {
|
||||
if (!inRibbon(x, y)) return TRANSPARENT;
|
||||
|
||||
// Inner mountain silhouette painted in cream so it pops against the
|
||||
// orange ribbon at any size.
|
||||
if (inMountain(x, y)) return CREAM;
|
||||
|
||||
// Right-edge shadow on the ribbon body (skip the V-cut zone so the cut
|
||||
// edges look clean rather than half-shaded).
|
||||
if (x === 22 && y >= 3 && y < 23) return ORANGE_DARK;
|
||||
// Right-edge shadow on the ribbon body, stopping before the V-cut so the
|
||||
// notch edges read clean rather than half-shaded.
|
||||
if (px >= rR - sw && py < vT) return ORANGE_DARK;
|
||||
|
||||
return ORANGE;
|
||||
};
|
||||
}
|
||||
|
||||
function buildRawImage() {
|
||||
const rowLen = SIZE * 4 + 1;
|
||||
const raw = Buffer.alloc(rowLen * SIZE);
|
||||
for (let y = 0; y < SIZE; y++) {
|
||||
function buildRawImage(size) {
|
||||
const colorAt = makeColorAt(size);
|
||||
const rowLen = size * 4 + 1;
|
||||
const raw = Buffer.alloc(rowLen * size);
|
||||
for (let y = 0; y < size; y++) {
|
||||
raw[y * rowLen] = 0;
|
||||
for (let x = 0; x < SIZE; x++) {
|
||||
for (let x = 0; x < size; x++) {
|
||||
const [r, g, b, a] = colorAt(x, y);
|
||||
const off = y * rowLen + 1 + x * 4;
|
||||
raw[off] = r;
|
||||
@@ -97,17 +116,17 @@ function chunk(type, data) {
|
||||
return Buffer.concat([len, typeBuf, data, crcBuf]);
|
||||
}
|
||||
|
||||
function buildPng() {
|
||||
function buildPng(size) {
|
||||
const sig = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
||||
const ihdr = Buffer.alloc(13);
|
||||
ihdr.writeUInt32BE(SIZE, 0);
|
||||
ihdr.writeUInt32BE(SIZE, 4);
|
||||
ihdr.writeUInt32BE(size, 0);
|
||||
ihdr.writeUInt32BE(size, 4);
|
||||
ihdr[8] = 8; // bit depth
|
||||
ihdr[9] = 6; // color type RGBA
|
||||
ihdr[10] = 0; // compression
|
||||
ihdr[11] = 0; // filter
|
||||
ihdr[12] = 0; // interlace
|
||||
const idat = zlib.deflateSync(buildRawImage());
|
||||
const idat = zlib.deflateSync(buildRawImage(size));
|
||||
return Buffer.concat([
|
||||
sig,
|
||||
chunk('IHDR', ihdr),
|
||||
@@ -116,6 +135,13 @@ function buildPng() {
|
||||
]);
|
||||
}
|
||||
|
||||
fs.mkdirSync(path.dirname(OUT), { recursive: true });
|
||||
fs.writeFileSync(OUT, buildPng());
|
||||
console.log(`Wrote ${OUT}`);
|
||||
const targets = [
|
||||
{ size: 32, file: path.join(__dirname, '..', 'assets', 'tray-icon.png') },
|
||||
{ size: 256, file: path.join(__dirname, '..', 'assets', 'app-icon.png') },
|
||||
];
|
||||
|
||||
for (const t of targets) {
|
||||
fs.mkdirSync(path.dirname(t.file), { recursive: true });
|
||||
fs.writeFileSync(t.file, buildPng(t.size));
|
||||
console.log(`Wrote ${t.file} (${t.size}x${t.size})`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user