Merge feature/next: gallery videos play in their card, fill the viewer
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -119,7 +119,7 @@
|
||||
- A DeepL key saved by hand in `config.json` before did not work any more: DeepL stopped taking the key as a request parameter in 2025. It now goes in the Authorization header.
|
||||
|
||||
### 🎬 Videos in the gallery (图库显示视频)
|
||||
- **The gallery shows your video outputs** (MP4, WebM, MOV, MKV) next to the pictures: each card shows the video's first frame with a ▶ badge naming its format, and clicking it plays the video in the viewer. Closing the viewer (×, a click beside it or Esc) stops it. Videos are deleted (to the Recycle Bin) like pictures, and a video dragged onto the canvas becomes a Load Video node; View parameters, the star and the right-click menu stay with pictures, which carry the parameters. A first contribution from outside: thanks to cocorang and wkdtjs (PR #21).
|
||||
- **The gallery shows your video outputs** (MP4, WebM, MOV, MKV) next to the pictures: each card shows the video's first frame with a ▶ badge naming its format and plays it muted while the pointer is on it (or always, as Settings → Video covers says for model covers); clicking it plays the video with sound in the viewer, filling the window, and a double-click goes full screen. Closing the viewer (×, a click beside it or Esc) stops it. Videos are deleted (to the Recycle Bin) like pictures, and a video dragged onto the canvas becomes a Load Video node; View parameters, the star and the right-click menu stay with pictures, which carry the parameters. A first contribution from outside: thanks to cocorang and wkdtjs (PR #21).
|
||||
- **All / Images / Videos (全部 / 图片 / 视频)** beside the gallery's search, shown once your output folder has a video. **Settings → Gallery** hides formats you never want to see (GIF, say); click a format again to show it. Both are remembered in this browser. The recipe cover picker and AI apps (MCP) still see pictures only.
|
||||
|
||||
### 🧪 Tests in the repository (测试公开)
|
||||
|
||||
@@ -1178,7 +1178,7 @@ export const i18n = {
|
||||
settingsThemeAbyssal: "深渊绯红",
|
||||
settingsCards: "模型卡片与内存",
|
||||
settingsCardQualityHelp: "缩略图是 512 像素的小图,存在 ComfyUI 的用户文件夹里,重启后不用重做;打开模型详情仍是原图。原图在卡片上更清楚,但一屏几十张大图会明显多占内存和显存。",
|
||||
settingsVideoHelp: "视频封面平时显示第一帧,鼠标停上才播放。一直播放会让屏幕上的视频同时解码,更占显存和电量。",
|
||||
settingsVideoHelp: "模型的视频封面和图库里的视频平时显示第一帧,鼠标停上才(静音)播放。一直播放会让屏幕上的视频同时解码,更占显存和电量。",
|
||||
settingsCacheTitle: "缩略图缓存",
|
||||
settingsCacheLoading: "正在统计…",
|
||||
settingsCacheHelp: "已缓存 {count} 张,占 {size}(最多 {limit},满了自动清掉最久没用的)。清空后看到时会重新生成,不影响模型和封面。",
|
||||
@@ -3550,7 +3550,7 @@ export const i18n = {
|
||||
settingsThemeAbyssal: "Abyssal Scarlet",
|
||||
settingsCards: "Model cards and memory",
|
||||
settingsCardQualityHelp: "Thumbnails are 512 px copies kept in ComfyUI's user folder, so a restart does not redo them; a model's detail still shows the original. Originals look sharper on cards but dozens of large pictures on screen take clearly more memory and video memory.",
|
||||
settingsVideoHelp: "A video cover shows its first frame and plays when the pointer is on it. Always playing decodes every video on screen at once, which takes more video memory and power.",
|
||||
settingsVideoHelp: "Video covers and the gallery's videos show their first frame and play (muted) when the pointer is on them. Always playing decodes every video on screen at once, which takes more video memory and power.",
|
||||
settingsCacheTitle: "Thumbnail cache",
|
||||
settingsCacheLoading: "Counting…",
|
||||
settingsCacheHelp: "{count} cached, {size} (at most {limit}; the least recently used go when it is full). Emptied, they are made again when shown; models and covers are not touched.",
|
||||
|
||||
@@ -465,6 +465,16 @@ export function showGalleryViewer(src, isVideo = false) {
|
||||
video.autoplay = true;
|
||||
video.loop = true;
|
||||
video.style.display = 'none';
|
||||
// Double-click: full screen and back. Some Chrome builds already do this for a video
|
||||
// with controls; the check afterwards keeps it from toggling twice.
|
||||
video.addEventListener('dblclick', () => {
|
||||
const before = Boolean(document.fullscreenElement);
|
||||
setTimeout(() => {
|
||||
if (Boolean(document.fullscreenElement) !== before) return;
|
||||
if (before) document.exitFullscreen?.().catch(() => {});
|
||||
else video.requestFullscreen?.().catch(() => {});
|
||||
}, 200);
|
||||
});
|
||||
|
||||
viewer.appendChild(img);
|
||||
viewer.appendChild(video);
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
* workbench), the star (ui_keep_menu.js: keep it as a workflow, a combo or its prompts;
|
||||
* filled once kept as anything), Delete (to the Recycle Bin, after a confirmation) and the
|
||||
* right-click menu (gallery_card_menu.js: insert as a Load Image node, parameters). A video's
|
||||
* card shows its first frame with a ▶ badge and plays in the viewer; it has no Parameters,
|
||||
* card shows its first frame with a ▶ badge, plays muted in place (previewInPlace) and with
|
||||
* sound in the viewer; it has no Parameters,
|
||||
* star or menu (those read a picture).
|
||||
*/
|
||||
|
||||
@@ -102,6 +103,42 @@ async function deleteImage(owner, image, card) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A video card plays its video muted where it is: while the pointer is on it, or while it is in
|
||||
* view when Settings → Video covers says always play (as model covers do). The <video> exists
|
||||
* only while it plays, so a page of videos decodes nothing at rest.
|
||||
*/
|
||||
function previewInPlace(owner, card, url) {
|
||||
let video = null;
|
||||
const start = () => {
|
||||
if (video) return;
|
||||
video = Object.assign(document.createElement('video'), {
|
||||
className: 'anomalous-gallery-card-video', src: url, muted: true, loop: true, playsInline: true,
|
||||
});
|
||||
card.querySelector('img').after(video);
|
||||
video.play().catch(() => {});
|
||||
};
|
||||
const stop = () => {
|
||||
if (!video) return;
|
||||
video.pause();
|
||||
video.removeAttribute('src');
|
||||
video.load(); // lets the decoder go
|
||||
video.remove();
|
||||
video = null;
|
||||
};
|
||||
if (owner.energySaving) {
|
||||
card.addEventListener('mouseenter', start);
|
||||
card.addEventListener('mouseleave', stop);
|
||||
return;
|
||||
}
|
||||
// Leaving the gallery hides the cards, and hidden or removed cards stop too.
|
||||
owner.galleryVideoObserver ??= new IntersectionObserver((entries) => {
|
||||
for (const entry of entries) entry.target.anomalousPreview?.[entry.isIntersecting ? 'start' : 'stop']();
|
||||
}, { rootMargin: '100px' });
|
||||
card.anomalousPreview = { start, stop };
|
||||
owner.galleryVideoObserver.observe(card);
|
||||
}
|
||||
|
||||
/** The star: ☆, or ★ once the image is kept as anything. */
|
||||
function keepButton(owner, image) {
|
||||
const star = button('anomalous-gallery-keep', t('galleryKeep'), () => openKeepMenu(owner, star, image, {
|
||||
@@ -162,6 +199,7 @@ export function createGalleryCard(owner, imgData, { showViewer }) {
|
||||
badge.innerHTML = PLAY_ICON; // static markup only
|
||||
badge.append(image.filename.split('.').pop().toUpperCase());
|
||||
card.append(img, remove, badge);
|
||||
previewInPlace(owner, card, url);
|
||||
return card;
|
||||
}
|
||||
card.addEventListener('contextmenu', (event) => {
|
||||
|
||||
@@ -168,7 +168,11 @@ function appearanceGroup(owner, redraw) {
|
||||
}
|
||||
|
||||
function cardsGroup(owner, redraw) {
|
||||
const reload = () => { owner.loadModels(); redraw(); };
|
||||
const reload = () => {
|
||||
owner.galleryLoaded = false; // the gallery's video cards follow Video covers too
|
||||
owner.loadModels();
|
||||
redraw();
|
||||
};
|
||||
return group('settingsCards',
|
||||
row('sidebarCardQuality', 'settingsCardQualityHelp', segment(
|
||||
[['balanced', 'sidebarOptimizedThumbnail'], ['original', 'sidebarOriginalCover']], owner.cardThumbnailMode, value => {
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
@import url("./styles/00-foundation-models.css?v=20261007-theme-2");
|
||||
@import url("./styles/01-shell-tools.css?v=20261007-theme-2");
|
||||
@import url("./styles/02-notebooks.css?v=20261012-click-2");
|
||||
@import url("./styles/03-gallery-recipes.css?v=20261013-video-1");
|
||||
@import url("./styles/03-gallery-recipes.css?v=20261013-video-2");
|
||||
@import url("./styles/04-dialogs-materials.css?v=20261007-theme-2");
|
||||
@import url("./styles/05-recipe-studio-overrides.css?v=20261007-theme-2");
|
||||
@import url("./styles/06-material-workbench.css?v=20261007-theme-2");
|
||||
|
||||
@@ -173,6 +173,17 @@
|
||||
background: var(--amb-bg-page);
|
||||
}
|
||||
|
||||
/* A video card's muted preview, over its first frame; clicks and drags go to the card. */
|
||||
.anomalous-gallery-card-video {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
background: var(--amb-bg-page);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.anomalous-gallery-delete {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
@@ -449,8 +460,7 @@ dialog.anomalous-gallery-viewer[open] {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.anomalous-gallery-viewer img,
|
||||
.anomalous-gallery-viewer video {
|
||||
.anomalous-gallery-viewer img {
|
||||
max-width: 95%;
|
||||
max-height: 95%;
|
||||
object-fit: contain;
|
||||
@@ -458,6 +468,14 @@ dialog.anomalous-gallery-viewer[open] {
|
||||
box-shadow: 0 0 30px rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
/* A video fills the window (keeping its shape), however small it was made. */
|
||||
.anomalous-gallery-viewer video {
|
||||
width: 95%;
|
||||
height: 95%;
|
||||
object-fit: contain;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.anomalous-gallery-viewer-close {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
|
||||
Reference in New Issue
Block a user