2020-10-19 20:25:58 +01:00
|
|
|
// eslint-disable-next-line import/named, import/namespace
|
|
|
|
import { Archive } from 'libarchive.js';
|
2020-10-18 18:58:09 +01:00
|
|
|
import loading from '../../components/loading/loading';
|
|
|
|
import dialogHelper from '../../components/dialogHelper/dialogHelper';
|
|
|
|
import keyboardnavigation from '../../scripts/keyboardNavigation';
|
|
|
|
import { appRouter } from '../../components/appRouter';
|
|
|
|
import ServerConnections from '../../components/ServerConnections';
|
2020-12-04 17:04:59 -05:00
|
|
|
// eslint-disable-next-line import/named, import/namespace
|
|
|
|
import { Swiper } from 'swiper/swiper-bundle.esm';
|
|
|
|
import 'swiper/swiper-bundle.css';
|
2020-05-29 23:32:45 +02:00
|
|
|
|
2021-01-26 12:40:50 -05:00
|
|
|
import './style.scss';
|
|
|
|
|
2020-05-29 23:32:45 +02:00
|
|
|
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) {
|
2021-10-05 20:20:33 +00:00
|
|
|
this.currentPage = 0;
|
|
|
|
this.pageCount = 0;
|
2020-05-29 23:32:45 +02:00
|
|
|
|
2020-09-12 05:46:15 +09:00
|
|
|
const elem = this.createMediaElement();
|
2020-05-29 23:32:45 +02:00
|
|
|
return this.setCurrentSrc(elem, options);
|
|
|
|
}
|
|
|
|
|
|
|
|
stop() {
|
|
|
|
this.unbindEvents();
|
2021-10-05 20:10:25 +00:00
|
|
|
|
2021-10-03 19:18:30 +00:00
|
|
|
const stopInfo = {
|
|
|
|
src: this.item
|
|
|
|
};
|
2020-05-29 23:32:45 +02:00
|
|
|
|
2021-10-03 19:18:30 +00:00
|
|
|
Events.trigger(this, 'stopped', [stopInfo]);
|
2021-10-05 20:10:25 +00:00
|
|
|
|
2020-12-04 17:04:59 -05:00
|
|
|
this.archiveSource?.release();
|
|
|
|
|
2020-09-12 05:46:15 +09:00
|
|
|
const elem = this.mediaElement;
|
2020-05-29 23:32:45 +02:00
|
|
|
if (elem) {
|
|
|
|
dialogHelper.close(elem);
|
2020-08-28 23:09:35 +09:00
|
|
|
this.mediaElement = null;
|
2020-05-29 23:32:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
loading.hide();
|
|
|
|
}
|
|
|
|
|
2021-10-05 20:10:25 +00:00
|
|
|
destroy() {
|
|
|
|
// Nothing to do here
|
|
|
|
}
|
|
|
|
|
2021-10-03 19:18:30 +00:00
|
|
|
currentTime() {
|
2021-10-05 20:20:33 +00:00
|
|
|
return this.currentPage;
|
2021-10-03 19:18:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
duration() {
|
2021-10-05 20:20:33 +00:00
|
|
|
return this.pageCount;
|
2021-10-03 19:18:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
currentItem() {
|
|
|
|
return this.item;
|
|
|
|
}
|
|
|
|
|
|
|
|
volume() {
|
|
|
|
return 100;
|
|
|
|
}
|
|
|
|
|
|
|
|
isMuted() {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
paused() {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
seekable() {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2020-05-29 23:32:45 +02:00
|
|
|
onDialogClosed() {
|
|
|
|
this.stop();
|
|
|
|
}
|
|
|
|
|
|
|
|
onWindowKeyUp(e) {
|
2020-09-12 05:46:15 +09:00
|
|
|
const key = keyboardnavigation.getKeyName(e);
|
2020-05-29 23:32:45 +02:00
|
|
|
switch (key) {
|
|
|
|
case 'Escape':
|
|
|
|
this.stop();
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-09 12:37:36 -05:00
|
|
|
bindMediaElementEvents() {
|
|
|
|
const elem = this.mediaElement;
|
|
|
|
|
2021-10-03 19:18:30 +00:00
|
|
|
elem?.addEventListener('close', this.onDialogClosed, { once: true });
|
|
|
|
elem?.querySelector('.btnExit').addEventListener('click', this.onDialogClosed, { once: true });
|
2021-03-09 12:37:36 -05:00
|
|
|
}
|
|
|
|
|
2020-05-29 23:32:45 +02:00
|
|
|
bindEvents() {
|
2021-03-09 12:37:36 -05:00
|
|
|
this.bindMediaElementEvents();
|
|
|
|
|
2020-05-29 23:32:45 +02:00
|
|
|
document.addEventListener('keyup', this.onWindowKeyUp);
|
|
|
|
}
|
|
|
|
|
2021-03-09 12:37:36 -05:00
|
|
|
unbindMediaElementEvents() {
|
|
|
|
const elem = this.mediaElement;
|
|
|
|
|
|
|
|
elem?.removeEventListener('close', this.onDialogClosed);
|
|
|
|
elem?.querySelector('.btnExit').removeEventListener('click', this.onDialogClosed);
|
|
|
|
}
|
|
|
|
|
2020-05-29 23:32:45 +02:00
|
|
|
unbindEvents() {
|
2021-03-09 12:37:36 -05:00
|
|
|
this.unbindMediaElementEvents();
|
|
|
|
|
2020-05-29 23:32:45 +02:00
|
|
|
document.removeEventListener('keyup', this.onWindowKeyUp);
|
|
|
|
}
|
|
|
|
|
|
|
|
createMediaElement() {
|
2020-08-28 23:09:35 +09:00
|
|
|
let elem = this.mediaElement;
|
2020-05-29 23:32:45 +02:00
|
|
|
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
|
|
|
|
});
|
|
|
|
|
2020-08-28 23:11:53 +09:00
|
|
|
elem.id = 'comicsPlayer';
|
2020-05-29 23:32:45 +02:00
|
|
|
elem.classList.add('slideshowDialog');
|
|
|
|
|
2021-03-09 12:37:36 -05:00
|
|
|
elem.innerHTML = `<div class="slideshowSwiperContainer"><div class="swiper-wrapper"></div></div>
|
|
|
|
<div class="actionButtons">
|
2022-02-24 20:15:24 +03:00
|
|
|
<button is="paper-icon-button-light" class="autoSize btnExit" tabindex="-1"><span class="material-icons actionButtonIcon close" aria-hidden="true"></span></button>
|
2021-03-09 12:37:36 -05:00
|
|
|
</div>`;
|
2020-05-29 23:32:45 +02:00
|
|
|
|
|
|
|
dialogHelper.open(elem);
|
|
|
|
}
|
|
|
|
|
2020-08-28 23:09:35 +09:00
|
|
|
this.mediaElement = elem;
|
2021-03-09 12:37:36 -05:00
|
|
|
this.bindEvents();
|
2020-05-29 23:32:45 +02:00
|
|
|
return elem;
|
|
|
|
}
|
|
|
|
|
|
|
|
setCurrentSrc(elem, options) {
|
2020-09-12 05:46:15 +09:00
|
|
|
const item = options.items[0];
|
2021-10-03 19:18:30 +00:00
|
|
|
this.item = item;
|
|
|
|
this.streamInfo = {
|
|
|
|
started: true,
|
|
|
|
ended: false,
|
|
|
|
item: this.item,
|
|
|
|
mediaSource: {
|
|
|
|
Id: item.Id
|
|
|
|
}
|
|
|
|
};
|
2020-05-29 23:32:45 +02:00
|
|
|
|
|
|
|
loading.show();
|
|
|
|
|
2020-09-12 05:46:15 +09:00
|
|
|
const serverId = item.ServerId;
|
2020-10-18 18:58:09 +01:00
|
|
|
const apiClient = ServerConnections.getApiClient(serverId);
|
2020-05-29 23:32:45 +02:00
|
|
|
|
2020-10-18 18:58:09 +01:00
|
|
|
Archive.init({
|
2020-05-29 23:32:45 +02:00
|
|
|
workerUrl: appRouter.baseUrl() + '/libraries/worker-bundle.js'
|
|
|
|
});
|
|
|
|
|
2020-12-04 17:04:59 -05:00
|
|
|
const downloadUrl = apiClient.getItemDownloadUrl(item.Id);
|
|
|
|
this.archiveSource = new ArchiveSource(downloadUrl);
|
|
|
|
|
|
|
|
return this.archiveSource.load().then(() => {
|
|
|
|
loading.hide();
|
2021-10-03 19:18:30 +00:00
|
|
|
|
2021-10-05 20:20:33 +00:00
|
|
|
this.pageCount = this.archiveSource.urls.length;
|
|
|
|
this.currentPage = options.startPositionTicks / 10000 || 0;
|
2021-10-05 20:41:47 +00:00
|
|
|
|
2020-12-04 17:04:59 -05:00
|
|
|
this.swiperInstance = new Swiper(elem.querySelector('.slideshowSwiperContainer'), {
|
|
|
|
direction: 'horizontal',
|
2020-12-04 18:19:08 -05:00
|
|
|
// loop is disabled due to the lack of Swiper support in virtual slides
|
2020-12-04 17:04:59 -05:00
|
|
|
loop: false,
|
|
|
|
zoom: {
|
|
|
|
minRatio: 1,
|
|
|
|
toggle: true,
|
|
|
|
containerClass: 'slider-zoom-container'
|
|
|
|
},
|
|
|
|
autoplay: false,
|
|
|
|
keyboard: {
|
|
|
|
enabled: true
|
|
|
|
},
|
|
|
|
preloadImages: true,
|
|
|
|
slidesPerView: 1,
|
|
|
|
slidesPerColumn: 1,
|
2021-10-05 20:20:33 +00:00
|
|
|
initialSlide: this.currentPage,
|
2020-12-04 17:04:59 -05:00
|
|
|
// reduces memory consumption for large libraries while allowing preloading of images
|
|
|
|
virtual: {
|
|
|
|
slides: this.archiveSource.urls,
|
|
|
|
cache: true,
|
|
|
|
renderSlide: this.getImgFromUrl,
|
|
|
|
addSlidesBefore: 1,
|
|
|
|
addSlidesAfter: 1
|
|
|
|
}
|
2020-05-29 23:32:45 +02:00
|
|
|
});
|
2021-10-03 19:18:30 +00:00
|
|
|
|
|
|
|
// save current page ( a page is an image file inside the archive )
|
|
|
|
this.swiperInstance.on('slideChange', () => {
|
2021-10-05 20:20:33 +00:00
|
|
|
this.currentPage = this.swiperInstance.activeIndex;
|
2021-10-03 19:18:30 +00:00
|
|
|
Events.trigger(this, 'pause');
|
|
|
|
});
|
2020-05-29 23:32:45 +02:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
2020-08-28 23:09:35 +09:00
|
|
|
|
2020-05-29 23:32:45 +02:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-22 21:17:57 +00:00
|
|
|
// the comic book archive supports any kind of image format as it's just a zip archive
|
2022-02-09 15:18:41 +00:00
|
|
|
const supportedFormats = ['jpg', 'jpeg', 'jpe', 'jif', 'jfif', 'jfi', 'png', 'avif', 'gif', 'bmp', 'dib', 'tiff', 'tif'];
|
2022-01-22 21:17:57 +00:00
|
|
|
|
2020-05-29 23:32:45 +02:00
|
|
|
class ArchiveSource {
|
|
|
|
constructor(url) {
|
|
|
|
this.url = url;
|
|
|
|
this.files = [];
|
|
|
|
this.urls = [];
|
|
|
|
}
|
|
|
|
|
|
|
|
async load() {
|
2020-09-12 05:46:15 +09:00
|
|
|
const res = await fetch(this.url);
|
2020-05-29 23:32:45 +02:00
|
|
|
if (!res.ok) {
|
|
|
|
return;
|
|
|
|
}
|
2020-08-28 23:09:35 +09:00
|
|
|
|
2020-09-12 05:46:15 +09:00
|
|
|
const blob = await res.blob();
|
2020-10-18 18:58:09 +01:00
|
|
|
this.archive = await Archive.open(blob);
|
2020-05-29 23:32:45 +02:00
|
|
|
this.raw = await this.archive.getFilesArray();
|
|
|
|
await this.archive.extractFiles();
|
|
|
|
|
2022-01-22 01:00:03 +00:00
|
|
|
let files = await this.archive.getFilesArray();
|
2022-02-17 16:31:28 +00:00
|
|
|
|
|
|
|
// metadata files and files without a file extension should not be considered as a page
|
2022-01-22 01:00:03 +00:00
|
|
|
files = files.filter((file) => {
|
|
|
|
const name = file.file.name;
|
|
|
|
const index = name.lastIndexOf('.');
|
2022-02-17 16:31:28 +00:00
|
|
|
return index !== -1 && supportedFormats.includes(name.slice(index + 1));
|
2022-01-22 01:00:03 +00:00
|
|
|
});
|
2020-05-29 23:32:45 +02:00
|
|
|
files.sort((a, b) => {
|
2020-08-28 23:09:35 +09:00
|
|
|
if (a.file.name < b.file.name) {
|
2020-05-29 23:32:45 +02:00
|
|
|
return -1;
|
2020-08-28 23:09:35 +09:00
|
|
|
} else {
|
2020-05-29 23:32:45 +02:00
|
|
|
return 1;
|
2020-08-28 23:09:35 +09:00
|
|
|
}
|
2020-05-29 23:32:45 +02:00
|
|
|
});
|
|
|
|
|
2020-09-12 05:46:15 +09:00
|
|
|
for (const file of files) {
|
2020-08-30 15:11:20 +09:00
|
|
|
/* eslint-disable-next-line compat/compat */
|
2020-09-12 05:46:15 +09:00
|
|
|
const url = URL.createObjectURL(file.file);
|
2020-05-29 23:32:45 +02:00
|
|
|
this.urls.push(url);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-12-04 17:04:59 -05:00
|
|
|
release() {
|
|
|
|
this.files = [];
|
|
|
|
/* eslint-disable-next-line compat/compat */
|
2020-12-04 17:14:16 -05:00
|
|
|
this.urls.forEach(URL.revokeObjectURL);
|
2020-12-04 17:04:59 -05:00
|
|
|
this.urls = [];
|
2020-05-29 23:32:45 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export default ComicsPlayer;
|