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).
|
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:
|
Persisted to `app.getPath('userData')/config.json`. On Windows that's:
|
||||||
|
|
||||||
```
|
```
|
||||||
%APPDATA%\Folder Bookmark Tray\config.json
|
%APPDATA%\Trailhead Path Bookmarker\config.json
|
||||||
```
|
```
|
||||||
|
|
||||||
Schema:
|
Schema:
|
||||||
@@ -78,7 +78,7 @@ assets/tray-icon.png orange folder pictogram, 32×32
|
|||||||
|
|
||||||
## Stack
|
## 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
|
## 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(() => {
|
app.whenReady().then(() => {
|
||||||
if (process.platform === 'win32') {
|
if (process.platform === 'win32') {
|
||||||
app.setAppUserModelId('com.dbeuttel.folder-bookmark-tray');
|
app.setAppUserModelId('com.dbeuttel.trailhead-path-bookmarker');
|
||||||
}
|
}
|
||||||
migrateConfigIfNeeded();
|
migrateConfigIfNeeded();
|
||||||
pinned = !!readConfig().pinned;
|
pinned = !!readConfig().pinned;
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "folder-bookmark-tray",
|
"name": "trailhead-path-bookmarker",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "folder-bookmark-tray",
|
"name": "trailhead-path-bookmarker",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
+3
-3
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "folder-bookmark-tray",
|
"name": "trailhead-path-bookmarker",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"description": "Windows system tray app for bookmarking folders with quick-action buttons (Explorer, Terminal, Claude, Visual Studio).",
|
"description": "Windows system tray app for bookmarking folders with quick-action buttons (Explorer, Terminal, Claude, Visual Studio).",
|
||||||
"main": "electron/main.js",
|
"main": "electron/main.js",
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
"wait-on": "^8.0.1"
|
"wait-on": "^8.0.1"
|
||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
"appId": "com.dbeuttel.folder-bookmark-tray",
|
"appId": "com.dbeuttel.trailhead-path-bookmarker",
|
||||||
"productName": "Trailhead Path Bookmarker",
|
"productName": "Trailhead Path Bookmarker",
|
||||||
"files": [
|
"files": [
|
||||||
"electron/**/*",
|
"electron/**/*",
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
],
|
],
|
||||||
"win": {
|
"win": {
|
||||||
"target": "nsis",
|
"target": "nsis",
|
||||||
"icon": "assets/tray-icon.png"
|
"icon": "assets/app-icon.png"
|
||||||
},
|
},
|
||||||
"nsis": {
|
"nsis": {
|
||||||
"oneClick": false,
|
"oneClick": false,
|
||||||
|
|||||||
+77
-51
@@ -1,66 +1,85 @@
|
|||||||
// Generates assets/tray-icon.png — Trailhead branding: a bookmark ribbon
|
// Generates two PNGs from the same procedural Trailhead glyph:
|
||||||
// (vertical rectangle with a V-cut at the bottom — the universal "save"
|
// assets/tray-icon.png — 32x32 (Windows tray)
|
||||||
// glyph) carrying a mountain peak motif inside it. The ribbon silhouette
|
// assets/app-icon.png — 256x256 (electron-builder requires >=256 for the
|
||||||
// communicates the app's *function* (bookmarking); the inner peak ties to
|
// win.icon used in the installer + .exe resource)
|
||||||
// the Trailhead theme.
|
//
|
||||||
// Strokes are kept >=2 source pixels wide so the silhouette survives the
|
// Glyph: a vertical bookmark ribbon (rectangle with a V-cut bottom — the
|
||||||
// downscale to 16-24px Windows tray sizes.
|
// 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.
|
// No external image deps; raw PNG bytes via zlib.
|
||||||
|
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const zlib = require('zlib');
|
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 = [217, 119, 87, 255]; // #d97757 — ribbon fill
|
||||||
const ORANGE_DARK = [165, 82, 56, 255]; // shadow on right edge
|
const ORANGE_DARK = [165, 82, 56, 255]; // shadow on right edge
|
||||||
const CREAM = [250, 230, 215, 255]; // mountain motif inside the ribbon
|
const CREAM = [250, 230, 215, 255]; // mountain motif inside the ribbon
|
||||||
const TRANSPARENT = [0, 0, 0, 0];
|
const TRANSPARENT = [0, 0, 0, 0];
|
||||||
|
|
||||||
// Ribbon spans 14px wide (x=9..22) and 25px tall (y=3..27). The bottom 5
|
// Geometry, in 0..1 fractions of canvas.
|
||||||
// rows form a V-cut, narrowing the ribbon into two prongs.
|
const G = {
|
||||||
function inRibbon(x, y) {
|
ribbonLeft: 9 / 32,
|
||||||
if (x < 9 || x > 22) return false;
|
ribbonRight: 23 / 32, // exclusive
|
||||||
if (y < 3 || y > 27) return false;
|
ribbonTop: 3 / 32,
|
||||||
if (y >= 23) {
|
ribbonBottom: 28 / 32, // exclusive
|
||||||
const depth = y - 23; // 0..4
|
vCutTop: 23 / 32, // V notch starts here, points up into the ribbon
|
||||||
const cutLeft = 15 - depth;
|
mountainTop: 7 / 32,
|
||||||
const cutRight = 16 + depth;
|
mountainBottom: 15 / 32, // exclusive
|
||||||
if (x >= cutLeft && x <= cutRight) return false;
|
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;
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mountain peak motif inside the upper half of the ribbon. Peak at y=7,
|
// Mountain peak silhouette inside the upper half of the ribbon.
|
||||||
// base at y=14, max half-width 4 (so base spans 8 source pixels).
|
if (py >= mT && py < mB) {
|
||||||
function inMountain(x, y) {
|
const t = (py - mT) / (mB - mT); // 0..1 (top..base)
|
||||||
if (y < 7 || y > 14) return false;
|
const halfW = t * baseHalf;
|
||||||
const halfW = Math.floor((y - 7) / 2) + 1;
|
if (Math.abs(px - cx) <= halfW) return CREAM;
|
||||||
return x >= 15 - halfW + 1 && x <= 15 + halfW;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function colorAt(x, y) {
|
// Right-edge shadow on the ribbon body, stopping before the V-cut so the
|
||||||
if (!inRibbon(x, y)) return TRANSPARENT;
|
// notch edges read clean rather than half-shaded.
|
||||||
|
if (px >= rR - sw && py < vT) return ORANGE_DARK;
|
||||||
// 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;
|
|
||||||
|
|
||||||
return ORANGE;
|
return ORANGE;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildRawImage() {
|
function buildRawImage(size) {
|
||||||
const rowLen = SIZE * 4 + 1;
|
const colorAt = makeColorAt(size);
|
||||||
const raw = Buffer.alloc(rowLen * SIZE);
|
const rowLen = size * 4 + 1;
|
||||||
for (let y = 0; y < SIZE; y++) {
|
const raw = Buffer.alloc(rowLen * size);
|
||||||
|
for (let y = 0; y < size; y++) {
|
||||||
raw[y * rowLen] = 0;
|
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 [r, g, b, a] = colorAt(x, y);
|
||||||
const off = y * rowLen + 1 + x * 4;
|
const off = y * rowLen + 1 + x * 4;
|
||||||
raw[off] = r;
|
raw[off] = r;
|
||||||
@@ -97,17 +116,17 @@ function chunk(type, data) {
|
|||||||
return Buffer.concat([len, typeBuf, data, crcBuf]);
|
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 sig = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
||||||
const ihdr = Buffer.alloc(13);
|
const ihdr = Buffer.alloc(13);
|
||||||
ihdr.writeUInt32BE(SIZE, 0);
|
ihdr.writeUInt32BE(size, 0);
|
||||||
ihdr.writeUInt32BE(SIZE, 4);
|
ihdr.writeUInt32BE(size, 4);
|
||||||
ihdr[8] = 8; // bit depth
|
ihdr[8] = 8; // bit depth
|
||||||
ihdr[9] = 6; // color type RGBA
|
ihdr[9] = 6; // color type RGBA
|
||||||
ihdr[10] = 0; // compression
|
ihdr[10] = 0; // compression
|
||||||
ihdr[11] = 0; // filter
|
ihdr[11] = 0; // filter
|
||||||
ihdr[12] = 0; // interlace
|
ihdr[12] = 0; // interlace
|
||||||
const idat = zlib.deflateSync(buildRawImage());
|
const idat = zlib.deflateSync(buildRawImage(size));
|
||||||
return Buffer.concat([
|
return Buffer.concat([
|
||||||
sig,
|
sig,
|
||||||
chunk('IHDR', ihdr),
|
chunk('IHDR', ihdr),
|
||||||
@@ -116,6 +135,13 @@ function buildPng() {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
fs.mkdirSync(path.dirname(OUT), { recursive: true });
|
const targets = [
|
||||||
fs.writeFileSync(OUT, buildPng());
|
{ size: 32, file: path.join(__dirname, '..', 'assets', 'tray-icon.png') },
|
||||||
console.log(`Wrote ${OUT}`);
|
{ 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