diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index f73bfe3d4e1..c79fa08ca97 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -94,6 +94,7 @@ - [iFraan](https://github.com/iFraan) - [Ali](https://github.com/bu3alwa) - [K. Kyle Puchkov](https://github.com/kepper104) +- [ItsAllAboutTheCode](https://github.com/ItsAllAboutTheCode) ## Emby Contributors diff --git a/src/apps/experimental/features/preferences/components/BackgroundPlaybackPreferences.scss b/src/apps/experimental/features/preferences/components/BackgroundPlaybackPreferences.scss new file mode 100644 index 00000000000..5945b07bdcc --- /dev/null +++ b/src/apps/experimental/features/preferences/components/BackgroundPlaybackPreferences.scss @@ -0,0 +1,79 @@ +.backgroundPlaybackDialog { + display: inherit; + margin-bottom: 0; + align-items: center; + position: relative; + min-width: 300px; +} + +.backgroundPlaybackPopover { + display: inherit; + margin-bottom: 0; + align-items: center; + position: relative; + min-width: 300px; +} + +.backgroundPlaybackStack { + position: relative; + min-width: 300px; +} + +#background-playback-settings-header { + margin-bottom: 1em; +} + +#background-playback-settings-play-theme-media-label { + margin-left: 9px; + margin-top: 0.5em; + position: relative; + max-width: calc(100% - 24px); +} + +#background-playback-sort-by-select-stack { + margin-left: 9px; + margin-bottom: 1.5em; +} + +#background-playback-sort-by-select-container { + display: inline-flex; + margin-bottom: 0; + align-items: center; + flex-direction: column; + position: relative; +} + +#background-playback-sort-by-select-dropdown { + min-width: 100px; +} + +#background-playback-sort-order-icon-button { + position: relative; + display: inline-block; + box-sizing: border-box; + margin: 0; + text-align: center; + font-size: inherit; + font-family: inherit; + color: inherit; + + /* These are getting an outline in opera tv browsers, which run chrome 30 */ + outline: none !important; + outline-width: 0; + user-select: none; + cursor: pointer; + z-index: 0; + vertical-align: middle; + border: 0; + border-radius: 0.2em; + font-weight: 600; + + /* Disable webkit tap highlighting */ + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); + text-decoration: none; + + /* Not crazy about this but it normalizes heights between anchors and buttons */ + line-height: 1.35; + transform-origin: center; + transition: 0.2s; +} diff --git a/src/apps/experimental/features/preferences/components/BackgroundPlaybackPreferences.tsx b/src/apps/experimental/features/preferences/components/BackgroundPlaybackPreferences.tsx new file mode 100644 index 00000000000..6b5ccdf3ac5 --- /dev/null +++ b/src/apps/experimental/features/preferences/components/BackgroundPlaybackPreferences.tsx @@ -0,0 +1,227 @@ +import Checkbox from '@mui/material/Checkbox'; +import FormControl from '@mui/material/FormControl'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import InputLabel from '@mui/material/InputLabel'; +import MenuItem from '@mui/material/MenuItem'; +import Select, { SelectChangeEvent } from '@mui/material/Select'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import React, { useCallback } from 'react'; + +import globalize from 'lib/globalize'; + +import type { DisplaySettingsValues } from '../types/displaySettingsValues'; + +import { ItemSortBy, SortOrder } from '@jellyfin/sdk/lib/generated-client'; +import { Button, IconButton, Popover, SxProps } from '@mui/material'; +import { ArrowDownward, ArrowUpward } from '@mui/icons-material'; + +import './BackgroundPlaybackPreferences.scss'; + +interface BackgroundPlaybackPreferencesProps { + onChange: (event: SelectChangeEvent | React.SyntheticEvent) => void; + values: DisplaySettingsValues; +} + +export function BackgroundPlaybackPreferences({ + onChange, + values +}: Readonly) { + const [anchorEl, setAnchorEl] = React.useState(null); + const openPopover = Boolean(anchorEl); + + const handleClick = useCallback((event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }, []); + + const handleClosePopover = useCallback(() => { + setAnchorEl(null); + }, []); + + // Create wrapper on change callback to toggle the Sort Order state + // when the Icon Button is pressed + const handleSortOrderChange = useCallback(( + e: SelectChangeEvent | React.SyntheticEvent + ) => { + const target = e.currentTarget as HTMLInputElement; + const fieldName = target.name as keyof DisplaySettingsValues; + const fieldValue = target.value; + + // Toggle the ascending/descending variable on the event target when the Sort Icon Button is pressed + if (Object.is(values.libraryThemeMediaSortOrder, values?.[fieldName])) { + let newSortOrder = null; + switch (fieldValue) { + case SortOrder.Ascending: + newSortOrder = SortOrder.Descending; + break; + case SortOrder.Descending: + newSortOrder = SortOrder.Ascending; + break; + } + + target.value = newSortOrder ?? target.value; + // For an Icon Button, the event target can be either the pressed icon + // or the
+ + {globalize.translate('BackgroundPlayback')} + + + + + + {globalize.translate('ThemeMediaPlayInBackgroundHelp')} + + + + } + label={globalize.translate('Backdrops')} + name='enableLibraryBackdrops' + sx={themeMediaCheckboxLabelStyleProps} + /> + + } + label={globalize.translate('ThemeSongs')} + name='enableLibraryThemeSongs' + sx={themeMediaCheckboxLabelStyleProps} + /> + + } + label={globalize.translate('ThemeVideos')} + name='enableLibraryThemeVideos' + sx={themeMediaCheckboxLabelStyleProps} + /> + + + + + + {globalize.translate('ThemeMediaSortBy')} + + + + + {values.libraryThemeMediaSortOrder + == SortOrder.Ascending && } + {values.libraryThemeMediaSortOrder + == SortOrder.Descending && } + + + + +
+ ); +} diff --git a/src/apps/experimental/features/preferences/components/LibraryPreferences.tsx b/src/apps/experimental/features/preferences/components/LibraryPreferences.tsx index 4b65960070e..630f3523f3d 100644 --- a/src/apps/experimental/features/preferences/components/LibraryPreferences.tsx +++ b/src/apps/experimental/features/preferences/components/LibraryPreferences.tsx @@ -43,57 +43,6 @@ export function LibraryPreferences({ onChange, values }: Readonly - - - } - label={globalize.translate('Backdrops')} - name='enableLibraryBackdrops' - /> - - {globalize.translate('EnableBackdropsHelp')} - - - - - - } - label={globalize.translate('ThemeSongs')} - name='enableLibraryThemeSongs' - /> - - {globalize.translate('EnableThemeSongsHelp')} - - - - - - } - label={globalize.translate('ThemeVideos')} - name='enableLibraryThemeVideos' - /> - - {globalize.translate('EnableThemeVideosHelp')} - - - + + + + + + diff --git a/src/components/displaySettings/displaySettings.js b/src/components/displaySettings/displaySettings.js index 354ef54d099..cc83c0114b0 100644 --- a/src/components/displaySettings/displaySettings.js +++ b/src/components/displaySettings/displaySettings.js @@ -67,6 +67,10 @@ function showOrHideMissingEpisodesField(context) { context.querySelector('.fldDisplayMissingEpisodes').classList.remove('hide'); } +function showBackgroundPlaybackSection(context, user) { + context.querySelector('.lnkBackgroundPlaybackPreferences').setAttribute('href', '#/mypreferencesbackgroundplayback.html?userId=' + user.Id); +} + function loadForm(context, user, userSettings) { if (appHost.supports('displaylanguage')) { context.querySelector('.languageSection').classList.remove('hide'); @@ -114,11 +118,8 @@ function loadForm(context, user, userSettings) { context.querySelector('.chkDisplayMissingEpisodes').checked = user.Configuration.DisplayMissingEpisodes || false; - context.querySelector('#chkThemeSong').checked = userSettings.enableThemeSongs(); - context.querySelector('#chkThemeVideo').checked = userSettings.enableThemeVideos(); context.querySelector('#chkFadein').checked = userSettings.enableFastFadein(); context.querySelector('#chkBlurhash').checked = userSettings.enableBlurhash(); - context.querySelector('#chkBackdrops').checked = userSettings.enableBackdrops(); context.querySelector('#chkDetailsBanner').checked = userSettings.detailsBanner(); context.querySelector('#chkDisableCustomCss').checked = userSettings.disableCustomCss(); @@ -137,6 +138,8 @@ function loadForm(context, user, userSettings) { showOrHideMissingEpisodesField(context); + showBackgroundPlaybackSection(context, user); + loading.hide(); } @@ -149,8 +152,6 @@ function saveUser(context, user, userSettingsInstance, apiClient) { userSettingsInstance.dateTimeLocale(context.querySelector('.selectDateTimeLocale').value); - userSettingsInstance.enableThemeSongs(context.querySelector('#chkThemeSong').checked); - userSettingsInstance.enableThemeVideos(context.querySelector('#chkThemeVideo').checked); userSettingsInstance.theme(context.querySelector('#selectTheme').value); userSettingsInstance.dashboardTheme(context.querySelector('#selectDashboardTheme').value); userSettingsInstance.screensaver(context.querySelector('.selectScreensaver').value); @@ -165,7 +166,6 @@ function saveUser(context, user, userSettingsInstance, apiClient) { userSettingsInstance.enableFastFadein(context.querySelector('#chkFadein').checked); userSettingsInstance.enableBlurhash(context.querySelector('#chkBlurhash').checked); - userSettingsInstance.enableBackdrops(context.querySelector('#chkBackdrops').checked); userSettingsInstance.detailsBanner(context.querySelector('#chkDetailsBanner').checked); userSettingsInstance.disableCustomCss(context.querySelector('#chkDisableCustomCss').checked); diff --git a/src/components/displaySettings/displaySettings.template.html b/src/components/displaySettings/displaySettings.template.html index 1b5579b4c30..41a30d0c3ee 100644 --- a/src/components/displaySettings/displaySettings.template.html +++ b/src/components/displaySettings/displaySettings.template.html @@ -239,30 +239,6 @@

${LabelLibraryPageSizeHelp}
-
- -
${EnableBackdropsHelp}
-
- -
- -
${EnableThemeSongsHelp}
-
- -
- -
${EnableThemeVideosHelp}
-
-
+

+ +
+ +
+
${BackgroundPlayback}
+
+
+
+

+ + diff --git a/src/components/themeMediaPlayer.js b/src/components/themeMediaPlayer.js index 87d86b7a4b7..fa93b285c6e 100644 --- a/src/components/themeMediaPlayer.js +++ b/src/components/themeMediaPlayer.js @@ -96,8 +96,13 @@ async function loadThemeMedia(serverId, itemId) { return; } - const { data: themeMedia } = await getLibraryApi(api) - .getThemeMedia({ userId, itemId: item.Id, inheritFromParent: true }); + const { data: themeMedia } = await getLibraryApi(api).getThemeMedia({ + userId, + itemId: item.Id, + inheritFromParent: true, + sortBy: [userSettings.themeMediaSortBy()], + sortOrder: [userSettings.themeMediaSortOrder()] + }); const result = userSettings.enableThemeVideos() && themeMedia.ThemeVideosResult?.Items?.length ? themeMedia.ThemeVideosResult : themeMedia.ThemeSongsResult; diff --git a/src/controllers/user/backgroundPlayback/index.html b/src/controllers/user/backgroundPlayback/index.html new file mode 100644 index 00000000000..c0a01c6fcb4 --- /dev/null +++ b/src/controllers/user/backgroundPlayback/index.html @@ -0,0 +1,4 @@ +
+
+
+
diff --git a/src/controllers/user/backgroundPlayback/index.js b/src/controllers/user/backgroundPlayback/index.js new file mode 100644 index 00000000000..2e6600dead2 --- /dev/null +++ b/src/controllers/user/backgroundPlayback/index.js @@ -0,0 +1,39 @@ +import BackgroundPlaybackSettings from '../../../components/backgroundPlaybackSettings/backgroundPlaybackSettings'; +import * as userSettings from '../../../scripts/settings/userSettings'; +import autoFocuser from '../../../components/autoFocuser'; + +// Shortcuts +const UserSettings = userSettings.UserSettings; + +export default function (view, params) { + let settingsInstance; + + const userId = params.userId || ApiClient.getCurrentUserId(); + const currentSettings = + userId === ApiClient.getCurrentUserId() ? + userSettings : + new UserSettings(); + + view.addEventListener('viewshow', function () { + if (settingsInstance) { + settingsInstance.loadData(); + } else { + settingsInstance = new BackgroundPlaybackSettings({ + serverId: ApiClient.serverId(), + userId: userId, + element: view.querySelector('.settingsContainer'), + userSettings: currentSettings, + enableSaveButton: true, + enableSaveConfirmation: true, + autoFocus: autoFocuser.isEnabled() + }); + } + }); + + view.addEventListener('viewdestroy', function () { + if (settingsInstance) { + settingsInstance.destroy(); + settingsInstance = null; + } + }); +} diff --git a/src/elements/emby-select/emby-select.js b/src/elements/emby-select/emby-select.js index 2ed9e7c89bb..a0b4b76fb0a 100644 --- a/src/elements/emby-select/emby-select.js +++ b/src/elements/emby-select/emby-select.js @@ -134,7 +134,7 @@ EmbySelectPrototype.attachedCallback = function () { this.parentNode?.insertBefore(label, this); if (this.classList.contains('emby-select-withcolor')) { - this.parentNode?.insertAdjacentHTML('beforeend', '
0
'); + this.insertAdjacentHTML('afterend', '
0
'); } }; diff --git a/src/elements/emby-select/emby-select.scss b/src/elements/emby-select/emby-select.scss index d909e8af61f..6d019002e94 100644 --- a/src/elements/emby-select/emby-select.scss +++ b/src/elements/emby-select/emby-select.scss @@ -80,6 +80,10 @@ align-items: center; } +.selectContainer-inline.selectContainerWithButton { + position: relative; +} + .selectLabel { display: block; margin-bottom: 0.25em; @@ -121,6 +125,10 @@ font-size: 90%; } +.selectContainerWithButton > .selectArrowContainer { + visibility: hidden; +} + .emby-select[disabled] + .selectArrowContainer { display: none; } diff --git a/src/scripts/settings/userSettings.js b/src/scripts/settings/userSettings.js index 23e1f000f3e..164168264c0 100644 --- a/src/scripts/settings/userSettings.js +++ b/src/scripts/settings/userSettings.js @@ -2,6 +2,7 @@ import Events from '../../utils/events.ts'; import { toBoolean } from '../../utils/string.ts'; import browser from '../browser'; import appSettings from './appSettings'; +import { ItemSortBy, SortOrder } from '@jellyfin/sdk/lib/generated-client'; function onSaveTimeout() { const self = this; @@ -243,6 +244,32 @@ export class UserSettings { return toBoolean(this.get('enableThemeVideos', false), false); } + /** + * Get or set 'Theme Media Sort By'' state. + * @param {string|undefined} [val] - Enumeration to set 'Theme Media Sort By' or undefined. + * @return {string} 'Theme Media Sort By' state or 'Random' if not set. + */ + themeMediaSortBy(val) { + if (val !== undefined) { + return this.set('themeMediaSortBy', val.toString(), false); + } + + return this.get('themeMediaSortBy', false) || ItemSortBy.Random; + } + + /** + * Get or set 'Theme Media Sort Order'' state. + * @param {string|undefined} [val] - Enumeration to set 'Theme Media Sort Order' or undefined. + * @return {string} 'Theme Media Sort Order' or 'Ascending' if not set. + */ + themeMediaSortOrder(val) { + if (val !== undefined) { + return this.set('themeMediaSortOrder', val.toString(), false); + } + + return this.get('themeMediaSortOrder', false) || SortOrder.Ascending; + } + /** * Get or set 'Fast Fade-in' state. * @param {boolean|undefined} [val] - Flag to enable 'Fast Fade-in' or undefined. @@ -709,3 +736,5 @@ export const disableCustomCss = currentSettings.disableCustomCss.bind(currentSet export const getSavedView = currentSettings.getSavedView.bind(currentSettings); export const saveViewSetting = currentSettings.saveViewSetting.bind(currentSettings); export const getSortValuesLegacy = currentSettings.getSortValuesLegacy.bind(currentSettings); +export const themeMediaSortBy = currentSettings.themeMediaSortBy.bind(currentSettings); +export const themeMediaSortOrder = currentSettings.themeMediaSortOrder.bind(currentSettings); diff --git a/src/strings/en-us.json b/src/strings/en-us.json index a2ef7d98437..008ef6d4c87 100644 --- a/src/strings/en-us.json +++ b/src/strings/en-us.json @@ -75,6 +75,7 @@ "Backdrop": "Backdrop", "Backdrops": "Backdrops", "BackdropScreensaver": "Backdrop Screensaver", + "BackgroundPlayback": "Background Playback", "Banner": "Banner", "BirthDateValue": "Born: {0}", "BirthLocation": "Birth location", @@ -1558,6 +1559,12 @@ "TagsValue": "Tags: {0}", "TellUsAboutYourself": "Tell us about yourself", "TextSent": "Text sent.", + "ThemeMediaFolderUsageHelp": "Used for \"backdrop\" folder (for theme videos) and \"theme-music\" folder (for theme music).", + "ThemeMediaSortBy": "Sort by", + "ThemeMediaSortByHelp": "Sort theme media using sort by criteria.", + "ThemeMediaSortOrder": "Order queue by:", + "ThemeMediaSortOrderHelp": "Sort theme media using sort order criteria.", + "ThemeMediaPlayInBackgroundHelp": "Play theme media in background", "ThemeSongs": "Theme songs", "ThemeVideos": "Theme videos", "TheseSettingsAffectSubtitlesOnThisDevice": "These settings affect subtitles on this device",