From 1542c79813ea13784d27478d757d69438819cf2c Mon Sep 17 00:00:00 2001 From: Codeon <313085171+codeon89@users.noreply.github.com> Date: Sun, 6 Sep 2026 18:12:13 -0700 Subject: [PATCH] feat: add {atlasId} to the supported key in install / import folder scheme --- CHANGELOG.md | 1 + electron/ipc/importer.js | 1 + electron/library/importRules.js | 1 + src/components/downloads/LibraryStructureModal.jsx | 3 ++- src/components/importer/Importer.jsx | 1 + src/components/importer/steps/SettingsStep.jsx | 4 +++- src/components/settings/Library.jsx | 2 +- tests/importer-helpers.test.js | 13 ++++++++++++- 8 files changed, 22 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f8cfb175..656cbcd3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - Path resolution highlighting: red if invalid, green if path exists or pass the check. ### Added +- Importer and Library folder scheme now support `{atlasId}`, so installs can be matched back to AtlasDB on re-import / rebuild. - Custom media uploads in the Game Details Media tab: add preview images from local files, a drag-and-drop zone, or an image URL, with live progress. Previews can be reordered by drag and the order persists in a new `preview_sort` table keyed by remote URL (or relative path for custom uploads), so it survives re-downloads, stream/download switches and metadata refreshes. Previews now carry a source logo and a storage-location badge, and custom previews can be deleted independently of downloaded ones. - Add Buzzheavier host support (`buzzheavier.com`, `bzzhr.to`, `bzzhr.co`). The download route is behind a Cloudflare challenge, so the resolve runs in a browser window: it clicks the htmx download button, captures the `HX-Redirect` (or an attachment via `will-download`), and hands the resolved CDN link plus the browser's own cookies/UA back to the downloader. The resolve partition is persistent so a solved challenge survives a restart -- only Cloudflare's own challenge cookies are kept, everything else is stripped after each resolve -- and resolves run one at a time, since a shared session cannot carry two concurrently. Each time your IP changes there is a brief auto-resolve window while the challenge is re-solved. - The version readout in the topnav and sidebar is now a button that opens that version's GitHub release page. The tag it builds matches what the release workflows publish -- `v` for stable and `v-nightly.` for nightly -- so it lands on the real release rather than a 404. (#143) diff --git a/electron/ipc/importer.js b/electron/ipc/importer.js index 842729cd..11e0d2ee 100644 --- a/electron/ipc/importer.js +++ b/electron/ipc/importer.js @@ -4771,6 +4771,7 @@ ipcMain.handle("downloads-install", async (event, { id, version, onComplete, kee currentConfig?.Library?.libraryFolderStructure, { f95Id: record.f95_id || record.f95Id || "", + atlasId: record.atlas_id || record.atlasId || "", engine: record.engine || "Unknown", creator: record.creator, title: record.title, diff --git a/electron/library/importRules.js b/electron/library/importRules.js index 3c1a830b..3ed7ecf4 100644 --- a/electron/library/importRules.js +++ b/electron/library/importRules.js @@ -73,6 +73,7 @@ function buildStructuredImportPath(targetLibrary, format, game) { if (key === "version") return normalizeVersionName(game.version); if (key === "engine") return game.engine || "Unknown"; if (key === "f95id") return game.f95Id || "Unknown"; + if (key === "atlasid") return game.atlasId || game.atlas_id || "Unknown"; if (key === "lcid" || key === "lewdcornerid") return game.lcId || game.lewdCornerId || "Unknown"; return "Unknown"; }), diff --git a/src/components/downloads/LibraryStructureModal.jsx b/src/components/downloads/LibraryStructureModal.jsx index 529060aa..8d7fe07b 100644 --- a/src/components/downloads/LibraryStructureModal.jsx +++ b/src/components/downloads/LibraryStructureModal.jsx @@ -59,7 +59,8 @@ const previewPath = (pattern) => .replace(/\{title\}/g, SAMPLE.title) .replace(/\{version\}/g, SAMPLE.version) .replace(/\{engine\}/g, 'RenPy') - .replace(/\{f95Id\}/g, '12345'), + .replace(/\{f95Id\}/g, '12345') + .replace(/\{atlasId\}/g, '6789'), ) .filter(Boolean) diff --git a/src/components/importer/Importer.jsx b/src/components/importer/Importer.jsx index bf23408c..dca0d775 100644 --- a/src/components/importer/Importer.jsx +++ b/src/components/importer/Importer.jsx @@ -995,6 +995,7 @@ const Importer = () => { engine: groups.engine || '', f95Id: groups.f95id || '', lcId: groups.lcid || '', + atlasId: groups.atlasid || '', } if (!cancelled) { setLivePreview({ diff --git a/src/components/importer/steps/SettingsStep.jsx b/src/components/importer/steps/SettingsStep.jsx index 0238bbff..f5c54304 100644 --- a/src/components/importer/steps/SettingsStep.jsx +++ b/src/components/importer/steps/SettingsStep.jsx @@ -31,6 +31,7 @@ export default function SettingsStep({ { label: 'Creator / Title - Version', value: '{creator}/{title} - {version}' }, { label: 'Title / Version, Creator', value: '{title}/{version},{creator}' }, { label: 'F95 ID / Title / Version', value: '{f95Id}/{title}/{version}' }, + { label: 'Atlas ID / Title / Version', value: '{atlasId}/{title}/{version}' }, { label: 'LewdCorner ID / Title / Version', value: '{lcId}/{title}/{version}' }, ] // "Auto detect" (unstructured name guessing) has been removed for now, so the @@ -167,6 +168,7 @@ export default function SettingsStep({ ['Engine', livePreview.fields.engine, false], ['F95 ID', livePreview.fields.f95Id, false], ['LC ID', livePreview.fields.lcId, false], + ['Atlas ID', livePreview.fields.atlasId, false], ].filter(([, value, always]) => always || value).map(([label, value]) => (
{label}
@@ -221,7 +223,7 @@ export default function SettingsStep({ onChange={(e) => setLibraryFormat(e.target.value)} spellCheck={false} placeholder="{creator}/{title}/{version}" - title="Destination path template. Tokens: {creator} {title} {version} {f95Id} {lcId}" + title="Destination path template. Tokens: {creator} {title} {version} {f95Id} {lcId} {atlasId}" className="sm:ml-2 flex-1 min-w-0 bg-secondary text-text border border-border rounded-buttonTheme p-1 focus:outline-none focus:ring-1 focus:ring-accent font-mono text-xs" />
diff --git a/src/components/settings/Library.jsx b/src/components/settings/Library.jsx index c072fb4e..ec754fac 100644 --- a/src/components/settings/Library.jsx +++ b/src/components/settings/Library.jsx @@ -277,7 +277,7 @@ const Library = () => {

Used when imports are moved or archives are extracted into the default library folder. Options: {"{creator}"}, {"{title}"}, {"{version}"},{" "} - {"{engine}"}, {"{f95Id}"}. + {"{engine}"}, {"{f95Id}"}, {"{atlasId}"}.
Example: {"{f95Id}/{creator}/{title}/{version}"}

diff --git a/tests/importer-helpers.test.js b/tests/importer-helpers.test.js index ea104ca6..281895f8 100644 --- a/tests/importer-helpers.test.js +++ b/tests/importer-helpers.test.js @@ -79,6 +79,17 @@ describe('buildStructuredImportPath', () => { const out = T.buildStructuredImportPath('/lib', '{lcid}', { lcId: 'LC9' }) expect(out).toBe(path.join('/lib', 'LC9')) }) + + it('supports atlasid from camelCase or snake_case, any casing', () => { + expect(T.buildStructuredImportPath('/lib', '{atlasId}', { atlasId: '456' })).toBe(path.join('/lib', '456')) + expect(T.buildStructuredImportPath('/lib', '{atlasId}', { atlas_id: 456 })).toBe(path.join('/lib', '456')) + expect(T.buildStructuredImportPath('/lib', '{AtlasID}', { atlasId: '456' })).toBe(path.join('/lib', '456')) + }) + + it('falls back to Unknown for a missing atlas id, even mid-segment', () => { + const out = T.buildStructuredImportPath('/lib', '{title} [{atlasId}]/{version}', { title: 'My Game', version: '1.0' }) + expect(out).toBe(path.join('/lib', 'My Game [Unknown]', '1.0')) + }) }) @@ -121,7 +132,7 @@ test('downloads-install passes required fields in buildStructuredImportPath', () if (!obj) throw new Error('buildStructuredImportPath was not given an object literal') const keys = obj.properties.map((p) => p.key.name) - const required = ['f95Id', 'engine', 'creator', 'title', 'version'] + const required = ['f95Id', 'atlasId', 'engine', 'creator', 'title', 'version'] for (const key of required) { expect(keys).toContain(key)