diff --git a/.ci/azure-pipelines.yml b/.ci/azure-pipelines.yml
index 31f00754f5..059c39aa56 100644
--- a/.ci/azure-pipelines.yml
+++ b/.ci/azure-pipelines.yml
@@ -21,8 +21,6 @@ jobs:
BuildConfiguration: development
Production:
BuildConfiguration: production
- Standalone:
- BuildConfiguration: standalone
pool:
vmImage: 'ubuntu-latest'
@@ -49,13 +47,9 @@ jobs:
condition: eq(variables['BuildConfiguration'], 'development')
- script: 'yarn build:production'
- displayName: 'Build Bundle'
+ displayName: 'Build Production'
condition: eq(variables['BuildConfiguration'], 'production')
- - script: 'yarn build:standalone'
- displayName: 'Build Standalone'
- condition: eq(variables['BuildConfiguration'], 'standalone')
-
- script: 'test -d dist'
displayName: 'Check Build'
diff --git a/.eslintrc.js b/.eslintrc.js
index 4a3fec9448..27b5c2a237 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -27,28 +27,30 @@ module.exports = {
'plugin:compat/recommended'
],
rules: {
- 'block-spacing': ["error"],
- 'brace-style': ["error"],
- 'comma-dangle': ["error", "never"],
- 'comma-spacing': ["error"],
- 'eol-last': ["error"],
- 'indent': ["error", 4, { "SwitchCase": 1 }],
- 'keyword-spacing': ["error"],
- 'max-statements-per-line': ["error"],
- 'no-floating-decimal': ["error"],
- 'no-multi-spaces': ["error"],
- 'no-multiple-empty-lines': ["error", { "max": 1 }],
- 'no-trailing-spaces': ["error"],
- 'one-var': ["error", "never"],
- 'quotes': ["error", "single", { "avoidEscape": true, "allowTemplateLiterals": false }],
- 'semi': ["error"],
- 'space-before-blocks': ["error"]
+ 'block-spacing': ['error'],
+ 'brace-style': ['error'],
+ 'comma-dangle': ['error', 'never'],
+ 'comma-spacing': ['error'],
+ 'eol-last': ['error'],
+ 'indent': ['error', 4, { 'SwitchCase': 1 }],
+ 'keyword-spacing': ['error'],
+ 'max-statements-per-line': ['error'],
+ 'no-floating-decimal': ['error'],
+ 'no-multi-spaces': ['error'],
+ 'no-multiple-empty-lines': ['error', { 'max': 1 }],
+ 'no-trailing-spaces': ['error'],
+ 'one-var': ['error', 'never'],
+ 'quotes': ['error', 'single', { 'avoidEscape': true, 'allowTemplateLiterals': false }],
+ 'semi': ['error'],
+ 'space-before-blocks': ['error'],
+ 'space-infix-ops': 'error'
},
overrides: [
{
files: [
'./src/**/*.js'
],
+ parser: 'babel-eslint',
env: {
node: false,
amd: true,
@@ -96,11 +98,11 @@ module.exports = {
},
rules: {
// TODO: Fix warnings and remove these rules
- 'no-redeclare': ["warn"],
- 'no-unused-vars': ["warn"],
- 'no-useless-escape': ["warn"],
+ 'no-redeclare': ['warn'],
+ 'no-unused-vars': ['warn'],
+ 'no-useless-escape': ['warn'],
// TODO: Remove after ES6 migration is complete
- 'import/no-unresolved': ["off"]
+ 'import/no-unresolved': ['off']
},
settings: {
polyfills: [
diff --git a/.gitignore b/.gitignore
index 4adf9558bf..9bccd32fb8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,4 +8,7 @@ node_modules
# ide
.idea
-.vscode
\ No newline at end of file
+.vscode
+
+#log
+yarn-error.log
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index 84ef26aa55..c69125a07d 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -36,6 +36,7 @@
- [MrTimscampi](https://github.com/MrTimscampi)
- [Sarab Singh](https://github.com/sarab97)
- [DesertCookie](https://github.com/desertcookie)
+ - [Andrei Oanca](https://github.com/OancaAndrei)
# Emby Contributors
diff --git a/README.md b/README.md
index e2aac6b155..f06e461320 100644
--- a/README.md
+++ b/README.md
@@ -44,7 +44,7 @@ Jellyfin Web is the frontend used for most of the clients available for end user
### Dependencies
-- Yarn
+- [Yarn 1.22.4](https://classic.yarnpkg.com/en/docs/install)
- Gulp-cli
### Getting Started
@@ -78,4 +78,4 @@ Jellyfin Web is the frontend used for most of the clients available for end user
```sh
yarn build:standalone
- ```
\ No newline at end of file
+ ```
diff --git a/gulpfile.js b/gulpfile.js
index 6c33167386..03826e8b6e 100644
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -16,7 +16,6 @@ const stream = require('webpack-stream');
const inject = require('gulp-inject');
const postcss = require('gulp-postcss');
const sass = require('gulp-sass');
-const gulpif = require('gulp-if');
const lazypipe = require('lazypipe');
sass.compiler = require('node-sass');
@@ -45,7 +44,7 @@ const options = {
query: ['src/**/*.png', 'src/**/*.jpg', 'src/**/*.gif', 'src/**/*.svg']
},
copy: {
- query: ['src/**/*.json', 'src/**/*.ico']
+ query: ['src/**/*.json', 'src/**/*.ico', 'src/**/*.mp3']
},
injectBundle: {
query: 'src/index.html'
@@ -68,7 +67,7 @@ function serve() {
}
});
- watch(options.apploader.query, apploader(true));
+ watch(options.apploader.query, apploader());
watch('src/bundle.js', webpack);
@@ -131,18 +130,12 @@ function javascript(query) {
.pipe(browserSync.stream());
}
-function apploader(standalone) {
- function task() {
- return src(options.apploader.query, { base: './src/' })
- .pipe(gulpif(standalone, concat('scripts/apploader.js')))
- .pipe(pipelineJavascript())
- .pipe(dest('dist/'))
- .pipe(browserSync.stream());
- }
-
- task.displayName = 'apploader';
-
- return task;
+function apploader() {
+ return src(options.apploader.query, { base: './src/' })
+ .pipe(concat('scripts/apploader.js'))
+ .pipe(pipelineJavascript())
+ .pipe(dest('dist/'))
+ .pipe(browserSync.stream());
}
function webpack() {
@@ -181,12 +174,6 @@ function copy(query) {
.pipe(browserSync.stream());
}
-function copyIndex() {
- return src(options.injectBundle.query, { base: './src/' })
- .pipe(dest('dist/'))
- .pipe(browserSync.stream());
-}
-
function injectBundle() {
return src(options.injectBundle.query, { base: './src/' })
.pipe(inject(
@@ -196,10 +183,5 @@ function injectBundle() {
.pipe(browserSync.stream());
}
-function build(standalone) {
- return series(clean, parallel(javascript, apploader(standalone), webpack, css, html, images, copy));
-}
-
-exports.default = series(build(false), copyIndex);
-exports.standalone = series(build(true), injectBundle);
-exports.serve = series(exports.standalone, serve);
+exports.default = series(clean, parallel(javascript, apploader, webpack, css, html, images, copy), injectBundle);
+exports.serve = series(exports.default, serve);
diff --git a/package.json b/package.json
index e580255bd5..58914207a5 100644
--- a/package.json
+++ b/package.json
@@ -5,27 +5,29 @@
"repository": "https://github.com/jellyfin/jellyfin-web",
"license": "GPL-2.0-or-later",
"devDependencies": {
- "@babel/core": "^7.9.6",
+ "@babel/core": "^7.10.2",
+ "@babel/plugin-proposal-class-properties": "^7.10.1",
+ "@babel/plugin-proposal-private-methods": "^7.10.1",
"@babel/plugin-transform-modules-amd": "^7.9.6",
"@babel/polyfill": "^7.8.7",
- "@babel/preset-env": "^7.8.6",
- "autoprefixer": "^9.7.6",
+ "@babel/preset-env": "^7.10.2",
+ "autoprefixer": "^9.8.0",
+ "babel-eslint": "^11.0.0-beta.2",
"babel-loader": "^8.0.6",
"browser-sync": "^2.26.7",
- "clean-webpack-plugin": "^3.0.0",
"copy-webpack-plugin": "^5.1.1",
"css-loader": "^3.4.2",
"cssnano": "^4.1.10",
"del": "^5.1.0",
"eslint": "^6.8.0",
"eslint-plugin-compat": "^3.5.1",
- "eslint-plugin-eslint-comments": "^3.1.2",
+ "eslint-plugin-eslint-comments": "^3.2.0",
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-promise": "^4.2.1",
"file-loader": "^6.0.0",
"gulp": "^4.0.2",
"gulp-babel": "^8.0.0",
- "gulp-cli": "^2.2.0",
+ "gulp-cli": "^2.2.1",
"gulp-concat": "^2.6.1",
"gulp-htmlmin": "^5.0.1",
"gulp-if": "^3.0.0",
@@ -42,14 +44,11 @@
"postcss-loader": "^3.0.0",
"postcss-preset-env": "^6.7.0",
"style-loader": "^1.1.3",
- "stylelint": "^13.3.3",
+ "stylelint": "^13.5.0",
"stylelint-config-rational-order": "^0.1.2",
"stylelint-no-browser-hacks": "^1.2.1",
"stylelint-order": "^4.0.0",
"webpack": "^4.41.5",
- "webpack-cli": "^3.3.10",
- "webpack-concat-plugin": "^3.0.0",
- "webpack-dev-server": "^3.11.0",
"webpack-merge": "^4.2.2",
"webpack-stream": "^5.2.1"
},
@@ -57,15 +56,16 @@
"alameda": "^1.4.0",
"classlist.js": "https://github.com/eligrey/classList.js/archive/1.2.20180112.tar.gz",
"core-js": "^3.6.5",
- "date-fns": "^2.13.0",
+ "date-fns": "^2.14.0",
"document-register-element": "^1.14.3",
+ "epubjs": "^0.3.85",
"fast-text-encoding": "^1.0.1",
"flv.js": "^1.5.0",
"headroom.js": "^0.11.0",
"hls.js": "^0.13.1",
- "howler": "^2.1.3",
+ "howler": "^2.2.0",
"intersection-observer": "^0.10.0",
- "jellyfin-apiclient": "^1.1.1",
+ "jellyfin-apiclient": "^1.2.0",
"jellyfin-noto": "https://github.com/jellyfin/jellyfin-noto",
"jquery": "^3.5.1",
"jstree": "^3.3.7",
@@ -76,9 +76,9 @@
"query-string": "^6.11.1",
"resize-observer-polyfill": "^1.5.1",
"screenfull": "^5.0.2",
- "shaka-player": "^2.5.11",
+ "shaka-player": "^2.5.12",
"sortablejs": "^1.10.2",
- "swiper": "^5.3.7",
+ "swiper": "^5.4.1",
"webcomponents.js": "^0.7.24",
"whatwg-fetch": "^3.0.0"
},
@@ -91,24 +91,37 @@
"test": [
"src/components/autoFocuser.js",
"src/components/cardbuilder/cardBuilder.js",
- "src/components/filedownloader.js",
+ "src/scripts/fileDownloader.js",
"src/components/images/imageLoader.js",
- "src/components/lazyloader/lazyloader-intersectionobserver.js",
+ "src/components/lazyLoader/lazyLoaderIntersectionObserver.js",
"src/components/playback/mediasession.js",
"src/components/sanatizefilename.js",
"src/components/scrollManager.js",
+ "src/components/bookPlayer/plugin.js",
+ "src/components/bookPlayer/tableOfContent.js",
+ "src/components/syncplay/playbackPermissionManager.js",
+ "src/components/syncplay/groupSelectionMenu.js",
+ "src/components/syncplay/timeSyncManager.js",
+ "src/components/syncplay/syncPlayManager.js",
"src/scripts/dfnshelper.js",
"src/scripts/dom.js",
"src/scripts/filesystem.js",
"src/scripts/imagehelper.js",
"src/scripts/inputManager.js",
- "src/scripts/keyboardnavigation.js",
+ "src/scripts/deleteHelper.js",
+ "src/components/actionSheet/actionSheet.js",
+ "src/components/playmenu.js",
+ "src/components/indicators/indicators.js",
+ "src/components/photoPlayer/plugin.js",
+ "src/scripts/keyboardNavigation.js",
"src/scripts/settings/appSettings.js",
"src/scripts/settings/userSettings.js",
"src/scripts/settings/webSettings.js"
],
"plugins": [
- "@babel/plugin-transform-modules-amd"
+ "@babel/plugin-transform-modules-amd",
+ "@babel/plugin-proposal-class-properties",
+ "@babel/plugin-proposal-private-methods"
]
}
]
@@ -133,7 +146,6 @@
"prepare": "gulp --production",
"build:development": "gulp --development",
"build:production": "gulp --production",
- "build:standalone": "gulp standalone --development",
"lint": "eslint \".\"",
"stylelint": "stylelint \"src/**/*.css\""
}
diff --git a/scripts/scdup.py b/scripts/scdup.py
index 468e31f14a..9b9ddf6466 100644
--- a/scripts/scdup.py
+++ b/scripts/scdup.py
@@ -15,6 +15,8 @@ print(langlst)
input('press enter to continue')
keysus = []
+missing = []
+
with open(langdir + '/' + 'en-us.json') as en:
langus = json.load(en)
for key in langus:
@@ -32,10 +34,19 @@ for lang in langlst:
for key in langjson:
if key in keysus:
langjnew[key] = langjson[key]
+ elif key not in missing:
+ missing.append(key)
f.seek(0)
f.write(json.dumps(langjnew, indent=inde, sort_keys=False, ensure_ascii=False))
f.write('\n')
f.truncate()
f.close()
+print(missing)
+print('LENGTH: ' + str(len(missing)))
+with open('missing.txt', 'w') as out:
+ for item in missing:
+ out.write(item + '\n')
+ out.close()
+
print('DONE')
diff --git a/scripts/scgen.py b/scripts/scgen.py
index 0d831426e6..12af27320a 100644
--- a/scripts/scgen.py
+++ b/scripts/scgen.py
@@ -34,7 +34,7 @@ for lang in langlst:
print(dep)
print('LENGTH: ' + str(len(dep)))
-with open('scout.txt', 'w') as out:
+with open('unused.txt', 'w') as out:
for item in dep:
out.write(item + '\n')
out.close()
diff --git a/src/assets/audio/silence.mp3 b/src/assets/audio/silence.mp3
new file mode 100644
index 0000000000..29dbef2185
Binary files /dev/null and b/src/assets/audio/silence.mp3 differ
diff --git a/src/assets/css/site.css b/src/assets/css/site.css
index 627145abc1..d489f77f01 100644
--- a/src/assets/css/site.css
+++ b/src/assets/css/site.css
@@ -120,3 +120,11 @@ div[data-role=page] {
.headroom--unpinned {
transform: translateY(-100%);
}
+
+.force-scroll {
+ overflow-y: scroll;
+}
+
+.hide-scroll {
+ overflow-y: hidden;
+}
diff --git a/src/assets/css/videoosd.css b/src/assets/css/videoosd.css
index f4f198325b..50cb41021b 100644
--- a/src/assets/css/videoosd.css
+++ b/src/assets/css/videoosd.css
@@ -30,7 +30,7 @@
opacity: 0;
}
-.osdHeader .headerButton:not(.headerBackButton):not(.headerCastButton) {
+.osdHeader .headerButton:not(.headerBackButton):not(.headerCastButton):not(.headerSyncButton) {
display: none;
}
diff --git a/src/bundle.js b/src/bundle.js
index d7ba6c6a51..d4a97247f8 100644
--- a/src/bundle.js
+++ b/src/bundle.js
@@ -102,6 +102,11 @@ _define('jellyfin-noto', function () {
return noto;
});
+var epubjs = require('epubjs');
+_define('epubjs', function () {
+ return epubjs;
+});
+
// page.js
var page = require('page');
_define('page', function() {
diff --git a/src/components/accessschedule/accessschedule.js b/src/components/accessSchedule/accessSchedule.js
similarity index 94%
rename from src/components/accessschedule/accessschedule.js
rename to src/components/accessSchedule/accessSchedule.js
index 870231cf03..768e310593 100644
--- a/src/components/accessschedule/accessschedule.js
+++ b/src/components/accessSchedule/accessSchedule.js
@@ -50,7 +50,7 @@ define(['dialogHelper', 'datetime', 'globalize', 'emby-select', 'paper-icon-butt
show: function (options) {
return new Promise(function (resolve, reject) {
var xhr = new XMLHttpRequest();
- xhr.open('GET', 'components/accessschedule/accessschedule.template.html', true);
+ xhr.open('GET', 'components/accessSchedule/accessSchedule.template.html', true);
xhr.onload = function (e) {
var template = this.response;
@@ -72,12 +72,12 @@ define(['dialogHelper', 'datetime', 'globalize', 'emby-select', 'paper-icon-butt
reject();
}
});
- dlg.querySelector('.btnCancel').addEventListener('click', function (e) {
+ dlg.querySelector('.btnCancel').addEventListener('click', function () {
dialogHelper.close(dlg);
});
- dlg.querySelector('form').addEventListener('submit', function (e) {
+ dlg.querySelector('form').addEventListener('submit', function (event) {
submitSchedule(dlg, options);
- e.preventDefault();
+ event.preventDefault();
return false;
});
};
diff --git a/src/components/accessschedule/accessschedule.template.html b/src/components/accessSchedule/accessSchedule.template.html
similarity index 100%
rename from src/components/accessschedule/accessschedule.template.html
rename to src/components/accessSchedule/accessSchedule.template.html
diff --git a/src/components/actionsheet/actionsheet.css b/src/components/actionSheet/actionSheet.css
similarity index 100%
rename from src/components/actionsheet/actionsheet.css
rename to src/components/actionSheet/actionSheet.css
diff --git a/src/components/actionSheet/actionSheet.js b/src/components/actionSheet/actionSheet.js
new file mode 100644
index 0000000000..c56f42a9d9
--- /dev/null
+++ b/src/components/actionSheet/actionSheet.js
@@ -0,0 +1,340 @@
+import dialogHelper from 'dialogHelper';
+import layoutManager from 'layoutManager';
+import globalize from 'globalize';
+import dom from 'dom';
+import 'emby-button';
+import 'css!./actionSheet';
+import 'material-icons';
+import 'scrollStyles';
+import 'listViewStyle';
+
+function getOffsets(elems) {
+
+ let results = [];
+
+ if (!document) {
+ return results;
+ }
+
+ for (let elem of elems) {
+ let box = elem.getBoundingClientRect();
+
+ results.push({
+ top: box.top,
+ left: box.left,
+ width: box.width,
+ height: box.height
+ });
+ }
+
+ return results;
+}
+
+function getPosition(options, dlg) {
+
+ const windowSize = dom.getWindowSize();
+ const windowHeight = windowSize.innerHeight;
+ const windowWidth = windowSize.innerWidth;
+
+ let pos = getOffsets([options.positionTo])[0];
+
+ if (options.positionY !== 'top') {
+ pos.top += (pos.height || 0) / 2;
+ }
+
+ pos.left += (pos.width || 0) / 2;
+
+ const height = dlg.offsetHeight || 300;
+ const width = dlg.offsetWidth || 160;
+
+ // Account for popup size
+ pos.top -= height / 2;
+ pos.left -= width / 2;
+
+ // Avoid showing too close to the bottom
+ const overflowX = pos.left + width - windowWidth;
+ const overflowY = pos.top + height - windowHeight;
+
+ if (overflowX > 0) {
+ pos.left -= (overflowX + 20);
+ }
+ if (overflowY > 0) {
+ pos.top -= (overflowY + 20);
+ }
+
+ pos.top += (options.offsetTop || 0);
+ pos.left += (options.offsetLeft || 0);
+
+ // Do some boundary checking
+ pos.top = Math.max(pos.top, 10);
+ pos.left = Math.max(pos.left, 10);
+
+ return pos;
+}
+
+function centerFocus(elem, horiz, on) {
+ require(['scrollHelper'], function (scrollHelper) {
+ const fn = on ? 'on' : 'off';
+ scrollHelper.centerFocus[fn](elem, horiz);
+ });
+}
+
+export function show(options) {
+
+ // items
+ // positionTo
+ // showCancel
+ // title
+ let dialogOptions = {
+ removeOnClose: true,
+ enableHistory: options.enableHistory,
+ scrollY: false
+ };
+
+ let isFullscreen;
+
+ if (layoutManager.tv) {
+ dialogOptions.size = 'fullscreen';
+ isFullscreen = true;
+ dialogOptions.autoFocus = true;
+ } else {
+
+ dialogOptions.modal = false;
+ dialogOptions.entryAnimation = options.entryAnimation;
+ dialogOptions.exitAnimation = options.exitAnimation;
+ dialogOptions.entryAnimationDuration = options.entryAnimationDuration || 140;
+ dialogOptions.exitAnimationDuration = options.exitAnimationDuration || 100;
+ dialogOptions.autoFocus = false;
+ }
+
+ let dlg = dialogHelper.createDialog(dialogOptions);
+
+ if (isFullscreen) {
+ dlg.classList.add('actionsheet-fullscreen');
+ } else {
+ dlg.classList.add('actionsheet-not-fullscreen');
+ }
+
+ dlg.classList.add('actionSheet');
+
+ if (options.dialogClass) {
+ dlg.classList.add(options.dialogClass);
+ }
+
+ let html = '';
+
+ const scrollClassName = layoutManager.tv ? 'scrollY smoothScrollY hiddenScrollY' : 'scrollY';
+ let style = '';
+
+ // Admittedly a hack but right now the scrollbar is being factored into the width which is causing truncation
+ if (options.items.length > 20) {
+ const minWidth = dom.getWindowSize().innerWidth >= 300 ? 240 : 200;
+ style += 'min-width:' + minWidth + 'px;';
+ }
+
+ let renderIcon = false;
+ let icons = [];
+ let itemIcon;
+ for (let item of options.items) {
+
+ itemIcon = item.icon || (item.selected ? 'check' : null);
+
+ if (itemIcon) {
+ renderIcon = true;
+ }
+ icons.push(itemIcon || '');
+ }
+
+ if (layoutManager.tv) {
+ html += `
+
+ `;
+ }
+
+ // If any items have an icon, give them all an icon just to make sure they're all lined up evenly
+ const center = options.title && (!renderIcon /*|| itemsWithIcons.length != options.items.length*/);
+
+ if (center || layoutManager.tv) {
+ html += '
';
+ } else {
+ html += '
';
+ }
+
+ if (options.title) {
+
+ html += '
' + options.title + ' ';
+ }
+ if (options.text) {
+ html += '
' + options.text + '
';
+ }
+
+ let scrollerClassName = 'actionSheetScroller';
+ if (layoutManager.tv) {
+ scrollerClassName += ' actionSheetScroller-tv focuscontainer-x focuscontainer-y';
+ }
+ html += '
';
+
+ dlg.innerHTML = html;
+
+ if (layoutManager.tv) {
+ centerFocus(dlg.querySelector('.actionSheetScroller'), false, true);
+ }
+
+ let btnCloseActionSheet = dlg.querySelector('.btnCloseActionSheet');
+ if (btnCloseActionSheet) {
+ btnCloseActionSheet.addEventListener('click', function () {
+ dialogHelper.close(dlg);
+ });
+ }
+
+ // Seeing an issue in some non-chrome browsers where this is requiring a double click
+ //var eventName = browser.firefox ? 'mousedown' : 'click';
+ let selectedId;
+
+ let timeout;
+ if (options.timeout) {
+ timeout = setTimeout(function () {
+ dialogHelper.close(dlg);
+ }, options.timeout);
+ }
+
+ return new Promise(function (resolve, reject) {
+
+ let isResolved;
+
+ dlg.addEventListener('click', function (e) {
+
+ const actionSheetMenuItem = dom.parentWithClass(e.target, 'actionSheetMenuItem');
+
+ if (actionSheetMenuItem) {
+ selectedId = actionSheetMenuItem.getAttribute('data-id');
+
+ if (options.resolveOnClick) {
+
+ if (options.resolveOnClick.indexOf) {
+
+ if (options.resolveOnClick.indexOf(selectedId) !== -1) {
+
+ resolve(selectedId);
+ isResolved = true;
+ }
+
+ } else {
+ resolve(selectedId);
+ isResolved = true;
+ }
+ }
+
+ dialogHelper.close(dlg);
+ }
+
+ });
+
+ dlg.addEventListener('close', function () {
+
+ if (layoutManager.tv) {
+ centerFocus(dlg.querySelector('.actionSheetScroller'), false, false);
+ }
+
+ if (timeout) {
+ clearTimeout(timeout);
+ timeout = null;
+ }
+
+ if (!isResolved) {
+ if (selectedId != null) {
+ if (options.callback) {
+ options.callback(selectedId);
+ }
+
+ resolve(selectedId);
+ } else {
+ reject();
+ }
+ }
+ });
+
+ dialogHelper.open(dlg);
+
+ const pos = options.positionTo && dialogOptions.size !== 'fullscreen' ? getPosition(options, dlg) : null;
+
+ if (pos) {
+ dlg.style.position = 'fixed';
+ dlg.style.margin = 0;
+ dlg.style.left = pos.left + 'px';
+ dlg.style.top = pos.top + 'px';
+ }
+ });
+}
+
+export default {
+ show: show
+};
diff --git a/src/components/actionsheet/actionsheet.js b/src/components/actionsheet/actionsheet.js
deleted file mode 100644
index e08fbf4a25..0000000000
--- a/src/components/actionsheet/actionsheet.js
+++ /dev/null
@@ -1,360 +0,0 @@
-define(['dialogHelper', 'layoutManager', 'globalize', 'browser', 'dom', 'emby-button', 'css!./actionsheet', 'material-icons', 'scrollStyles', 'listViewStyle'], function (dialogHelper, layoutManager, globalize, browser, dom) {
- 'use strict';
-
- function getOffsets(elems) {
-
- var doc = document;
- var results = [];
-
- if (!doc) {
- return results;
- }
-
- var box;
- var elem;
-
- for (var i = 0, length = elems.length; i < length; i++) {
-
- elem = elems[i];
- // Support: BlackBerry 5, iOS 3 (original iPhone)
- // If we don't have gBCR, just use 0,0 rather than error
- if (elem.getBoundingClientRect) {
- box = elem.getBoundingClientRect();
- } else {
- box = { top: 0, left: 0 };
- }
-
- results[i] = {
- top: box.top,
- left: box.left,
- width: box.width,
- height: box.height
- };
- }
-
- return results;
- }
-
- function getPosition(options, dlg) {
-
- var windowSize = dom.getWindowSize();
- var windowHeight = windowSize.innerHeight;
- var windowWidth = windowSize.innerWidth;
-
- var pos = getOffsets([options.positionTo])[0];
-
- if (options.positionY !== 'top') {
- pos.top += (pos.height || 0) / 2;
- }
-
- pos.left += (pos.width || 0) / 2;
-
- var height = dlg.offsetHeight || 300;
- var width = dlg.offsetWidth || 160;
-
- // Account for popup size
- pos.top -= height / 2;
- pos.left -= width / 2;
-
- // Avoid showing too close to the bottom
- var overflowX = pos.left + width - windowWidth;
- var overflowY = pos.top + height - windowHeight;
-
- if (overflowX > 0) {
- pos.left -= (overflowX + 20);
- }
- if (overflowY > 0) {
- pos.top -= (overflowY + 20);
- }
-
- pos.top += (options.offsetTop || 0);
- pos.left += (options.offsetLeft || 0);
-
- // Do some boundary checking
- pos.top = Math.max(pos.top, 10);
- pos.left = Math.max(pos.left, 10);
-
- return pos;
- }
-
- function centerFocus(elem, horiz, on) {
- require(['scrollHelper'], function (scrollHelper) {
- var fn = on ? 'on' : 'off';
- scrollHelper.centerFocus[fn](elem, horiz);
- });
- }
-
- function show(options) {
-
- // items
- // positionTo
- // showCancel
- // title
- var dialogOptions = {
- removeOnClose: true,
- enableHistory: options.enableHistory,
- scrollY: false
- };
-
- var backButton = false;
- var isFullscreen;
-
- if (layoutManager.tv) {
- dialogOptions.size = 'fullscreen';
- isFullscreen = true;
- backButton = true;
- dialogOptions.autoFocus = true;
- } else {
-
- dialogOptions.modal = false;
- dialogOptions.entryAnimation = options.entryAnimation;
- dialogOptions.exitAnimation = options.exitAnimation;
- dialogOptions.entryAnimationDuration = options.entryAnimationDuration || 140;
- dialogOptions.exitAnimationDuration = options.exitAnimationDuration || 100;
- dialogOptions.autoFocus = false;
- }
-
- var dlg = dialogHelper.createDialog(dialogOptions);
-
- if (isFullscreen) {
- dlg.classList.add('actionsheet-fullscreen');
- } else {
- dlg.classList.add('actionsheet-not-fullscreen');
- }
-
- dlg.classList.add('actionSheet');
-
- if (options.dialogClass) {
- dlg.classList.add(options.dialogClass);
- }
-
- var html = '';
-
- var scrollClassName = layoutManager.tv ? 'scrollY smoothScrollY hiddenScrollY' : 'scrollY';
- var style = '';
-
- // Admittedly a hack but right now the scrollbar is being factored into the width which is causing truncation
- if (options.items.length > 20) {
- var minWidth = dom.getWindowSize().innerWidth >= 300 ? 240 : 200;
- style += 'min-width:' + minWidth + 'px;';
- }
-
- var i;
- var length;
- var option;
- var renderIcon = false;
- var icons = [];
- var itemIcon;
- for (i = 0, length = options.items.length; i < length; i++) {
-
- option = options.items[i];
-
- itemIcon = option.icon || (option.selected ? 'check' : null);
-
- if (itemIcon) {
- renderIcon = true;
- }
- icons.push(itemIcon || '');
- }
-
- if (layoutManager.tv) {
- html += '
';
- }
-
- // If any items have an icon, give them all an icon just to make sure they're all lined up evenly
- var center = options.title && (!renderIcon /*|| itemsWithIcons.length != options.items.length*/);
-
- if (center || layoutManager.tv) {
- html += '
';
- } else {
- html += '
';
- }
-
- if (options.title) {
-
- html += '
';
- html += options.title;
- html += ' ';
- }
- if (options.text) {
- html += '
';
- html += options.text;
- html += '
';
- }
-
- var scrollerClassName = 'actionSheetScroller';
- if (layoutManager.tv) {
- scrollerClassName += ' actionSheetScroller-tv focuscontainer-x focuscontainer-y';
- }
- html += '
';
-
- dlg.innerHTML = html;
-
- if (layoutManager.tv) {
- centerFocus(dlg.querySelector('.actionSheetScroller'), false, true);
- }
-
- var btnCloseActionSheet = dlg.querySelector('.btnCloseActionSheet');
- if (btnCloseActionSheet) {
- dlg.querySelector('.btnCloseActionSheet').addEventListener('click', function () {
- dialogHelper.close(dlg);
- });
- }
-
- // Seeing an issue in some non-chrome browsers where this is requiring a double click
- //var eventName = browser.firefox ? 'mousedown' : 'click';
- var selectedId;
-
- var timeout;
- if (options.timeout) {
- timeout = setTimeout(function () {
- dialogHelper.close(dlg);
- }, options.timeout);
- }
-
- return new Promise(function (resolve, reject) {
-
- var isResolved;
-
- dlg.addEventListener('click', function (e) {
-
- var actionSheetMenuItem = dom.parentWithClass(e.target, 'actionSheetMenuItem');
-
- if (actionSheetMenuItem) {
- selectedId = actionSheetMenuItem.getAttribute('data-id');
-
- if (options.resolveOnClick) {
-
- if (options.resolveOnClick.indexOf) {
-
- if (options.resolveOnClick.indexOf(selectedId) !== -1) {
-
- resolve(selectedId);
- isResolved = true;
- }
-
- } else {
- resolve(selectedId);
- isResolved = true;
- }
- }
-
- dialogHelper.close(dlg);
- }
-
- });
-
- dlg.addEventListener('close', function () {
-
- if (layoutManager.tv) {
- centerFocus(dlg.querySelector('.actionSheetScroller'), false, false);
- }
-
- if (timeout) {
- clearTimeout(timeout);
- timeout = null;
- }
-
- if (!isResolved) {
- if (selectedId != null) {
- if (options.callback) {
- options.callback(selectedId);
- }
-
- resolve(selectedId);
- } else {
- reject();
- }
- }
- });
-
- dialogHelper.open(dlg);
-
- var pos = options.positionTo && dialogOptions.size !== 'fullscreen' ? getPosition(options, dlg) : null;
-
- if (pos) {
- dlg.style.position = 'fixed';
- dlg.style.margin = 0;
- dlg.style.left = pos.left + 'px';
- dlg.style.top = pos.top + 'px';
- }
- });
- }
-
- return {
- show: show
- };
-});
diff --git a/src/components/activitylog.js b/src/components/activitylog.js
index a7b3f48bc2..bbb0995063 100644
--- a/src/components/activitylog.js
+++ b/src/components/activitylog.js
@@ -34,10 +34,14 @@ define(['events', 'globalize', 'dom', 'date-fns', 'dfnshelper', 'userSettings',
html += '
';
if (entry.Overview) {
- html += '
';
+ html += `
+
+ `;
}
- return html += '
';
+ html += '
';
+
+ return html;
}
function renderList(elem, apiClient, result, startIndex, limit) {
diff --git a/src/components/alphapicker/alphapicker.js b/src/components/alphaPicker/alphaPicker.js
similarity index 100%
rename from src/components/alphapicker/alphapicker.js
rename to src/components/alphaPicker/alphaPicker.js
diff --git a/src/components/alphapicker/style.css b/src/components/alphaPicker/style.css
similarity index 100%
rename from src/components/alphapicker/style.css
rename to src/components/alphaPicker/style.css
diff --git a/src/components/appfooter/appfooter.css b/src/components/appFooter/appFooter.css
similarity index 100%
rename from src/components/appfooter/appfooter.css
rename to src/components/appFooter/appFooter.css
diff --git a/src/components/appfooter/appfooter.js b/src/components/appFooter/appFooter.js
similarity index 93%
rename from src/components/appfooter/appfooter.js
rename to src/components/appFooter/appFooter.js
index 07d7701ff2..033a0b008d 100644
--- a/src/components/appfooter/appfooter.js
+++ b/src/components/appFooter/appFooter.js
@@ -1,4 +1,4 @@
-define(['browser', 'css!./appfooter'], function (browser) {
+define(['browser', 'css!./appFooter'], function (browser) {
'use strict';
function render(options) {
diff --git a/src/components/appRouter.js b/src/components/appRouter.js
index 2e11ef88d9..0861cf7e00 100644
--- a/src/components/appRouter.js
+++ b/src/components/appRouter.js
@@ -1,4 +1,4 @@
-define(['loading', 'globalize', 'events', 'viewManager', 'layoutManager', 'skinManager', 'pluginManager', 'backdrop', 'browser', 'page', 'appSettings', 'apphost', 'connectionManager'], function (loading, globalize, events, viewManager, layoutManager, skinManager, pluginManager, backdrop, browser, page, appSettings, appHost, connectionManager) {
+define(['loading', 'globalize', 'events', 'viewManager', 'skinManager', 'backdrop', 'browser', 'page', 'appSettings', 'apphost', 'connectionManager'], function (loading, globalize, events, viewManager, skinManager, backdrop, browser, page, appSettings, appHost, connectionManager) {
'use strict';
var appRouter = {
@@ -26,11 +26,11 @@ define(['loading', 'globalize', 'events', 'viewManager', 'layoutManager', 'skinM
connectionManager.connect({
enableAutoLogin: appSettings.enableAutoLogin()
}).then(function (result) {
- handleConnectionResult(result, loading);
+ handleConnectionResult(result);
});
}
- function handleConnectionResult(result, loading) {
+ function handleConnectionResult(result) {
switch (result.State) {
case 'SignedIn':
loading.hide();
@@ -246,13 +246,11 @@ define(['loading', 'globalize', 'events', 'viewManager', 'layoutManager', 'skinM
}
if (setQuality) {
-
- var quality = 100;
-
+ var quality;
var type = options.type || 'Primary';
if (browser.tv || browser.slow) {
-
+ // TODO: wtf
if (browser.chrome) {
// webp support
quality = type === 'Primary' ? 40 : 50;
@@ -384,7 +382,7 @@ define(['loading', 'globalize', 'events', 'viewManager', 'layoutManager', 'skinM
if (firstResult.State !== 'SignedIn' && !route.anonymous) {
- handleConnectionResult(firstResult, loading);
+ handleConnectionResult(firstResult);
return;
}
}
@@ -463,7 +461,6 @@ define(['loading', 'globalize', 'events', 'viewManager', 'layoutManager', 'skinM
return Promise.resolve();
}
- var isHandlingBackToDefault;
var isDummyBackToHome;
function loadContent(ctx, route, html, request) {
@@ -589,8 +586,7 @@ define(['loading', 'globalize', 'events', 'viewManager', 'layoutManager', 'skinM
path = '/' + path;
}
- var baseRoute = baseUrl();
- path = path.replace(baseRoute, '');
+ path = path.replace(baseUrl(), '');
if (currentRouteInfo && currentRouteInfo.path === path) {
// can't use this with home right now due to the back menu
@@ -621,10 +617,11 @@ define(['loading', 'globalize', 'events', 'viewManager', 'layoutManager', 'skinM
}
function showItem(item, serverId, options) {
+ // TODO: Refactor this so it only gets items, not strings.
if (typeof (item) === 'string') {
var apiClient = serverId ? connectionManager.getApiClient(serverId) : connectionManager.currentApiClient();
- apiClient.getItem(apiClient.getCurrentUserId(), item).then(function (item) {
- appRouter.showItem(item, options);
+ apiClient.getItem(apiClient.getCurrentUserId(), item).then(function (itemObject) {
+ appRouter.showItem(itemObject, options);
});
} else {
if (arguments.length === 2) {
diff --git a/src/components/apphost.js b/src/components/apphost.js
index 75e8ba17f1..f200b9a642 100644
--- a/src/components/apphost.js
+++ b/src/components/apphost.js
@@ -5,7 +5,7 @@ define(['appSettings', 'browser', 'events', 'htmlMediaHelper', 'webSettings', 'g
var disableHlsVideoAudioCodecs = [];
if (item && htmlMediaHelper.enableHlsJsPlayer(item.RunTimeTicks, item.MediaType)) {
- if (browser.edge || browser.msie) {
+ if (browser.edge) {
disableHlsVideoAudioCodecs.push('mp3');
}
@@ -93,18 +93,36 @@ define(['appSettings', 'browser', 'events', 'htmlMediaHelper', 'webSettings', 'g
function getDeviceName() {
var deviceName;
- deviceName = browser.tizen ? 'Samsung Smart TV' : browser.web0s ? 'LG Smart TV' : browser.operaTv ? 'Opera TV' : browser.xboxOne ? 'Xbox One' : browser.ps4 ? 'Sony PS4' : browser.chrome ? 'Chrome' : browser.edge ? 'Edge' : browser.firefox ? 'Firefox' : browser.msie ? 'Internet Explorer' : browser.opera ? 'Opera' : browser.safari ? 'Safari' : 'Web Browser';
+ if (browser.tizen) {
+ deviceName = 'Samsung Smart TV';
+ } else if (browser.web0s) {
+ deviceName = 'LG Smart TV';
+ } else if (browser.operaTv) {
+ deviceName = 'Opera TV';
+ } else if (browser.xboxOne) {
+ deviceName = 'Xbox One';
+ } else if (browser.ps4) {
+ deviceName = 'Sony PS4';
+ } else if (browser.chrome) {
+ deviceName = 'Chrome';
+ } else if (browser.edge) {
+ deviceName = 'Edge';
+ } else if (browser.firefox) {
+ deviceName = 'Firefox';
+ } else if (browser.opera) {
+ deviceName = 'Opera';
+ } else if (browser.safari) {
+ deviceName = 'Safari';
+ } else {
+ deviceName = 'Web Browser';
+ }
if (browser.ipad) {
deviceName += ' iPad';
- } else {
- if (browser.iphone) {
- deviceName += ' iPhone';
- } else {
- if (browser.android) {
- deviceName += ' Android';
- }
- }
+ } else if (browser.iphone) {
+ deviceName += ' iPhone';
+ } else if (browser.android) {
+ deviceName += ' Android';
}
return deviceName;
@@ -267,7 +285,7 @@ define(['appSettings', 'browser', 'events', 'htmlMediaHelper', 'webSettings', 'g
if (enabled) features.push('multiserver');
});
- if (!browser.orsay && !browser.msie && (browser.firefox || browser.ps4 || browser.edge || supportsCue())) {
+ if (!browser.orsay && (browser.firefox || browser.ps4 || browser.edge || supportsCue())) {
features.push('subtitleappearancesettings');
}
@@ -359,7 +377,6 @@ define(['appSettings', 'browser', 'events', 'htmlMediaHelper', 'webSettings', 'g
return -1 !== supportedFeatures.indexOf(command.toLowerCase());
},
preferVisualCards: browser.android || browser.chrome,
- moreIcon: browser.android ? 'more_vert' : 'more_horiz',
getSyncProfile: getSyncProfile,
getDefaultLayout: function () {
if (window.NativeShell) {
diff --git a/src/components/backdropscreensaver/plugin.js b/src/components/backdropScreensaver/plugin.js
similarity index 100%
rename from src/components/backdropscreensaver/plugin.js
rename to src/components/backdropScreensaver/plugin.js
diff --git a/src/components/bookPlayer/plugin.js b/src/components/bookPlayer/plugin.js
new file mode 100644
index 0000000000..b655b038a8
--- /dev/null
+++ b/src/components/bookPlayer/plugin.js
@@ -0,0 +1,278 @@
+import connectionManager from 'connectionManager';
+import loading from 'loading';
+import keyboardnavigation from 'keyboardnavigation';
+import dialogHelper from 'dialogHelper';
+import events from 'events';
+import 'css!./style';
+import 'material-icons';
+import 'paper-icon-button-light';
+
+import TableOfContent from './tableOfContent';
+
+export class BookPlayer {
+ constructor() {
+ this.name = 'Book Player';
+ this.type = 'mediaplayer';
+ this.id = 'bookplayer';
+ this.priority = 1;
+
+ this.onDialogClosed = this.onDialogClosed.bind(this);
+ this.openTableOfContents = this.openTableOfContents.bind(this);
+ this.onWindowKeyUp = this.onWindowKeyUp.bind(this);
+ }
+
+ play(options) {
+ this._progress = 0;
+ this._loaded = false;
+
+ loading.show();
+ let elem = this.createMediaElement();
+ return this.setCurrentSrc(elem, options);
+ }
+
+ stop() {
+ this.unbindEvents();
+
+ let elem = this._mediaElement;
+ let tocElement = this._tocElement;
+ let rendition = this._rendition;
+
+ if (elem) {
+ dialogHelper.close(elem);
+ this._mediaElement = null;
+ }
+
+ if (tocElement) {
+ tocElement.destroy();
+ this._tocElement = null;
+ }
+
+ if (rendition) {
+ rendition.destroy();
+ }
+
+ // Hide loader in case player was not fully loaded yet
+ loading.hide();
+ this._cancellationToken.shouldCancel = true;
+ }
+
+ currentItem() {
+ return this._currentItem;
+ }
+
+ currentTime() {
+ return this._progress * 1000;
+ }
+
+ duration() {
+ return 1000;
+ }
+
+ getBufferedRanges() {
+ return [{
+ start: 0,
+ end: 10000000
+ }];
+ }
+
+ volume() {
+ return 100;
+ }
+
+ isMuted() {
+ return false;
+ }
+
+ paused() {
+ return false;
+ }
+
+ seekable() {
+ return true;
+ }
+
+ onWindowKeyUp(e) {
+ let key = keyboardnavigation.getKeyName(e);
+ let rendition = this._rendition;
+ let book = rendition.book;
+
+ switch (key) {
+ case 'l':
+ case 'ArrowRight':
+ case 'Right':
+ if (this._loaded) {
+ book.package.metadata.direction === 'rtl' ? rendition.prev() : rendition.next();
+ }
+ break;
+ case 'j':
+ case 'ArrowLeft':
+ case 'Left':
+ if (this._loaded) {
+ book.package.metadata.direction === 'rtl' ? rendition.next() : rendition.prev();
+ }
+ break;
+ case 'Escape':
+ if (this._tocElement) {
+ // Close table of contents on ESC if it is open
+ this._tocElement.destroy();
+ } else {
+ // Otherwise stop the entire book player
+ this.stop();
+ }
+ break;
+ }
+ }
+
+ onDialogClosed() {
+ this.stop();
+ }
+
+ bindMediaElementEvents() {
+ let elem = this._mediaElement;
+
+ elem.addEventListener('close', this.onDialogClosed, {once: true});
+ elem.querySelector('.btnBookplayerExit').addEventListener('click', this.onDialogClosed, {once: true});
+ elem.querySelector('.btnBookplayerToc').addEventListener('click', this.openTableOfContents);
+ }
+
+ bindEvents() {
+ this.bindMediaElementEvents();
+
+ document.addEventListener('keyup', this.onWindowKeyUp);
+ // FIXME: I don't really get why document keyup event is not triggered when epub is in focus
+ this._rendition.on('keyup', this.onWindowKeyUp);
+ }
+
+ unbindMediaElementEvents() {
+ let elem = this._mediaElement;
+
+ elem.removeEventListener('close', this.onDialogClosed);
+ elem.querySelector('.btnBookplayerExit').removeEventListener('click', this.onDialogClosed);
+ elem.querySelector('.btnBookplayerToc').removeEventListener('click', this.openTableOfContents);
+ }
+
+ unbindEvents() {
+ if (this._mediaElement) {
+ this.unbindMediaElementEvents();
+ }
+ document.removeEventListener('keyup', this.onWindowKeyUp);
+ if (this._rendition) {
+ this._rendition.off('keyup', this.onWindowKeyUp);
+ }
+ }
+
+ openTableOfContents() {
+ if (this._loaded) {
+ this._tocElement = new TableOfContent(this);
+ }
+ }
+
+ createMediaElement() {
+ let elem = this._mediaElement;
+
+ if (elem) {
+ return elem;
+ }
+
+ elem = document.getElementById('bookPlayer');
+
+ if (!elem) {
+ elem = dialogHelper.createDialog({
+ exitAnimationDuration: 400,
+ size: 'fullscreen',
+ autoFocus: false,
+ scrollY: false,
+ exitAnimation: 'fadeout',
+ removeOnClose: true
+ });
+ elem.id = 'bookPlayer';
+
+ let html = '';
+ html += '
';
+ html += ' ';
+ html += '
';
+ html += '
';
+ html += ' ';
+ html += '
';
+
+ elem.innerHTML = html;
+
+ dialogHelper.open(elem);
+ }
+
+ this._mediaElement = elem;
+
+ return elem;
+ }
+
+ setCurrentSrc(elem, options) {
+ let item = options.items[0];
+ this._currentItem = item;
+ this.streamInfo = {
+ started: true,
+ ended: false,
+ mediaSource: {
+ Id: item.Id
+ }
+ };
+
+ let serverId = item.ServerId;
+ let apiClient = connectionManager.getApiClient(serverId);
+
+ return new Promise((resolve, reject) => {
+ require(['epubjs'], (epubjs) => {
+ let downloadHref = apiClient.getItemDownloadUrl(item.Id);
+ let book = epubjs.default(downloadHref, {openAs: 'epub'});
+ let rendition = book.renderTo(elem, {width: '100%', height: '97%'});
+
+ this._currentSrc = downloadHref;
+ this._rendition = rendition;
+ let cancellationToken = {
+ shouldCancel: false
+ };
+ this._cancellationToken = cancellationToken;
+
+ return rendition.display().then(() => {
+ let epubElem = document.querySelector('.epub-container');
+ epubElem.style.display = 'none';
+
+ this.bindEvents();
+
+ return this._rendition.book.locations.generate(1024).then(() => {
+ if (cancellationToken.shouldCancel) {
+ return reject();
+ }
+
+ this._loaded = true;
+ epubElem.style.display = 'block';
+ rendition.on('relocated', (locations) => {
+ this._progress = book.locations.percentageFromCfi(locations.start.cfi);
+
+ events.trigger(this, 'timeupdate');
+ });
+
+ loading.hide();
+
+ return resolve();
+ });
+ }, () => {
+ console.error('Failed to display epub');
+ return reject();
+ });
+ });
+ });
+ }
+
+ canPlayMediaType(mediaType) {
+ return (mediaType || '').toLowerCase() === 'book';
+ }
+
+ canPlayItem(item) {
+ if (item.Path && (item.Path.endsWith('epub'))) {
+ return true;
+ }
+ return false;
+ }
+}
+
+export default BookPlayer;
diff --git a/src/components/bookPlayer/style.css b/src/components/bookPlayer/style.css
new file mode 100644
index 0000000000..e37b995f31
--- /dev/null
+++ b/src/components/bookPlayer/style.css
@@ -0,0 +1,39 @@
+#bookPlayer {
+ position: relative;
+ height: 100%;
+ width: 100%;
+ overflow: auto;
+ z-index: 100;
+ background: #fff;
+}
+
+.topRightActionButtons {
+ right: 0.5vh;
+ top: 0.5vh;
+ z-index: 1002;
+ position: absolute;
+}
+
+.topLeftActionButtons {
+ left: 0.5vh;
+ top: 0.5vh;
+ z-index: 1002;
+ position: absolute;
+}
+
+.bookplayerButtonIcon {
+ color: black;
+ opacity: 0.7;
+}
+
+#dialogToc {
+ background-color: white;
+}
+
+.toc li {
+ margin-bottom: 5px;
+}
+
+.bookplayerErrorMsg {
+ text-align: center;
+}
diff --git a/src/components/bookPlayer/tableOfContent.js b/src/components/bookPlayer/tableOfContent.js
new file mode 100644
index 0000000000..6a35966b1b
--- /dev/null
+++ b/src/components/bookPlayer/tableOfContent.js
@@ -0,0 +1,90 @@
+import dialogHelper from 'dialogHelper';
+
+export default class TableOfContent {
+ constructor(bookPlayer) {
+ this._bookPlayer = bookPlayer;
+ this._rendition = bookPlayer._rendition;
+
+ this.onDialogClosed = this.onDialogClosed.bind(this);
+
+ this.createMediaElement();
+ }
+
+ destroy() {
+ let elem = this._elem;
+ if (elem) {
+ this.unbindEvents();
+ dialogHelper.close(elem);
+ }
+
+ this._bookPlayer._tocElement = null;
+ }
+
+ bindEvents() {
+ let elem = this._elem;
+
+ elem.addEventListener('close', this.onDialogClosed, {once: true});
+ elem.querySelector('.btnBookplayerTocClose').addEventListener('click', this.onDialogClosed, {once: true});
+ }
+
+ unbindEvents() {
+ let elem = this._elem;
+
+ elem.removeEventListener('close', this.onDialogClosed);
+ elem.querySelector('.btnBookplayerTocClose').removeEventListener('click', this.onDialogClosed);
+ }
+
+ onDialogClosed() {
+ this.destroy();
+ }
+
+ replaceLinks(contents, f) {
+ let links = contents.querySelectorAll('a[href]');
+
+ links.forEach((link) => {
+ let href = link.getAttribute('href');
+
+ link.onclick = () => {
+ f(href);
+ return false;
+ };
+ });
+ }
+
+ createMediaElement() {
+ let rendition = this._rendition;
+
+ let elem = dialogHelper.createDialog({
+ size: 'small',
+ autoFocus: false,
+ removeOnClose: true
+ });
+ elem.id = 'dialogToc';
+
+ let tocHtml = '
';
+ tocHtml += ' ';
+ tocHtml += '
';
+ tocHtml += '
';
+ rendition.book.navigation.forEach((chapter) => {
+ tocHtml += '';
+ // Remove '../' from href
+ let link = chapter.href.startsWith('../') ? chapter.href.substr(3) : chapter.href;
+ tocHtml += `${chapter.label} `;
+ tocHtml += ' ';
+ });
+ tocHtml += ' ';
+ elem.innerHTML = tocHtml;
+
+ this.replaceLinks(elem, (href) => {
+ let relative = rendition.book.path.relative(href);
+ rendition.display(relative);
+ this.destroy();
+ });
+
+ this._elem = elem;
+
+ this.bindEvents();
+
+ dialogHelper.open(elem);
+ }
+}
diff --git a/src/components/cardbuilder/card.css b/src/components/cardbuilder/card.css
index 3cd038cd09..c24fcf6ba6 100644
--- a/src/components/cardbuilder/card.css
+++ b/src/components/cardbuilder/card.css
@@ -306,6 +306,10 @@ button::-moz-focus-inner {
text-align: left;
}
+.dialog .cardText {
+ text-overflow: initial;
+}
+
.cardText-secondary {
font-size: 86%;
}
diff --git a/src/components/cardbuilder/cardBuilder.js b/src/components/cardbuilder/cardBuilder.js
index 43ca28f01d..d4d4d7f73b 100644
--- a/src/components/cardbuilder/cardBuilder.js
+++ b/src/components/cardbuilder/cardBuilder.js
@@ -869,7 +869,7 @@ import 'programStyles';
if (isOuterFooter && options.cardLayout && layoutManager.mobile) {
if (options.cardFooterAside !== 'none') {
- html += '
';
+ html += '
';
}
}
@@ -1426,7 +1426,7 @@ import 'programStyles';
}
if (options.overlayMoreButton) {
- overlayButtons += '
';
+ overlayButtons += '
';
}
}
@@ -1580,7 +1580,7 @@ import 'programStyles';
html += '
';
}
- html += '
';
+ html += '
';
html += '
';
html += '';
diff --git a/src/components/channelmapper/channelmapper.js b/src/components/channelMapper/channelMapper.js
similarity index 98%
rename from src/components/channelmapper/channelmapper.js
rename to src/components/channelMapper/channelMapper.js
index 83ae4d09c6..f2ad88e713 100644
--- a/src/components/channelmapper/channelmapper.js
+++ b/src/components/channelMapper/channelMapper.js
@@ -79,7 +79,7 @@ define(['dom', 'dialogHelper', 'loading', 'connectionManager', 'globalize', 'act
function getEditorHtml() {
var html = '';
- html += '