diff --git a/src/assets/css/dashboard.scss b/src/assets/css/dashboard.scss index a5209d2604..0bd22a2954 100644 --- a/src/assets/css/dashboard.scss +++ b/src/assets/css/dashboard.scss @@ -117,8 +117,15 @@ div[data-role=controlgroup] a[data-role=button]:last-child { } div[data-role=controlgroup] a[data-role=button] + a[data-role=button] { - border-left-width: 0 !important; - margin: 0 0 0 -0.4em !important; + [dir="ltr"] & { + border-left-width: 0 !important; + margin: 0 0 0 -0.4em !important; + } + + [dir="rtl"] & { + border-right-width: 0 !important; + margin: 0 -0.4em 0 0 !important; + } } div[data-role=controlgroup] a.ui-btn-active { diff --git a/src/assets/css/librarybrowser.scss b/src/assets/css/librarybrowser.scss index 202fd66e08..319c629fe5 100644 --- a/src/assets/css/librarybrowser.scss +++ b/src/assets/css/librarybrowser.scss @@ -32,6 +32,39 @@ } } +@mixin header-poster-padding-rtl() { + padding-left: unset; + padding-right: 37.5%; + + @media all and (min-width: 43.75em) { + padding-right: 25%; + } + + @media all and (min-width: 50em) { + padding-right: 20%; + } + + @media all and (min-width: 75em) { + padding-right: 16.666666666666666666666666666667%; + } + + @media all and (min-width: 87.5em) { + padding-right: 14.285714285714285714285714285714%; + } + + @media all and (min-width: 100em) { + padding-right: 12.5%; + } + + @media all and (min-width: 120em) { + padding-right: 11.111111111111111111111111111111%; + } + + @media all and (min-width: 131.25em) { + padding-right: 10%; + } +} + .headerUserImage, .navMenuOption, .pageTitle { @@ -137,10 +170,17 @@ .pageTitle { display: inline-flex; - margin: 0 0 0 0.5em; height: 1.7em; align-items: center; flex-shrink: 1; + + [dir="ltr"] & { + margin: 0 0 0 0.5em; + } + + [dir="rtl"] & { + margin: 0 0.5em 0 0; + } } .pageTitleWithDefaultLogo { @@ -162,6 +202,10 @@ background-size: contain; background-repeat: no-repeat; width: 13.2em; + + [dir='rtl'] & { + background-position: right center; + } } .skinHeader { @@ -211,16 +255,30 @@ align-items: center; text-decoration: none; color: inherit; - padding: 0.9em 0 0.9em 2.4em !important; flex-grow: 1; font-weight: 400 !important; margin: 0 !important; border-radius: 0 !important; + + [dir="ltr"] & { + padding: 0.9em 0 0.9em 2.4em !important; + } + + [dir="rtl"] & { + padding: 0.9em 2.4em 0.9em 0 !important; + } } .navMenuOptionIcon { - margin-right: 1.2em; flex-shrink: 0; + + [dir="ltr"] & { + margin-right: 1.2em; + } + + [dir="rtl"] & { + margin-left: 1.2em; + } } .navMenuOptionText { @@ -229,8 +287,15 @@ } .sidebarHeader { - padding-left: 1.2em; margin: 1em 0 0.5em; + + [dir="ltr"] & { + padding-left: 1.2em; + } + + [dir="rtl"] & { + padding-right: 1.2em; + } } .dashboardDocument .skinBody { @@ -240,6 +305,10 @@ right: 0; bottom: 0; left: 0; + + [dir="rtl"] & { + transition: right ease-in-out 0.3s, padding ease-in-out 0.3s; + } } .centerMessage { @@ -267,6 +336,14 @@ box-shadow: none !important; width: 20.205em !important; font-size: 94%; + + [dir="ltr"] & { + left: 0 !important; + } + + [dir="rtl"] & { + right: 0 !important; + } } .dashboardDocument .mainDrawer-scrollContainer { @@ -274,7 +351,13 @@ } .dashboardDocument .skinBody { - left: 20em; + [dir="ltr"] & { + left: 20em; + } + + [dir="rtl"] & { + right: 20em; + } } } @@ -340,7 +423,14 @@ .headerArrowImage { height: 20px; - margin-left: 0.5em; + + [dir="ltr"] & { + margin-left: 0.5em; + } + + [dir="rtl"] & { + margin-right: 0.5em; + } } .backdropContainer { @@ -370,15 +460,28 @@ } .viewControls + .listTopPaging { - margin-left: 0.5em !important; + [dir="ltr"] & { + margin-left: 0.5em !important; + } + + [dir="rtl"] & { + margin-right: 0.5em !important; + } } .criticReview { margin: 1.5em 0; background: #222; - padding: 0.8em 0.8em 0.8em 3em; border-radius: 0.3em; position: relative; + + [dir="ltr"] & { + padding: 0.8em 0.8em 0.8em 3em; + } + + [dir="rtl"] & { + padding: 0.8em 3em 0.8em 0.8em; + } } .detailLogo { @@ -431,12 +534,25 @@ } .reviewDate { - margin-left: 1em; + [dir="ltr"] & { + margin-left: 1em; + } + + [dir="rtl"] & { + margin-right: 1em; + } } .reviewScore { position: absolute; - left: 0.8em; + + [dir="ltr"] & { + left: 0.8em; + } + + [dir="rtl"] & { + right: 0.8em; + } } .itemBackdrop { @@ -473,8 +589,6 @@ .detailPageContent { display: flex; flex-direction: column; - padding-left: 32.45vw; - padding-right: 2%; .layout-mobile & { padding-left: 5%; @@ -484,9 +598,25 @@ .layout-desktop &, .layout-tv & { .emby-scroller { - margin-left: 0; + [dir="ltr"] & { + margin-left: 0; + } + + [dir="rtl"] & { + margin-right: 0; + } } } + + [dir="ltr"] & { + padding-left: 32.45vw; + padding-right: 2%; + } + + [dir="rtl"] & { + padding-right: 32.45vw; + padding-left: 2%; + } } .detailSectionContent a { @@ -613,7 +743,6 @@ .layout-mobile .mainDetailButtons { margin-top: 1em; margin-bottom: 0.5em; - margin-left: 0; @include header-poster-padding; @@ -622,6 +751,21 @@ margin-bottom: 0; padding-left: 0; } + + [dir="ltr"] & { + margin-left: 0; + } + + [dir="rtl"] & { + margin-right: 0; + padding-left: unset !important; + + @include header-poster-padding-rtl; + + @media all and (max-width: 32em) { + padding-right: 0 !important; + } + } } .subtitle { @@ -651,15 +795,21 @@ .layout-desktop & { position: relative; - padding-left: 32.45vw; } .layout-tv & { display: block; - padding-left: 32.45vw; } } +.layout-desktop [dir="rtl"] .detailPagePrimaryContainer { + padding-right: 32.45vw; +} + +.layout-desktop [dir="ltr"] .detailPagePrimaryContainer { + padding-left: 32.45vw; +} + .layout-desktop .detailRibbon { margin-top: -7.2em; height: 7.2em; @@ -676,7 +826,13 @@ flex: 1 0 0; .layout-mobile & { - @include header-poster-padding; + [dir="ltr"] { + @include header-poster-padding; + } + + [dir="rtl"] & { + @include header-poster-padding-rtl; + } @media all and (max-width: 32em) { position: relative; @@ -685,9 +841,16 @@ } .infoText { - text-align: left; min-width: 0; max-width: 100%; + + [dir="ltr"] & { + text-align: left; + } + + [dir="rtl"] & { + text-align: right; + } } .detailPageSecondaryContainer { @@ -749,6 +912,19 @@ width: 25vw; transform: translateY(-50%); } + + [dir="rtl"] & { + left: unset; + + .layout-mobile &, + .layout-tv & { + right: 5%; + } + + .layout-desktop & { + right: 3.3%; + } + } } .detailPagePrimaryContent { @@ -768,6 +944,10 @@ .itemDetailImage { width: 100% !important; box-shadow: 0 0.1em 0.5em 0 rgba(0, 0, 0, 0.75); + + [dir="rtl"] & { + box-shadow: 0 0 0.5em 0.1em rgba(0, 0, 0, 0.75); + } } div.itemDetailGalleryLink.defaultCardBackground { @@ -871,9 +1051,17 @@ div.itemDetailGalleryLink.defaultCardBackground { } .recordingFields button { - margin-left: 0; - margin-right: 0.5em; flex-shrink: 0; + + [dir="ltr"] & { + margin-left: 0; + margin-right: 0.5em; + } + + [dir="rtl"] & { + margin-right: 0; + margin-left: 0.5em; + } } .mainDetailButtons.hide + .recordingFields { @@ -1016,8 +1204,15 @@ div.itemDetailGalleryLink.defaultCardBackground { } .mediaInfoLabel { - margin-right: 1em; font-weight: 600; + + [dir="ltr"] & { + margin-right: 1em; + } + + [dir="rtl"] & { + margin-left: 1em; + } } .recordingProgressBar::-moz-progress-bar { @@ -1107,19 +1302,39 @@ div:not(.sectionTitleContainer-cards) > .sectionTitle-cards { } .sectionTitleButton { - margin-left: 1.5em !important; flex-shrink: 0; + + [dir="ltr"] & { + margin-left: 1.5em !important; + } + + [dir="rtl"] & { + margin-right: 1.5em !important; + } } .sectionTitleButton + .sectionTitleButton { - margin-left: 0.5em !important; + [dir="ltr"] & { + margin-left: 0.5em !important; + } + + [dir="rtl"] & { + margin-right: 0.5em !important; + } } .sectionTitleIconButton { - margin-left: 1.5em !important; flex-shrink: 0; font-size: 84% !important; padding: 0.5em !important; + + [dir="ltr"] & { + margin-left: 1.5em !important; + } + + [dir="rtl"] & { + margin-right: 1.5em !important; + } } .horizontalItemsContainer { @@ -1148,13 +1363,27 @@ div:not(.sectionTitleContainer-cards) > .sectionTitle-cards { } .padded-left { - padding-left: 3.3%; - padding-left: max(env(safe-area-inset-left), 3.3%); + [dir="ltr"] & { + padding-left: 3.3%; + padding-left: max(env(safe-area-inset-left), 3.3%); + } + + [dir="rtl"] & { + padding-right: 3.3%; + padding-right: max(env(safe-area-inset-right), 3.3%); + } } .padded-right { - padding-right: 3.3%; - padding-right: max(env(safe-area-inset-right), 3.3%); + [dir="ltr"] & { + padding-right: 3.3%; + padding-right: max(env(safe-area-inset-right), 3.3%); + } + + [dir="rtl"] & { + padding-left: 3.3%; + padding-left: max(env(safe-area-inset-left), 3.3%); + } } .padded-top { diff --git a/src/assets/css/metadataeditor.scss b/src/assets/css/metadataeditor.scss index 190f53187f..9521728a6e 100644 --- a/src/assets/css/metadataeditor.scss +++ b/src/assets/css/metadataeditor.scss @@ -7,7 +7,13 @@ } .libraryTree { - margin-left: 0.25em; + [dir="ltr"] & { + margin-left: 0.25em; + } + + [dir="rtl"] & { + margin-right: 0.25em; + } } .offlineEditorNode { @@ -56,15 +62,30 @@ position: fixed; top: 5.2em; bottom: 0; - left: 0; width: 30%; - border-right: 1px solid #555; display: block; + + [dir="ltr"] & { + left: 0; + border-right: 1px solid #555; + } + + [dir="rtl"] & { + right: 0; + border-left: 1px solid #555; + } } .editPageInnerContent { - float: right; width: 68.5%; + + [dir="ltr"] & { + float: right; + } + + [dir="rtl"] & { + float: left; + } } } diff --git a/src/assets/css/videoosd.scss b/src/assets/css/videoosd.scss index efe9ab83e3..ff5e94e727 100644 --- a/src/assets/css/videoosd.scss +++ b/src/assets/css/videoosd.scss @@ -125,6 +125,10 @@ -webkit-box-align: center; -webkit-align-items: center; align-items: center; + + [dir="rtl"] & { + flex-direction: row-reverse; + } } .osdVolumeSliderContainer { diff --git a/src/components/actionSheet/actionSheet.scss b/src/components/actionSheet/actionSheet.scss index 7c1da7ad18..9a9234ffcf 100644 --- a/src/components/actionSheet/actionSheet.scss +++ b/src/components/actionSheet/actionSheet.scss @@ -45,7 +45,13 @@ } .actionsheetListItemBody { - padding: 0.4em 1em 0.4em 0.6em !important; + [dir="ltr"] & { + padding: 0.4em 1em 0.4em 0.6em !important; + } + + [dir="rtl"] & { + padding: 0.4em 0.6em 0.4em 1em !important; + } } .actionSheetItemText { @@ -64,8 +70,16 @@ display: flex; justify-content: flex-end; flex-shrink: 0; - margin-left: 5em; - margin-right: 0.5em; + + [dir="ltr"] & { + margin-left: 5em; + margin-right: 0.5em; + } + + [dir="rtl"] & { + margin-right: 5em; + margin-left: 0.5em; + } } .actionSheetScroller { @@ -101,8 +115,15 @@ } .actionsheetMenuItemIcon { - margin: 0 0.85em 0 0.45em !important; padding: 0 !important; + + [dir="ltr"] & { + margin: 0 0.85em 0 0.45em !important; + } + + [dir="rtl"] & { + margin: 0 0.45em 0 0.85em !important; + } } .actionsheet-xlargeFont { @@ -112,5 +133,12 @@ .btnCloseActionSheet { position: fixed; top: 0.75em; - left: 0.5em; + + [dir="ltr"] & { + left: 0.5em; + } + + [dir="rtl"] & { + right: 0.5em; + } } diff --git a/src/components/alphaPicker/style.scss b/src/components/alphaPicker/style.scss index 7a3f6eaf87..86d7e773e1 100644 --- a/src/components/alphaPicker/style.scss +++ b/src/components/alphaPicker/style.scss @@ -110,14 +110,28 @@ } .alphaPicker-fixed-right { - right: 0.4em; - right: max(env(safe-area-inset-right), 0.4em); + [dir="ltr"] & { + right: 0.4em; + right: max(env(safe-area-inset-right), 0.4em); + } + + [dir="rtl"] & { + left: 0.4em; + left: max(env(safe-area-inset-left), 0.4em); + } } @media all and (min-width: 62.5em) { .alphaPicker-fixed-right { - right: 1em; - right: max(env(safe-area-inset-right), 1em); + [dir="ltr"] & { + right: 1em; + right: max(env(safe-area-inset-right), 1em); + } + + [dir="rtl"] & { + left: 1em; + left: max(env(safe-area-inset-left), 1em); + } } } diff --git a/src/components/cardbuilder/card.scss b/src/components/cardbuilder/card.scss index ffd395a733..e43b488dbe 100644 --- a/src/components/cardbuilder/card.scss +++ b/src/components/cardbuilder/card.scss @@ -302,7 +302,14 @@ button::-moz-focus-inner { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - text-align: left; + + [dir="ltr"] & { + text-align: left; + } + + [dir="rtl"] & { + text-align: right; + } } .dialog .cardText { @@ -364,7 +371,7 @@ button::-moz-focus-inner { .cardTextCentered, .cardTextCentered > .textActionButton { - text-align: center; + text-align: center !important; } .cardText-rightmargin { @@ -396,21 +403,35 @@ button::-moz-focus-inner { } .cardIndicators { - right: 0.225em; top: 0.225em; position: absolute; display: flex; align-items: center; contain: layout style; + + [dir="ltr"] & { + right: 0.225em; + } + + [dir="rtl"] & { + left: 0.225em; + } } .cardProgramAttributeIndicators { top: 0; - left: 0; position: absolute; display: flex; text-transform: uppercase; font-size: 92%; + + [dir="ltr"] & { + left: 0; + } + + [dir="rtl"] & { + right: 0; + } } .programAttributeIndicator { @@ -430,7 +451,14 @@ button::-moz-focus-inner { .cardOverlayButton-br { position: absolute; bottom: 0; - right: 0; + + [dir="ltr"] & { + right: 0; + } + + [dir="rtl"] & { + left: 0; + } } .cardOverlayButtonIcon { diff --git a/src/components/cardbuilder/cardBuilder.js b/src/components/cardbuilder/cardBuilder.js index 5bc6bf88ad..658837df83 100644 --- a/src/components/cardbuilder/cardBuilder.js +++ b/src/components/cardbuilder/cardBuilder.js @@ -705,7 +705,7 @@ import { appRouter } from '../appRouter'; if (text) { html += "
"; - html += text; + html += '' + text + ''; html += '
'; valid++; @@ -908,19 +908,20 @@ import { appRouter } from '../appRouter'; } if (options.showYear || options.showSeriesYear) { + const productionYear = item.ProductionYear && datetime.toLocaleString(item.ProductionYear, {useGrouping: false}); if (item.Type === 'Series') { if (item.Status === 'Continuing') { - lines.push(globalize.translate('SeriesYearToPresent', item.ProductionYear || '')); + lines.push(globalize.translate('SeriesYearToPresent', productionYear || '')); } else { if (item.EndDate && item.ProductionYear) { - const endYear = datetime.parseISO8601Date(item.EndDate).getFullYear(); - lines.push(item.ProductionYear + ((endYear === item.ProductionYear) ? '' : (' - ' + endYear))); + const endYear = datetime.toLocaleString(datetime.parseISO8601Date(item.EndDate).getFullYear(), {useGrouping: false}); + lines.push(productionYear + ((endYear === item.ProductionYear) ? '' : (' - ' + endYear))); } else { - lines.push(item.ProductionYear || ''); + lines.push(productionYear || ''); } } } else { - lines.push(item.ProductionYear || ''); + lines.push(productionYear || ''); } } diff --git a/src/components/dialog/dialog.template.html b/src/components/dialog/dialog.template.html index 6d4310c0f4..a424edab9e 100644 --- a/src/components/dialog/dialog.template.html +++ b/src/components/dialog/dialog.template.html @@ -1,5 +1,5 @@
-

+

diff --git a/src/components/formdialog.scss b/src/components/formdialog.scss index 9edd2aa1b2..68dca09589 100644 --- a/src/components/formdialog.scss +++ b/src/components/formdialog.scss @@ -12,15 +12,27 @@ } .formDialogHeaderTitle { - margin-left: 0.25em; - /* In case of h1, h2, h3 */ margin-top: 0; margin-bottom: 0; + + [dir="ltr"] & { + margin-left: 0.25em; + } + + [dir="rtl"] & { + margin-right: 0.25em; + } } .formDialogHeaderTitle:first-child { - margin-left: 1em; + [dir="ltr"] & { + margin-left: 1em; + } + + [dir="rtl"] & { + margin-right: 1em; + } } .formDialogContent:not(.no-grow) { @@ -98,8 +110,8 @@ .formDialogFooterItem-autosize { flex-basis: initial; flex-grow: initial; - padding-left: 2em; padding-right: 2em; + padding-left: 2em; } @media all and (min-width: 50em) { diff --git a/src/components/guide/guide.scss b/src/components/guide/guide.scss index 4373f697ec..0ec6885281 100644 --- a/src/components/guide/guide.scss +++ b/src/components/guide/guide.scss @@ -307,13 +307,20 @@ } .programIcon { - margin-left: 0.5em; height: 1em; width: 1em; font-size: 1.6em; color: #ddd; flex-shrink: 0; flex-grow: 0; + + [dir="ltr"] & { + margin-left: 0.5em; + } + + [dir="rtl"] & { + margin-right: 0.5em; + } } .guide-programTextIcon { @@ -340,11 +347,19 @@ } .guideChannelName { - margin-left: auto; - margin-right: 1em; text-overflow: ellipsis; overflow: hidden; max-width: 70%; + + [dir="ltr"] & { + margin-left: auto; + margin-right: 1em; + } + + [dir="rtl"] & { + margin-right: auto; + margin-left: 1em; + } } .guideChannelImage { diff --git a/src/components/imageUploader/imageUploader.template.html b/src/components/imageUploader/imageUploader.template.html index 8903aa0106..0286991c63 100644 --- a/src/components/imageUploader/imageUploader.template.html +++ b/src/components/imageUploader/imageUploader.template.html @@ -13,7 +13,7 @@

${HeaderAddUpdateImage}

- diff --git a/src/components/imageUploader/style.scss b/src/components/imageUploader/style.scss index dc4fecb108..3d67181c4f 100644 --- a/src/components/imageUploader/style.scss +++ b/src/components/imageUploader/style.scss @@ -10,3 +10,7 @@ align-items: center; justify-content: center; } + +.raised.raised-mini.btnBrowse { + margin-left: 1.5em; +} diff --git a/src/components/imageeditor/imageeditor.scss b/src/components/imageeditor/imageeditor.scss index 5587e7d8d1..a127d58055 100644 --- a/src/components/imageeditor/imageeditor.scss +++ b/src/components/imageeditor/imageeditor.scss @@ -7,3 +7,23 @@ .first-imageEditor-buttons { margin-top: 2em; } + +.btnBrowseAllImages { + [dir="ltr"] & { + margin-left: 1em; + } + + [dir="rtl"] & { + margin-right: 1em; + } +} + +.btnOpenUploadMenu { + [dir="ltr"] & { + margin-left: 0.5em; + } + + [dir="rtl"] & { + margin-right: 0.5em; + } +} diff --git a/src/components/imageeditor/imageeditor.template.html b/src/components/imageeditor/imageeditor.template.html index f878bf9e7f..f8679bd0a0 100644 --- a/src/components/imageeditor/imageeditor.template.html +++ b/src/components/imageeditor/imageeditor.template.html @@ -11,10 +11,10 @@

${Images}

- -
@@ -26,10 +26,10 @@

${Backdrops}

- -
diff --git a/src/components/indicators/indicators.js b/src/components/indicators/indicators.js index cd621c2fcc..9abfffd8e1 100644 --- a/src/components/indicators/indicators.js +++ b/src/components/indicators/indicators.js @@ -78,7 +78,7 @@ export function getPlayedIndicatorHtml(item) { if (enablePlayedIndicator(item)) { const userData = item.UserData || {}; if (userData.UnplayedItemCount) { - return '
' + userData.UnplayedItemCount + '
'; + return '
' + datetime.toLocaleString(userData.UnplayedItemCount) + '
'; } if (userData.PlayedPercentage && userData.PlayedPercentage >= 100 || (userData.Played)) { @@ -93,7 +93,7 @@ export function getChildCountIndicatorHtml(item, options) { const minCount = options && options.minCount ? options.minCount : 0; if (item.ChildCount && item.ChildCount > minCount) { - return '
' + item.ChildCount + '
'; + return '
' + datetime.toLocaleString(item.ChildCount) + '
'; } return ''; diff --git a/src/components/indicators/indicators.scss b/src/components/indicators/indicators.scss index e4e7985817..3130150d31 100644 --- a/src/components/indicators/indicators.scss +++ b/src/components/indicators/indicators.scss @@ -8,8 +8,15 @@ .itemProgressBarForeground { position: absolute; top: 0; - left: 0; bottom: 0; + + [dir="ltr"] & { + left: 0; + } + + [dir="rtl"] & { + right: 0; + } } .indicator { @@ -32,7 +39,13 @@ } .indicator + .indicator { - margin-left: 0.25em; + [dir="ltr"] & { + margin-left: 0.25em; + } + + [dir="rtl"] & { + margin-right: 0.25em; + } } .indicatorIcon { diff --git a/src/components/itemMediaInfo/itemMediaInfo.js b/src/components/itemMediaInfo/itemMediaInfo.js index 520dccaa4d..d149a633a0 100644 --- a/src/components/itemMediaInfo/itemMediaInfo.js +++ b/src/components/itemMediaInfo/itemMediaInfo.js @@ -66,7 +66,7 @@ const attributeDelimiterHtml = layoutManager.tv ? '' : ': `; } if (version.Path && user && user.Policy.IsAdministrator) { - html += `${createAttribute(globalize.translate('MediaInfoPath'), version.Path)}
`; + html += `${createAttribute(globalize.translate('MediaInfoPath'), version.Path, true)}
`; } if (version.Size) { const size = `${(version.Size / (1024 * 1024)).toFixed(0)} MB`; @@ -212,8 +212,9 @@ const attributeDelimiterHtml = layoutManager.tv ? '' : ': ${label}${attributeDelimiterHtml}${escapeHtml(value)}\n`; + // File Paths should be always ltr. The isLtr parameter allows this. + function createAttribute(label, value, isLtr) { + return `${label}${attributeDelimiterHtml}${escapeHtml(value)}\n`; } function loadMediaInfo(itemId, serverId) { diff --git a/src/components/itemidentifier/itemidentifier.js b/src/components/itemidentifier/itemidentifier.js index 976e54f6f0..f9244f22c1 100644 --- a/src/components/itemidentifier/itemidentifier.js +++ b/src/components/itemidentifier/itemidentifier.js @@ -22,6 +22,7 @@ import '../cardbuilder/card.scss'; import ServerConnections from '../ServerConnections'; import toast from '../toast/toast'; import template from './itemidentifier.template.html'; +import datetime from '../../scripts/datetime'; const enableFocusTransform = !browser.slow && !browser.edge; @@ -166,7 +167,7 @@ import template from './itemidentifier.template.html'; lines.push(escapeHtml(identifyResult.Name)); if (identifyResult.ProductionYear) { - lines.push(identifyResult.ProductionYear); + lines.push(datetime.toLocaleString(identifyResult.ProductionYear, {useGrouping: false})); } let resultHtml = lines.join('
'); diff --git a/src/components/libraryoptionseditor/libraryoptionseditor.js b/src/components/libraryoptionseditor/libraryoptionseditor.js index 00cac68136..522ee3058b 100644 --- a/src/components/libraryoptionseditor/libraryoptionseditor.js +++ b/src/components/libraryoptionseditor/libraryoptionseditor.js @@ -11,6 +11,7 @@ import dom from '../../scripts/dom'; import '../../elements/emby-checkbox/emby-checkbox'; import '../../elements/emby-select/emby-select'; import '../../elements/emby-input/emby-input'; +import './style.scss'; import template from './libraryoptionseditor.template.html'; function populateLanguages(parent) { @@ -231,7 +232,7 @@ import template from './libraryoptionseditor.template.html'; html += '

' + globalize.translate('HeaderTypeImageFetchers', globalize.translate('TypeOptionPlural' + availableTypeOptions.Type)) + '

'; const supportedImageTypes = availableTypeOptions.SupportedImageTypes || []; if (supportedImageTypes.length > 1 || supportedImageTypes.length === 1 && supportedImageTypes[0] !== 'Primary') { - html += ''; + html += ''; } html += '
'; html += '
'; diff --git a/src/components/libraryoptionseditor/style.scss b/src/components/libraryoptionseditor/style.scss new file mode 100644 index 0000000000..a3760f36c3 --- /dev/null +++ b/src/components/libraryoptionseditor/style.scss @@ -0,0 +1,9 @@ +.raised.btnImageOptionsForType { + [dir="ltr"] & { + margin-left: 1.5em; + } + + [dir="rtl"] & { + margin-right: 1.5em; + } +} diff --git a/src/components/listview/listview.js b/src/components/listview/listview.js index eeb2946ec0..d6c34172f8 100644 --- a/src/components/listview/listview.js +++ b/src/components/listview/listview.js @@ -149,7 +149,7 @@ import ServerConnections from '../ServerConnections'; elem.classList.add('listItemBodyText'); - elem.innerText = text; + elem.innerHTML = '' + text + ''; html += elem.outerHTML; } @@ -414,7 +414,7 @@ import ServerConnections from '../ServerConnections'; if (enableOverview && item.Overview) { html += '
'; - html += item.Overview; + html += '' + item.Overview + ''; html += '
'; } @@ -477,7 +477,7 @@ import ServerConnections from '../ServerConnections'; if (enableOverview && item.Overview) { html += '
'; - html += item.Overview; + html += '' + item.Overview + ''; html += '
'; } } diff --git a/src/components/listview/listview.scss b/src/components/listview/listview.scss index a6d1fc7efa..9270331252 100644 --- a/src/components/listview/listview.scss +++ b/src/components/listview/listview.scss @@ -1,3 +1,10 @@ +.listItem, +.listItemBody, +.listItemMediaInfo { + display: flex; + contain: layout style; +} + .listItem { background: transparent; border: 0; @@ -9,10 +16,18 @@ margin: 0; display: block; align-items: center; - text-align: left; - padding: 0.25em 0.25em 0.25em 0.5em; cursor: pointer; overflow: hidden; + + [dir="ltr"] & { + text-align: left; + padding: 0.25em 0.25em 0.25em 0.5em; + } + + [dir="rtl"] & { + text-align: right; + padding: 0.25em 0.5em 0.25em 0.25em; + } } .listItem-withContentWrapper { @@ -75,8 +90,15 @@ } .listViewDragHandle { - margin-left: -0.25em !important; touch-action: none; + + [dir="ltr"] & { + margin-left: -0.25em !important; + } + + [dir="rtl"] & { + margin-right: -0.25em !important; + } } .listItemBody { @@ -89,13 +111,6 @@ justify-content: center; } -.listItem, -.listItemBody, -.listItemMediaInfo { - display: flex; - contain: layout style; -} - .layout-tv .listItemBody { padding: 0.35em 0.75em; } @@ -211,7 +226,14 @@ width: 1em !important; height: 1em !important; font-size: 143%; - padding: 0 0.25em 0 0; + + [dir="ltr"] & { + margin: 0 0.25em 0 0; + } + + [dir="rtl"] & { + margin: 0 0 0 0.25em; + } } .listItemIcon:not(.listItemIcon-transparent) { @@ -219,7 +241,14 @@ color: #fff; padding: 0.5em; border-radius: 100em; - margin: 0 0.2em 0 0.4em; + + [dir="ltr"] & { + margin: 0 0.2em 0 0.4em; + } + + [dir="rtl"] & { + margin: 0 0.4em 0 0.2em; + } } .listItemProgressBar { diff --git a/src/components/loading/loading.js b/src/components/loading/loading.js index 66b4be421a..bda1ff84bf 100644 --- a/src/components/loading/loading.js +++ b/src/components/loading/loading.js @@ -13,6 +13,7 @@ export function show() { if (!elem) { elem = document.createElement('div'); + elem.setAttribute('dir', 'ltr'); loadingElem = elem; elem.classList.add('docspinner'); diff --git a/src/components/mediaLibraryCreator/mediaLibraryCreator.js b/src/components/mediaLibraryCreator/mediaLibraryCreator.js index c1e6826f6e..05c0fb02f8 100644 --- a/src/components/mediaLibraryCreator/mediaLibraryCreator.js +++ b/src/components/mediaLibraryCreator/mediaLibraryCreator.js @@ -20,6 +20,7 @@ import '../../elements/emby-toggle/emby-toggle'; import '../listview/listview.scss'; import '../formdialog.scss'; import '../../assets/css/flexstyles.scss'; +import './style.scss'; import toast from '../toast/toast'; import alert from '../alert'; import template from './mediaLibraryCreator.template.html'; @@ -119,12 +120,12 @@ import template from './mediaLibraryCreator.template.html'; function getFolderHtml(pathInfo, index) { let html = ''; - html += '
'; + html += '
'; html += `
`; - html += `
${escapeHtml(pathInfo.Path)}
`; + html += `
${escapeHtml(pathInfo.Path)}
`; if (pathInfo.NetworkPath) { - html += `
${escapeHtml(pathInfo.NetworkPath)}
`; + html += `
${escapeHtml(pathInfo.NetworkPath)}
`; } html += '
'; diff --git a/src/components/mediaLibraryCreator/mediaLibraryCreator.template.html b/src/components/mediaLibraryCreator/mediaLibraryCreator.template.html index d130c820eb..d62919227f 100644 --- a/src/components/mediaLibraryCreator/mediaLibraryCreator.template.html +++ b/src/components/mediaLibraryCreator/mediaLibraryCreator.template.html @@ -19,7 +19,7 @@

${Folders}

-
diff --git a/src/components/mediaLibraryCreator/style.scss b/src/components/mediaLibraryCreator/style.scss new file mode 100644 index 0000000000..567d054b4c --- /dev/null +++ b/src/components/mediaLibraryCreator/style.scss @@ -0,0 +1,19 @@ +.fab.btnAddFolder.submit { + [dir="ltr"] & { + margin-left: 1em; + } + + [dir="rtl"] & { + margin-right: 1em; + } +} + +.listItem.listItem-border.lnkPath { + [dir="ltr"] & { + padding-left: 0.5em; + } + + [dir="rtl"] & { + padding-right: 0.5em; + } +} diff --git a/src/components/mediaLibraryEditor/mediaLibraryEditor.js b/src/components/mediaLibraryEditor/mediaLibraryEditor.js index 9cbe9b0519..da0dc2909c 100644 --- a/src/components/mediaLibraryEditor/mediaLibraryEditor.js +++ b/src/components/mediaLibraryEditor/mediaLibraryEditor.js @@ -18,6 +18,7 @@ import '../../elements/emby-button/paper-icon-button-light'; import '../formdialog.scss'; import '../../elements/emby-toggle/emby-toggle'; import '../../assets/css/flexstyles.scss'; +import './style.scss'; import toast from '../toast/toast'; import confirm from '../confirm/confirm'; import template from './mediaLibraryEditor.template.html'; @@ -109,7 +110,7 @@ import template from './mediaLibraryEditor.template.html'; function getFolderHtml(pathInfo, index) { let html = ''; - html += `
`; + html += `
`; html += `
`; html += '

'; html += escapeHtml(pathInfo.Path); diff --git a/src/components/mediaLibraryEditor/mediaLibraryEditor.template.html b/src/components/mediaLibraryEditor/mediaLibraryEditor.template.html index 8f1822520e..b5ff971d92 100644 --- a/src/components/mediaLibraryEditor/mediaLibraryEditor.template.html +++ b/src/components/mediaLibraryEditor/mediaLibraryEditor.template.html @@ -12,7 +12,7 @@

${Folders}

-
diff --git a/src/components/mediaLibraryEditor/style.scss b/src/components/mediaLibraryEditor/style.scss new file mode 100644 index 0000000000..567d054b4c --- /dev/null +++ b/src/components/mediaLibraryEditor/style.scss @@ -0,0 +1,19 @@ +.fab.btnAddFolder.submit { + [dir="ltr"] & { + margin-left: 1em; + } + + [dir="rtl"] & { + margin-right: 1em; + } +} + +.listItem.listItem-border.lnkPath { + [dir="ltr"] & { + padding-left: 0.5em; + } + + [dir="rtl"] & { + padding-right: 0.5em; + } +} diff --git a/src/components/mediainfo/mediainfo.js b/src/components/mediainfo/mediainfo.js index 6f7876fcf1..b0201162f1 100644 --- a/src/components/mediainfo/mediainfo.js +++ b/src/components/mediainfo/mediainfo.js @@ -176,16 +176,16 @@ import '../../elements/emby-button/emby-button'; if (options.year !== false && item.ProductionYear && item.Type === 'Series') { if (item.Status === 'Continuing') { - miscInfo.push(globalize.translate('SeriesYearToPresent', item.ProductionYear)); + miscInfo.push(globalize.translate('SeriesYearToPresent', datetime.toLocaleString(item.ProductionYear, {useGrouping: false}))); } else if (item.ProductionYear) { - text = item.ProductionYear; + text = datetime.toLocaleString(item.ProductionYear, {useGrouping: false}); if (item.EndDate) { try { - const endYear = datetime.parseISO8601Date(item.EndDate).getFullYear(); + const endYear = datetime.toLocaleString(datetime.parseISO8601Date(item.EndDate).getFullYear(), {useGrouping: false}); if (endYear !== item.ProductionYear) { - text += `-${datetime.parseISO8601Date(item.EndDate).getFullYear()}`; + text += `-${endYear}`; } } catch (e) { console.error('error parsing date:', item.EndDate); @@ -247,7 +247,7 @@ import '../../elements/emby-button/emby-button'; miscInfo.push(item.ProductionYear); } else if (item.PremiereDate) { try { - text = datetime.parseISO8601Date(item.PremiereDate).getFullYear(); + text = datetime.toLocaleString(datetime.parseISO8601Date(item.PremiereDate).getFullYear(), {useGrouping: false}); miscInfo.push(text); } catch (e) { console.error('error parsing date:', item.PremiereDate); diff --git a/src/components/mediainfo/mediainfo.scss b/src/components/mediainfo/mediainfo.scss index 508c9d96ad..cb623b685d 100644 --- a/src/components/mediainfo/mediainfo.scss +++ b/src/components/mediainfo/mediainfo.scss @@ -1,6 +1,13 @@ .mediaInfoItem { - margin: 0 1em 0 0; padding: 0; + + [dir="ltr"] & { + margin: 0 1em 0 0; + } + + [dir="rtl"] & { + margin: 0 0 0 1em; + } } .mediaInfoText { @@ -25,7 +32,13 @@ } .mediaInfoItem:last-child { - margin-right: 0; + [dir="ltr"] & { + margin-right: 0; + } + + [dir="rtl"] & { + margin-left: 0; + } } .starRatingContainer { @@ -46,13 +59,21 @@ } .mediaInfoCriticRating { - padding-left: 1.5em; - background-position: left center; background-repeat: no-repeat; background-size: auto 1.2em; min-height: 1.2em; display: flex; align-items: center; + + [dir="ltr"] & { + padding-left: 1.5em; + background-position: left center; + } + + [dir="rtl"] & { + padding-right: 1.5em; + background-position: right center; + } } .mediaInfoCriticRatingFresh { diff --git a/src/components/metadataEditor/metadataEditor.js b/src/components/metadataEditor/metadataEditor.js index 10112a8453..a36779ba53 100644 --- a/src/components/metadataEditor/metadataEditor.js +++ b/src/components/metadataEditor/metadataEditor.js @@ -17,6 +17,7 @@ import '../../elements/emby-button/paper-icon-button-light'; import '../formdialog.scss'; import '../../assets/css/clearbutton.scss'; import '../../assets/css/flexstyles.scss'; +import './style.scss'; import ServerConnections from '../ServerConnections'; import toast from '../toast/toast'; import { appRouter } from '../appRouter'; diff --git a/src/components/metadataEditor/metadataEditor.template.html b/src/components/metadataEditor/metadataEditor.template.html index d301687a45..eda5d84e0d 100644 --- a/src/components/metadataEditor/metadataEditor.template.html +++ b/src/components/metadataEditor/metadataEditor.template.html @@ -3,7 +3,7 @@

${Edit}

-
+
@@ -203,7 +203,7 @@

${People}

-
@@ -213,7 +213,7 @@

${Studios}

-
@@ -222,7 +222,7 @@

${Tags}

-
diff --git a/src/components/metadataEditor/style.scss b/src/components/metadataEditor/style.scss new file mode 100644 index 0000000000..2b36a96eda --- /dev/null +++ b/src/components/metadataEditor/style.scss @@ -0,0 +1,19 @@ +.dialogHeader { + [dir="ltr"] & { + margin-left: auto; + } + + [dir="rtl"] & { + margin-right: auto; + } +} + +.metadataFormFields .marginStart { + [dir="ltr"] & { + margin-left: 1em; + } + + [dir="rtl"] & { + margin-right: 1em; + } +} diff --git a/src/components/multiSelect/multiSelect.js b/src/components/multiSelect/multiSelect.js index fe82788f6d..370d306d4b 100644 --- a/src/components/multiSelect/multiSelect.js +++ b/src/components/multiSelect/multiSelect.js @@ -9,6 +9,7 @@ import alert from '../alert'; import playlistEditor from '../playlisteditor/playlisteditor'; import confirm from '../confirm/confirm'; import itemHelper from '../itemHelper'; +import datetime from '../../scripts/datetime'; /* eslint-disable indent */ @@ -78,7 +79,7 @@ import itemHelper from '../itemHelper'; if (selectedItems.length) { const itemSelectionCount = document.querySelector('.itemSelectionCount'); if (itemSelectionCount) { - itemSelectionCount.innerHTML = selectedItems.length; + itemSelectionCount.innerHTML = datetime.toLocaleString(selectedItems.length); } } else { hideSelections(); @@ -129,7 +130,7 @@ import itemHelper from '../itemHelper'; html += '

'; const moreIcon = 'more_vert'; - html += ``; + html += ``; selectionCommandsPanel.innerHTML = html; diff --git a/src/components/multiSelect/multiSelect.scss b/src/components/multiSelect/multiSelect.scss index 63db296621..2475d213b5 100644 --- a/src/components/multiSelect/multiSelect.scss +++ b/src/components/multiSelect/multiSelect.scss @@ -40,3 +40,7 @@ .withMultiSelect { position: relative; } + +.btnSelectionPanelOptions { + margin-left: auto; +} diff --git a/src/components/nowPlayingBar/nowPlayingBar.js b/src/components/nowPlayingBar/nowPlayingBar.js index 121a24db41..de61589f23 100644 --- a/src/components/nowPlayingBar/nowPlayingBar.js +++ b/src/components/nowPlayingBar/nowPlayingBar.js @@ -48,7 +48,7 @@ import { appRouter } from '../appRouter'; html += '
'; html += '
'; - html += '
'; + html += '
'; html += ''; html += '
'; @@ -58,7 +58,7 @@ import { appRouter } from '../appRouter'; html += '
'; // The onclicks are needed due to the return false above - html += '
'; + html += '
'; html += ''; diff --git a/src/components/nowPlayingBar/nowPlayingBar.scss b/src/components/nowPlayingBar/nowPlayingBar.scss index b90ff2008d..2d24cbaf7e 100644 --- a/src/components/nowPlayingBar/nowPlayingBar.scss +++ b/src/components/nowPlayingBar/nowPlayingBar.scss @@ -54,11 +54,20 @@ white-space: nowrap; text-overflow: ellipsis; vertical-align: middle; - text-align: left; flex-grow: 1; font-size: 92%; - margin-right: 1em; - margin-left: 0.5em; + + [dir="ltr"] & { + text-align: left; + margin-right: 1em; + margin-left: 0.5em; + } + + [dir="rtl"] & { + text-align: right; + margin-left: 1em; + margin-right: 0.5em; + } } .nowPlayingBarCenter { @@ -92,7 +101,6 @@ .nowPlayingBarRight { position: relative; - margin: 0 0.5em 0 auto; /* Need this to make sure it's on top of nowPlayingBarPositionContainer so that buttons are fully clickable */ z-index: 2; @@ -100,6 +108,14 @@ justify-content: flex-end; align-items: center; flex-shrink: 0; + + [dir="ltr"] & { + margin: 0 0.5em 0 auto; + } + + [dir="rtl"] & { + margin: 0 auto 0 0.5em; + } } .nowPlayingBarCurrentTime { @@ -110,7 +126,13 @@ } .nowPlayingBarVolumeSliderContainer { - margin-right: 2em; + [dir="ltr"] & { + margin-right: 2em; + } + + [dir="rtl"] & { + margin-left: 2em; + } } .nowPlayingBarUserDataButtons { diff --git a/src/components/playerstats/playerstats.scss b/src/components/playerstats/playerstats.scss index dfdf75a2e0..f74f647a37 100644 --- a/src/components/playerstats/playerstats.scss +++ b/src/components/playerstats/playerstats.scss @@ -2,9 +2,16 @@ background: rgba(28, 28, 28, 0.8); border-radius: 0.3em; color: #fff; - left: 1.5em; position: absolute; top: 5em; + + [dir="ltr"] & { + left: 1.5em; + } + + [dir="rtl"] & { + right: 1.5em; + } } .playerStats-tv { @@ -31,23 +38,50 @@ .playerStats-stats { display: flex; flex-direction: column; - padding: 0 3em 1em 1em; max-width: 50em; overflow: hidden; + + [dir="ltr"] & { + padding: 0 3em 1em 1em; + } + + [dir="rtl"] & { + padding: 0 1em 1em 3em; + } } .playerStats-stat { display: flex; - margin-left: 1em; + + [dir="ltr"] & { + margin-left: 1em; + } + + [dir="rtl"] & { + margin-right: 1em; + } } .playerStats-stat-label { font-weight: 500; - margin: 0 0.5em 0 0; + + [dir="ltr"] & { + margin: 0 0.5em 0 0; + } + + [dir="rtl"] & { + margin: 0 0 0 0.5em; + } } .playerStats-stat-header { - margin: 1em 1em 0 0; + [dir="ltr"] & { + margin: 1em 1em 0 0; + } + + [dir="rtl"] & { + margin: 1em 0 0 1em; + } } .playerStats-stat-value { diff --git a/src/components/recordingcreator/recordingfields.scss b/src/components/recordingcreator/recordingfields.scss index db0c7e0692..9835138209 100644 --- a/src/components/recordingcreator/recordingfields.scss +++ b/src/components/recordingcreator/recordingfields.scss @@ -1,6 +1,13 @@ .recordingButton { - margin-left: 0; min-width: 10em; + + [dir="ltr"] & { + margin-left: 0; + } + + [dir="rtl"] & { + margin-right: 0; + } } .recordingIcon-active { diff --git a/src/components/remotecontrol/remotecontrol.scss b/src/components/remotecontrol/remotecontrol.scss index b19be860d7..c05a0b7a3c 100644 --- a/src/components/remotecontrol/remotecontrol.scss +++ b/src/components/remotecontrol/remotecontrol.scss @@ -39,7 +39,13 @@ } .nowPlayingPageTitle { - margin: 0 0 0.5em 0.5em; + [dir="ltr"] & { + margin: 0 0 0.5em 0.5em; + } + + [dir="rtl"] & { + margin: 0 0.5em 0.5em 0; + } } .nowPlayingAlbum a, @@ -51,6 +57,10 @@ .nowPlayingButtonsContainer { display: flex; + + [dir="rtl"] & { + flex-direction: row-reverse; + } } .infoContainer, @@ -94,10 +104,17 @@ .nowPlayingPageImageContainer { width: 16%; - margin-right: 1em; position: relative; -webkit-flex-shrink: 0; flex-shrink: 0; + + [dir="ltr"] & { + margin-right: 1em; + } + + [dir="rtl"] & { + margin-left: 1em; + } } .nowPlayingPageImageContainerNoAlbum { @@ -268,7 +285,13 @@ @media all and (min-width: 80em) { .nowPlayingPageImageContainer { - margin-right: 0.75em; + [dir="ltr"] & { + margin-right: 0.75em; + } + + [dir="rtl"] & { + margin-left: 0.75em; + } } } @@ -365,24 +388,49 @@ .nowPlayingInfoControls .nowPlayingPageUserDataButtonsTitle button { padding-top: 0; - padding-right: 0; - margin-right: 0; - float: right; border-radius: 0; + + [dir="ltr"] & { + padding-right: 0; + margin-right: 0; + float: right; + } + + [dir="rtl"] & { + padding-left: 0; + margin-left: 0; + float: left; + } } .nowPlayingInfoButtons .btnRepeat, .nowPlayingInfoButtons .btnRewind { - margin-left: 0; - margin-right: auto; font-size: smaller; + + [dir="ltr"] & { + margin-left: 0; + margin-right: auto; + } + + [dir="rtl"] & { + margin-right: 0; + margin-left: auto; + } } .nowPlayingInfoButtons .btnShuffleQueue, .nowPlayingInfoButtons .btnFastForward { - margin-left: auto; - margin-right: 0; font-size: smaller; + + [dir="ltr"] & { + margin-left: auto; + margin-right: 0; + } + + [dir="rtl"] & { + margin-right: auto; + margin-left: 0; + } } .paper-icon-button-light { @@ -413,7 +461,7 @@ .nowPlayingButtonsContainer { display: flex; - flex-direction: column; + flex-direction: column !important; } } diff --git a/src/components/subtitleuploader/style.scss b/src/components/subtitleuploader/style.scss index c9d5eb980d..c0bfebe912 100644 --- a/src/components/subtitleuploader/style.scss +++ b/src/components/subtitleuploader/style.scss @@ -9,3 +9,7 @@ align-items: center; justify-content: center; } + +.raised.raised-mini.btnBrowse { + margin-left: 1.5em; +} diff --git a/src/components/subtitleuploader/subtitleuploader.template.html b/src/components/subtitleuploader/subtitleuploader.template.html index 4b5bc6c7d0..a695b0a17b 100644 --- a/src/components/subtitleuploader/subtitleuploader.template.html +++ b/src/components/subtitleuploader/subtitleuploader.template.html @@ -13,7 +13,7 @@

${HeaderAddUpdateSubtitle}

- diff --git a/src/components/toast/toast.scss b/src/components/toast/toast.scss index a2105f61f5..18a2c6d155 100644 --- a/src/components/toast/toast.scss +++ b/src/components/toast/toast.scss @@ -1,6 +1,5 @@ .toastContainer { position: fixed; - left: 0; bottom: 0; pointer-events: none; z-index: 9999999; @@ -12,6 +11,14 @@ padding-bottom: max(env(safe-area-inset-bottom), 1em); display: flex; flex-direction: column; + + [dir="ltr"] & { + left: 0; + } + + [dir="rtl"] & { + right: 0; + } } .toast { diff --git a/src/components/tvproviders/schedulesdirect.js b/src/components/tvproviders/schedulesdirect.js index 91ab7275e8..285f083ef2 100644 --- a/src/components/tvproviders/schedulesdirect.js +++ b/src/components/tvproviders/schedulesdirect.js @@ -8,6 +8,7 @@ import '../../elements/emby-button/paper-icon-button-light'; import '../../elements/emby-select/emby-select'; import '../../elements/emby-button/emby-button'; import '../../assets/css/flexstyles.scss'; +import './style.scss'; import Dashboard from '../../utils/dashboard'; import Events from '../../utils/events.ts'; diff --git a/src/components/tvproviders/schedulesdirect.template.html b/src/components/tvproviders/schedulesdirect.template.html index 8783987875..aee9bd4f72 100644 --- a/src/components/tvproviders/schedulesdirect.template.html +++ b/src/components/tvproviders/schedulesdirect.template.html @@ -37,7 +37,7 @@

2

-

+

${GuideProviderSelectListings}

diff --git a/src/components/tvproviders/style.scss b/src/components/tvproviders/style.scss new file mode 100644 index 0000000000..6618e5c0e0 --- /dev/null +++ b/src/components/tvproviders/style.scss @@ -0,0 +1,9 @@ +.guideProviderSelectListings { + [dir="ltr"] & { + margin: 0 0 0 0.5em; + } + + [dir="rtl"] & { + margin: 0 0.5em 0 0; + } +} diff --git a/src/components/upnextdialog/upnextdialog.scss b/src/components/upnextdialog/upnextdialog.scss index efb0366488..08c4904bb5 100644 --- a/src/components/upnextdialog/upnextdialog.scss +++ b/src/components/upnextdialog/upnextdialog.scss @@ -4,7 +4,6 @@ bottom: 0; width: 30em; padding: 1em; - margin: 0 2em 2em 0; display: flex; flex-direction: column; will-change: transform, opacity; @@ -13,6 +12,14 @@ color: #fff; user-select: none; -webkit-touch-callout: none; + + [dir="ltr"] & { + margin: 0 2em 2em 0; + } + + [dir="rtl"] & { + margin: 0 0 2em 2em; + } } .upNextDialog-hidden { diff --git a/src/controllers/dashboard/dashboard.html b/src/controllers/dashboard/dashboard.html index da3a595330..92794fb4ba 100644 --- a/src/controllers/dashboard/dashboard.html +++ b/src/controllers/dashboard/dashboard.html @@ -80,31 +80,31 @@
${LabelCache}
-
+
${LabelLogs}
-
+
${LabelMetadata}
-
+
${LabelTranscodes}
-
+
${LabelWeb}
-
+
diff --git a/src/controllers/dashboard/dashboard.js b/src/controllers/dashboard/dashboard.js index 481f6d1948..f2c042b07c 100644 --- a/src/controllers/dashboard/dashboard.js +++ b/src/controllers/dashboard/dashboard.js @@ -280,7 +280,7 @@ import confirm from '../../components/confirm/confirm'; html += clientImage; } - html += '
'; + html += '
'; html += '
' + escapeHtml(session.DeviceName) + '
'; html += '
' + escapeHtml(DashboardPage.getAppSecondaryText(session)) + '
'; html += '
'; diff --git a/src/controllers/dashboard/devices/devices.js b/src/controllers/dashboard/devices/devices.js index 7fe0b8d062..d3644da467 100644 --- a/src/controllers/dashboard/devices/devices.js +++ b/src/controllers/dashboard/devices/devices.js @@ -115,7 +115,10 @@ import confirm from '../../../components/confirm/confirm'; deviceHtml += '
'; if (canEdit || canDelete(device.Id)) { - deviceHtml += '
'; + if (globalize.getIsRTL()) + deviceHtml += '
'; + else + deviceHtml += '
'; deviceHtml += ''; deviceHtml += '
'; } diff --git a/src/controllers/dashboard/encodingsettings.html b/src/controllers/dashboard/encodingsettings.html index 048a957b4e..88fe128aac 100644 --- a/src/controllers/dashboard/encodingsettings.html +++ b/src/controllers/dashboard/encodingsettings.html @@ -220,7 +220,7 @@
- +
@@ -231,7 +231,7 @@
- +
@@ -240,7 +240,7 @@
- +
diff --git a/src/controllers/dashboard/general.html b/src/controllers/dashboard/general.html index 53a22036e3..9b9df89952 100644 --- a/src/controllers/dashboard/general.html +++ b/src/controllers/dashboard/general.html @@ -30,7 +30,7 @@
- +
@@ -40,7 +40,7 @@
- +
diff --git a/src/controllers/dashboard/library.js b/src/controllers/dashboard/library.js index 8a43d7b690..550cf9dc0e 100644 --- a/src/controllers/dashboard/library.js +++ b/src/controllers/dashboard/library.js @@ -310,7 +310,10 @@ import cardBuilder from '../../components/cardbuilder/cardBuilder'; html += '
'; // always show menu unless explicitly hidden if (virtualFolder.showMenu !== false) { - html += '
'; + let dirTextAlign = 'right'; + if (globalize.getIsRTL()) + dirTextAlign = 'left'; + html += '
'; html += ''; html += '
'; } @@ -343,7 +346,7 @@ import cardBuilder from '../../components/cardbuilder/cardBuilder'; html += ' '; html += '
'; } else if (virtualFolder.Locations.length && virtualFolder.Locations.length === 1) { - html += "
"; + html += "
"; html += virtualFolder.Locations[0]; html += '
'; } else { diff --git a/src/controllers/dashboard/logs.js b/src/controllers/dashboard/logs.js index 5abb56275d..aa6436962d 100644 --- a/src/controllers/dashboard/logs.js +++ b/src/controllers/dashboard/logs.js @@ -42,7 +42,7 @@ import alert from '../../components/alert'; let logHtml = ''; logHtml += ''; logHtml += '
'; - logHtml += "

" + log.Name + '

'; + logHtml += "

" + log.Name + '

'; const date = datetime.parseISO8601Date(log.DateModified, true); let text = datetime.toLocaleDateString(date); text += ' ' + datetime.getDisplayTime(date); diff --git a/src/controllers/dashboard/plugins/installed/index.js b/src/controllers/dashboard/plugins/installed/index.js index 16d9654351..b8ddbbbf85 100644 --- a/src/controllers/dashboard/plugins/installed/index.js +++ b/src/controllers/dashboard/plugins/installed/index.js @@ -83,7 +83,10 @@ function getPluginCardHtml(plugin, pluginConfigurationPages) { html += '
'; if (configPage || plugin.CanUninstall) { - html += '
'; + if (globalize.getIsRTL()) + html += '
'; + else + html += '
'; html += ''; html += '
'; } diff --git a/src/controllers/dashboard/scheduledtasks/scheduledtasks.js b/src/controllers/dashboard/scheduledtasks/scheduledtasks.js index d4df7d89e0..2db84227c9 100644 --- a/src/controllers/dashboard/scheduledtasks/scheduledtasks.js +++ b/src/controllers/dashboard/scheduledtasks/scheduledtasks.js @@ -59,7 +59,10 @@ import '../../../elements/emby-button/emby-button'; html += ''; html += '
'; html += '
'; - html += ""; + let textAlignStyle = 'left'; + if (globalize.getIsRTL()) + textAlignStyle = 'right'; + html += ""; html += "

" + task.Name + '

'; html += "
" + getTaskProgressHtml(task) + '
'; html += '
'; diff --git a/src/controllers/itemDetails/index.js b/src/controllers/itemDetails/index.js index 192198f141..1de235ffe3 100644 --- a/src/controllers/itemDetails/index.js +++ b/src/controllers/itemDetails/index.js @@ -476,7 +476,7 @@ function renderName(item, container, context) { html = '

' + parentNameHtml.join(' - ') + '

'; } } else { - html = '

' + tvShowHtml + '

'; + html = '

' + tvShowHtml + '

'; } } @@ -486,14 +486,14 @@ function renderName(item, container, context) { if (html && !parentNameLast) { if (tvSeasonHtml) { - html += '

' + tvSeasonHtml + ' - ' + name + '

'; + html += '

' + tvSeasonHtml + ' - ' + name + '

'; } else { - html += '

' + name + '

'; + html += '

' + name + '

'; } } else if (item.OriginalTitle && item.OriginalTitle != item.Name) { - html = '

' + name + '

' + html; + html = '

' + name + '

' + html; } else { - html = '

' + name + '

' + html; + html = '

' + name + '

' + html; } if (item.OriginalTitle && item.OriginalTitle != item.Name) { @@ -915,7 +915,7 @@ function renderOverview(page, item) { if (overview) { for (const overviewElemnt of overviewElements) { - overviewElemnt.innerHTML = overview; + overviewElemnt.innerHTML = '' + overview + ''; overviewElemnt.classList.remove('hide'); overviewElemnt.classList.add('detail-clamp-text'); @@ -1068,7 +1068,7 @@ function renderTagline(page, item) { if (item.Taglines && item.Taglines.length) { taglineElement.classList.remove('hide'); - taglineElement.innerText = item.Taglines[0]; + taglineElement.innerHTML = '' + item.Taglines[0] + ''; } else { taglineElement.classList.add('hide'); } diff --git a/src/controllers/playback/queue/index.html b/src/controllers/playback/queue/index.html index b5f5bf2cf6..cb4706f152 100644 --- a/src/controllers/playback/queue/index.html +++ b/src/controllers/playback/queue/index.html @@ -18,7 +18,7 @@
-
+
@@ -28,7 +28,7 @@
-
+
+
+ - + - + - + - + - + - + - + +
diff --git a/src/controllers/playback/video/index.js b/src/controllers/playback/video/index.js index 72edb80292..4958e1e314 100644 --- a/src/controllers/playback/video/index.js +++ b/src/controllers/playback/video/index.js @@ -216,7 +216,7 @@ import { setBackdropTransparency, TRANSPARENCY_LEVEL } from '../../../components let title = itemName; if (item.PremiereDate) { try { - const year = datetime.parseISO8601Date(item.PremiereDate).getFullYear(); + const year = datetime.toLocaleString(datetime.parseISO8601Date(item.PremiereDate).getFullYear(), {useGrouping: false}); title += ` (${year})`; } catch (e) { console.error(e); diff --git a/src/elements/emby-collapse/emby-collapse.scss b/src/elements/emby-collapse/emby-collapse.scss index dd22c20b5b..e6f84750bb 100644 --- a/src/elements/emby-collapse/emby-collapse.scss +++ b/src/elements/emby-collapse/emby-collapse.scss @@ -17,20 +17,36 @@ align-items: center; text-transform: none; width: 100%; - text-align: left; border-width: 0 0 0.1em 0; border-style: solid; padding-left: 0.1em; background: transparent; box-shadow: none; + + [dir="ltr"] & { + text-align: left; + padding-left: 0.1em; + } + + [dir="rtl"] & { + text-align: right; + padding-right: 0.1em; + } } .emby-collapse-expandIcon { transform-origin: 50% 50%; transition: transform 180ms ease-out; position: absolute; - right: 0.5em; font-size: 1.5em; + + [dir="ltr"] & { + right: 0.5em; + } + + [dir="rtl"] & { + left: 0.5em; + } } .emby-collapse-expandIconExpanded { diff --git a/src/elements/emby-input/emby-input.scss b/src/elements/emby-input/emby-input.scss index 08376e5dea..c71069981b 100644 --- a/src/elements/emby-input/emby-input.scss +++ b/src/elements/emby-input/emby-input.scss @@ -45,13 +45,17 @@ margin: 0 0.5em 1.8em; } -.inlineForm .inputContainer:first-child, -.inlineForm .selectContainer:first-child { +[dir="rtl"] .inlineForm .inputContainer:last-child, +[dir="rtl"] .inlineForm .selectContainer:last-child, +[dir="ltr"] .inlineForm .inputContainer:first-child, +[dir="ltr"] .inlineForm .selectContainer:first-child { margin-left: 0; } -.inlineForm .inputContainer:last-child, -.inlineForm .selectContainer:last-child { +[dir="ltr"] .inlineForm .inputContainer:last-child, +[dir="ltr"] .inlineForm .selectContainer:last-child, +[dir="rtl"] .inlineForm .inputContainer:first-child, +[dir="rtl"] .inlineForm .selectContainer:first-child { margin-right: 0; } diff --git a/src/elements/emby-progressring/emby-progressring.js b/src/elements/emby-progressring/emby-progressring.js index 8c7f890ac6..f174fd6170 100644 --- a/src/elements/emby-progressring/emby-progressring.js +++ b/src/elements/emby-progressring/emby-progressring.js @@ -1,6 +1,7 @@ import './emby-progressring.scss'; import 'webcomponents.js/webcomponents-lite'; import template from './emby-progressring.template.html'; +import { getCurrentDateTimeLocale } from '../../scripts/globalize'; /* eslint-disable indent */ @@ -8,6 +9,7 @@ import template from './emby-progressring.template.html'; EmbyProgressRing.createdCallback = function () { this.classList.add('progressring'); + this.setAttribute('dir', 'ltr'); const instance = this; instance.innerHTML = template; @@ -70,7 +72,10 @@ import template from './emby-progressring.template.html'; this.querySelector('.animate-75-100-b').style.transform = 'rotate(' + angle + 'deg)'; } - this.querySelector('.progressring-text').innerHTML = progress + '%'; + this.querySelector('.progressring-text').innerHTML = new Intl.NumberFormat(getCurrentDateTimeLocale(), { + style: 'percent', + maximumFractionDigits: 0 + }).format(progress / 100); }; EmbyProgressRing.attachedCallback = function () { diff --git a/src/elements/emby-radio/emby-radio.scss b/src/elements/emby-radio/emby-radio.scss index 2d7f584171..5f743c9d79 100644 --- a/src/elements/emby-radio/emby-radio.scss +++ b/src/elements/emby-radio/emby-radio.scss @@ -32,11 +32,18 @@ .mdl-radio__circles { position: relative; - margin-right: 0.54em; width: 1.08em; height: 1.08em; border-radius: 50%; cursor: pointer; + + [dir="ltr"] & { + margin-right: 0.54em; + } + + [dir="rtl"] & { + margin-left: 0.54em; + } } .mdl-radio__circles svg { @@ -44,9 +51,16 @@ height: 100%; position: absolute; top: 0; - left: 0; z-index: 1; overflow: visible; + + [dir="ltr"] & { + left: 0; + } + + [dir="rtl"] & { + right: 0; + } } .mdl-radio__button:disabled + .mdl-radio__circles { diff --git a/src/elements/emby-scrollbuttons/emby-scrollbuttons.js b/src/elements/emby-scrollbuttons/emby-scrollbuttons.js index cb8a487ee8..e7fa1df29f 100644 --- a/src/elements/emby-scrollbuttons/emby-scrollbuttons.js +++ b/src/elements/emby-scrollbuttons/emby-scrollbuttons.js @@ -40,6 +40,11 @@ const EmbyScrollButtonsPrototype = Object.create(HTMLDivElement.prototype); } function updateScrollButtons(scrollButtons, scrollSize, scrollPos, scrollWidth) { + let localeAwarePos = scrollPos; + if (globalize.getIsElementRTL(scrollButtons)) { + localeAwarePos *= -1; + } + // TODO: Check if hack is really needed // hack alert add twenty for rounding errors if (scrollWidth <= scrollSize + 20) { @@ -50,13 +55,13 @@ const EmbyScrollButtonsPrototype = Object.create(HTMLDivElement.prototype); scrollButtons.scrollButtonsRight.classList.remove('hide'); } - if (scrollPos > 0) { + if (localeAwarePos > 0) { scrollButtons.scrollButtonsLeft.disabled = false; } else { scrollButtons.scrollButtonsLeft.disabled = true; } - const scrollPosEnd = scrollPos + scrollSize; + const scrollPosEnd = localeAwarePos + scrollSize; if (scrollWidth > 0 && scrollPosEnd >= scrollWidth) { scrollButtons.scrollButtonsRight.disabled = true; } else { @@ -138,6 +143,12 @@ const EmbyScrollButtonsPrototype = Object.create(HTMLDivElement.prototype); newPos = scrollPos + scrollSize; } + if (globalize.getIsRTL() && direction === 'left') { + newPos = scrollPos + scrollSize; + } else if (globalize.getIsRTL()) { + newPos = Math.min(0, scrollPos - scrollSize); + } + scroller.scrollToPosition(newPos, false); } diff --git a/src/elements/emby-scrollbuttons/emby-scrollbuttons.scss b/src/elements/emby-scrollbuttons/emby-scrollbuttons.scss index 5af739bac1..de1ba39472 100644 --- a/src/elements/emby-scrollbuttons/emby-scrollbuttons.scss +++ b/src/elements/emby-scrollbuttons/emby-scrollbuttons.scss @@ -1,7 +1,6 @@ .emby-scrollbuttons { position: absolute; top: 0; - right: 0; align-items: center; justify-content: center; min-width: 104px; @@ -10,6 +9,14 @@ z-index: 1; color: #fff; display: flex; + + [dir="ltr"] & { + right: 0; + } + + [dir="rtl"] & { + left: 0; + } } .emby-scrollbuttons-button > .material-icons { diff --git a/src/elements/emby-scroller/emby-scroller.scss b/src/elements/emby-scroller/emby-scroller.scss index 09a5856b0e..b02fb00096 100644 --- a/src/elements/emby-scroller/emby-scroller.scss +++ b/src/elements/emby-scroller/emby-scroller.scss @@ -9,17 +9,24 @@ margin-right: max(env(safe-area-inset-right), 3.3%); } -/* align first card in scroller to heading */ -.itemsContainer > .card > .cardBox { - margin-left: 0; - margin-right: 1.2em; -} - .servers > .card > .cardBox { margin-left: 0.6em; margin-right: 0.6em; } +/* align first card in scroller to heading */ +.itemsContainer > .card > .cardBox { + [dir="ltr"] & { + margin-left: 0; + margin-right: 1.2em; + } + + [dir="rtl"] & { + margin-right: 0; + margin-left: 1.2em; + } +} + .layout-tv .emby-scroller, .layout-mobile .emby-scroller { padding-left: 3.3%; diff --git a/src/elements/emby-select/emby-select.scss b/src/elements/emby-select/emby-select.scss index 32aec69c46..62205c8b90 100644 --- a/src/elements/emby-select/emby-select.scss +++ b/src/elements/emby-select/emby-select.scss @@ -11,13 +11,20 @@ /* General select styles: change as needed */ font-family: inherit; font-weight: inherit; - padding: 0.5em 1.9em 0.5em 0.5em; /* Prevent padding from causing width overflow */ box-sizing: border-box; outline: none !important; -webkit-tap-highlight-color: rgba(0, 0, 0, 0); width: 100%; + + [dir="ltr"] & { + padding: 0.5em 1.9em 0.5em 0.5em; + } + + [dir="rtl"] & { + padding: 0.5em 0.5em 0.5em 1.9em; + } } .emby-select[disabled] { @@ -34,8 +41,15 @@ } .selectContainer-inline > .emby-select { - padding: 0.3em 1.9em 0.3em 0.5em; font-size: inherit; + + [dir="ltr"] & { + padding: 0.3em 1.9em 0.3em 0.5em; + } + + [dir="rtl"] & { + padding: 0.3em 0.5em 0.3em 1.9em; + } } .selectContainer-inline > .emby-select[disabled] { @@ -92,10 +106,17 @@ .selectArrowContainer { position: absolute; - right: 0.3em; top: 0.2em; color: inherit; pointer-events: none; + + [dir="ltr"] & { + right: 0.3em; + } + + [dir="rtl"] & { + left: 0.3em; + } } .selectContainer-inline > .selectArrowContainer { diff --git a/src/elements/emby-slider/emby-slider.js b/src/elements/emby-slider/emby-slider.js index 208e8d9db7..7ce7b02ab6 100644 --- a/src/elements/emby-slider/emby-slider.js +++ b/src/elements/emby-slider/emby-slider.js @@ -5,6 +5,7 @@ import keyboardnavigation from '../../scripts/keyboardNavigation'; import './emby-slider.scss'; import 'webcomponents.js/webcomponents-lite'; import '../emby-input/emby-input'; +import globalize from '../../scripts/globalize'; /* eslint-disable indent */ @@ -31,6 +32,8 @@ import '../emby-input/emby-input'; const rect = range.sliderBubbleTrack.getBoundingClientRect(); let fraction = (clientX - rect.left) / rect.width; + if (globalize.getIsElementRTL(range)) + fraction = (rect.right - clientX) / rect.width; // Snap to step const valueRange = range.max - range.min; @@ -118,6 +121,9 @@ import '../emby-input/emby-input'; const bubbleRect = bubble.getBoundingClientRect(); let bubblePos = bubbleTrackRect.width * value / 100; + if (globalize.getIsElementRTL(range)) { + bubblePos = bubbleTrackRect.width - bubblePos; + } bubblePos = Math.min(Math.max(bubblePos, bubbleRect.width / 2), bubbleTrackRect.width - bubbleRect.width / 2); bubble.style.left = bubblePos + 'px'; @@ -484,7 +490,10 @@ import '../emby-input/emby-input'; function setRange(elem, startPercent, endPercent) { const style = elem.style; - style.left = Math.max(startPercent, 0) + '%'; + if (globalize.getIsRTL()) + style.right = Math.max(startPercent, 0) + '%'; + else + style.left = Math.max(startPercent, 0) + '%'; const widthPercent = endPercent - startPercent; style.width = Math.max(Math.min(widthPercent, 100), 0) + '%'; diff --git a/src/elements/emby-slider/emby-slider.scss b/src/elements/emby-slider/emby-slider.scss index eb9ab75c4a..f40e6329d9 100644 --- a/src/elements/emby-slider/emby-slider.scss +++ b/src/elements/emby-slider/emby-slider.scss @@ -151,9 +151,16 @@ width: 100%; box-sizing: border-box; top: 50%; - left: 0; position: absolute; padding: 0 0.54em; /* half of slider thumb size */ + + [dir="ltr"] & { + left: 0; + } + + [dir="rtl"] & { + right: 0; + } } .mdl-slider-background-flex { @@ -162,11 +169,18 @@ margin-top: -0.1em; width: 100%; top: 50%; - left: 0; display: flex; overflow: hidden; border: 0; padding: 0; + + [dir="ltr"] & { + left: 0; + } + + [dir="rtl"] & { + right: 0; + } } .mdl-slider-background-flex-inner { @@ -177,11 +191,18 @@ .mdl-slider-background-lower { /* transition: width 0.18s cubic-bezier(0.4, 0, 0.2, 1); */ position: absolute; - left: 0; width: 0; top: 0; bottom: 0; background-color: #00a4dc; + + [dir="ltr"] & { + left: 0; + } + + [dir="rtl"] & { + right: 0; + } } .mdl-slider-background-lower-clear { diff --git a/src/elements/emby-toggle/emby-toggle.scss b/src/elements/emby-toggle/emby-toggle.scss index 3b5c5a5f64..b4583336a3 100644 --- a/src/elements/emby-toggle/emby-toggle.scss +++ b/src/elements/emby-toggle/emby-toggle.scss @@ -59,7 +59,6 @@ .mdl-switch__thumb { background: #999; position: absolute; - left: 0; top: -0.25em; height: 1.44em; width: 1.44em; @@ -72,6 +71,14 @@ display: flex; align-items: center; justify-content: center; + + [dir="ltr"] & { + left: 0; + } + + [dir="rtl"] & { + right: 0; + } } .mdl-switch__input:checked + .mdl-switch__label + .mdl-switch__trackContainer > .mdl-switch__thumb { diff --git a/src/index.html b/src/index.html index 62659deb47..1837c30a21 100644 --- a/src/index.html +++ b/src/index.html @@ -1,5 +1,5 @@ - + @@ -112,13 +112,20 @@ .mainDrawerHandle { position: fixed; top: 0; - left: 0; bottom: 0; z-index: 1; width: 0.8em; padding-left: env(safe-area-inset-left); } + [dir="ltr"] .mainDrawerHandle { + left: 0; + } + + [dir="rtl"] .mainDrawerHandle { + left: 0; + } + @-webkit-keyframes fadein { from { opacity: 0; @@ -163,7 +170,7 @@ } - +
diff --git a/src/libraries/navdrawer/navdrawer.js b/src/libraries/navdrawer/navdrawer.js index a152c1c203..bd27b6e55c 100644 --- a/src/libraries/navdrawer/navdrawer.js +++ b/src/libraries/navdrawer/navdrawer.js @@ -7,6 +7,7 @@ import browser from '../../scripts/browser'; import dom from '../../scripts/dom'; import './navdrawer.scss'; import '../../assets/css/scrollstyles.scss'; +import globalize from '../../scripts/globalize'; function getTouches(e) { return e.changedTouches || e.targetTouches || e.touches; @@ -73,7 +74,10 @@ class NavDrawer { const touch = touches[0] || {}; const endX = touch.clientX || 0; const endY = touch.clientY || 0; - const deltaX = endX - (this.menuTouchStartX || 0); + let deltaX = endX - (this.menuTouchStartX || 0); + if (globalize.getIsRTL()) { + deltaX *= -1; + } const deltaY = endY - (this.menuTouchStartY || 0); this.setVelocity(deltaX); @@ -106,7 +110,10 @@ class NavDrawer { const touch = touches[0] || {}; const endX = touch.clientX || 0; const endY = touch.clientY || 0; - const deltaX = endX - (this.menuTouchStartX || 0); + let deltaX = endX - (this.menuTouchStartX || 0); + if (globalize.getIsRTL()) { + deltaX *= -1; + } const deltaY = endY - (this.menuTouchStartY || 0); this.currentPos = deltaX; this.checkMenuState(deltaX, deltaY); @@ -161,7 +168,10 @@ class NavDrawer { if (endX <= options.width && this.isVisible) { this.countStart++; - const deltaX = endX - (this.backgroundTouchStartX || 0); + let deltaX = endX - (this.backgroundTouchStartX || 0); + if (globalize.getIsRTL()) { + deltaX *= -1; + } if (this.countStart == 1) { this.startPoint = deltaX; @@ -183,7 +193,10 @@ class NavDrawer { const touches = getTouches(e); const touch = touches[0] || {}; const endX = touch.clientX || 0; - const deltaX = endX - (this.backgroundTouchStartX || 0); + let deltaX = endX - (this.backgroundTouchStartX || 0); + if (globalize.getIsRTL()) { + deltaX *= -1; + } this.checkMenuState(deltaX); this.countStart = 0; }; @@ -193,7 +206,10 @@ class NavDrawer { options.target.classList.add('touch-menu-la'); options.target.style.width = options.width + 'px'; - options.target.style.left = -options.width + 'px'; + if (globalize.getIsRTL()) + options.target.style.right = -options.width + 'px'; + else + options.target.style.left = -options.width + 'px'; if (!options.disableMask) { this.mask = document.createElement('div'); @@ -204,8 +220,9 @@ class NavDrawer { animateToPosition(pos) { const options = this.options; + const languageAwarePos = globalize.getIsRTL() ? -pos : pos; requestAnimationFrame(function () { - options.target.style.transform = pos ? 'translateX(' + pos + 'px)' : 'none'; + options.target.style.transform = pos ? 'translateX(' + languageAwarePos + 'px)' : 'none'; }); } diff --git a/src/libraries/navdrawer/navdrawer.scss b/src/libraries/navdrawer/navdrawer.scss index 361d855154..1f475ce13a 100644 --- a/src/libraries/navdrawer/navdrawer.scss +++ b/src/libraries/navdrawer/navdrawer.scss @@ -17,6 +17,13 @@ -webkit-transition: -webkit-transform ease-out 40ms, left ease-out 260ms; -o-transition: transform ease-out 40ms, left ease-out 260ms; transition: transform ease-out 40ms, left ease-out 260ms; + + [div="rtl"] & { + -webkit-transition: -webkit-transform ease-out 40ms, right ease-out 260ms; + -o-transition: transform ease-out 40ms, right ease-out 260ms; + transition: transform ease-out 40ms, right ease-out 260ms; + } + z-index: 1099; } @@ -24,6 +31,12 @@ -webkit-transition: -webkit-transform ease-out 240ms, left ease-out 260ms; -o-transition: transform ease-out 240ms, left ease-out 260ms; transition: transform ease-out 240ms, left ease-out 260ms; + + [div="rtl"] & { + -webkit-transition: -webkit-transform ease-out 240ms, right ease-out 260ms; + -o-transition: transform ease-out 240ms, right ease-out 260ms; + transition: transform ease-out 240ms, right ease-out 260ms; + } } .drawer-open { diff --git a/src/libraries/scroller.js b/src/libraries/scroller.js index 49b84e734d..c388c52fe8 100644 --- a/src/libraries/scroller.js +++ b/src/libraries/scroller.js @@ -9,6 +9,7 @@ import dom from '../scripts/dom'; import focusManager from '../components/focusManager'; import ResizeObserver from 'resize-observer-polyfill'; import '../assets/css/scrollstyles.scss'; +import globalize from '../scripts/globalize'; /** * Return type of the value. @@ -52,7 +53,14 @@ function disableOneEvent(event) { * * @return {Number} */ -function within(number, min, max) { +function within(number, num1, num2) { + if (num2 === undefined && globalize.getIsRTL()) { + return number > num1 ? num1 : number; + } else if (num2 === undefined) { + return number < num1 ? num1 : number; + } + const min = Math.min(num1, num2); + const max = Math.max(num1, num2); if (number < min) { return min; } else if (number > max) { @@ -173,6 +181,8 @@ const scrollerFactory = function (frame, options) { // Set position limits & relativess self._pos.end = Math.max(slideeSize - frameSize, 0); + if (globalize.getIsRTL()) + self._pos.end *= -1; } } @@ -262,7 +272,9 @@ const scrollerFactory = function (frame, options) { ensureSizeInfo(); const pos = self._pos; - if (layoutManager.tv) { + if (layoutManager.tv && globalize.getIsRTL()) { + newPos = within(-newPos, pos.start); + } else if (layoutManager.tv) { newPos = within(newPos, pos.start); } else { newPos = within(newPos, pos.start, pos.end); @@ -354,7 +366,12 @@ const scrollerFactory = function (frame, options) { const slideeOffset = getBoundingClientRect(scrollElement); const itemOffset = getBoundingClientRect(item); - let offset = o.horizontal ? itemOffset.left - slideeOffset.left : itemOffset.top - slideeOffset.top; + let horizontalOffset = itemOffset.left - slideeOffset.left; + if (globalize.getIsRTL()) { + horizontalOffset = slideeOffset.right - itemOffset.right; + } + + let offset = o.horizontal ? horizontalOffset : itemOffset.top - slideeOffset.top; let size = o.horizontal ? itemOffset.width : itemOffset.height; if (!size && size !== 0) { @@ -375,17 +392,21 @@ const scrollerFactory = function (frame, options) { ensureSizeInfo(); const currentStart = self._pos.cur; - const currentEnd = currentStart + frameSize; + let currentEnd = currentStart + frameSize; + if (globalize.getIsRTL()) { + currentEnd = currentStart - frameSize; + } console.debug('offset:' + offset + ' currentStart:' + currentStart + ' currentEnd:' + currentEnd); - const isVisible = offset >= currentStart && (offset + size) <= currentEnd; + const isVisible = offset >= Math.min(currentStart, currentEnd) + && (globalize.getIsRTL() ? (offset - size) : (offset + size)) <= Math.max(currentStart, currentEnd); return { start: offset, center: offset + centerOffset - (frameSize / 2) + (size / 2), end: offset - frameSize + size, - size: size, - isVisible: isVisible + size, + isVisible }; }; diff --git a/src/plugins/htmlVideoPlayer/plugin.js b/src/plugins/htmlVideoPlayer/plugin.js index 35dd2b797a..d9bdcc7b9d 100644 --- a/src/plugins/htmlVideoPlayer/plugin.js +++ b/src/plugins/htmlVideoPlayer/plugin.js @@ -1347,6 +1347,7 @@ function tryRemoveElement(elem) { loading.show(); const dlg = document.createElement('div'); + dlg.setAttribute('dir', 'ltr'); dlg.classList.add('videoPlayerContainer'); diff --git a/src/scripts/datetime.js b/src/scripts/datetime.js index 796ec3a6d7..fb71112fc6 100644 --- a/src/scripts/datetime.js +++ b/src/scripts/datetime.js @@ -84,7 +84,7 @@ import globalize from './globalize'; hours = Math.floor(hours); if (hours) { - parts.push(hours); + parts.push(hours.toLocaleString(globalize.getCurrentDateTimeLocale())); } ticks -= (hours * ticksPerHour); @@ -95,7 +95,9 @@ import globalize from './globalize'; ticks -= (minutes * ticksPerMinute); if (minutes < 10 && hours) { - minutes = '0' + minutes; + minutes = (0).toLocaleString(globalize.getCurrentDateTimeLocale()) + minutes.toLocaleString(globalize.getCurrentDateTimeLocale()); + } else { + minutes = minutes.toLocaleString(globalize.getCurrentDateTimeLocale()); } parts.push(minutes); @@ -103,7 +105,9 @@ import globalize from './globalize'; seconds = Math.floor(seconds); if (seconds < 10) { - seconds = '0' + seconds; + seconds = (0).toLocaleString(globalize.getCurrentDateTimeLocale()) + seconds.toLocaleString(globalize.getCurrentDateTimeLocale()); + } else { + seconds = seconds.toLocaleString(globalize.getCurrentDateTimeLocale()); } parts.push(seconds); diff --git a/src/scripts/globalize.js b/src/scripts/globalize.js index 128e3b16ef..7a98dd2a4c 100644 --- a/src/scripts/globalize.js +++ b/src/scripts/globalize.js @@ -3,13 +3,20 @@ import isEmpty from 'lodash-es/isEmpty'; import { currentSettings as userSettings } from './settings/userSettings'; import Events from '../utils/events.ts'; +const Direction = { + rtl: 'rtl', + ltr: 'ltr' +}; + /* eslint-disable indent */ const fallbackCulture = 'en-us'; + const RTL_LANGS = ['ar', 'fa', 'ur', 'he']; const allTranslations = {}; let currentCulture; let currentDateTimeCulture; + let isRTL = false; export function getCurrentLocale() { return currentCulture; @@ -38,6 +45,37 @@ import Events from '../utils/events.ts'; return fallbackCulture; } + export function getIsRTL() { + return isRTL; + } + + function checkAndProcessDir(culture) { + isRTL = false; + console.log(culture); + for (const lang of RTL_LANGS) { + if (culture.includes(lang)) { + isRTL = true; + break; + } + } + + setDocumentDirection(isRTL ? Direction.rtl : Direction.ltr); + } + + function setDocumentDirection(direction) { + document.getElementsByTagName('body')[0].setAttribute('dir', direction); + document.getElementsByTagName('html')[0].setAttribute('dir', direction); + if (direction === Direction.rtl) + import('../styles/rtl.scss'); + } + + export function getIsElementRTL(element) { + if (window.getComputedStyle) { // all browsers + return window.getComputedStyle(element, null).getPropertyValue('direction') == 'rtl'; + } + return element.currentStyle.direction == 'rtl'; + } + export function updateCurrentCulture() { let culture; try { @@ -46,6 +84,7 @@ import Events from '../utils/events.ts'; console.error('no language set in user settings'); } culture = culture || getDefaultLanguage(); + checkAndProcessDir(culture); currentCulture = normalizeLocaleName(culture); @@ -200,7 +239,7 @@ import Events from '../utils/events.ts'; export function translate(key) { let val = translateKey(key); for (let i = 1; i < arguments.length; i++) { - val = replaceAll(val, '{' + (i - 1) + '}', arguments[i]); + val = replaceAll(val, '{' + (i - 1) + '}', arguments[i].toLocaleString(currentCulture)); } return val; } @@ -257,7 +296,9 @@ export default { getCurrentLocale, getCurrentDateTimeLocale, register, - updateCurrentCulture + updateCurrentCulture, + getIsRTL, + getIsElementRTL }; /* eslint-enable indent */ diff --git a/src/scripts/libraryBrowser.js b/src/scripts/libraryBrowser.js index 9cb54a8c4d..eaec777401 100644 --- a/src/scripts/libraryBrowser.js +++ b/src/scripts/libraryBrowser.js @@ -88,7 +88,7 @@ export function getQueryPagingHtml (options) { if (showControls) { html += ''; - html += globalize.translate('ListPaging', (totalRecordCount ? startIndex + 1 : 0), recordsEnd, totalRecordCount); + html += globalize.translate('ListPaging', totalRecordCount ? startIndex + 1 : 0, recordsEnd, totalRecordCount); html += ''; } diff --git a/src/styles/rtl.scss b/src/styles/rtl.scss new file mode 100644 index 0000000000..3806ca0691 --- /dev/null +++ b/src/styles/rtl.scss @@ -0,0 +1,18 @@ +.chevron_right, +.chevron_left, +.arrow_back, +.arrow_forward, +.playlist_add, +.shuffle, +.input, +.dvr, +.shopping_cart, +.vpn_key, +.volume_up, +.volume_off, +.message, +.clear_all { + [dir='rtl'] & { + transform: scale(-1, 1); + } +}