diff --git a/static/css/components/lora-modal/lora-modal.css b/static/css/components/lora-modal/lora-modal.css index a13a84b4..32b1bd3f 100644 --- a/static/css/components/lora-modal/lora-modal.css +++ b/static/css/components/lora-modal/lora-modal.css @@ -9,6 +9,42 @@ border-bottom: 1px solid var(--lora-border); } +.modal-header-actions { + display: flex; + align-items: center; + gap: var(--space-2); + flex-wrap: wrap; + width: 100%; + margin-bottom: var(--space-1); +} + +.modal-header-actions .license-restrictions { + margin-left: auto; +} + +.license-restrictions { + display: flex; + align-items: center; + gap: 8px; + padding: 4px 0; +} + +.license-restrictions .license-icon { + width: 22px; + height: 22px; + display: inline-block; + background-color: var(--text-muted); + -webkit-mask: var(--license-icon-image) center/contain no-repeat; + mask: var(--license-icon-image) center/contain no-repeat; + transition: background-color 0.2s ease, transform 0.2s ease; + cursor: default; +} + +.license-restrictions .license-icon:hover { + background-color: var(--text-color); + transform: translateY(-1px); +} + /* Info Grid */ .info-grid { display: grid; @@ -798,7 +834,7 @@ display: flex; align-items: center; gap: 10px; - margin-bottom: var(--space-1); + margin-bottom: 0; flex-wrap: wrap; } diff --git a/static/images/tabler/brush-off.svg b/static/images/tabler/brush-off.svg new file mode 100644 index 00000000..30db3d59 --- /dev/null +++ b/static/images/tabler/brush-off.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/images/tabler/exchange-off.svg b/static/images/tabler/exchange-off.svg new file mode 100644 index 00000000..ce5ed6f0 --- /dev/null +++ b/static/images/tabler/exchange-off.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/images/tabler/photo-off.svg b/static/images/tabler/photo-off.svg new file mode 100644 index 00000000..6fb1537b --- /dev/null +++ b/static/images/tabler/photo-off.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/images/tabler/rotate-2.svg b/static/images/tabler/rotate-2.svg new file mode 100644 index 00000000..0b627305 --- /dev/null +++ b/static/images/tabler/rotate-2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/images/tabler/shopping-cart-off.svg b/static/images/tabler/shopping-cart-off.svg new file mode 100644 index 00000000..a8a71aed --- /dev/null +++ b/static/images/tabler/shopping-cart-off.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/images/tabler/user-check.svg b/static/images/tabler/user-check.svg new file mode 100644 index 00000000..ce0a0cb1 --- /dev/null +++ b/static/images/tabler/user-check.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/images/tabler/world-off.svg b/static/images/tabler/world-off.svg new file mode 100644 index 00000000..f5376df2 --- /dev/null +++ b/static/images/tabler/world-off.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/js/components/shared/ModelModal.js b/static/js/components/shared/ModelModal.js index d1cca163..53e8e946 100644 --- a/static/js/components/shared/ModelModal.js +++ b/static/js/components/shared/ModelModal.js @@ -29,6 +29,166 @@ function getModalFilePath(fallback = '') { return fallback; } +const COMMERCIAL_ICON_CONFIG = [ + { + key: 'sell', + icon: 'shopping-cart-off.svg', + titleKey: 'modals.model.license.noSell', + fallback: 'Selling not allowed' + }, + { + key: 'rent', + icon: 'world-off.svg', + titleKey: 'modals.model.license.noRent', + fallback: 'Rental not allowed' + }, + { + key: 'rentcivit', + icon: 'brush-off.svg', + titleKey: 'modals.model.license.noRentCivit', + fallback: 'Civitai rental not allowed' + }, + { + key: 'image', + icon: 'photo-off.svg', + titleKey: 'modals.model.license.noImageUse', + fallback: 'Image use not allowed' + } +]; + +function hasLicenseField(license, field) { + return Object.prototype.hasOwnProperty.call(license || {}, field); +} + +function escapeAttribute(value) { + return String(value ?? '') + .replace(/&/g, '&') + .replace(/"/g, '"'); +} + +function indentMarkup(markup, spaces) { + if (!markup) { + return ''; + } + const padding = ' '.repeat(spaces); + return markup + .split('\n') + .map(line => (line ? padding + line : line)) + .join('\n'); +} + +function normalizeCommercialValues(value) { + if (!value && value !== '') { + return ['Sell']; + } + if (Array.isArray(value)) { + return value.filter(item => item !== null && item !== undefined); + } + if (typeof value === 'string') { + return [value]; + } + if (value && typeof value[Symbol.iterator] === 'function') { + const result = []; + for (const item of value) { + if (item === null || item === undefined) { + continue; + } + result.push(String(item)); + } + if (result.length > 0) { + return result; + } + } + return ['Sell']; +} + +function sanitiseCommercialValue(value) { + if (!value && value !== '') { + return ''; + } + return String(value) + .trim() + .toLowerCase() + .replace(/[\s_-]+/g, '') + .replace(/[^a-z]/g, ''); +} + +function resolveCommercialRestrictions(value) { + const normalizedValues = normalizeCommercialValues(value); + const allowed = new Set(); + normalizedValues.forEach(item => { + const cleaned = sanitiseCommercialValue(item); + if (!cleaned) { + return; + } + allowed.add(cleaned); + }); + + if (allowed.has('sell')) { + allowed.add('rent'); + allowed.add('rentcivit'); + allowed.add('image'); + } + if (allowed.has('rent')) { + allowed.add('rentcivit'); + } + + const disallowed = []; + COMMERCIAL_ICON_CONFIG.forEach(config => { + if (!allowed.has(config.key)) { + disallowed.push(config); + } + }); + return disallowed; +} + +function createLicenseIconMarkup(icon, label) { + const safeLabel = escapeAttribute(label); + const iconPath = `/loras_static/images/tabler/${icon}`; + return ``; +} + +function renderLicenseIcons(modelData) { + const license = modelData?.civitai?.model; + if (!license) { + return ''; + } + + const icons = []; + if (hasLicenseField(license, 'allowNoCredit') && license.allowNoCredit === false) { + const label = translate('modals.model.license.creditRequired', {}, 'Credit required'); + icons.push(createLicenseIconMarkup('user-check.svg', label)); + } + + if (hasLicenseField(license, 'allowCommercialUse')) { + const restrictions = resolveCommercialRestrictions(license.allowCommercialUse); + restrictions.forEach(({ icon, titleKey, fallback }) => { + const label = translate(titleKey, {}, fallback); + icons.push(createLicenseIconMarkup(icon, label)); + }); + } + + if (hasLicenseField(license, 'allowDerivatives') && license.allowDerivatives === false) { + const label = translate('modals.model.license.noDerivatives', {}, 'Derivatives not allowed'); + icons.push(createLicenseIconMarkup('exchange-off.svg', label)); + } + + if (hasLicenseField(license, 'allowDifferentLicense') && license.allowDifferentLicense === false) { + const label = translate('modals.model.license.noReLicense', {}, 'Relicensing not allowed'); + icons.push(createLicenseIconMarkup('rotate-2.svg', label)); + } + + if (!icons.length) { + return ''; + } + + const containerLabel = translate('modals.model.license.restrictionsLabel', {}, 'License restrictions'); + const safeContainerLabel = escapeAttribute(containerLabel); + return `
+ ${icons.join('\n ')} +
`; +} + /** * Display the model modal with the given model data * @param {Object} model - Model data object @@ -55,6 +215,51 @@ export async function showModelModal(model, modelType) { ...model, civitai: completeCivitaiData }; + const licenseIcons = renderLicenseIcons(modelWithFullData); + const viewOnCivitaiAction = modelWithFullData.from_civitai ? ` +
+ ${translate('modals.model.actions.viewOnCivitaiText', {}, 'View on Civitai')} +
`.trim() : ''; + const creatorInfoAction = modelWithFullData.civitai?.creator ? ` +
+ ${modelWithFullData.civitai.creator.image ? + `
+ ${modelWithFullData.civitai.creator.username} +
` : + `
+ +
` + } + ${modelWithFullData.civitai.creator.username} +
`.trim() : ''; + const creatorActionItems = []; + if (viewOnCivitaiAction) { + creatorActionItems.push(indentMarkup(viewOnCivitaiAction, 24)); + } + if (creatorInfoAction) { + creatorActionItems.push(indentMarkup(creatorInfoAction, 24)); + } + const creatorActionsMarkup = creatorActionItems.length + ? [ + '
', + creatorActionItems.join('\n'), + '
' + ].join('\n') + : ''; + const headerActionItems = []; + if (creatorActionsMarkup) { + headerActionItems.push(creatorActionsMarkup); + } + if (licenseIcons) { + headerActionItems.push(indentMarkup(licenseIcons.trim(), 20)); + } + const headerActionsMarkup = headerActionItems.length + ? [ + ' ' + ].join('\n') + : ''; const hasUpdateAvailable = Boolean(modelWithFullData.update_available); // Prepare LoRA specific data with complete civitai data @@ -172,25 +377,7 @@ export async function showModelModal(model, modelType) { -
- ${modelWithFullData.from_civitai ? ` -
- ${translate('modals.model.actions.viewOnCivitaiText', {}, 'View on Civitai')} -
` : ''} - - ${modelWithFullData.civitai?.creator ? ` -
- ${modelWithFullData.civitai.creator.image ? - `
- ${modelWithFullData.civitai.creator.username} -
` : - `
- -
` - } - ${modelWithFullData.civitai.creator.username} -
` : ''} -
+ ${headerActionsMarkup} ${renderCompactTags(modelWithFullData.tags || [], modelWithFullData.file_path)}