Skip to content

Commit 028a518

Browse files
committed
v.0.3.1
1 parent 1be33fd commit 028a518

5 files changed

Lines changed: 136 additions & 29 deletions

File tree

.github/workflows/release.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ jobs:
196196
body_path: ${{ steps.extract.outputs.notes_path }}
197197
generate_release_notes: false
198198
make_latest: true
199-
allow_updates: true
199+
overwrite_files: true
200200
fail_on_unmatched_files: false
201201
files: |
202202
artifacts/linux/**
@@ -211,7 +211,7 @@ jobs:
211211
name: ${{ github.ref_name }}
212212
generate_release_notes: true
213213
make_latest: true
214-
allow_updates: true
214+
overwrite_files: true
215215
fail_on_unmatched_files: false
216216
files: |
217217
artifacts/linux/**

CHANGELOG.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,35 @@
33
All notable changes to this project will be documented in this file.
44
The format roughly follows Keep a Changelog, and dates are in YYYY-MM-DD.
55

6+
7+
## [v0.3.1] — 2025-10-18
8+
9+
### Added
10+
- Download options popover
11+
- Shift-click, Alt-click, or right-click “Download All” to open options.
12+
- Filename templates with presets and “Custom…” entry; selection is used by both single “Download” and “Download All”.
13+
- “Shift for options” tooltip shown below the button for visibility.
14+
15+
### Changed
16+
- Naming templates
17+
- Tokens supported: `{site} {site_type} {id} {score} {favorites} {rating} {width} {height} {index} {ext} {original_name} {created} {created_yyyy} {created_mm} {created_dd} {created_hhmm} {artist} {copyright} {character}`.
18+
- Card thumbnails
19+
- Prefer `sample_url` (then `file_url`, then `preview_url`) and add a simple `srcset` to keep Danbooru previews sharp.
20+
21+
### Fixed
22+
- Favorites (local)
23+
- “♥ Save” button works again even if the preload method is missing: renderer includes a safe localStorage fallback.
24+
- Preload bridge restored back‑compat names (`toggleLocalFavorite`, `getLocalFavoriteKeys`, etc.).
25+
- Popular view sorting
26+
- Corrected popularity computation (typo fix `st.scoresP95``st.scoreP95` and added guards), restoring true global popularity sorting.
27+
- Manage Sites – Test row
28+
- Restored full test flow (API probe, Auth check with info, Danbooru rate‑limit) and brought back “Open Account Page” and “API Help” buttons.
29+
- Fixed info text rendering (no more “[object Object]”) and polished badges.
30+
- Release workflow
31+
- Release notes are reliably extracted from CHANGELOG and passed via `body_path`; removed unsupported `allow_updates` input. Falls back to auto‑notes when no section matches.
32+
33+
[v0.3.1]: https://github.com/Amateur-God/StreamBooru/releases/tag/v0.3.1
34+
635
## [v0.3.0] — 2025-10-18
736

837
### Added

electron/preload.js

Lines changed: 48 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,72 @@
1-
const { contextBridge, ipcRenderer } = require('electron');
1+
'use strict';
22

3-
contextBridge.exposeInMainWorld('api', {
4-
// Fetch posts
5-
fetchBooru: (payload) => ipcRenderer.invoke('booru:fetch', payload),
3+
const { contextBridge, ipcRenderer, shell } = require('electron');
64

7-
// Config
5+
// Normalize site argument for helpers that accept either a site object or { site }
6+
const pickSite = (arg) => (arg && typeof arg === 'object' && 'site' in arg ? arg.site : arg);
7+
8+
contextBridge.exposeInMainWorld('api', {
9+
// ---------------- Config ----------------
810
loadConfig: () => ipcRenderer.invoke('config:load'),
911
saveConfig: (cfg) => ipcRenderer.invoke('config:save', cfg),
1012

11-
// External
12-
openExternal: (url) => ipcRenderer.invoke('openExternal', url),
13+
// ---------------- Fetch posts ----------------
14+
fetchBooru: (payload) => ipcRenderer.invoke('booru:fetch', payload),
15+
16+
// ---------------- External/open ----------------
17+
// Prefer shell.openExternal, fall back to a main-process handler
18+
openExternal: async (url) => {
19+
try {
20+
await shell.openExternal(url);
21+
return true;
22+
} catch {
23+
try {
24+
return await ipcRenderer.invoke('openExternal', url);
25+
} catch {
26+
return false;
27+
}
28+
}
29+
},
1330

31+
// ---------------- Images ----------------
1432
// Single image download
1533
downloadImage: ({ url, siteName, fileName }) =>
1634
ipcRenderer.invoke('download:image', { url, siteName, fileName }),
1735

18-
// BULK download
36+
// Bulk download
1937
downloadBulk: (items, options = {}) =>
2038
ipcRenderer.invoke('download:bulk', { items, options }),
2139

2240
// Image proxy (to data URL)
2341
proxyImage: (url) => ipcRenderer.invoke('image:proxy', { url }),
2442

25-
// Favorites remote
43+
// ---------------- Remote favorites (site APIs) ----------------
44+
// New name kept
2645
booruFavorite: (payload) => ipcRenderer.invoke('booru:favorite', payload),
46+
// Back-compat alias
47+
favoritePost: (payload) => ipcRenderer.invoke('booru:favorite', payload),
48+
49+
// ---------------- helpers ----------------
50+
// Accept (site) or ({ site })
51+
authCheck: (siteOrPayload) =>
52+
ipcRenderer.invoke('booru:authCheck', { site: pickSite(siteOrPayload) }),
53+
54+
// New helper name
55+
rateLimit: (siteOrPayload) =>
56+
ipcRenderer.invoke('booru:rateLimit', { site: pickSite(siteOrPayload) }),
57+
// Back-compat alias used in older code
58+
rateLimitCheck: (siteOrPayload) =>
59+
ipcRenderer.invoke('booru:rateLimit', { site: pickSite(siteOrPayload) }),
2760

28-
// Favorites local
61+
// ---------------- Local favorites (app storage) ----------------
62+
// New short names
2963
favKeys: () => ipcRenderer.invoke('favorites:keys'),
3064
favList: () => ipcRenderer.invoke('favorites:list'),
3165
favToggle: (post) => ipcRenderer.invoke('favorites:toggle', { post }),
3266
favClear: () => ipcRenderer.invoke('favorites:clear'),
3367

34-
// Optional helpers
35-
authCheck: (site) => ipcRenderer.invoke('booru:authCheck', { site }),
36-
rateLimit: (site) => ipcRenderer.invoke('booru:rateLimit', { site })
68+
getLocalFavoriteKeys: () => ipcRenderer.invoke('favorites:keys'),
69+
getLocalFavorites: () => ipcRenderer.invoke('favorites:list'),
70+
toggleLocalFavorite: (post) => ipcRenderer.invoke('favorites:toggle', { post }),
71+
clearLocalFavorites: () => ipcRenderer.invoke('favorites:clear'),
3772
});

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "streambooru",
3-
"version": "0.3.0",
3+
"version": "0.3.1",
44
"description": "StreamBooru — Electron app to browse multiple booru sites with New and Popular feeds, merged across sources.",
55
"main": "electron/main.js",
66
"author": {

renderer/renderer.js

Lines changed: 56 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// Renderer with Search/New/Popular/Favorites, Manage Sites,
2-
// Bulk Download, and filename templates (moved into a popover opened from Download All)
2+
// Bulk Download, and filename templates (popover from Download All)
33

44
const state = {
55
config: { sites: [] },
@@ -77,17 +77,19 @@ function recencyBoost(p, now=Date.now()) {
7777
const half = 48;
7878
return Math.exp(-ageH/half);
7979
}
80+
function clamp01(x) { const n = Number(x); return Number.isFinite(n) ? Math.max(0, Math.min(1, n)) : 0; }
8081
function computePopularity(items) {
8182
const stats = buildSiteStats(items);
8283
const now = Date.now();
8384
const map = new Map();
8485
for (const p of items) {
8586
const sk = siteKey(p.site || {});
8687
const st = stats.get(sk) || { favP95: 0, scoreP95: 0 };
87-
const favNorm = st.favP95>0 ? Math.min(1, Math.max(0, safeNum(p.favorites,0)/st.favP95)) : 0;
88-
const scoreNorm = st.scoreP95>0 ? Math.min(1, Math.max(0, safeNum(p.score,0)/st.scoresP95)) : 0;
88+
const favNorm = st.favP95>0 ? clamp01(safeNum(p.favorites,0)/st.favP95) : 0;
89+
// FIX: correct property and guard
90+
const scoreNorm = st.scoreP95>0 ? clamp01(safeNum(p.score,0)/st.scoreP95) : 0;
8991
const pop = 1.0*favNorm + 0.6*scoreNorm + 0.15*recencyBoost(p, now);
90-
map.set(itemKey(p), pop);
92+
map.set(itemKey(p), Number.isFinite(pop) ? pop : 0);
9193
}
9294
return map;
9395
}
@@ -537,23 +539,64 @@ function setupInfiniteScroll() {
537539
});
538540
}
539541

540-
// Local favorites helpers
542+
// ---------- Local favorites with fallback ----------
541543
window.isLocalFavorite = (post) => (window.__localFavsSet || new Set()).has(itemKey(post));
544+
545+
function localFavToggleFallback(post) {
546+
const KEY_KEYS = 'sb_local_favs_keys_v1';
547+
const KEY_POSTS = 'sb_local_favs_posts_v1';
548+
const key = itemKey(post);
549+
const loadKeys = () => { try { return new Set(JSON.parse(localStorage.getItem(KEY_KEYS) || '[]')); } catch { return new Set(); } };
550+
const saveKeys = (set) => { try { localStorage.setItem(KEY_KEYS, JSON.stringify([...set])); } catch {} };
551+
const loadMap = () => { try { return new Map(Object.entries(JSON.parse(localStorage.getItem(KEY_POSTS) || '{}'))); } catch { return new Map(); } };
552+
const saveMap = (map) => { try { localStorage.setItem(KEY_POSTS, JSON.stringify(Object.fromEntries(map))); } catch {} };
553+
554+
const keys = loadKeys();
555+
const map = loadMap();
556+
let favorited;
557+
if (keys.has(key)) {
558+
keys.delete(key);
559+
map.delete(key);
560+
favorited = false;
561+
} else {
562+
keys.add(key);
563+
map.set(key, JSON.stringify({ ...post, _added_at: Date.now() }));
564+
favorited = true;
565+
}
566+
saveKeys(keys);
567+
saveMap(map);
568+
return { ok: true, favorited, key };
569+
}
570+
542571
window.toggleLocalFavorite = async (post) => {
543-
const res = await window.api.toggleLocalFavorite(post);
544-
window.__localFavsSet = window.__localFavsSet || new Set(await window.api.getLocalFavoriteKeys());
545-
if (res?.ok) {
546-
if (res.favorited) window.__localFavsSet.add(res.key);
547-
else window.__localFavsSet.delete(res.key);
548-
if (state.viewType === 'faves') { clearFeed(); await fetchBatch(); }
572+
try {
573+
let res;
574+
if (typeof window.api?.toggleLocalFavorite === 'function') {
575+
res = await window.api.toggleLocalFavorite(post);
576+
} else {
577+
res = localFavToggleFallback(post);
578+
}
579+
window.__localFavsSet = window.__localFavsSet || new Set(await (window.api?.getLocalFavoriteKeys?.() || []));
580+
const key = res?.key || itemKey(post);
581+
if (res?.ok) {
582+
if (res.favorited) window.__localFavsSet.add(key);
583+
else window.__localFavsSet.delete(key);
584+
if (state.viewType === 'faves') { clearFeed(); await fetchBatch(); }
585+
} else {
586+
alert(`Save failed: ${res?.error || 'unknown error'}`);
587+
}
588+
return res;
589+
} catch (e) {
590+
console.error('toggleLocalFavorite error:', e);
591+
alert(`Save failed: ${e?.message || e}`);
592+
return { ok: false, error: String(e?.message || e) };
549593
}
550-
return res;
551594
};
552595

553596
// ---------- bootstrap ----------
554597
async function init() {
555598
await loadConfig();
556-
try { const keys = await window.api.getLocalFavoriteKeys(); window.__localFavsSet = new Set(keys || []); } catch {}
599+
try { const keys = await (window.api?.getLocalFavoriteKeys?.() || []); window.__localFavsSet = new Set(keys || []); } catch {}
557600
setupTabs();
558601
setupSearch();
559602
setupManageSites();

0 commit comments

Comments
 (0)