diff --git a/electron/release-notes.ts b/electron/release-notes.ts new file mode 100644 index 000000000..7ba4fd61f --- /dev/null +++ b/electron/release-notes.ts @@ -0,0 +1,42 @@ +import { convert } from 'html-to-text' +import type { UpdateInfo } from 'electron-updater' + +const MAX_RELEASE_NOTES_CHARS = 16_384 + +const htmlToText = (html: string): string => convert(html, { + wordwrap: false, + selectors: [ + { selector: 'a', options: { ignoreHref: true } }, + ], +}) + +function rawNotes(info: UpdateInfo): string | undefined { + const notes = info.releaseNotes + if (typeof notes === 'string' && notes.length > 0) return notes + if (Array.isArray(notes)) { + const parts = notes + .map((entry) => (typeof entry.note === 'string' ? entry.note : '')) + .filter((note) => note.length > 0) + return parts.length > 0 ? parts.join('\n\n') : undefined + } + return undefined +} + +function looksLikeHtml(value: string): boolean { + return /<\/?[a-z][\s\S]*>/i.test(value) +} + +function cap(value: string): string { + if (value.length <= MAX_RELEASE_NOTES_CHARS) return value + return `${value.slice(0, MAX_RELEASE_NOTES_CHARS)}\n…` +} + +/** GitHub's feed is HTML. The modal renders notes as text, so convert first. */ +export function releaseNotesFromFeed(info: UpdateInfo): string | undefined { + const raw = rawNotes(info) + if (!raw) return undefined + const bounded = cap(raw) + const text = (looksLikeHtml(bounded) ? htmlToText(bounded) : bounded).trim() + if (!text) return undefined + return cap(text) +} diff --git a/electron/updater.ts b/electron/updater.ts index da3424f73..fceeae188 100644 --- a/electron/updater.ts +++ b/electron/updater.ts @@ -3,6 +3,7 @@ import { app } from 'electron' import { logger } from './logger' import { preDownloadPythonForUpdate } from './python-setup' import { getMainWindow } from './window' +import { releaseNotesFromFeed } from './release-notes' import { getSkippedUpdateVersion, setSkippedUpdateVersion, getAutoCheckUpdates, setAutoCheckUpdates, @@ -12,9 +13,6 @@ import type { UpdateStatePayload } from '../shared/electron-api-schema' export type UpdateChannel = 'latest' | 'beta' | 'alpha' -// Cap untrusted feed notes so a huge GitHub body cannot bloat IPC / the modal. -const MAX_RELEASE_NOTES_CHARS = 16_384 - // The single in-memory value of update state. Broadcast on every change. let state: UpdateStatePayload = { status: 'idle', currentVersion: app.getVersion() } let periodicHandle: ReturnType | null = null @@ -27,12 +25,6 @@ function setState(patch: Partial): void { getMainWindow()?.webContents.send('update-event', state) } -function releaseNotesFromFeed(info: UpdateInfo): string | undefined { - if (typeof info.releaseNotes !== 'string' || info.releaseNotes.length === 0) return undefined - if (info.releaseNotes.length <= MAX_RELEASE_NOTES_CHARS) return info.releaseNotes - return `${info.releaseNotes.slice(0, MAX_RELEASE_NOTES_CHARS)}\n…` -} - export function getUpdateState(): UpdateStatePayload { return state } diff --git a/package.json b/package.json index 8aeb9a0b5..6365720a0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ltx-desktop", - "version": "1.2.3", + "version": "1.2.4", "description": "LTX-2 Video Generation - Desktop App", "type": "module", "main": "dist-electron/main.js", @@ -39,6 +39,7 @@ "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "electron-updater": "^6.8.3", + "html-to-text": "10.0.0", "js-yaml": "^4.1.1", "koffi": "3.1.4", "lucide-react": "^0.400.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 350e19fa4..d78851fc3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -22,6 +22,9 @@ importers: electron-updater: specifier: ^6.8.3 version: 6.8.3 + html-to-text: + specifier: 10.0.0 + version: 10.0.0 js-yaml: specifier: ^4.1.1 version: 4.1.1 @@ -766,6 +769,11 @@ packages: cpu: [x64] os: [win32] + '@selderee/plugin-htmlparser2@0.12.0': + resolution: {integrity: sha512-oELmoyA6ML9jDRMV3kgcMQFKxUfBU0yFVn6yTctVaLT5ygXnxH52I3TZEgV9EhXJC68/uFvE5Daj1/25c0Xa/A==} + peerDependencies: + selderee: ~0.12.0 + '@sindresorhus/is@4.6.0': resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} engines: {node: '>=10'} @@ -1148,6 +1156,10 @@ packages: resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} engines: {node: '>=10'} + deepmerge-ts@7.1.6: + resolution: {integrity: sha512-gQhL1ksGBLQbHeAo47YU6cs2ahd3Pv+8PFYoWogNZbQnNwQyOh9Ad5kbeHFiWwbKdeWQJ5Z7Y7mwb9ew2hXBLw==} + engines: {node: '>=16.0.0'} + defaults@1.0.4: resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} @@ -1192,6 +1204,19 @@ packages: os: [darwin] hasBin: true + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + dotenv-expand@11.0.7: resolution: {integrity: sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==} engines: {node: '>=12'} @@ -1250,6 +1275,14 @@ packages: end-of-stream@1.4.5: resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + env-paths@2.2.1: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} @@ -1459,6 +1492,13 @@ packages: resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} engines: {node: '>=10'} + html-to-text@10.0.0: + resolution: {integrity: sha512-2OH59Gtprdczel+7Rxgpz9hGVJREaf8Lt1H4kZwWHpEn70VQKRuMNGsb2eDbwaTzrYzb0hheiOG1P7Dim0B4dQ==} + engines: {node: '>=20.19.0'} + + htmlparser2@10.1.0: + resolution: {integrity: sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==} + http-cache-semantics@4.2.0: resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} @@ -1616,6 +1656,9 @@ packages: lazy-val@1.0.5: resolution: {integrity: sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==} + leac@0.7.0: + resolution: {integrity: sha512-qMrZeyEekgdRQ9o6a4NAB2EQZrv827GJdn1vnapwSJ90hWRB4TzUSunvacPkxQ2TnNqHNI1/zSt0hlo0crG8Jw==} + lilconfig@3.1.3: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} @@ -1842,6 +1885,9 @@ packages: resolution: {integrity: sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==} engines: {node: '>=18'} + parseley@0.13.1: + resolution: {integrity: sha512-uNBJZzmb60l6p6VWLTmevizNAGnE0xoSf1n0B4q3ntegDNzcS68NRCcBDZTcyXHxt2XhBChsCuqj4M+nChvE/A==} + path-is-absolute@1.0.1: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} engines: {node: '>=0.10.0'} @@ -1861,6 +1907,9 @@ packages: resolution: {integrity: sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==} engines: {node: '>=12', npm: '>=6'} + peberminta@0.10.0: + resolution: {integrity: sha512-80B2AsU+I4Qdb0ZAPSfe9UwvGzwkM37IKIFEvdS3D/3Ndgv2bsuJ0bfG1+iEYO+l7Gfd4EUJmuRyq7efLgRMzQ==} + pend@1.2.0: resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} @@ -2077,6 +2126,9 @@ packages: scheduler@0.23.2: resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + selderee@0.12.0: + resolution: {integrity: sha512-b1YMh3+DHZp59DLna3qVwQ5iOla/nrI6mLBNW02XxU77M3046Df6VLkoaJyFz20VsGIG5kkp+FK0kg4K4HnUFw==} + semver-compare@1.0.0: resolution: {integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==} @@ -3034,6 +3086,12 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.59.0': optional: true + '@selderee/plugin-htmlparser2@0.12.0(selderee@0.12.0)': + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + selderee: 0.12.0 + '@sindresorhus/is@4.6.0': {} '@szmarczak/http-timer@4.0.6': @@ -3483,6 +3541,8 @@ snapshots: dependencies: mimic-response: 3.1.0 + deepmerge-ts@7.1.6: {} + defaults@1.0.4: dependencies: clone: 1.0.4 @@ -3544,6 +3604,24 @@ snapshots: verror: 1.10.1 optional: true + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + domelementtype@2.3.0: {} + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + domutils@3.2.2: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + dotenv-expand@11.0.7: dependencies: dotenv: 16.6.1 @@ -3648,6 +3726,10 @@ snapshots: dependencies: once: 1.4.0 + entities@4.5.0: {} + + entities@7.0.1: {} + env-paths@2.2.1: {} err-code@2.0.3: {} @@ -3915,6 +3997,21 @@ snapshots: dependencies: lru-cache: 6.0.0 + html-to-text@10.0.0: + dependencies: + '@selderee/plugin-htmlparser2': 0.12.0(selderee@0.12.0) + deepmerge-ts: 7.1.6 + dom-serializer: 2.0.0 + htmlparser2: 10.1.0 + selderee: 0.12.0 + + htmlparser2@10.1.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + entities: 7.0.1 + http-cache-semantics@4.2.0: {} http-proxy-agent@7.0.2: @@ -4062,6 +4159,8 @@ snapshots: lazy-val@1.0.5: {} + leac@0.7.0: {} + lilconfig@3.1.3: {} lines-and-columns@1.2.4: {} @@ -4286,6 +4385,11 @@ snapshots: index-to-position: 1.2.0 type-fest: 4.41.0 + parseley@0.13.1: + dependencies: + leac: 0.7.0 + peberminta: 0.10.0 + path-is-absolute@1.0.1: {} path-key@3.1.1: {} @@ -4299,6 +4403,8 @@ snapshots: pe-library@0.4.1: {} + peberminta@0.10.0: {} + pend@1.2.0: {} picocolors@1.1.1: {} @@ -4520,6 +4626,10 @@ snapshots: dependencies: loose-envify: 1.4.0 + selderee@0.12.0: + dependencies: + parseley: 0.13.1 + semver-compare@1.0.0: optional: true