diff --git a/plugins/jellyfin/Jellyfin.Plugin.MovieNight/Configuration/ui.js b/plugins/jellyfin/Jellyfin.Plugin.MovieNight/Configuration/ui.js index 156eb1c..baa6a11 100644 --- a/plugins/jellyfin/Jellyfin.Plugin.MovieNight/Configuration/ui.js +++ b/plugins/jellyfin/Jellyfin.Plugin.MovieNight/Configuration/ui.js @@ -205,6 +205,66 @@ box-sizing: border-box; width: 100%; } + .movieNightOnboardingDialog { + display: flex; + flex-direction: column; + max-height: min(820px, calc(100vh - 2em)); + width: min(720px, calc(100vw - 2em)); + } + .movieNightOnboardingDialog .dialog-content { + overflow-y: auto; + padding-right: .25em; + } + .movieNightOnboardingGrid { + display: grid; + gap: 1em; + grid-template-columns: 1fr 1fr; + } + .movieNightOnboardingWide { + grid-column: 1 / -1; + } + .movieNightChipGroup { + display: flex; + flex-wrap: wrap; + gap: .5em; + } + .movieNightChip { + background: rgba(255,255,255,.045); + border: var(--defaultLighterBorder, 1px solid rgba(255,255,255,.18)); + border-radius: 999px; + color: var(--textColor, #fff); + cursor: pointer; + font-size: .9em; + line-height: 1.2; + padding: .45em .85em; + transition: background-color .16s ease, border-color .16s ease; + user-select: none; + } + .movieNightChip:hover, + .movieNightChip:focus { + border-color: var(--uiAccentColor, #00a4dc); + outline: none; + } + .movieNightChip.is-selected { + background: var(--uiAccentColor, #0064d2); + border-color: var(--uiAccentColor, #0064d2); + color: #fff; + } + .movieNightSelect, + .movieNightTextarea { + background: rgba(0,0,0,.18); + border: var(--defaultLighterBorder, 1px solid rgba(255,255,255,.22)); + border-radius: var(--smallRadius, 8px); + box-sizing: border-box; + color: var(--textColor, #fff); + min-height: 2.7em; + padding: .55em .7em; + width: 100%; + } + .movieNightTextarea { + min-height: 5.5em; + resize: vertical; + } @media (max-width: 42em) { .movieNightRecommendation { grid-template-columns: 56px minmax(0, 1fr); @@ -231,6 +291,9 @@ grid-template-columns: 1fr; gap: 0; } + .movieNightOnboardingGrid { + grid-template-columns: 1fr; + } } `; document.head.appendChild(style); @@ -470,54 +533,108 @@ async function showOnboardingDialog() { const overlay = createOverlay(); - const dialog = createDialogBase('Welcome to MovieNight!'); - dialog.style.minWidth = '450px'; + const dialog = createDialogBase('Welcome to MovieNight'); + dialog.classList.add('movieNightOnboardingDialog'); + dialog.style.minWidth = '0'; const content = dialog.querySelector('.dialog-content'); const footer = dialog.querySelector('.dialog-footer'); content.innerHTML = ` -

Pick your preferences to get better recommendations.

-
- -
-
-
- -
-
-
- -
+
+
+ +
+
+
+ +
+
+
+ +
+ +
+
+ +
+
+
+ +
+
+
+ + +
+
+ + +
`; - const genres = ["Action", "Comedy", "Drama", "Sci-Fi", "Horror", "Thriller", "Animation", "Documentary"]; - const eras = ["1980s", "1990s", "2000s", "2010s", "2020s"]; - const types = ["FILM", "SERIES"]; + const genres = [ + "Action", + "Adventure", + "Animation", + "Comedy", + "Crime", + "Documentary", + "Drama", + "Fantasy", + "Horror", + "Mystery", + "Romance", + "Sci-Fi", + "Thriller" + ]; + const eras = ["1970s", "1980s", "1990s", "2000s", "2010s", "2020s"]; + const plotTypes = [ + "coming of age", + "crime investigation", + "family drama", + "heist", + "mind bending", + "political intrigue", + "quest", + "road trip", + "space adventure", + "survival" + ]; + const moods = [ + { label: "Tense", value: "tense" }, + { label: "Slow burn", value: "slow-burn" }, + { label: "Feel good", value: "feel-good" }, + { label: "Dark", value: "dark" }, + { label: "Romantic", value: "romantic" }, + { label: "Focused", value: "focused" } + ]; + const types = [ + { label: "Movies", value: "FILM" }, + { label: "Series", value: "SERIES" }, + { label: "Episodes", value: "EPISODE" }, + { label: "Other", value: "OTHER" } + ]; - const selections = { genres: new Set(), eras: new Set(), types: new Set() }; - - const createChip = (text, container, type) => { - const chip = document.createElement('div'); - chip.innerText = text; - chip.style.cssText = 'padding:0.4em 1em; border-radius:2em; border:1px solid #444; cursor:pointer; font-size:0.9em; transition:all 0.2s;'; - chip.onclick = () => { - if (selections[type].has(text)) { - selections[type].delete(text); - chip.style.backgroundColor = 'transparent'; - chip.style.borderColor = '#444'; - } else { - selections[type].add(text); - chip.style.backgroundColor = '#0064d2'; - chip.style.borderColor = '#0064d2'; - } - }; - container.appendChild(chip); + const selections = { + genres: new Set(), + eras: new Set(), + plotTypes: new Set(), + moods: new Set(), + types: new Set(["FILM"]) }; - genres.forEach(g => createChip(g, content.querySelector('.genre-chips'), 'genres')); - eras.forEach(e => createChip(e, content.querySelector('.era-chips'), 'eras')); - types.forEach(t => createChip(t, content.querySelector('.type-chips'), 'types')); + genres.forEach((genre) => createOnboardingChip(genre, content.querySelector('.genre-chips'), selections.genres)); + eras.forEach((era) => createOnboardingChip(era, content.querySelector('.era-chips'), selections.eras)); + plotTypes.forEach((plotType) => createOnboardingChip(plotType, content.querySelector('.plot-chips'), selections.plotTypes)); + moods.forEach((mood) => createOnboardingChip(mood, content.querySelector('.mood-chips'), selections.moods)); + types.forEach((type) => createOnboardingChip(type, content.querySelector('.type-chips'), selections.types)); const btnSave = document.createElement('button'); btnSave.className = 'emby-button raised button-submit'; @@ -529,10 +646,20 @@ const cleanup = () => { if (overlay.parentNode) document.body.removeChild(overlay); }; btnSave.onclick = async () => { + const customPlotTypes = splitOnboardingList(content.querySelector('.plot-input').value); + const people = splitOnboardingList(content.querySelector('.people-input').value); const payload = { weightedGenres: Object.fromEntries([...selections.genres].map(g => [g, 5])), + plotTypes: uniqueValues([...selections.plotTypes, ...customPlotTypes]), eras: [...selections.eras], - contentTypes: [...selections.types] + castAndDirectors: people, + moods: [...selections.moods], + contentTypes: [...selections.types], + likedFilmIds: [], + dislikedFilmIds: [], + libraryFilmIds: [], + watchedFilmIds: [], + recommendationStyle: content.querySelector('.recommendation-style').value }; cleanup(); await completeOnboarding(payload); @@ -544,6 +671,39 @@ document.body.appendChild(overlay); } + function createOnboardingChip(option, container, selections) { + const normalized = typeof option === 'string' ? { label: option, value: option } : option; + const chip = document.createElement('button'); + chip.type = 'button'; + chip.className = 'movieNightChip'; + chip.textContent = normalized.label; + chip.setAttribute('aria-pressed', selections.has(normalized.value) ? 'true' : 'false'); + if (selections.has(normalized.value)) { + chip.classList.add('is-selected'); + } + + chip.onclick = () => { + if (selections.has(normalized.value)) { + selections.delete(normalized.value); + } else { + selections.add(normalized.value); + } + + const selected = selections.has(normalized.value); + chip.classList.toggle('is-selected', selected); + chip.setAttribute('aria-pressed', selected ? 'true' : 'false'); + }; + container.appendChild(chip); + } + + function splitOnboardingList(value) { + return uniqueValues((value || '').split(/[,\n;]+/).map((item) => item.trim()).filter(Boolean)); + } + + function uniqueValues(values) { + return [...new Set(values.map((value) => value.trim()).filter(Boolean))]; + } + async function checkOnboarding() { if (window.movieNightOnboardingChecked) return; @@ -553,7 +713,7 @@ try { const prefs = await ApiClient.getJSON(ApiClient.getUrl(`MovieNight/Users/${userId}/Preferences`)); - if (!prefs || (!Object.keys(prefs.weightedGenres || {}).length && !prefs.eras?.length)) { + if (!hasOnboardingPreferences(prefs)) { showOnboardingDialog(); } } catch (err) { @@ -561,6 +721,19 @@ } } + function hasOnboardingPreferences(prefs) { + if (!prefs) return false; + + return Boolean( + Object.keys(prefs.weightedGenres || {}).length || + prefs.plotTypes?.length || + prefs.eras?.length || + prefs.castAndDirectors?.length || + prefs.moods?.length || + prefs.contentTypes?.length + ); + } + async function completeOnboarding(payload) { const userId = ApiClient.getCurrentUserId(); try {