mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
Add barebones comic book reader
This commit is contained in:
parent
aae276dad1
commit
2ea2132740
8 changed files with 252 additions and 6 deletions
|
@ -69,6 +69,7 @@
|
||||||
"jellyfin-noto": "https://github.com/jellyfin/jellyfin-noto",
|
"jellyfin-noto": "https://github.com/jellyfin/jellyfin-noto",
|
||||||
"jquery": "^3.5.1",
|
"jquery": "^3.5.1",
|
||||||
"jstree": "^3.3.7",
|
"jstree": "^3.3.7",
|
||||||
|
"libarchive.js": "^1.3.0",
|
||||||
"libass-wasm": "https://github.com/jellyfin/JavascriptSubtitlesOctopus#4.0.0-jf-smarttv",
|
"libass-wasm": "https://github.com/jellyfin/JavascriptSubtitlesOctopus#4.0.0-jf-smarttv",
|
||||||
"material-design-icons-iconfont": "^5.0.1",
|
"material-design-icons-iconfont": "^5.0.1",
|
||||||
"native-promise-only": "^0.8.0-a",
|
"native-promise-only": "^0.8.0-a",
|
||||||
|
@ -92,6 +93,7 @@
|
||||||
"src/components/autoFocuser.js",
|
"src/components/autoFocuser.js",
|
||||||
"src/components/cardbuilder/cardBuilder.js",
|
"src/components/cardbuilder/cardBuilder.js",
|
||||||
"src/scripts/fileDownloader.js",
|
"src/scripts/fileDownloader.js",
|
||||||
|
"src/components/comicsPlayer/plugin.js",
|
||||||
"src/components/images/imageLoader.js",
|
"src/components/images/imageLoader.js",
|
||||||
"src/components/lazyLoader/lazyLoaderIntersectionObserver.js",
|
"src/components/lazyLoader/lazyLoaderIntersectionObserver.js",
|
||||||
"src/components/playback/mediasession.js",
|
"src/components/playback/mediasession.js",
|
||||||
|
|
|
@ -176,3 +176,9 @@ _define('connectionManagerFactory', function () {
|
||||||
_define('appStorage', function () {
|
_define('appStorage', function () {
|
||||||
return apiclient.AppStorage;
|
return apiclient.AppStorage;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// libarchive.js
|
||||||
|
var libarchive = require('libarchive.js');
|
||||||
|
_define('libarchive', function () {
|
||||||
|
return libarchive;
|
||||||
|
});
|
||||||
|
|
215
src/components/comicsPlayer/plugin.js
Normal file
215
src/components/comicsPlayer/plugin.js
Normal file
|
@ -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 = '<div class="slideshowSwiperContainer"><div class="swiper-wrapper"></div></div>';
|
||||||
|
|
||||||
|
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 `<div class="swiper-slide">
|
||||||
|
<div class="slider-zoom-container">
|
||||||
|
<img src="${url}" class="swiper-slide-img">
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
|
@ -2187,7 +2187,7 @@ define(['events', 'datetime', 'appSettings', 'itemHelper', 'pluginManager', 'pla
|
||||||
// Only used internally
|
// Only used internally
|
||||||
self.getCurrentTicks = getCurrentTicks;
|
self.getCurrentTicks = getCurrentTicks;
|
||||||
|
|
||||||
function playPhotos(items, options, user) {
|
function playOther(items, options, user) {
|
||||||
|
|
||||||
var playStartIndex = options.startIndex || 0;
|
var playStartIndex = options.startIndex || 0;
|
||||||
var player = getPlayer(items[playStartIndex], options);
|
var player = getPlayer(items[playStartIndex], options);
|
||||||
|
@ -2216,9 +2216,9 @@ define(['events', 'datetime', 'appSettings', 'itemHelper', 'pluginManager', 'pla
|
||||||
return Promise.reject();
|
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);
|
var apiClient = connectionManager.getApiClient(firstItem.ServerId);
|
||||||
|
|
|
@ -58,7 +58,7 @@ define(['events', 'globalize'], function (events, globalize) {
|
||||||
|
|
||||||
return new Promise(function (resolve, reject) {
|
return new Promise(function (resolve, reject) {
|
||||||
require([pluginSpec], (pluginFactory) => {
|
require([pluginSpec], (pluginFactory) => {
|
||||||
var plugin = new pluginFactory();
|
var plugin = pluginFactory.default ? new pluginFactory.default() : new pluginFactory();
|
||||||
|
|
||||||
// See if it's already installed
|
// See if it's already installed
|
||||||
var existing = instance.pluginsList.filter(function (p) {
|
var existing = instance.pluginsList.filter(function (p) {
|
||||||
|
|
|
@ -490,6 +490,7 @@ var AppInfo = {};
|
||||||
'components/playback/experimentalwarnings',
|
'components/playback/experimentalwarnings',
|
||||||
'components/htmlAudioPlayer/plugin',
|
'components/htmlAudioPlayer/plugin',
|
||||||
'components/htmlVideoPlayer/plugin',
|
'components/htmlVideoPlayer/plugin',
|
||||||
|
'components/comicsPlayer/plugin',
|
||||||
'components/photoPlayer/plugin',
|
'components/photoPlayer/plugin',
|
||||||
'components/youtubeplayer/plugin',
|
'components/youtubeplayer/plugin',
|
||||||
'components/backdropScreensaver/plugin',
|
'components/backdropScreensaver/plugin',
|
||||||
|
@ -701,7 +702,8 @@ var AppInfo = {};
|
||||||
'events',
|
'events',
|
||||||
'credentialprovider',
|
'credentialprovider',
|
||||||
'connectionManagerFactory',
|
'connectionManagerFactory',
|
||||||
'appStorage'
|
'appStorage',
|
||||||
|
'comicReader'
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
urlArgs: urlArgs,
|
urlArgs: urlArgs,
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
const CopyPlugin = require('copy-webpack-plugin');
|
const CopyPlugin = require('copy-webpack-plugin');
|
||||||
|
const WorkerPlugin = require('worker-plugin');
|
||||||
|
|
||||||
const Assets = [
|
const Assets = [
|
||||||
'alameda/alameda.js',
|
'alameda/alameda.js',
|
||||||
'native-promise-only/npo.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.js',
|
||||||
'libass-wasm/dist/js/subtitles-octopus-worker.data',
|
'libass-wasm/dist/js/subtitles-octopus-worker.data',
|
||||||
'libass-wasm/dist/js/subtitles-octopus-worker.wasm',
|
'libass-wasm/dist/js/subtitles-octopus-worker.wasm',
|
||||||
|
@ -13,6 +15,11 @@ const Assets = [
|
||||||
'libass-wasm/dist/js/subtitles-octopus-worker-legacy.js.mem'
|
'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 = {
|
module.exports = {
|
||||||
context: path.resolve(__dirname, 'src'),
|
context: path.resolve(__dirname, 'src'),
|
||||||
entry: './bundle.js',
|
entry: './bundle.js',
|
||||||
|
@ -34,6 +41,15 @@ module.exports = {
|
||||||
to: path.resolve(__dirname, './dist/libraries')
|
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()
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
|
@ -6783,6 +6783,11 @@ levn@^0.3.0, levn@~0.3.0:
|
||||||
prelude-ls "~1.1.2"
|
prelude-ls "~1.1.2"
|
||||||
type-check "~0.3.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":
|
"libass-wasm@https://github.com/jellyfin/JavascriptSubtitlesOctopus#4.0.0-jf-smarttv":
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
resolved "https://github.com/jellyfin/JavascriptSubtitlesOctopus#58e9a3f1a7f7883556ee002545f445a430120639"
|
resolved "https://github.com/jellyfin/JavascriptSubtitlesOctopus#58e9a3f1a7f7883556ee002545f445a430120639"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue