Dvesign improvemments

This commit is contained in:
2026-04-27 21:54:20 +12:00
parent cd563d67b6
commit 6ca5822246
15 changed files with 4857 additions and 955 deletions
File diff suppressed because it is too large Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
View File
+22 -22
View File
@@ -577,12 +577,12 @@
color: var(--text);
}
h2 {
font-size: 26px;
font-size: 24px;
font-weight: 800;
}
h3 {
font-size: 15px;
font-weight: 700;
font-weight: 800;
}
.page-copy,
.card-copy {
@@ -617,11 +617,10 @@
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.builder-card {
background: var(--surface);
background: #0f1116;
border: 1px solid var(--border);
border-radius: 18px;
border-radius: 12px;
padding: 18px;
box-shadow: 0 10px 28px rgba(0, 0, 0, 0.16);
}
.hero-card {
padding-bottom: 14px;
@@ -671,9 +670,9 @@
width: 100%;
box-sizing: border-box;
padding: 10px 12px;
border-radius: 12px;
border-radius: 10px;
border: 1px solid var(--border);
background: #0b0f14;
background: #12151b;
color: var(--text);
font-size: 13px;
font-family: inherit;
@@ -703,16 +702,16 @@
.target-option,
.preview-item {
border: 1px solid var(--border);
border-radius: 14px;
background: var(--bg-secondary);
border-radius: 10px;
background: #111419;
padding: 12px 14px;
}
.result-card {
all: unset;
cursor: pointer;
border: 1px solid var(--border);
border-radius: 14px;
background: var(--bg-secondary);
border-radius: 10px;
background: #111419;
padding: 12px 14px;
transition: background 0.12s ease, border-color 0.12s ease;
}
@@ -762,8 +761,8 @@
}
.empty-note {
padding: 14px;
border-radius: 14px;
background: var(--bg-secondary);
border-radius: 10px;
background: #111419;
border: 1px dashed var(--border);
color: var(--text-muted);
font-size: 13px;
@@ -782,9 +781,9 @@
}
.status {
padding: 10px 12px;
border-radius: 14px;
border-radius: 10px;
font-size: 12px;
background: var(--bg-secondary);
background: #111419;
border: 1px solid var(--border);
color: var(--text-muted);
margin-top: 12px;
@@ -801,7 +800,7 @@
all: unset;
cursor: pointer;
padding: 9px 16px;
border-radius: 12px;
border-radius: 10px;
font-size: 13px;
font-weight: 600;
font-family: inherit;
@@ -809,13 +808,14 @@
text-align: center;
}
.btn.ghost {
color: #d9e7f8;
color: var(--text);
border-color: var(--border);
background: var(--surface);
background: #15181e;
}
.btn.accent {
background: var(--accent);
color: #fff;
border-color: rgba(42, 215, 239, 0.35);
color: #031014;
}
.btn:disabled {
opacity: 0.45;
@@ -827,9 +827,9 @@
justify-content: center;
width: 34px;
height: 34px;
border-radius: 10px;
background: var(--surface-active);
color: #a9d6ff;
border-radius: 8px;
background: rgba(40, 193, 220, 0.1);
color: var(--accent);
}
@media (max-width: 1100px) {
+35 -36
View File
@@ -495,21 +495,18 @@
<style>
.section-card {
background: var(--surface);
border-radius: 20px;
background: #101318;
border-radius: 12px;
border: 1px solid var(--border);
margin-bottom: 12px;
overflow: hidden;
box-shadow: 0 18px 36px rgba(0, 0, 0, 0.18);
transition: border-color 0.15s, transform 0.15s ease, box-shadow 0.15s ease;
transition: border-color 0.15s ease, background 0.15s ease;
}
.section-card:hover {
transform: translateY(-1px);
box-shadow: 0 22px 42px rgba(0, 0, 0, 0.22);
background: #12161d;
}
.section-card.expanded {
border-color: var(--border-strong);
box-shadow: 0 24px 48px rgba(0, 0, 0, 0.26);
}
.section-header {
all: unset;
@@ -534,9 +531,9 @@
cursor: pointer;
font-size: 10px;
line-height: 1;
opacity: 0.6;
opacity: 0.7;
padding: 3px 4px;
color: var(--text);
color: var(--text-muted);
}
.move-btn:disabled {
opacity: 0.15;
@@ -556,12 +553,12 @@
display: inline-flex;
align-items: center;
justify-content: center;
width: 34px;
height: 34px;
border-radius: 12px;
background: var(--bg-secondary);
width: 30px;
height: 30px;
border-radius: 8px;
background: rgba(40, 193, 220, 0.1);
border: 1px solid var(--border);
color: #a9d6ff;
color: var(--accent);
flex-shrink: 0;
}
.section-info {
@@ -570,7 +567,7 @@
}
.section-name {
font-weight: 600;
font-size: 15px;
font-size: 14px;
color: var(--text);
white-space: nowrap;
overflow: hidden;
@@ -588,10 +585,10 @@
all: unset;
cursor: pointer;
color: var(--danger);
font-size: 22px;
font-size: 20px;
line-height: 1;
padding: 0 4px;
opacity: 0.5;
opacity: 0.55;
}
.remove-btn:hover {
opacity: 1;
@@ -604,8 +601,9 @@
/* Editor panel */
.section-editor {
padding: 8px 16px 18px;
padding: 10px 16px 18px;
border-top: 1px solid var(--border);
background: #0d1015;
}
.field-grid {
display: grid;
@@ -629,10 +627,10 @@
}
.field-label {
font-size: 11px;
font-weight: 600;
font-weight: 700;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
letter-spacing: 0.08em;
}
.hint-inline {
font-size: 10px;
@@ -644,9 +642,9 @@
.field input[type='text'],
.field select {
padding: 10px 12px;
border-radius: 12px;
border-radius: 10px;
border: 1px solid var(--border);
background: #0b0f14;
background: #12151b;
color: var(--text);
font-size: 13px;
font-family: inherit;
@@ -668,10 +666,10 @@
}
.lookup-btn {
padding: 10px 12px;
border-radius: 12px;
border-radius: 10px;
text-align: center;
color: var(--text);
background: var(--surface);
background: #15181e;
}
.lookup-btn:disabled {
opacity: 0.45;
@@ -690,9 +688,9 @@
.inline-status {
margin-top: 8px;
padding: 10px 12px;
border-radius: 12px;
border-radius: 10px;
border: 1px solid var(--border);
background: var(--surface);
background: #12151b;
font-size: 12px;
color: var(--text-muted);
}
@@ -714,6 +712,7 @@
font-size: 12px;
border: 1px solid var(--border);
color: var(--text-muted);
background: #12151b;
transition: all 0.12s;
}
.chip:hover {
@@ -722,8 +721,8 @@
}
.chip.active {
background: var(--accent);
border-color: transparent;
color: #fff;
border-color: rgba(42, 215, 239, 0.3);
color: #031014;
}
.field-inline {
@@ -740,9 +739,9 @@
width: 100%;
margin-top: 6px;
padding: 10px 12px;
border-radius: 12px;
border-radius: 10px;
border: 1px solid var(--border);
background: #0b0f14;
background: #12151b;
color: var(--text);
font-size: 12px;
font-family: monospace;
@@ -762,7 +761,7 @@
display: inline-flex;
align-items: center;
gap: 3px;
background: #0b0f14;
background: #12151b;
border: 1px solid var(--border);
border-radius: 999px;
padding: 4px 8px 4px 10px;
@@ -784,7 +783,7 @@
.badge {
display: inline-block;
background: var(--accent);
color: #fff;
color: #031014;
border-radius: 999px;
padding: 2px 6px;
font-size: 10px;
@@ -803,8 +802,8 @@
color: var(--text);
margin-top: 6px;
padding: 10px 12px;
background: #0b0f14;
border-radius: 12px;
background: #12151b;
border-radius: 10px;
}
.text-muted {
color: var(--text-muted);
@@ -821,8 +820,8 @@
justify-content: space-between;
gap: 12px;
padding: 8px 10px;
border-radius: 12px;
background: var(--bg-secondary);
border-radius: 10px;
background: #12151b;
border: 1px solid var(--border);
}
.lookup-result:hover {
+21 -21
View File
@@ -215,19 +215,19 @@
<style>
.panel {
padding: 0 4px;
padding: 0;
}
h3 {
font-size: 18px;
font-weight: 700;
margin: 0 0 18px;
font-size: 16px;
font-weight: 800;
margin: 0 0 16px;
color: var(--text);
}
h4 {
font-size: 12px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
letter-spacing: 0.08em;
color: var(--text-muted);
margin: 0 0 10px;
}
@@ -235,9 +235,8 @@
margin-bottom: 18px;
padding: 16px;
border: 1px solid var(--border);
border-radius: 18px;
background: var(--surface);
box-shadow: 0 16px 32px rgba(0, 0, 0, 0.16);
border-radius: 12px;
background: #111419;
}
section:last-of-type {
margin-bottom: 12px;
@@ -254,9 +253,9 @@
font-weight: 500;
}
input {
background: #0b0f14;
background: #12151b;
border: 1px solid var(--border);
border-radius: 12px;
border-radius: 10px;
padding: 10px 12px;
font-size: 13px;
color: var(--text);
@@ -276,7 +275,7 @@
}
code {
font-size: 11px;
background: #0b0f14;
background: #0c0e12;
padding: 2px 5px;
border-radius: 6px;
}
@@ -289,24 +288,25 @@
all: unset;
cursor: pointer;
padding: 9px 14px;
border-radius: 12px;
border-radius: 10px;
font-size: 12px;
font-weight: 600;
font-family: inherit;
transition: all 0.12s;
}
.btn.ghost {
color: #d9e7f8;
color: var(--text);
border: 1px solid var(--border);
background: var(--bg-secondary);
background: #15181e;
}
.btn.ghost:hover:not(:disabled) {
background: var(--surface-hover);
color: var(--text);
border-color: var(--border-strong);
}
.btn.accent {
background: var(--accent);
color: #fff;
color: #031014;
border: 1px solid rgba(42, 215, 239, 0.35);
}
.btn.accent:hover:not(:disabled) {
opacity: 0.9;
@@ -318,18 +318,18 @@
.status {
margin-top: 8px;
padding: 10px 12px;
border-radius: 14px;
border-radius: 10px;
font-size: 12px;
background: var(--surface);
background: #111419;
color: var(--text-muted);
border-left: 3px solid var(--border);
border: 1px solid var(--border);
}
.status.ok {
border-left-color: #22c55e;
border-color: rgba(34, 197, 94, 0.3);
color: #86efac;
}
.status.error {
border-left-color: var(--danger);
border-color: rgba(239, 68, 68, 0.3);
color: #fca5a5;
}
</style>
+12 -9
View File
@@ -54,7 +54,7 @@
.overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.72);
background: rgba(0, 0, 0, 0.82);
display: flex;
align-items: center;
justify-content: center;
@@ -62,14 +62,15 @@
padding: 20px;
}
.modal {
background: var(--surface-strong);
background: #0f1116;
border: 1px solid var(--border);
border-radius: 14px;
width: 100%;
max-width: 720px;
max-height: 80vh;
display: flex;
flex-direction: column;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.42);
}
.modal-header {
display: flex;
@@ -103,9 +104,9 @@
white-space: pre-wrap;
word-break: break-all;
margin: 0;
background: var(--surface);
background: #12151b;
padding: 14px;
border-radius: 8px;
border-radius: 10px;
border: 1px solid var(--border);
}
.modal-actions {
@@ -119,7 +120,7 @@
all: unset;
cursor: pointer;
padding: 8px 18px;
border-radius: 8px;
border-radius: 10px;
font-size: 13px;
font-weight: 600;
font-family: inherit;
@@ -127,15 +128,17 @@
.action-btn.secondary {
border: 1px solid var(--border);
color: var(--text);
background: #15181e;
}
.action-btn.secondary:hover {
background: var(--surface);
background: var(--surface-hover);
}
.action-btn.primary {
background: var(--accent);
color: #fff;
border: 1px solid rgba(42, 215, 239, 0.35);
color: #031014;
}
.action-btn.primary:hover {
opacity: 0.9;
background: #35d2ea;
}
</style>
+19 -16
View File
@@ -158,8 +158,8 @@
padding: 0;
}
h3 {
font-size: 18px;
font-weight: 700;
font-size: 16px;
font-weight: 800;
margin: 0 0 18px;
color: var(--text);
}
@@ -168,10 +168,10 @@
}
.step-label {
font-size: 11px;
font-weight: 600;
font-weight: 700;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
letter-spacing: 0.08em;
display: block;
margin-bottom: 6px;
}
@@ -184,9 +184,9 @@
.select-input {
width: 100%;
padding: 10px 12px;
border-radius: 12px;
border-radius: 10px;
border: 1px solid var(--border);
background: #0b0f14;
background: #12151b;
color: var(--text);
font-size: 13px;
font-family: inherit;
@@ -202,9 +202,9 @@
align-items: flex-start;
gap: 8px;
padding: 10px 12px;
border-radius: 16px;
border-radius: 10px;
border: 1px solid var(--border);
background: var(--surface);
background: #111419;
cursor: pointer;
transition: all 0.12s;
}
@@ -237,7 +237,8 @@
align-items: center;
gap: 8px;
padding: 8px 10px;
border-radius: 12px;
border-radius: 10px;
border: 1px solid transparent;
cursor: pointer;
font-size: 13px;
}
@@ -246,6 +247,7 @@
}
.section-pick.selected {
background: var(--surface-active);
border: 1px solid var(--border-strong);
}
.pick-name {
flex: 1;
@@ -266,7 +268,8 @@
align-items: center;
gap: 8px;
padding: 8px 10px;
border-radius: 12px;
border-radius: 10px;
border: 1px solid transparent;
cursor: pointer;
font-size: 13px;
}
@@ -275,6 +278,7 @@
}
.target-option.selected {
background: var(--surface-active);
border: 1px solid var(--border-strong);
}
.target-name {
flex: 1;
@@ -302,19 +306,18 @@
width: 100%;
text-align: center;
padding: 12px;
border-radius: 16px;
border-radius: 10px;
background: var(--accent);
color: #fff;
border: 1px solid rgba(42, 215, 239, 0.35);
color: #031014;
font-size: 14px;
font-weight: 600;
font-family: inherit;
box-sizing: border-box;
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.2);
transition: opacity 0.12s, transform 0.12s ease;
transition: opacity 0.12s, background 0.12s ease;
}
.sync-btn:hover:not(:disabled) {
opacity: 0.95;
transform: translateY(-1px);
background: #35d2ea;
}
.sync-btn:disabled {
opacity: 0.4;
+83 -2
View File
@@ -90,7 +90,7 @@ export function createEmptySection(userId) {
SectionType: 'items',
ImageType: 'Thumb',
CollectionType: 'movies',
SortBy: 'DateLastContentAdded,SortName',
SortBy: 'Random',
SortOrder: 'Descending',
Monitor: [],
ItemTypes: ['Movie'],
@@ -142,7 +142,7 @@ export function createBoxSetSection(userId, collectionName, collectionId) {
SectionType: 'boxset',
ImageType: 'Thumb',
ItemTypes: [],
SortBy: 'default',
SortBy: 'Random',
SortOrder: 'Descending',
Monitor: [],
ExcludedFolders: [],
@@ -186,6 +186,40 @@ export function isWatchlistSection(section) {
return !!section.Query?.IsFavorite || name.includes('watchlist') || name.includes('watch list');
}
export function isUpNextSection(section) {
if (!section) return false;
if (section.SectionType === 'resume') return true;
const name = `${section.CustomName || ''} ${section.Name || ''}`.trim().toLowerCase();
return name === 'up next' || name === 'next up' || name === 'resume / up next';
}
export function isNewToEmbySection(section) {
if (!section) return false;
const name = `${section.CustomName || ''} ${section.Name || ''}`.toLowerCase();
return name.includes('new to emby');
}
export function isRecentlyWatchedSection(section) {
if (!section) return false;
const name = `${section.CustomName || ''} ${section.Name || ''}`.toLowerCase();
return section.Query?.IsPlayed === true || name.includes('recently watched');
}
export function isFixedOrderSection(section) {
if (!section) return false;
return (
isUpNextSection(section) ||
isWatchlistSection(section) ||
isNewToEmbySection(section) ||
isRecentlyWatchedSection(section) ||
section.SectionType === 'latestepisodereleases' ||
section.SectionType === 'latestmoviereleases' ||
section.SectionType === 'latestmediablock' ||
section.SectionType === 'userviews'
);
}
function renameWatchlistLabel(label, targetName) {
if (!label || !targetName) return label;
if (/^\s*watch\s+list\s*$/i.test(label)) return 'Watch List';
@@ -259,6 +293,53 @@ export function getWatchlistLabelsForTarget(sourceSection, targetUser) {
};
}
export function applySectionStandards(sourceSection, targetUser) {
const section = JSON.parse(JSON.stringify(sourceSection || {}));
if (isUpNextSection(section)) {
section.Name = 'Up Next';
section.CustomName = 'Up Next';
return section;
}
if (isWatchlistSection(section)) {
const labels = getWatchlistLabelsForTarget(section, targetUser);
if (labels.Name) section.Name = labels.Name;
if (labels.CustomName) section.CustomName = labels.CustomName;
return section;
}
if (isNewToEmbySection(section)) {
section.Name = 'New to Emby';
section.CustomName = 'New to Emby';
section.SortBy = 'DateLastContentAdded,SortName';
section.SortOrder = 'Descending';
return section;
}
if (isRecentlyWatchedSection(section)) {
const targetName = getPreferredUserName(targetUser);
const label = `Recently Watched${targetName ? ` - ${targetName}` : ''}`;
section.Name = label;
section.CustomName = label;
section.SortBy = 'DatePlayed';
section.SortOrder = 'Descending';
return section;
}
if (!isFixedOrderSection(section) && ['items', 'collections', 'boxset'].includes(section.SectionType)) {
section.SortBy = 'Random';
section.SortOrder = 'Descending';
}
if (section.SectionType === 'userviews') {
section.Name = 'Libraries';
section.CustomName = 'Libraries';
}
return section;
}
/**
* Build SQL UPDATE statements from modified user data.
* Each user's entire homescreensettings JSON is replaced.
+895 -576
View File
File diff suppressed because it is too large Load Diff
+104 -1
View File
@@ -2,7 +2,12 @@ import assert from 'node:assert/strict';
import { mkdtempSync, rmSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import { createBoxSetSection, createRecentlyWatchedSection, getWatchlistLabelsForTarget } from '../src/lib/constants.js';
import {
applySectionStandards,
createBoxSetSection,
createRecentlyWatchedSection,
getWatchlistLabelsForTarget
} from '../src/lib/constants.js';
import { normalizeLookupItem, rankRecommendationResults } from '../src/lib/collection-tools.js';
import * as embyUserCache from '../src/lib/server/emby-user-cache.js';
@@ -152,6 +157,104 @@ test('box set section template links a created collection', () => {
});
});
test('section standards normalize up next label', () => {
const normalized = applySectionStandards(
{
Name: 'Resume / Up Next',
CustomName: 'Resume / Up Next',
SectionType: 'resume'
},
{
embyName: 'Bob',
sections: []
}
);
assert.equal(normalized.Name, 'Up Next');
assert.equal(normalized.CustomName, 'Up Next');
});
test('section standards normalize watchlist label for target user', () => {
const normalized = applySectionStandards(
{
Name: "Matt's Watchlist",
CustomName: "Matt's Watchlist",
SectionType: 'items',
Query: { IsFavorite: true }
},
{
embyName: 'Bob',
sections: []
}
);
assert.equal(normalized.Name, "Bob's Watchlist");
assert.equal(normalized.CustomName, "Bob's Watchlist");
});
test('section standards keep new to emby fixed to date added descending', () => {
const normalized = applySectionStandards(
{
Name: 'New To Emby',
CustomName: 'New To Emby',
SectionType: 'items',
SortBy: 'ProductionYear,PremiereDate,SortName',
SortOrder: 'Ascending'
},
{
embyName: 'Bob',
sections: []
}
);
assert.equal(normalized.Name, 'New to Emby');
assert.equal(normalized.CustomName, 'New to Emby');
assert.equal(normalized.SortBy, 'DateLastContentAdded,SortName');
assert.equal(normalized.SortOrder, 'Descending');
});
test('section standards randomize regular curated sections', () => {
const normalized = applySectionStandards(
{
Name: 'Crime / Drama Shows',
CustomName: 'Crime / Drama Shows',
SectionType: 'items',
SortBy: 'ProductionYear,PremiereDate,SortName',
SortOrder: 'Ascending',
Query: { GenreIds: ['4910', '62'] }
},
{
embyName: 'Bob',
sections: []
}
);
assert.equal(normalized.SortBy, 'Random');
assert.equal(normalized.SortOrder, 'Descending');
});
test('section standards preserve recently watched ordering semantics', () => {
const normalized = applySectionStandards(
{
Name: 'Recently Watched - Matt',
CustomName: 'Recently Watched - Matt',
SectionType: 'items',
SortBy: 'Random',
SortOrder: 'Ascending',
Query: { IsPlayed: true }
},
{
embyName: 'Bob',
sections: []
}
);
assert.equal(normalized.Name, 'Recently Watched - Bob');
assert.equal(normalized.CustomName, 'Recently Watched - Bob');
assert.equal(normalized.SortBy, 'DatePlayed');
assert.equal(normalized.SortOrder, 'Descending');
});
test('normalizeLookupItem preserves already-normalized recommendation items', () => {
const normalized = normalizeLookupItem({
id: 'pick-1',
BIN
View File
Binary file not shown.
View File