From 2ea2132740238f79136e7e398d78a9d25329c9fa Mon Sep 17 00:00:00 2001 From: MrTimscampi Date: Fri, 29 May 2020 23:32:45 +0200 Subject: [PATCH 1/7] Add barebones comic book reader --- package.json | 2 + src/bundle.js | 6 + src/components/comicsPlayer/plugin.js | 215 +++++++++++++++++++++ src/components/playback/playbackmanager.js | 6 +- src/components/pluginManager.js | 2 +- src/scripts/site.js | 4 +- webpack.common.js | 18 +- yarn.lock | 5 + 8 files changed, 252 insertions(+), 6 deletions(-) create mode 100644 src/components/comicsPlayer/plugin.js diff --git a/package.json b/package.json index 749c62d39..aefec20d7 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "jellyfin-noto": "https://github.com/jellyfin/jellyfin-noto", "jquery": "^3.5.1", "jstree": "^3.3.7", + "libarchive.js": "^1.3.0", "libass-wasm": "https://github.com/jellyfin/JavascriptSubtitlesOctopus#4.0.0-jf-smarttv", "material-design-icons-iconfont": "^5.0.1", "native-promise-only": "^0.8.0-a", @@ -92,6 +93,7 @@ "src/components/autoFocuser.js", "src/components/cardbuilder/cardBuilder.js", "src/scripts/fileDownloader.js", + "src/components/comicsPlayer/plugin.js", "src/components/images/imageLoader.js", "src/components/lazyLoader/lazyLoaderIntersectionObserver.js", "src/components/playback/mediasession.js", diff --git a/src/bundle.js b/src/bundle.js index d7ba6c6a5..fd9099aaf 100644 --- a/src/bundle.js +++ b/src/bundle.js @@ -176,3 +176,9 @@ _define('connectionManagerFactory', function () { _define('appStorage', function () { return apiclient.AppStorage; }); + +// libarchive.js +var libarchive = require('libarchive.js'); +_define('libarchive', function () { + return libarchive; +}); diff --git a/src/components/comicsPlayer/plugin.js b/src/components/comicsPlayer/plugin.js new file mode 100644 index 000000000..43469bfed --- /dev/null +++ b/src/components/comicsPlayer/plugin.js @@ -0,0 +1,215 @@ +import connectionManager from 'connectionManager'; +import loading from 'loading'; +import dialogHelper from 'dialogHelper'; +import keyboardnavigation from 'keyboardnavigation'; +import appRouter from 'appRouter'; +import 'css!../slideshow/style'; +import * as libarchive from 'libarchive'; + +export class ComicsPlayer { + constructor() { + this.name = 'Comics Player'; + this.type = 'mediaplayer'; + this.id = 'comicsplayer'; + this.priority = 1; + this.imageMap = new Map(); + + this.onDialogClosed = this.onDialogClosed.bind(this); + this.onWindowKeyUp = this.onWindowKeyUp.bind(this); + } + + play(options) { + this._progress = 0; + + let elem = this.createMediaElement(); + return this.setCurrentSrc(elem, options); + } + + stop() { + this.unbindEvents(); + + let elem = this._mediaElement; + + if (elem) { + dialogHelper.close(elem); + this._mediaElement = null; + } + + // Hide loader in case player was not fully loaded yet + loading.hide(); + } + + onDialogClosed() { + this.stop(); + } + + onWindowKeyUp(e) { + let key = keyboardnavigation.getKeyName(e); + switch (key) { + case 'Escape': + this.stop(); + break; + } + } + + bindEvents() { + document.addEventListener('keyup', this.onWindowKeyUp); + } + + unbindEvents() { + document.removeEventListener('keyup', this.onWindowKeyUp); + } + + createMediaElement() { + let elem = this._mediaElement; + + if (elem) { + return elem; + } + + elem = document.getElementById('comicsPlayer'); + if (!elem) { + elem = dialogHelper.createDialog({ + exitAnimationDuration: 400, + size: 'fullscreen', + autoFocus: false, + scrollY: false, + exitAnimation: 'fadeout', + removeOnClose: true + }); + elem.id = 'bookPlayer'; + + elem.classList.add('slideshowDialog'); + + elem.innerHTML = '
'; + + this.bindEvents(); + + dialogHelper.open(elem); + } + + this._mediaElement = elem; + + return elem; + } + + setCurrentSrc(elem, options) { + let item = options.items[0]; + this._currentItem = item; + + loading.show(); + + let serverId = item.ServerId; + let apiClient = connectionManager.getApiClient(serverId); + + libarchive.Archive.init({ + workerUrl: appRouter.baseUrl() + '/libraries/worker-bundle.js' + }); + + return new Promise((resolve, reject) => { + let downloadUrl = apiClient.getItemDownloadUrl(item.Id); + const archiveSource = new ArchiveSource(downloadUrl); + var instance = this; + import('swiper').then(({default: Swiper}) => { + archiveSource.load().then(() => { + loading.hide(); + this.swiperInstance = new Swiper(elem.querySelector('.slideshowSwiperContainer'), { + direction: 'horizontal', + // Loop is disabled due to the virtual slides option not supporting it. + loop: false, + zoom: { + minRatio: 1, + toggle: true, + containerClass: 'slider-zoom-container' + }, + autoplay: false, + keyboard: { + enabled: true + }, + preloadImages: true, + slidesPerView: 1, + slidesPerColumn: 1, + initialSlide: 0, + // Virtual slides reduce memory consumption for large libraries while allowing preloading of images; + virtual: { + slides: archiveSource.urls, + cache: true, + renderSlide: instance.getImgFromUrl, + addSlidesBefore: 1, + addSlidesAfter: 1 + } + }); + }); + }); + }); + } + + getImgFromUrl(url) { + return `
+
+ +
+
`; + } + + canPlayMediaType(mediaType) { + return (mediaType || '').toLowerCase() === 'book'; + } + + canPlayItem(item) { + if (item.Path && (item.Path.endsWith('cbz') || item.Path.endsWith('cbr'))) { + return true; + } + return false; + } +} + +class ArchiveSource { + constructor(url) { + this.url = url; + this.files = []; + this.urls = []; + this.loadPromise = this.load(); + this.itemsLoaded = 0; + } + + async load() { + let res = await fetch(this.url); + if (!res.ok) { + return; + } + let blob = await res.blob(); + this.archive = await libarchive.Archive.open(blob); + this.raw = await this.archive.getFilesArray(); + this.numberOfFiles = this.raw.length; + await this.archive.extractFiles(); + + let files = await this.archive.getFilesArray(); + files.sort((a, b) => { + if (a.file.name < b.file.name) + return -1; + else + return 1; + }); + + for (let file of files) { + let url = URL.createObjectURL(file.file); + this.urls.push(url); + } + } + + getLength() { + return this.raw.length; + } + + async item(index) { + if (this.urls[index]) { + return this.urls[index]; + } + + await this.loadPromise; + return this.urls[index]; + } +} + +export default ComicsPlayer; diff --git a/src/components/playback/playbackmanager.js b/src/components/playback/playbackmanager.js index 59108cf72..053088ef2 100644 --- a/src/components/playback/playbackmanager.js +++ b/src/components/playback/playbackmanager.js @@ -2187,7 +2187,7 @@ define(['events', 'datetime', 'appSettings', 'itemHelper', 'pluginManager', 'pla // Only used internally self.getCurrentTicks = getCurrentTicks; - function playPhotos(items, options, user) { + function playOther(items, options, user) { var playStartIndex = options.startIndex || 0; var player = getPlayer(items[playStartIndex], options); @@ -2216,9 +2216,9 @@ define(['events', 'datetime', 'appSettings', 'itemHelper', 'pluginManager', 'pla return Promise.reject(); } - if (firstItem.MediaType === 'Photo') { + if (firstItem.MediaType === 'Photo' || firstItem.MediaType === 'Book') { - return playPhotos(items, options, user); + return playOther(items, options, user); } var apiClient = connectionManager.getApiClient(firstItem.ServerId); diff --git a/src/components/pluginManager.js b/src/components/pluginManager.js index 6cb56d767..fd35d344b 100644 --- a/src/components/pluginManager.js +++ b/src/components/pluginManager.js @@ -58,7 +58,7 @@ define(['events', 'globalize'], function (events, globalize) { return new Promise(function (resolve, reject) { require([pluginSpec], (pluginFactory) => { - var plugin = new pluginFactory(); + var plugin = pluginFactory.default ? new pluginFactory.default() : new pluginFactory(); // See if it's already installed var existing = instance.pluginsList.filter(function (p) { diff --git a/src/scripts/site.js b/src/scripts/site.js index aeb651d88..2e83928f9 100644 --- a/src/scripts/site.js +++ b/src/scripts/site.js @@ -490,6 +490,7 @@ var AppInfo = {}; 'components/playback/experimentalwarnings', 'components/htmlAudioPlayer/plugin', 'components/htmlVideoPlayer/plugin', + 'components/comicsPlayer/plugin', 'components/photoPlayer/plugin', 'components/youtubeplayer/plugin', 'components/backdropScreensaver/plugin', @@ -701,7 +702,8 @@ var AppInfo = {}; 'events', 'credentialprovider', 'connectionManagerFactory', - 'appStorage' + 'appStorage', + 'comicReader' ] }, urlArgs: urlArgs, diff --git a/webpack.common.js b/webpack.common.js index 03beb63a7..2cc8478d8 100644 --- a/webpack.common.js +++ b/webpack.common.js @@ -1,10 +1,12 @@ const path = require('path'); const CopyPlugin = require('copy-webpack-plugin'); +const WorkerPlugin = require('worker-plugin'); const Assets = [ 'alameda/alameda.js', 'native-promise-only/npo.js', + 'libarchive.js/dist/worker-bundle.js', 'libass-wasm/dist/js/subtitles-octopus-worker.js', 'libass-wasm/dist/js/subtitles-octopus-worker.data', 'libass-wasm/dist/js/subtitles-octopus-worker.wasm', @@ -13,6 +15,11 @@ const Assets = [ 'libass-wasm/dist/js/subtitles-octopus-worker-legacy.js.mem' ]; +const LibarchiveWasm = [ + 'libarchive.js/dist/wasm-gen/libarchive.js', + 'libarchive.js/dist/wasm-gen/libarchive.wasm' +]; + module.exports = { context: path.resolve(__dirname, 'src'), entry: './bundle.js', @@ -34,6 +41,15 @@ module.exports = { to: path.resolve(__dirname, './dist/libraries') }; }) - ) + ), + new CopyPlugin( + LibarchiveWasm.map(asset => { + return { + from: path.resolve(__dirname, `./node_modules/${asset}`), + to: path.resolve(__dirname, './dist/libraries/wasm-gen/') + }; + }) + ), + new WorkerPlugin() ] }; diff --git a/yarn.lock b/yarn.lock index 20fdef5de..5b8df85f4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6783,6 +6783,11 @@ levn@^0.3.0, levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" +libarchive.js@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/libarchive.js/-/libarchive.js-1.3.0.tgz#18c42c6b4ce727a02359c90769e4e454cf3743cd" + integrity sha512-EkQfRXt9DhWwj6BnEA2TNpOf4jTnzSTUPGgE+iFxcdNqjktY8GitbDeHnx8qZA0/IukNyyBUR3oQKRdYkO+HFg== + "libass-wasm@https://github.com/jellyfin/JavascriptSubtitlesOctopus#4.0.0-jf-smarttv": version "4.0.0" resolved "https://github.com/jellyfin/JavascriptSubtitlesOctopus#58e9a3f1a7f7883556ee002545f445a430120639" From 0e45c25e403515cf5fdf243eccfcde863a4815de Mon Sep 17 00:00:00 2001 From: dkanada Date: Fri, 28 Aug 2020 23:04:16 +0900 Subject: [PATCH 2/7] move comics player to plugin directory and update web config --- package.json | 2 +- src/config.template.json | 1 + src/{components => plugins}/comicsPlayer/plugin.js | 0 3 files changed, 2 insertions(+), 1 deletion(-) rename src/{components => plugins}/comicsPlayer/plugin.js (100%) diff --git a/package.json b/package.json index 775fc2f9c..ee1907d4f 100644 --- a/package.json +++ b/package.json @@ -107,7 +107,6 @@ "src/components/channelMapper/channelMapper.js", "src/components/collectionEditor/collectionEditor.js", "src/components/confirm/confirm.js", - "src/components/comicsPlayer/plugin.js", "src/components/dialog/dialog.js", "src/components/dialogHelper/dialogHelper.js", "src/components/directorybrowser/directorybrowser.js", @@ -180,6 +179,7 @@ "src/plugins/experimentalWarnings/plugin.js", "src/plugins/sessionPlayer/plugin.js", "src/plugins/htmlAudioPlayer/plugin.js", + "src/plugins/comicsPlayer/plugin.js", "src/plugins/chromecastPlayer/plugin.js", "src/components/slideshow/slideshow.js", "src/components/sortmenu/sortmenu.js", diff --git a/src/config.template.json b/src/config.template.json index 9d98e8b6f..0f308ccc1 100644 --- a/src/config.template.json +++ b/src/config.template.json @@ -28,6 +28,7 @@ "plugins/htmlAudioPlayer/plugin", "plugins/htmlVideoPlayer/plugin", "plugins/photoPlayer/plugin", + "plugins/comicsPlayer/plugin", "plugins/bookPlayer/plugin", "plugins/youtubePlayer/plugin", "plugins/backdropScreensaver/plugin", diff --git a/src/components/comicsPlayer/plugin.js b/src/plugins/comicsPlayer/plugin.js similarity index 100% rename from src/components/comicsPlayer/plugin.js rename to src/plugins/comicsPlayer/plugin.js From 90babaca9f98d68b806ea1082bfd564cc1a4742f Mon Sep 17 00:00:00 2001 From: dkanada Date: Fri, 28 Aug 2020 23:09:35 +0900 Subject: [PATCH 3/7] minor code tweaks for comics player --- src/plugins/comicsPlayer/plugin.js | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/src/plugins/comicsPlayer/plugin.js b/src/plugins/comicsPlayer/plugin.js index 43469bfed..723dd2010 100644 --- a/src/plugins/comicsPlayer/plugin.js +++ b/src/plugins/comicsPlayer/plugin.js @@ -28,14 +28,12 @@ export class ComicsPlayer { stop() { this.unbindEvents(); - let elem = this._mediaElement; - + let elem = this.mediaElement; if (elem) { dialogHelper.close(elem); - this._mediaElement = null; + this.mediaElement = null; } - // Hide loader in case player was not fully loaded yet loading.hide(); } @@ -61,8 +59,7 @@ export class ComicsPlayer { } createMediaElement() { - let elem = this._mediaElement; - + let elem = this.mediaElement; if (elem) { return elem; } @@ -77,19 +74,17 @@ export class ComicsPlayer { exitAnimation: 'fadeout', removeOnClose: true }); - elem.id = 'bookPlayer'; + elem.id = 'bookPlayer'; elem.classList.add('slideshowDialog'); elem.innerHTML = '
'; this.bindEvents(); - dialogHelper.open(elem); } - this._mediaElement = elem; - + this.mediaElement = elem; return elem; } @@ -109,13 +104,14 @@ export class ComicsPlayer { return new Promise((resolve, reject) => { let downloadUrl = apiClient.getItemDownloadUrl(item.Id); const archiveSource = new ArchiveSource(downloadUrl); + var instance = this; import('swiper').then(({default: Swiper}) => { archiveSource.load().then(() => { loading.hide(); this.swiperInstance = new Swiper(elem.querySelector('.slideshowSwiperContainer'), { direction: 'horizontal', - // Loop is disabled due to the virtual slides option not supporting it. + // loop is disabled due to the lack of support in virtual slides loop: false, zoom: { minRatio: 1, @@ -130,7 +126,7 @@ export class ComicsPlayer { slidesPerView: 1, slidesPerColumn: 1, initialSlide: 0, - // Virtual slides reduce memory consumption for large libraries while allowing preloading of images; + // reduces memory consumption for large libraries while allowing preloading of images virtual: { slides: archiveSource.urls, cache: true, @@ -160,6 +156,7 @@ export class ComicsPlayer { if (item.Path && (item.Path.endsWith('cbz') || item.Path.endsWith('cbr'))) { return true; } + return false; } } @@ -178,6 +175,7 @@ class ArchiveSource { if (!res.ok) { return; } + let blob = await res.blob(); this.archive = await libarchive.Archive.open(blob); this.raw = await this.archive.getFilesArray(); @@ -186,10 +184,11 @@ class ArchiveSource { let files = await this.archive.getFilesArray(); files.sort((a, b) => { - if (a.file.name < b.file.name) + if (a.file.name < b.file.name) { return -1; - else + } else { return 1; + } }); for (let file of files) { From f75ad14c324cccb1ade24859c8def5357a021d81 Mon Sep 17 00:00:00 2001 From: dkanada Date: Fri, 28 Aug 2020 23:11:53 +0900 Subject: [PATCH 4/7] update dialog id and rename some variables --- src/plugins/comicsPlayer/plugin.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/plugins/comicsPlayer/plugin.js b/src/plugins/comicsPlayer/plugin.js index 723dd2010..33e8e8d25 100644 --- a/src/plugins/comicsPlayer/plugin.js +++ b/src/plugins/comicsPlayer/plugin.js @@ -19,7 +19,7 @@ export class ComicsPlayer { } play(options) { - this._progress = 0; + this.progress = 0; let elem = this.createMediaElement(); return this.setCurrentSrc(elem, options); @@ -75,7 +75,7 @@ export class ComicsPlayer { removeOnClose: true }); - elem.id = 'bookPlayer'; + elem.id = 'comicsPlayer'; elem.classList.add('slideshowDialog'); elem.innerHTML = '
'; @@ -90,7 +90,7 @@ export class ComicsPlayer { setCurrentSrc(elem, options) { let item = options.items[0]; - this._currentItem = item; + this.currentItem = item; loading.show(); From dfc906e7d31ac06cd006ff4cb1a230437964e0f2 Mon Sep 17 00:00:00 2001 From: dkanada Date: Sun, 30 Aug 2020 14:57:36 +0900 Subject: [PATCH 5/7] fix build issues --- package.json | 3 ++- src/plugins/comicsPlayer/plugin.js | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index ee1907d4f..0154195c9 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,8 @@ "stylelint-order": "^4.1.0", "webpack": "^4.44.1", "webpack-merge": "^4.2.2", - "webpack-stream": "^5.2.1" + "webpack-stream": "^5.2.1", + "worker-plugin": "^5.0.0" }, "dependencies": { "alameda": "^1.4.0", diff --git a/src/plugins/comicsPlayer/plugin.js b/src/plugins/comicsPlayer/plugin.js index 33e8e8d25..7881a6d06 100644 --- a/src/plugins/comicsPlayer/plugin.js +++ b/src/plugins/comicsPlayer/plugin.js @@ -3,7 +3,6 @@ import loading from 'loading'; import dialogHelper from 'dialogHelper'; import keyboardnavigation from 'keyboardnavigation'; import appRouter from 'appRouter'; -import 'css!../slideshow/style'; import * as libarchive from 'libarchive'; export class ComicsPlayer { From 924928fa0f9918616b95088ed3ccc24c749ebf42 Mon Sep 17 00:00:00 2001 From: dkanada Date: Sun, 30 Aug 2020 15:01:10 +0900 Subject: [PATCH 6/7] update yarn.lock --- yarn.lock | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/yarn.lock b/yarn.lock index 4f8dd29c4..f1c6f1d58 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12041,6 +12041,13 @@ worker-farm@^1.7.0: dependencies: errno "~0.1.7" +worker-plugin@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/worker-plugin/-/worker-plugin-5.0.0.tgz#113b5fe1f4a5d6a957cecd29915bedafd70bb537" + integrity sha512-AXMUstURCxDD6yGam2r4E34aJg6kW85IiaeX72hi+I1cxyaMUtrvVY6sbfpGKAj5e7f68Acl62BjQF5aOOx2IQ== + dependencies: + loader-utils "^1.1.0" + wrap-ansi@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" From 6b776aac44919a35fe88228f85b322e6699ffab5 Mon Sep 17 00:00:00 2001 From: dkanada Date: Sun, 30 Aug 2020 15:11:20 +0900 Subject: [PATCH 7/7] fix linting error with url global --- src/plugins/comicsPlayer/plugin.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/plugins/comicsPlayer/plugin.js b/src/plugins/comicsPlayer/plugin.js index 7881a6d06..5f9546b9f 100644 --- a/src/plugins/comicsPlayer/plugin.js +++ b/src/plugins/comicsPlayer/plugin.js @@ -191,6 +191,7 @@ class ArchiveSource { }); for (let file of files) { + /* eslint-disable-next-line compat/compat */ let url = URL.createObjectURL(file.file); this.urls.push(url); }