diff --git a/docs/i18n-translation-guidelines.md b/docs/i18n-translation-guidelines.md index 922116e7..eca53c6f 100644 --- a/docs/i18n-translation-guidelines.md +++ b/docs/i18n-translation-guidelines.md @@ -133,6 +133,13 @@ Locales: `en`, `zh-CN`, `zh-TW`, `ja`, `ko`, `fr`, `de`, `es`, `ru`, `he` (RTL). > now always on. All 9 locales are translated (terminology in §2, "Civitai ids feature"), > so the "no remaining placeholders" claim holds again. +> **Status (2026-10, showcase layout option):** the model modal's example images became +> switchable between the on-demand gallery and the classic vertical list (issue #1136), +> adding 6 keys (`settings.layoutSettings.showcaseLayout`, `.showcaseLayoutHelp`, +> `.showcaseLayoutOptions.gallery` / `.vertical`, `modals.model.showcase.layoutGallery` / +> `.layoutList`). All 9 locales are translated (terminology in §2, "Showcase layout +> feature"), so the "no remaining placeholders" claim holds again. + --- ## 1. Hard rules (do not violate) @@ -629,6 +636,28 @@ verbatim (R9). The copy/copied strings reuse each locale's existing clipboard pa Hebrew uses its established מזהה ("identifier") noun instead of Latin `ID` in these labels, matching `recipes.*.copyId` (העתק מזהה מתכון). +### Showcase layout feature (gallery / vertical list toggle) + +The model modal's example images can switch between a one-at-a-time **gallery** and the +classic **vertical list**, via a Settings select (`settings.layoutSettings.showcaseLayout*`, +mirroring the `recipesLayout*` select shape) and an in-modal segmented toggle whose two +icon buttons are labelled by `modals.model.showcase.layoutGallery` / `.layoutList` +(kept short — they are icon-button tooltips). + +"Showcase Layout" is rendered as the **example-images layout** in most locales (the +section's user-facing content), reusing each locale's fixed "example images" noun +(`modelCardFooterActionOptions.exampleImages`); ja keeps its established ショーケース +loanword instead. The `layoutList` tooltip reuses the fixed "list view" noun from the +folder-sidebar row (above), so it stays byte-consistent with `sidebar.listView` where +that form fits a tooltip (ru shortens both toggle labels to bare «Галерея» / «Список»). + +| Term | Rendering | +|---|---| +| showcase layout (settings label) | zh-CN 示例图片布局 · zh-TW 範例圖片版面 · ja ショーケースのレイアウト · ko 예시 이미지 레이아웃 · fr Disposition des images d'exemple · de Beispielbilder-Layout · es Diseño de imágenes de ejemplo · ru Макет примеров изображений · he פריסת תמונות דוגמה | +| example images | zh-CN 示例图片 · zh-TW 範例圖片 · ja 例画像 · ko 예시 이미지 · fr images d'exemple · de Beispielbilder · es imágenes de ejemplo · ru примеры изображений · he תמונות דוגמה | +| gallery / vertical list (option labels) | zh-CN 画廊 / 纵向列表 · zh-TW 圖庫 / 垂直清單 · ja ギャラリー / 縦並びリスト · ko 갤러리 / 세로 목록 · fr Galerie / Liste verticale · de Galerie / Vertikale Liste · es Galería / Lista vertical · ru Галерея / Вертикальный список · he גלריה / רשימה אנכית | +| gallery view / list view (toggle tooltips) | zh-CN 画廊视图 / 列表视图 · zh-TW 圖庫檢視 / 清單檢視 · ja ギャラリー表示 / リスト表示 · ko 갤러리 보기 / 목록 보기 · fr Vue galerie / Vue liste · de Galerieansicht / Listenansicht · es Vista de galería / Vista de lista · ru Галерея / Список · he תצוגת גלריה / תצוגת רשימה | + --- ## 3. Cross-cutting confusion hot-spots (must-fix list) diff --git a/locales/de.json b/locales/de.json index 950fffa4..c39795ef 100644 --- a/locales/de.json +++ b/locales/de.json @@ -525,6 +525,12 @@ "hover": "Bei Hover anzeigen" }, "cardInfoDisplayHelp": "Wählen Sie, wann Modellinformationen und Aktionsschaltflächen angezeigt werden sollen", + "showcaseLayout": "Beispielbilder-Layout", + "showcaseLayoutHelp": "Wählen Sie, wie Beispielbilder im Modell-Dialog angezeigt werden: eine Galerie mit jeweils einem Bild oder die klassische vertikale Liste.", + "showcaseLayoutOptions": { + "gallery": "Galerie", + "vertical": "Vertikale Liste" + }, "showVersionOnCard": "Version auf Karte anzeigen", "showVersionOnCardHelp": "Den Versionsnamen auf Modellkarten ein- oder ausblenden", "modelCardFooterAction": "Aktion der Modellkarten-Schaltfläche", @@ -1977,7 +1983,9 @@ "nsfwMature": "Nicht jugendfreie Inhalte", "nsfwR": "Inhalte ab 18 (R)", "nsfwX": "Inhalte mit X-Einstufung", - "nsfwXxx": "Inhalte mit XXX-Einstufung" + "nsfwXxx": "Inhalte mit XXX-Einstufung", + "layoutGallery": "Galerieansicht", + "layoutList": "Listenansicht" }, "versions": { "heading": "Modellversionen", diff --git a/locales/en.json b/locales/en.json index 4450155d..12c7fe64 100644 --- a/locales/en.json +++ b/locales/en.json @@ -525,6 +525,12 @@ "hover": "Reveal on Hover" }, "cardInfoDisplayHelp": "Choose when to display model information and action buttons", + "showcaseLayout": "Showcase Layout", + "showcaseLayoutHelp": "Choose how example images are displayed in the model modal: a one-at-a-time gallery or the classic vertical list.", + "showcaseLayoutOptions": { + "gallery": "Gallery", + "vertical": "Vertical List" + }, "showVersionOnCard": "Show Version on Card", "showVersionOnCardHelp": "Show or hide the version name on model cards", "modelCardFooterAction": "Model Card Button Action", @@ -1977,7 +1983,9 @@ "nsfwMature": "Mature Content", "nsfwR": "R-rated Content", "nsfwX": "X-rated Content", - "nsfwXxx": "XXX-rated Content" + "nsfwXxx": "XXX-rated Content", + "layoutGallery": "Gallery view", + "layoutList": "List view" }, "versions": { "heading": "Model versions", diff --git a/locales/es.json b/locales/es.json index 3436ddee..5b98470a 100644 --- a/locales/es.json +++ b/locales/es.json @@ -525,6 +525,12 @@ "hover": "Mostrar al pasar el ratón" }, "cardInfoDisplayHelp": "Elige cuándo mostrar información del modelo y botones de acción", + "showcaseLayout": "Diseño de imágenes de ejemplo", + "showcaseLayoutHelp": "Elige cómo se muestran las imágenes de ejemplo en el modal del modelo: una galería que las muestra de una en una o la clásica lista vertical.", + "showcaseLayoutOptions": { + "gallery": "Galería", + "vertical": "Lista vertical" + }, "showVersionOnCard": "Mostrar versión en la tarjeta", "showVersionOnCardHelp": "Mostrar u ocultar el nombre de versión en las tarjetas de modelo", "modelCardFooterAction": "Acción del botón de tarjeta de modelo", @@ -1977,7 +1983,9 @@ "nsfwMature": "Contenido para adultos", "nsfwR": "Contenido clasificación R", "nsfwX": "Contenido clasificación X", - "nsfwXxx": "Contenido clasificación XXX" + "nsfwXxx": "Contenido clasificación XXX", + "layoutGallery": "Vista de galería", + "layoutList": "Vista de lista" }, "versions": { "heading": "Versiones del modelo", diff --git a/locales/fr.json b/locales/fr.json index 9dcdb70b..f45a4b30 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -525,6 +525,12 @@ "hover": "Révéler au survol" }, "cardInfoDisplayHelp": "Choisissez quand afficher les informations du modèle et les boutons d'action", + "showcaseLayout": "Disposition des images d'exemple", + "showcaseLayoutHelp": "Choisissez comment les images d'exemple sont affichées dans la fenêtre modale du modèle : une galerie une image à la fois ou la liste verticale classique.", + "showcaseLayoutOptions": { + "gallery": "Galerie", + "vertical": "Liste verticale" + }, "showVersionOnCard": "Afficher la version sur la carte", "showVersionOnCardHelp": "Afficher ou masquer le nom de version sur les cartes de modèle", "modelCardFooterAction": "Action du bouton de carte de modèle", @@ -1977,7 +1983,9 @@ "nsfwMature": "Contenu pour adultes", "nsfwR": "Contenu classé R", "nsfwX": "Contenu classé X", - "nsfwXxx": "Contenu classé XXX" + "nsfwXxx": "Contenu classé XXX", + "layoutGallery": "Vue galerie", + "layoutList": "Vue liste" }, "versions": { "heading": "Versions du modèle", diff --git a/locales/he.json b/locales/he.json index 59e85aae..3bac6305 100644 --- a/locales/he.json +++ b/locales/he.json @@ -525,6 +525,12 @@ "hover": "חשוף בריחוף" }, "cardInfoDisplayHelp": "בחר מתי להציג מידע על המודל וכפתורי פעולה", + "showcaseLayout": "פריסת תמונות דוגמה", + "showcaseLayoutHelp": "בחר כיצד יוצגו תמונות הדוגמה בחלון המודל: גלריה המציגה תמונה אחת בכל פעם או הרשימה האנכית הקלאסית.", + "showcaseLayoutOptions": { + "gallery": "גלריה", + "vertical": "רשימה אנכית" + }, "showVersionOnCard": "הצג גרסה בכרטיס", "showVersionOnCardHelp": "הצג או הסתר את שם הגרסה בכרטיסי המודל", "modelCardFooterAction": "פעולת כפתור כרטיס מודל", @@ -1977,7 +1983,9 @@ "nsfwMature": "תוכן למבוגרים", "nsfwR": "תוכן בדירוג R", "nsfwX": "תוכן בדירוג X", - "nsfwXxx": "תוכן בדירוג XXX" + "nsfwXxx": "תוכן בדירוג XXX", + "layoutGallery": "תצוגת גלריה", + "layoutList": "תצוגת רשימה" }, "versions": { "heading": "גרסאות המודל", diff --git a/locales/ja.json b/locales/ja.json index df6d00f0..fcfe18d6 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -525,6 +525,12 @@ "hover": "ホバー時に表示" }, "cardInfoDisplayHelp": "モデル情報とアクションボタンの表示タイミングを選択", + "showcaseLayout": "ショーケースのレイアウト", + "showcaseLayoutHelp": "モデルモーダルでの例画像の表示方法を選択:1枚ずつ表示するギャラリー、または従来の縦並びリスト。", + "showcaseLayoutOptions": { + "gallery": "ギャラリー", + "vertical": "縦並びリスト" + }, "showVersionOnCard": "カードにバージョンを表示", "showVersionOnCardHelp": "モデルカード上のバージョン名の表示/非表示を切り替えます", "modelCardFooterAction": "モデルカードボタンのアクション", @@ -1977,7 +1983,9 @@ "nsfwMature": "成人向けコンテンツ", "nsfwR": "R指定コンテンツ", "nsfwX": "X指定コンテンツ", - "nsfwXxx": "XXX指定コンテンツ" + "nsfwXxx": "XXX指定コンテンツ", + "layoutGallery": "ギャラリー表示", + "layoutList": "リスト表示" }, "versions": { "heading": "モデルバージョン", diff --git a/locales/ko.json b/locales/ko.json index a9f5db33..d583214c 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -525,6 +525,12 @@ "hover": "호버 시 표시" }, "cardInfoDisplayHelp": "모델 정보 및 액션 버튼을 언제 표시할지 선택하세요", + "showcaseLayout": "예시 이미지 레이아웃", + "showcaseLayoutHelp": "모델 모달에서 예시 이미지의 표시 방식을 선택하세요: 한 번에 하나씩 보여주는 갤러리 또는 기존 세로 목록.", + "showcaseLayoutOptions": { + "gallery": "갤러리", + "vertical": "세로 목록" + }, "showVersionOnCard": "카드에 버전 표시", "showVersionOnCardHelp": "모델 카드에 버전 이름 표시 여부를 전환합니다", "modelCardFooterAction": "모델 카드 버튼 동작", @@ -1977,7 +1983,9 @@ "nsfwMature": "성인 콘텐츠", "nsfwR": "R등급 콘텐츠", "nsfwX": "X등급 콘텐츠", - "nsfwXxx": "XXX등급 콘텐츠" + "nsfwXxx": "XXX등급 콘텐츠", + "layoutGallery": "갤러리 보기", + "layoutList": "목록 보기" }, "versions": { "heading": "모델 버전", diff --git a/locales/ru.json b/locales/ru.json index 992e64e4..61548626 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -525,6 +525,12 @@ "hover": "Показать при наведении" }, "cardInfoDisplayHelp": "Выберите когда отображать информацию о модели и кнопки действий", + "showcaseLayout": "Макет примеров изображений", + "showcaseLayoutHelp": "Выберите, как отображаются примеры изображений в окне модели: галерея с просмотром по одному изображению или классический вертикальный список.", + "showcaseLayoutOptions": { + "gallery": "Галерея", + "vertical": "Вертикальный список" + }, "showVersionOnCard": "Показывать версию на карточке", "showVersionOnCardHelp": "Показать или скрыть название версии на карточках моделей", "modelCardFooterAction": "Действие кнопки карточки модели", @@ -1977,7 +1983,9 @@ "nsfwMature": "Контент для взрослых", "nsfwR": "Контент с рейтингом R", "nsfwX": "Контент с рейтингом X", - "nsfwXxx": "Контент с рейтингом XXX" + "nsfwXxx": "Контент с рейтингом XXX", + "layoutGallery": "Галерея", + "layoutList": "Список" }, "versions": { "heading": "Версии модели", diff --git a/locales/zh-CN.json b/locales/zh-CN.json index 81532243..26970753 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -525,6 +525,12 @@ "hover": "悬停时显示" }, "cardInfoDisplayHelp": "选择何时显示模型信息和操作按钮", + "showcaseLayout": "示例图片布局", + "showcaseLayoutHelp": "选择示例图片在模型弹窗中的显示方式:一次显示一张的画廊,或经典的纵向列表。", + "showcaseLayoutOptions": { + "gallery": "画廊", + "vertical": "纵向列表" + }, "showVersionOnCard": "在卡片上显示版本", "showVersionOnCardHelp": "在模型卡片上显示或隐藏版本名称", "modelCardFooterAction": "模型卡片按钮操作", @@ -1977,7 +1983,9 @@ "nsfwMature": "成熟内容", "nsfwR": "R 级内容", "nsfwX": "X 级内容", - "nsfwXxx": "XXX 级内容" + "nsfwXxx": "XXX 级内容", + "layoutGallery": "画廊视图", + "layoutList": "列表视图" }, "versions": { "heading": "模型版本", diff --git a/locales/zh-TW.json b/locales/zh-TW.json index ca730e0b..d37d2c58 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -525,6 +525,12 @@ "hover": "滑鼠懸停顯示" }, "cardInfoDisplayHelp": "選擇何時顯示模型資訊與操作按鈕", + "showcaseLayout": "範例圖片版面", + "showcaseLayoutHelp": "選擇範例圖片在模型彈窗中的顯示方式:一次一張的圖庫,或傳統的垂直清單。", + "showcaseLayoutOptions": { + "gallery": "圖庫", + "vertical": "垂直清單" + }, "showVersionOnCard": "在卡片上顯示版本", "showVersionOnCardHelp": "在模型卡片上顯示或隱藏版本名稱", "modelCardFooterAction": "模型卡片按鈕操作", @@ -1977,7 +1983,9 @@ "nsfwMature": "成熟內容", "nsfwR": "R 級內容", "nsfwX": "X 級內容", - "nsfwXxx": "XXX 級內容" + "nsfwXxx": "XXX 級內容", + "layoutGallery": "圖庫檢視", + "layoutList": "清單檢視" }, "versions": { "heading": "模型版本", diff --git a/py/services/settings_manager.py b/py/services/settings_manager.py index 594844ab..24216116 100644 --- a/py/services/settings_manager.py +++ b/py/services/settings_manager.py @@ -121,6 +121,7 @@ DEFAULT_SETTINGS: Dict[str, Any] = { "display_density": "default", "recipes_layout": "grid", "card_info_display": "always", + "showcase_layout": "gallery", "include_trigger_words": False, "compact_mode": False, "priority_tags": DEFAULT_PRIORITY_TAG_CONFIG.copy(), diff --git a/static/css/components/lora-modal/showcase.css b/static/css/components/lora-modal/showcase.css index 869b4205..d093fa10 100644 --- a/static/css/components/lora-modal/showcase.css +++ b/static/css/components/lora-modal/showcase.css @@ -297,6 +297,64 @@ margin-top: 0; } +/* Segmented layout toggle (gallery / vertical list), shown in the collapsed + indicator bar and in each layout's toolbar */ +.showcase-layout-toggle { + display: inline-flex; + flex-shrink: 0; + border: 1px solid var(--border-color); + border-radius: var(--border-radius-xs); + overflow: hidden; +} + +.showcase-layout-toggle .layout-toggle-btn { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 6px 10px; + background: var(--card-bg); + border: none; + color: var(--text-color); + font-size: 0.9em; + cursor: pointer; + transition: var(--transition-base); +} + +.showcase-layout-toggle .layout-toggle-btn + .layout-toggle-btn { + border-left: 1px solid var(--border-color); +} + +.showcase-layout-toggle .layout-toggle-btn:hover { + color: var(--lora-accent); +} + +.showcase-layout-toggle .layout-toggle-btn.active { + background: var(--lora-accent); + color: var(--lora-text); +} + +/* Vertical list layout (showcase_layout = vertical): every example stacked + full-width. Each .media-wrapper carries an inline padding-bottom aspect + ratio, so the column is stable before any media loads; the media is + absolutely positioned into that box (revived legacy carousel styles, + scoped under .showcase-vertical to avoid clashing with the gallery) */ +.showcase-vertical .vertical-list-container { + display: flex; + flex-direction: column; + gap: var(--space-2); +} + +.showcase-vertical .media-wrapper img, +.showcase-vertical .media-wrapper video { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + object-fit: contain; + cursor: zoom-in; +} + .media-wrapper { position: relative; width: 100%; diff --git a/static/js/components/shared/showcase/MediaRenderers.js b/static/js/components/shared/showcase/MediaRenderers.js index 3b1e2c7b..a9099280 100644 --- a/static/js/components/shared/showcase/MediaRenderers.js +++ b/static/js/components/shared/showcase/MediaRenderers.js @@ -2,6 +2,11 @@ * MediaRenderers.js * HTML generators for media items (images/videos) in the showcase */ +import { state } from '../../../state/index.js'; +import { translate } from '../../../utils/i18nHelpers.js'; +import { NSFW_LEVELS, getMatureBlurThreshold } from '../../../utils/constants.js'; +import { generateMetadataPanel } from './MetadataPanel.js'; +import { getDisplayUrl, getShowcaseUrl } from '../../../utils/civitaiUtils.js'; /** * Generate video wrapper HTML. The wrapper fills its container (the gallery's @@ -13,13 +18,16 @@ * @param {string} localUrl - Local file URL * @param {string} remoteUrl - Remote file URL * @param {string} mediaControlsHtml - HTML for media control buttons + * @param {number|null} heightPercent - Vertical-list aspect ratio (padding-bottom + * percent) sizing the wrapper before the media loads; null in gallery mode * @returns {string} HTML content */ -export function generateVideoWrapper(media, shouldBlur, nsfwText, metadataPanel, localUrl, remoteUrl, mediaControlsHtml = '') { +export function generateVideoWrapper(media, shouldBlur, nsfwText, metadataPanel, localUrl, remoteUrl, mediaControlsHtml = '', heightPercent = null) { const nsfwLevel = media.nsfwLevel !== undefined ? media.nsfwLevel : 0; + const wrapperStyle = heightPercent !== null ? ` style="padding-bottom: ${heightPercent}%"` : ''; return ` -