diff --git a/app/e2e/viewports.mjs b/app/e2e/viewports.mjs index d19e3517..7e5e88f5 100644 --- a/app/e2e/viewports.mjs +++ b/app/e2e/viewports.mjs @@ -1,10 +1,10 @@ // The app at the screens people really have, in a real browser: no page overflow, every window inside the viewport, -// both timeline lanes visible, the timeline header on one row from 1000px wide, idle and while a render is running (a fake ComfyUI queue and socket). Not part of `npm test` (it needs browsers): run it with `npm run test:viewport`. +// both timeline lanes visible, the timeline header on one row from 1024px wide, idle and while a render is running (a fake ComfyUI queue and socket). Not part of `npm test` (it needs browsers): run it with `npm run test:viewport`. // FUNUI_BROWSERS=chromium,firefox picks the engines (default: every one that is installed). import playwright from "playwright"; import { start } from "./stub.mjs"; -const SIZES = [[1280, 720], [1024, 1024], [1366, 768], [1920, 1080], [2560, 1440], [768, 1024], [720, 1280], [1080, 1920]]; +const SIZES = [[1280, 720], [1024, 1024], [1000, 700], [1366, 768], [1920, 1080], [2560, 1440], [768, 1024], [720, 1280], [1080, 1920]]; const WINDOWS = [["Settings", /^Settings$/], ["Log", /^Log$/], ["Composer", /Composer/], ["Sampler", /Sampler/], ["Training", /Training/]]; const CLIENT = "viewport-test"; const wanted = (process.env.FUNUI_BROWSERS || "chromium,firefox").split(","); @@ -15,7 +15,7 @@ const inspect = () => { const out = seen.map((e) => ({ name: e.className.toString().split(" ")[0], r: box(e) })); const nle = document.querySelector(".cx-nle"), lanes = [...document.querySelectorAll(".cx-nle-gutter-lane")]; const head = [...document.querySelectorAll(".cx-panel-head")].find((h) => h.textContent.startsWith("Timeline")), act = head && head.querySelector(".cx-panel-actions"); - const header = innerWidth < 1000 || !head ? [] : [box(head).height > 50 && `Timeline header is ${Math.round(box(head).height)}px tall (more than one row)`, act.scrollWidth > act.clientWidth + 1 && `Timeline header clips ${act.scrollWidth - act.clientWidth}px of its controls`].filter(Boolean); + const header = innerWidth < 1024 || !head ? [] : [box(head).height > 50 && `Timeline header is ${Math.round(box(head).height)}px tall (more than one row)`, act.scrollWidth > act.clientWidth + 1 && `Timeline header clips ${act.scrollWidth - act.clientWidth}px of its controls`].filter(Boolean); return { header, over: [document.documentElement.scrollWidth - innerWidth, document.documentElement.scrollHeight - innerHeight], diff --git a/app/ui/features/upscale/index.js b/app/ui/features/upscale/index.js index bf86f6ec..d54d42b0 100644 --- a/app/ui/features/upscale/index.js +++ b/app/ui/features/upscale/index.js @@ -36,9 +36,13 @@ export default { if (pick) app.openSettings(pick); } - async function run(media, pressed = false) { + let told = false; // "waits" is said once, however many renders wait + const stillThere = (pid, key) => p.project && p.project.id === pid + && Object.values(p.project.scene_renders || {}).some((r) => r && r.media && keyOf(r.media) === key); + async function run(media, pressed = false, project = null) { + if (!p.project) return; if (!media || wentThrough(media, "upscale")) return; // never upscale twice, even after an interpolation in between - const key = keyOf(media), pid = p.project.id; + const key = keyOf(media), pid = project || p.project.id; if (busy.has(key)) return; busy.add(key); draw(); try { @@ -50,15 +54,19 @@ export default { c.toast.info({ text: `Upscaling with ${model}…` }); const out = await upscale(app.api, media, model, undefined, rtxOn() ? rtxOpts() : undefined); derived(media, out, "upscale"); + waited.delete(key); if (!waited.size) told = false; const swapped = await p.editFor(pid, (pr) => swap(pr, media, out) > 0); if (!swapped) c.toast.warn({ text: "Upscaled, but that render was replaced meanwhile, so nothing was swapped." }); else c.toast.good({ text: "Upscaled: the render now plays the larger video." }); } catch (err) { if (err.stopped) c.toast.info({ text: "Upscale stopped. The render stays as it was." }); else if (!pressed && /^ComfyUI is generating/.test(err.message || "") && (waited.get(key) || 0) < 60) { // Always: not lost, tried again when the generation is over - if (!waited.has(key)) c.toast.info({ text: "Upscale waits for the generation to finish." }); + if (!told) { told = true; c.toast.info({ text: "Upscale waits for the generation to finish." }); } waited.set(key, (waited.get(key) || 0) + 1); - setTimeout(() => run(media), 15000); + setTimeout(() => { // only for the same project, the same render and while Always is still on + if (stillThere(pid, key) && p.pref("upscaleMode", "never") === "always") run(media, false, pid); + else { waited.delete(key); if (!waited.size) told = false; } + }, 15000); } else { gaveUp(media, "upscale"); c.toast.warn({ text: `Upscale failed: ${err.message}` }); } } diff --git a/app/ui/frame.css b/app/ui/frame.css index 890792cd..4d01193c 100644 --- a/app/ui/frame.css +++ b/app/ui/frame.css @@ -50,10 +50,10 @@ :root.fp-peek.fp-peek-open .fp-peek-strip { display: none; } :root.fp-peek .fp-split-bottom { display: none; } -/* The timeline's header is ONE line wherever its controls can physically fit (about 1000px and up). It never spreads +/* The timeline's header is ONE line wherever its controls can physically fit (about 1024px and up). It never spreads its controls apart: the room goes to the text that can give way (hints, the step counter), which truncates, and below the widest layouts a button that has an icon shows only the icon (its label stays for screen readers). */ -@media (min-width: 1000px) { +@media (min-width: 1024px) { .fp-main > :nth-child(2) > .cx-panel-head { flex-wrap: nowrap; column-gap: var(--sp-3); } .fp-main > :nth-child(2) > .cx-panel-head .cx-panel-actions { flex: 1 1 0; flex-wrap: nowrap; min-inline-size: 0; overflow: hidden; } .fp-main > :nth-child(2) > .cx-panel-head .cx-panel-status { flex: none; flex-wrap: nowrap; gap: var(--sp-2); } @@ -62,7 +62,7 @@ .fp-main > :nth-child(2) > .cx-panel-head .cx-btn, .fp-main > :nth-child(2) > .cx-panel-head .cx-chip { flex: none; } .fp-main > :nth-child(2) > .cx-panel-head :is(.cx-hint, .cx-t) { flex: 0 1 auto; min-inline-size: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } } -@media (min-width: 1000px) and (max-width: 1500px) { +@media (min-width: 1024px) and (max-width: 1500px) { .fp-main > :nth-child(2) > .cx-panel-head .cx-btn:has(.cx-btn-icon) .cx-btn-label { position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }