diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 517f67e..02c7de6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -67,3 +67,18 @@ jobs: release/GetBox-*-portable.exe release/GetBox-*-portable.exe.sha256 fail_on_unmatched_files: true + # Rebuild getbox.ne-nas.ru with the new version right away (GetBoxLanding's + # release-sync workflow). Needs a token with access to GetBoxLanding in the + # LANDING_DISPATCH_TOKEN secret; skipped without it — the site then catches + # up within an hour on its own. + - name: Update landing + env: + LANDING_TOKEN: ${{ secrets.LANDING_DISPATCH_TOKEN }} + if: env.LANDING_TOKEN != '' + shell: bash + run: > + curl -fsS -X POST + -H "Authorization: Bearer $LANDING_TOKEN" + -H "Accept: application/vnd.github+json" + https://api.github.com/repos/IncludeSpecial/GetBoxLanding/dispatches + -d '{"event_type":"getbox-release"}' diff --git a/.gitignore b/.gitignore index 98c5b2f..f793651 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ build/ # Generated icons (from data/logo.svg) data/logo.png +data/logo.ico data/bootstrap/ # Caches & logs diff --git a/CHANGELOG.md b/CHANGELOG.md index fee5288..aea798b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 1.3.1 + +### Внешний вид +- Новый логотип: плитка в стиле Windows 11 с открытой коробкой и стрелкой. Старый тёмный значок терялся на тёмной панели задач и не читался в 16–32 px. +- Иконка exe и окна — многоразмерный `.ico` (16–256 px); для 16–32 px нарисован упрощённый вариант (`data/logo-small.svg`), он же стоит в заголовке окна. +- Скриншоты в README пересняты; `promo/docs-screenshots.cjs` собирает их из кадров `promo/capture13.cjs`. + ## 1.3.0 ### GetBox обновляется сам diff --git a/data/logo-small.svg b/data/logo-small.svg new file mode 100644 index 0000000..5535c82 --- /dev/null +++ b/data/logo-small.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/data/logo.svg b/data/logo.svg index bbe6fe0..f387e48 100644 --- a/data/logo.svg +++ b/data/logo.svg @@ -1,39 +1,23 @@ - + - - - + + + - - - + + + + + + + - - - - - - - - - - - + + + + + + + diff --git a/data/whats-new.json b/data/whats-new.json index 7f5d44d..5788411 100644 --- a/data/whats-new.json +++ b/data/whats-new.json @@ -1,4 +1,16 @@ { + "1.3.1": [ + { "icon": "palette", "title": "Новый значок", "text": "GetBox сменил логотип: он чётко виден в панели задач — и на тёмной, и на светлой.", + "titleEn": "New icon", "textEn": "GetBox has a new logo that stays crisp in the taskbar, light or dark." }, + { "icon": "refresh", "title": "Обновления программ", "text": "Всё, что можно обновить, — на одной вкладке. История и откат в один клик.", "action": "updates", "actionLabel": "Открыть обновления", + "titleEn": "Program updates", "textEn": "Everything that can be updated in one tab, with history and one-click rollback.", "actionLabelEn": "Open updates" }, + { "icon": "palette", "title": "Пять тем и свой акцент", "text": "Светлая, OLED, высокий контраст, Windows 11, свой цвет и масштаб.", "action": "settings", "actionLabel": "Выбрать тему", + "titleEn": "Five themes and your accent", "textEn": "Light, OLED, high contrast, Windows 11, your own colour and scale.", "actionLabelEn": "Pick a theme" }, + { "icon": "grid", "title": "Четыре вида списка", "text": "Карточки, список с описанием, таблица и плитки — переключатель над любым списком.", "action": "catalog", "actionLabel": "Попробовать", + "titleEn": "Four list views", "textEn": "Cards, a detailed list, a table and tiles — switch above any list.", "actionLabelEn": "Try it" }, + { "icon": "shield", "title": "GetBox обновляется сам", "text": "Новая версия скачивается в фоне и проверяется по SHA256 — кнопка появится в заголовке окна.", + "titleEn": "GetBox updates itself", "textEn": "New versions download in the background and are checked by SHA256 — a button appears in the title bar." } + ], "1.3.0": [ { "icon": "refresh", "title": "Обновления программ", "text": "Всё, что можно обновить, — на одной вкладке. История и откат в один клик.", "action": "updates", "actionLabel": "Открыть обновления", "titleEn": "Program updates", "textEn": "Everything that can be updated in one tab, with history and one-click rollback.", "actionLabelEn": "Open updates" }, diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index d36ffcb..0382c4c 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -68,6 +68,7 @@ npm run dist 2. После слияния в `main` поставьте тег `git tag vX.Y.Z && git push origin vX.Y.Z` — или запустите workflow **Release** вручную на вкладке *Actions*: тег `v<версия из package.json>` создастся сам. 3. Workflow `release.yml` на `windows-latest` прогонит проверки, соберёт `GetBox-X.Y.Z-portable.exe`, посчитает `GetBox-X.Y.Z-portable.exe.sha256` и опубликует релиз «GetBox X.Y.Z» с разделом из `CHANGELOG.md`. 4. Добавьте карточки для экрана «Что нового» в `data/whats-new.json` (ключ — версия), иначе после обновления экран не покажется. +5. Сайт [getbox.ne-nas.ru](https://getbox.ne-nas.ru/) подхватит релиз сам: последний шаг workflow просит [GetBoxLanding](https://github.com/IncludeSpecial/GetBoxLanding) пересобраться (нужен секрет `LANDING_DISPATCH_TOKEN`), без секрета сайт обновится в течение часа. Самообновление берёт только релизы с ассетом `GetBox-*-portable.exe` и скачивает его, лишь если есть `.sha256` (или `digest` ассета от GitHub). Пре-релизы получают пользователи канала «Бета». Не удаляйте `.sha256` из релизов: без него клиенты не обновятся. diff --git a/docs/screenshots/appearance.webp b/docs/screenshots/appearance.webp index e38c1ad..ef97ff9 100644 Binary files a/docs/screenshots/appearance.webp and b/docs/screenshots/appearance.webp differ diff --git a/docs/screenshots/catalog.webp b/docs/screenshots/catalog.webp index 41082fa..623cc89 100644 Binary files a/docs/screenshots/catalog.webp and b/docs/screenshots/catalog.webp differ diff --git a/docs/screenshots/details.webp b/docs/screenshots/details.webp index a272e86..2b5b7af 100644 Binary files a/docs/screenshots/details.webp and b/docs/screenshots/details.webp differ diff --git a/docs/screenshots/home.webp b/docs/screenshots/home.webp index 175a31f..363e080 100644 Binary files a/docs/screenshots/home.webp and b/docs/screenshots/home.webp differ diff --git a/docs/screenshots/packages.webp b/docs/screenshots/packages.webp index da84711..23953f1 100644 Binary files a/docs/screenshots/packages.webp and b/docs/screenshots/packages.webp differ diff --git a/docs/screenshots/profiles.webp b/docs/screenshots/profiles.webp index 6ba5a04..813a466 100644 Binary files a/docs/screenshots/profiles.webp and b/docs/screenshots/profiles.webp differ diff --git a/docs/screenshots/queue.webp b/docs/screenshots/queue.webp index 9d66afd..8a410e4 100644 Binary files a/docs/screenshots/queue.webp and b/docs/screenshots/queue.webp differ diff --git a/docs/screenshots/themes.webp b/docs/screenshots/themes.webp index 42396dd..4d1ba02 100644 Binary files a/docs/screenshots/themes.webp and b/docs/screenshots/themes.webp differ diff --git a/docs/screenshots/updates.webp b/docs/screenshots/updates.webp index be9be3c..a9f5b5c 100644 Binary files a/docs/screenshots/updates.webp and b/docs/screenshots/updates.webp differ diff --git a/electron/main.ts b/electron/main.ts index ac6890e..12a8d33 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -113,7 +113,8 @@ function createWindow(): void { minWidth: 960, minHeight: 600, backgroundColor: '#0a1118', - icon: path.join(getDataDir(), 'logo.png'), + // Multi-size .ico keeps the taskbar icon crisp on Windows. + icon: path.join(getDataDir(), process.platform === 'win32' ? 'logo.ico' : 'logo.png'), frame: false, webPreferences: { preload: preloadPath, diff --git a/package-lock.json b/package-lock.json index f112427..14ec591 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "getbox", - "version": "1.3.0", + "version": "1.3.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "getbox", - "version": "1.3.0", + "version": "1.3.1", "dependencies": { "electron-store": "^10.0.0" }, diff --git a/package.json b/package.json index babcd62..e0702e1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "getbox", - "version": "1.3.0", + "version": "1.3.1", "description": "GetBox — локальный магазин программ для Windows", "main": "dist-electron/main.js", "type": "module", @@ -21,7 +21,7 @@ "build": { "appId": "com.getbox.app", "productName": "GetBox", - "icon": "build/icon.png", + "icon": "build/icon.ico", "directories": { "output": "release" }, diff --git a/promo/README.md b/promo/README.md index 397ad39..e007a07 100644 --- a/promo/README.md +++ b/promo/README.md @@ -24,6 +24,9 @@ python3 promo/music13.py ffmpeg -i promo/promo13-240p.mp4 -i promo/music13.wav -c:v copy -c:a aac -b:a 96k -shortest promo/GetBox-1.3-preview-240p.mp4 # 1.3 — финал: QUERY="h=1080" node promo/render.cjs video promo13.html +# Скриншоты для README (docs/screenshots/*.webp) — из кадров capture13 +node promo/docs-screenshots.cjs + # 1.3 — эдит 360p QUERY="h=360" OUT_SUFFIX=-360p CRF=24 node promo/render.cjs video edit13.html python3 promo/music-edit13.py diff --git a/promo/capture13.cjs b/promo/capture13.cjs index 59a7835..419e475 100644 --- a/promo/capture13.cjs +++ b/promo/capture13.cjs @@ -175,6 +175,9 @@ server.listen(0, async () => { await shot(browser, base, 'modal-light', { owned: me, look: { theme: 'light' }, act: async (p) => { await catalogTab(p); await p.waitForTimeout(300); await p.locator('.capsule').first().click(); } }); + await shot(browser, base, 'modal', { owned: me, look: { listView: 'tiles' }, act: async (p) => { + await catalogTab(p); await p.waitForTimeout(300); await p.locator('.capsule').first().click(); + } }); fs.writeFileSync(path.join(OUT, 'marks.json'), JSON.stringify(MARKS)); await browser.close(); server.close(); }); diff --git a/promo/docs-screenshots.cjs b/promo/docs-screenshots.cjs new file mode 100644 index 0000000..4b00169 --- /dev/null +++ b/promo/docs-screenshots.cjs @@ -0,0 +1,52 @@ +// Builds docs/screenshots/*.webp (1440×900) from the promo/shots/13 captures, +// including the five-theme strip. Run after `node promo/capture13.cjs`. +// Encoding happens in Chromium (canvas → WebP), so no image library is needed. +const { chromium } = require(process.env.PLAYWRIGHT_MODULE ?? 'playwright'); +const launchOpts = process.env.CHROMIUM_PATH ? { executablePath: process.env.CHROMIUM_PATH } : {}; +const fs = require('fs'); +const path = require('path'); + +const SHOTS = path.join(__dirname, 'shots', '13'); +const OUT = path.join(__dirname, '..', 'docs', 'screenshots'); + +// docs name → capture name(s); several captures make vertical slices, left to right. +const MAP = { + home: ['home-classic'], + catalog: ['cat-tiles'], + details: ['modal'], + queue: ['install-queue'], + updates: ['updates'], + packages: ['feat-packages'], + profiles: ['feat-profiles'], + appearance: ['L-appearance'], + themes: ['home-classic', 'home-light', 'home-oled', 'home-contrast', 'home-mica'], +}; + +const dataUrl = (name) => `data:image/png;base64,${fs.readFileSync(path.join(SHOTS, `${name}.png`)).toString('base64')}`; + +(async () => { + const browser = await chromium.launch(launchOpts); + const page = await browser.newPage(); + fs.mkdirSync(OUT, { recursive: true }); + for (const [doc, parts] of Object.entries(MAP)) { + const webp = await page.evaluate(async (srcs) => { + const W = 1440, H = 900; + const canvas = document.createElement('canvas'); + canvas.width = W; canvas.height = H; + const ctx = canvas.getContext('2d'); + ctx.imageSmoothingQuality = 'high'; + const slice = W / srcs.length; + for (let i = 0; i < srcs.length; i++) { + const img = new Image(); + img.src = srcs[i]; + await img.decode(); + const k = img.width / W; // captures are 2× (2880×1800) + ctx.drawImage(img, i * slice * k, 0, slice * k, img.height, i * slice, 0, slice, H); + } + return canvas.toDataURL('image/webp', 0.86); + }, parts.map(dataUrl)); + fs.writeFileSync(path.join(OUT, `${doc}.webp`), Buffer.from(webp.split(',')[1], 'base64')); + console.log('docs', `${doc}.webp`); + } + await browser.close(); +})(); diff --git a/scripts/render-app-icon.cjs b/scripts/render-app-icon.cjs index 741bc5b..2905a04 100644 --- a/scripts/render-app-icon.cjs +++ b/scripts/render-app-icon.cjs @@ -1,23 +1,49 @@ /** - * Renders data/logo.svg → build/icon.png and data/logo.png (256×256) - * for electron-builder and BrowserWindow. + * Renders the app icon from data/logo.svg (and data/logo-small.svg for 16–32 px): + * build/icon.ico, data/logo.ico — multi-size Windows icon for the exe and window + * build/icon.png, data/logo.png — 256×256 PNG */ const fs = require('fs'); const path = require('path'); const { Resvg } = require('@resvg/resvg-js'); const root = path.join(__dirname, '..'); -const svgPath = path.join(root, 'data', 'logo.svg'); -const outBuild = path.join(root, 'build', 'icon.png'); -const outData = path.join(root, 'data', 'logo.png'); +const full = fs.readFileSync(path.join(root, 'data', 'logo.svg')); +const small = fs.readFileSync(path.join(root, 'data', 'logo-small.svg')); -const svg = fs.readFileSync(svgPath); -const resvg = new Resvg(svg, { - fitTo: { mode: 'width', value: 256 }, -}); -const png = resvg.render().asPng(); +const render = (svg, size) => new Resvg(svg, { fitTo: { mode: 'width', value: size } }).render().asPng(); -fs.mkdirSync(path.dirname(outBuild), { recursive: true }); -fs.writeFileSync(outBuild, png); -fs.writeFileSync(outData, png); -console.log('app icon:', path.relative(root, outBuild), path.relative(root, outData)); +/** ICO container with PNG-compressed entries (supported since Windows Vista). */ +function ico(images) { + const header = Buffer.alloc(6); + header.writeUInt16LE(0, 0); + header.writeUInt16LE(1, 2); + header.writeUInt16LE(images.length, 4); + const dir = Buffer.alloc(16 * images.length); + let offset = header.length + dir.length; + images.forEach(({ size, png }, i) => { + const e = i * 16; + dir.writeUInt8(size >= 256 ? 0 : size, e); + dir.writeUInt8(size >= 256 ? 0 : size, e + 1); + dir.writeUInt16LE(1, e + 4); // colour planes + dir.writeUInt16LE(32, e + 6); // bits per pixel + dir.writeUInt32LE(png.length, e + 8); + dir.writeUInt32LE(offset, e + 12); + offset += png.length; + }); + return Buffer.concat([header, dir, ...images.map((i) => i.png)]); +} + +const sizes = [16, 20, 24, 32, 40, 48, 64, 128, 256]; +const icon = ico(sizes.map((size) => ({ size, png: render(size <= 32 ? small : full, size) }))); +const png256 = render(full, 256); + +const out = [ + ['build/icon.ico', icon], + ['data/logo.ico', icon], + ['build/icon.png', png256], + ['data/logo.png', png256], +]; +fs.mkdirSync(path.join(root, 'build'), { recursive: true }); +for (const [file, data] of out) fs.writeFileSync(path.join(root, file), data); +console.log('app icon:', out.map(([f]) => f).join(', ')); diff --git a/src/components/TitleBar.tsx b/src/components/TitleBar.tsx index 195c951..be4d91d 100644 --- a/src/components/TitleBar.tsx +++ b/src/components/TitleBar.tsx @@ -1,5 +1,6 @@ import { useEffect, useState } from 'react'; -import logoUrl from '../../data/logo.svg'; +// 18 px: the simplified mark stays crisp where the full one turns to mush. +import logoUrl from '../../data/logo-small.svg'; import { IconClose, IconMaximize, IconMinimize, IconRestore } from './Icons'; function waitForApi(): Promise {