This commit is contained in:
2025-09-18 15:31:27 +02:00
commit 2abba0c2b7
406 changed files with 31879 additions and 0 deletions

View File

@@ -0,0 +1,52 @@
import Tagify from '@yaireo/tagify';
import '@yaireo/tagify/dist/tagify.css';
const taginput = document.querySelector("#tags");
const studioinput = document.querySelector("#studio");
const episode_id = document.getElementById('e_id').value;
// Get Tags from API
window.axios.get('/admin/tags/' + episode_id).then(function (response) {
if (response.status != 200) {
return;
}
var tagify = new Tagify(taginput, {
whitelist: response.data.tags,
dropdown: {
classname: "color-blue",
enabled: 0, // show the dropdown immediately on focus
maxItems: 10,
position: "text", // place the dropdown near the typed text
closeOnSelect: false, // keep the dropdown open after selecting a suggestion
highlightFirst: true
}
});
tagify.addTags(response.data.episodetags);
}).catch(function (error) {
console.log(error);
});
// Get Studio from API
window.axios.get('/admin/studio/' + episode_id).then(function (response) {
if (response.status != 200) {
return;
}
var tagify = new Tagify(studioinput, {
whitelist: response.data.studios,
dropdown: {
classname: "color-blue",
enabled: 0, // show the dropdown immediately on focus
maxItems: 10,
position: "text", // place the dropdown near the typed text
closeOnSelect: false, // keep the dropdown open after selecting a suggestion
highlightFirst: true
}
});
tagify.addTags(response.data.episodestudios);
}).catch(function (error) {
console.log(error);
});

View File

@@ -0,0 +1,29 @@
import Tagify from '@yaireo/tagify';
import '@yaireo/tagify/dist/tagify.css';
const taginput = document.querySelector("#subtitles");
const episode_id = document.getElementById('e_id').value;
// Get Tags from API
window.axios.get('/admin/subtitles/' + episode_id).then(function (response) {
if (response.status != 200) {
return;
}
var tagify = new Tagify(taginput, {
whitelist: response.data.subs,
dropdown: {
classname: "color-blue",
enabled: 0, // show the dropdown immediately on focus
maxItems: 10,
position: "text", // place the dropdown near the typed text
closeOnSelect: false, // keep the dropdown open after selecting a suggestion
highlightFirst: true
}
});
tagify.addTags(response.data.episodesubs);
}).catch(function (error) {
console.log(error);
});

20
resources/js/app.js Normal file
View File

@@ -0,0 +1,20 @@
import './bootstrap';
// import { Alpine } from '../../vendor/livewire/livewire/dist/livewire.esm';
// Alpine.start();
import {
Collapse,
Carousel,
Clipboard,
Modal,
Lightbox,
Tooltip,
Tab,
Ripple,
initTE,
} from "tw-elements";
initTE({ Collapse, Carousel, Clipboard, Modal, Tab, Lightbox, Tooltip, Ripple });
import 'hammerjs';

32
resources/js/bootstrap.js vendored Normal file
View File

@@ -0,0 +1,32 @@
/**
* We'll load the axios HTTP library which allows us to easily issue requests
* to our Laravel back-end. This library automatically handles sending the
* CSRF token as a header based on the value of the "XSRF" token cookie.
*/
import axios from 'axios';
window.axios = axios;
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
/**
* Echo exposes an expressive API for subscribing to channels and listening
* for events that are broadcast by Laravel. Echo and event broadcasting
* allows your team to easily build robust real-time web applications.
*/
// import Echo from 'laravel-echo';
// import Pusher from 'pusher-js';
// window.Pusher = Pusher;
// window.Echo = new Echo({
// broadcaster: 'pusher',
// key: import.meta.env.VITE_PUSHER_APP_KEY,
// cluster: import.meta.env.VITE_PUSHER_APP_CLUSTER ?? 'mt1',
// wsHost: import.meta.env.VITE_PUSHER_HOST ? import.meta.env.VITE_PUSHER_HOST : `ws-${import.meta.env.VITE_PUSHER_APP_CLUSTER}.pusher.com`,
// wsPort: import.meta.env.VITE_PUSHER_PORT ?? 80,
// wssPort: import.meta.env.VITE_PUSHER_PORT ?? 443,
// forceTLS: (import.meta.env.VITE_PUSHER_SCHEME ?? 'https') === 'https',
// enabledTransports: ['ws', 'wss'],
// });

View File

@@ -0,0 +1,12 @@
export function isIOS() {
return [
'iPad Simulator',
'iPhone Simulator',
'iPod Simulator',
'iPad',
'iPhone',
'iPod'
].includes(navigator.platform)
// iPad on iOS 13 detection
|| (navigator.userAgent.includes("Mac") && "ontouchend" in document)
}

View File

@@ -0,0 +1,59 @@
if (document.getElementById("playlist-add")) {
function createPlaylist() {
console.log('Adding to Playlist: ' + document.querySelector("#playlist").value)
window.axios.post('/hentai/add-to-playlist', {
playlist: document.getElementById('playlist').value,
episode_id: document.getElementById('e_id').value
}).then(function (response) {
if (response.status == 200) {
document.getElementById("playlist-cancel").click();
if (response.data.message == 'already-added') {
Swal.fire({
title: "Already added!",
text: "Episode was already added to that Playlist!",
icon: "warning"
});
}
if (response.data.message == 'success') {
Swal.fire({
title: "Success!",
text: "Added episode to the playlist!",
icon: "success"
});
}
}
}).catch(function (error) {
console.log(error);
});
}
document.querySelector("#playlist-add").addEventListener("click", createPlaylist);
}
if (document.getElementById("playlist-create-and-add")) {
function createAndAddPlaylist() {
window.axios.post('/hentai/create-playlist', {
name: document.getElementById('name').value,
visiblity: document.getElementById('visiblity').value
}).then(function (response) {
window.axios.post('/hentai/add-to-playlist', {
playlist: response.data.playlist_id,
episode_id: document.getElementById('e_id').value
}).then(function (response) {
if (response.status == 200) {
document.getElementById("playlist-cancel").click();
}
}).catch(function (error) {
console.log(error);
});
}).catch(function (error) {
console.log(error);
});
}
document.querySelector("#playlist-create-and-add").addEventListener("click", createAndAddPlaylist);
}

View File

@@ -0,0 +1,94 @@
export function addVideoTracks(streamServer, apiResponse, av1Supported, dashSupported) {
if (dashSupported) {
return addDashTracks(streamServer, apiResponse, av1Supported);
}
return addLegacyTracks(streamServer, apiResponse, av1Supported);
}
function addDashTracks(streamServer, apiResponse, av1Supported) {
var data = [];
// 720p
data.push({
src: streamServer + '/' + apiResponse.stream_url + '/720/manifest.mpd',
size: 720,
mode: 'mpd',
});
if (av1Supported) {
// 1080p
data.push({
src: streamServer + '/' + apiResponse.stream_url + '/1080/manifest.mpd',
size: 1080,
mode: 'mpd',
});
// 2160p
data.push({
src: streamServer + '/' + apiResponse.stream_url + '/2160/manifest.mpd',
size: 2160,
mode: 'mpd',
});
if (apiResponse.interpolated == 1) {
// 1080p Interpolated
data.push({
src: streamServer + '/' + apiResponse.stream_url + '/1080i/manifest.mpd',
size: 1081,
mode: 'mpd',
});
}
if (apiResponse.interpolated_uhd == 1) {
// 2160p Interpolated
data.push({
src: streamServer + '/' + apiResponse.stream_url + '/2160i/manifest.mpd',
size: 2161,
mode: 'mpd',
});
}
}
return data;
}
function addLegacyTracks(streamServer, apiResponse, av1Supported) {
var data = [];
// 720p
data.push({
src: streamServer + '/' + apiResponse.stream_url + '/x264.720p.mp4',
type: 'video/mp4',
size: 720,
});
return data;
}
export function addSubtitleTracks(streamServer, apiResponse) {
var data = [];
// Default
data.push({
kind: 'captions',
label: 'English',
srclang: 'en',
src: '',
default: true,
});
for (var key in apiResponse.extra_subtitles) {
data.push({
kind: 'captions',
label: apiResponse.extra_subtitles[key] + ' (Auto Transl.)',
srclang: key,
src: '',
default: false,
});
}
return data;
}

View File

@@ -0,0 +1,144 @@
export function initMobileWidescreen() {
// Mobile widescreen button
if(/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) {
// Add Button to fit screen
var controls = document.getElementsByClassName('plyr__controls')[0];
var newButton = `
<button class="plyr__controls__item plyr__control" id="mobile-screen" type="button" aria-pressed="false" style="padding: 5px; padding-top: 2px; padding-bottom: 2px;">
<i class="fa-solid fa-arrows-left-right-to-line" ></i>
</button>
`;
controls.insertAdjacentHTML('beforeend', newButton);
// Add event listener
var stateButton = true;
var videoTemp = document.querySelector('video');
const mobilebutton = document.getElementById('mobile-screen');
mobilebutton.addEventListener('click', function() {
if (! stateButton) {
videoTemp.style.objectFit = 'cover';
stateButton = true;
} else {
videoTemp.style.objectFit = null;
stateButton = false;
}
});
// Set default
videoTemp.style.objectFit = 'cover';
}
}
export function mobileDoubleClick(player) {
if(/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) {
const byClass = document.getElementsByClassName.bind(document),
createElement = document.createElement.bind(document);
// Remove all dblclick stuffs
player.eventListeners.forEach(function (eventListener) {
if (eventListener.type === 'dblclick') {
eventListener.element.removeEventListener(eventListener.type, eventListener.callback, eventListener.options);
}
});
// Create overlay that will show the skipped time
const skip_ol = createElement("div");
skip_ol.id = "plyr__time_skip"
byClass("plyr")[0].appendChild(skip_ol)
// A class to manage multi click count and remember last clicked side (may cause issue otherwise)
class multiclick_counter {
constructor() {
this.timers = []; // collection of timers. Important
this.count = 0; // click count
this.reseted = 0; // before resetting what was the count
this.last_side = null; // L C R 3sides
}
clicked() {
this.count += 1
var xcount = this.count; // will be checked if click count increased in the time
this.timers.push(setTimeout(this.reset.bind(this, xcount), 500)); // wait till 500ms for next click
return this.count
}
reset_count(n) {
// Reset count if clicked on the different side
this.reseted = this.count
this.count = n
for (var i = 0; i < this.timers.length; i++) {
clearTimeout(this.timers[i]);
}
this.timer = []
}
reset(xcount) {
if (this.count > xcount) { return } // return if clicked after timer started
// Reset otherwise
this.count = 0;
this.last_side = null;
this.reseted = 0;
skip_ol.style.opacity = "0";
this.timer = []
}
}
var counter = new multiclick_counter();
const poster = byClass("plyr__poster")[0]
poster.onclick = function (e) {
const count = counter.clicked()
if (count < 2) { return } // if not double click
const rect = e.target.getBoundingClientRect();
const x = e.clientX - rect.left; //x position within the element.
// The relative position of click on video
const width = e.target.offsetWidth;
const perc = x * 100 / width;
var panic = true; // panic if the side needs to be checked
var last_click = counter.last_side
if (last_click == null) {
panic = false
}
if (perc < 40) {
if (player.currentTime == 0) {
return // won't seek beyond 0
}
counter.last_side = "L"
if (panic && last_click != "L") {
counter.reset_count(1)
return
}
skip_ol.style.opacity = "0.9";
player.rewind()
skip_ol.innerHTML = "<i class=\"fa-solid fa-backward\"></i> " + ((count - 1) * 10) + "s";
}
else if (perc > 60) {
if (player.currentTime == player.duration) {
return // won't seek beyond duration
}
counter.last_side = "R"
if (panic && last_click != "R") {
counter.reset_count(1)
return
}
skip_ol.style.opacity = "0.9";
last_click = "R"
player.forward()
skip_ol.innerHTML = "<i class=\"fa-solid fa-forward\"></i> " + ((count - 1) * 10) + "s";
}
else {
player.togglePlay()
counter.last_click = "C"
}
}
}
}

View File

@@ -0,0 +1,45 @@
export function serverSelectMenuItem(selectedIndex) {
return `
<button id="server-select" data-plyr="settings" type="button" class="plyr__control plyr__control--forward" role="menuitem" aria-haspopup="true">
<span>Server<span class="plyr__menu__value">CDN` + (selectedIndex + 1) + `</span></span>
</button>`;
}
export function serverSelectSubmenu(selectedIndex, serverCount) {
let htmlList = `
<div id="server-select-list" hidden>
<button type="button" class="plyr__control plyr__control--back" id="server-select-list-back-btn">
<span aria-hidden="true">Server</span><span class="plyr__sr-only">Go back to previous menu</span>
</button>
<div role="menu">
`;
for (let i = 0; i < serverCount; i++) {
let checked = selectedIndex == i ? 'true' : 'false';
let index = i + 1;
htmlList += `
<button data-plyr="server" type="button" role="menuitemradio" class="plyr__control change_server" aria-checked="` + checked + `" value="` + i +`">
<span>Server ` + index + `<span class="plyr__menu__value"><span class="plyr__badge">CDN` + index + `</span></span></span>
</button>
`;
}
htmlList += `
</div>
</div>
`;
return htmlList;
}
export function serverSelectMenuClickToggle() {
if (document.getElementById('server-select-list').hidden) {
document.querySelector('div[role="menu"]').hidden = true;
document.getElementById('server-select-list').hidden = false;
return;
}
document.getElementById('server-select-list').hidden = true;
document.querySelector('div[role="menu"]').hidden = false;
}

462
resources/js/player.js Normal file
View File

@@ -0,0 +1,462 @@
// Plyr Player
import Plyr from 'plyr/dist/plyr.polyfilled.min.js';
import 'plyr/dist/plyr.css';
// Vidstack Player
import 'vidstack/player/styles/default/theme.css';
import 'vidstack/player/styles/default/layouts/video.css';
import { VidstackPlayer, VidstackPlayerLayout } from 'vidstack/global/player';
// Dash Support
import dashjs from 'dashjs';
// Subtitle Support
import SubtitlesOctopus from '@jellyfin/libass-wasm';
// Custom JS
import { initMobileWidescreen } from './player-mobile';
import { mobileDoubleClick } from './player-mobile'
import { playNextPlaylistVideo } from './playlist';
import { addVideoTracks } from './player-data';
import { addSubtitleTracks } from './player-data';
import { serverSelectMenuItem, serverSelectSubmenu, serverSelectMenuClickToggle } from './player-server-select';
import { isIOS } from './detect-ios';
// Variables
var player = null;
var av1Supported = (!!document.createElement('video').canPlayType('video/webm; codecs="av01.0.05M.08, opus"'));
var dashSupported = dashjs.supportsMediaSource();
var apiResponse = {};
var volume = 0.5;
var captions = true;
var lastTime = 0.0;
var streamServer = '';
var streamServers = [];
var streamServerIndex = 0;
var streamServerCount = 0;
var ambientMode = true;
var serverFallback = false;
var saveInterval;
var subtitleInstance = null;
var controls = [
'play-large', // The large play button in the center
'play', // Play/pause playback
'progress', // The progress bar and scrubber for playback and buffering
'current-time', // The current time of playback
'duration', // The full duration of the media
'mute', // Toggle mute
'volume', // Volume control
'captions', // Toggle captions
'settings', // Settings menu
'fullscreen', // Toggle fullscreen
];
// Load Volume from LocalStorage
if (localStorage.hstreamVolume) {
volume = parseFloat(localStorage.getItem('hstreamVolume')).toFixed(2);
console.log('Loaded Audio Volume from Local Storage: ' + volume);
}
// Load Captions from LocalStorage
if (localStorage.hstreamCaptions) {
captions = (localStorage.getItem('hstreamCaptions') == 'true');
console.log('Loaded Captions Status from Local Storage: ' + captions);
}
// Asia Server Fallback
if (localStorage.hstreamServerFallback) {
serverFallback = (localStorage.getItem('hstreamServerFallback') == 'true');
console.log('Loaded Captions Status from Local Storage: ' + captions);
}
// Alert User when AV1 is not supported
if (!av1Supported) {
document.getElementById("av1-unsupported").classList.remove("hidden");
}
function initDash(data, player) {
const video = document.querySelector('video');
data.forEach(function (el) {
if (el.mode === 'mpd' && el.size === player.config.quality.selected) {
const dash = dashjs.MediaPlayer().create();
dash.initialize(video, el.src, true);
// Expose player and dash so they can be used from the console
window.player = player;
window.dash = dash;
}
});
}
function setCanvasDimension(canvas, video) {
canvas.height = video.offsetHeight;
canvas.width = video.offsetWidth;
}
function paintStaticVideo(ctx, video) {
if (localStorage.theme == 'light') {
return;
}
if (!ambientMode) {
return;
}
ctx.drawImage(video, 0, 0, video.offsetWidth, video.offsetHeight);
}
function toggleAmbientMode() {
let canvas = document.getElementById("ambientVideo"), ctx = canvas.getContext("2d"), video = document.getElementsByTagName('video')[0];
if (ambientMode) {
ambientMode = false;
localStorage.ambientMode = 'false';
setCanvasDimension(canvas, video);
document.getElementById('ambient-mode-toggle').innerHTML = '<span>Ambient Mode<span class="plyr__menu__value">Off</span></span>';
} else {
ambientMode = true;
localStorage.ambientMode = 'true';
setCanvasDimension(canvas, video);
paintStaticVideo(ctx, video);
document.getElementById('ambient-mode-toggle').innerHTML = '<span>Ambient Mode<span class="plyr__menu__value">On</span></span>';
}
}
function toggleAsiaServer() {
if (serverFallback) {
serverFallback = false;
localStorage.hstreamServerFallback = 'false';
document.getElementById('server-fallback-toggle').innerHTML = '<span>Fallback Server<span class="plyr__menu__value">Off</span></span>';
streamServers = apiResponse.stream_domains;
} else {
serverFallback = true;
localStorage.hstreamServerFallback = 'true';
document.getElementById('server-fallback-toggle').innerHTML = '<span>Fallback Server<span class="plyr__menu__value">On</span></span>';
streamServers = apiResponse.asia_stream_domains;
}
streamServerCount = streamServers.length;
streamServerIndex = Math.floor(Math.random() * streamServerCount);
streamServer = streamServers[streamServerIndex];
console.log('Selected Server: ' + streamServer);
if (player) {
clearInterval(saveInterval);
player.destroy();
}
initPlayer();
}
function initSubtitles(lang) {
if (isIOS()) {
return;
}
// Dispose old instance
if (subtitleInstance != null && subtitleInstance instanceof SubtitlesOctopus) {
subtitleInstance.dispose();
}
let newSubUrl = streamServer + '/' + apiResponse.stream_url + '/';
if (lang != 'en') {
newSubUrl += 'autotrans/' + lang + '.ass';
}
else {
newSubUrl += 'eng.ass'
}
let subFont = '/fonts/Figtree-ExtraBold.woff2';
// Hindi font
if (lang == 'hi') {
subFont = '/fonts/Hind-SemiBold.ttf';
}
// Subtitles
var options = {
video: document.getElementsByTagName('video')[0], // HTML5 video element
subUrl: newSubUrl, // Link to subtitles
workerUrl: '/build/js/subtitles-octopus-worker.js', // Link to WebAssembly-based file "libassjs-worker.js"
legacyWorkerUrl: '/build/js/subtitles-octopus-worker-legacy.js', // Link to non-WebAssembly worker
fonts: [subFont],
renderMode: 'wasm-blend',
};
subtitleInstance = new SubtitlesOctopus(options);
}
function initPlayer() {
player = new Plyr('#player', {
controls,
quality: {
default: 720,
options: [2161, 2160, 1081, 1080, 720]
},
i18n: {
qualityLabel: {
2161: "2160p48",
2160: "2160p",
1081: "1080p48",
1080: "1080p",
720: "720p"
},
qualityBadge: {
2161: "UHD@48",
1081: "FHD@48",
1080: "FHD",
},
},
fullscreen: { enabled: true, fallback: true, iosNative: true }
});
// Player Track Data
var data = addVideoTracks(streamServer, apiResponse, av1Supported, dashSupported);
player.source = {
type: 'video',
title: apiResponse.title,
poster: apiResponse.poster,
previewThumbnails: {
enabled: true,
src: streamServer + '/' + apiResponse.stream_url + '/thumbs.vtt',
},
sources: data,
tracks: addSubtitleTracks(streamServer, apiResponse)
};
player.volume = volume;
//player.captions.languages = ['en'];
player.captions.language = 'en';
player.captions.active = captions;
if (dashSupported && !apiResponse.legacy) {
player.on('qualitychange', () => {
initDash(data, player);
});
initDash(data, player);
}
// Ambient Mode
let canvas = document.getElementById("ambientVideo"), ctx = canvas.getContext("2d"), video = document.getElementsByTagName('video')[0];
setCanvasDimension(canvas, video);
paintStaticVideo(ctx, video);
var allItems = document.getElementsByClassName('plyr__control--forward');
var lastItem = allItems[allItems.length - 1];
lastItem.insertAdjacentHTML('afterend', '<button id="ambient-mode-toggle" type="button" class="plyr__control" role="menuitem" aria-haspopup="true"><span>Ambient Mode<span class="plyr__menu__value">On</span></span></button>');
document.getElementById('ambient-mode-toggle').addEventListener('click', toggleAmbientMode);
if (localStorage.ambientMode == 'false') {
toggleAmbientMode();
}
// Server select (Asia)
lastItem = allItems[allItems.length - 1];
let value = 'Off';
if (serverFallback) { value = 'On'; }
lastItem.insertAdjacentHTML('afterend', '<button id="server-fallback-toggle" type="button" class="plyr__control" role="menuitem" aria-haspopup="true"><span>Fallback Server<span class="plyr__menu__value">' + value + '</span></span></button>');
document.getElementById('server-fallback-toggle').addEventListener('click', toggleAsiaServer);
var clickedPlay = false;
player.on('play', () => {
if (!clickedPlay) {
player.stop();
console.log("Stopped video, because user didn't click play.")
}
setCanvasDimension(canvas, video);
console.log('Play => Function Loop()');
var $this = video;
(function loop() {
if (!player.paused && !player.ended && localStorage.theme == 'dark' && ambientMode) {
ctx.drawImage($this, 0, 0, $this.offsetWidth, $this.offsetHeight);
setTimeout(loop, 24000 / 1001); // drawing at 30fps
}
})();
});
player.on('seeked', () => {
paintStaticVideo(ctx, video);
if (player.currentTime > 0) {
lastTime = player.currentTime;
}
console.log('Seeked => paintStaticVideo() at ' + player.currentTime);
});
window.addEventListener("resize", () => {
setCanvasDimension(canvas, video);
if (player.paused) {
paintStaticVideo(ctx, video);
}
});
player.on('captionsenabled', () => {
document.getElementsByClassName('libassjs-canvas-parent')[0].style.visibility = 'visible';
localStorage.setItem('hstreamCaptions', 'true');
console.log('Set Captions Status to Local Storage: true');
});
player.on('captionsdisabled', () => {
document.getElementsByClassName('libassjs-canvas-parent')[0].style.visibility = 'hidden';
localStorage.setItem('hstreamCaptions', 'false');
console.log('Set Captions Status to Local Storage: false');
});
player.on('volumechange', () => {
console.log('Saving Audio Volume to Local Storage: ' + player.volume);
localStorage.setItem('hstreamVolume', player.volume.toString())
});
player.on('ended', () => {
playNextPlaylistVideo();
});
player.on('languagechange', (event) => {
let lang = event.detail.plyr.captions.language;
console.log('Subtitle Event ' + lang);
initSubtitles(lang);
});
function playerPlayTemp() {
clickedPlay = true;
}
document.querySelectorAll('[data-plyr="play"]').forEach(play =>
play.addEventListener('click', playerPlayTemp)
);
document.getElementsByClassName('plyr--video')[0].addEventListener('click', playerPlayTemp);
initMobileWidescreen();
// Start time
setTimeout(function () {
const params = new URLSearchParams(window.location.search);
const time = parseInt(params.get("t"));
if (!isNaN(time)) {
player.currentTime = time;
console.log("Skipping to " + time)
}
if (lastTime > 0) {
player.currentTime = lastTime;
console.log("Skipping to " + lastTime)
}
}, 500);
player.on('ready', () => {
mobileDoubleClick(player);
});
// Server Select
// I hate this...
var settingElements = document.getElementsByClassName('plyr__control--forward');
if (settingElements.length == 3) {
settingElements[2].insertAdjacentHTML('afterend', serverSelectMenuItem(streamServerIndex));
var settingNodes = document.getElementsByClassName('plyr__menu__container')[0].childNodes[0].childNodes;
if (settingNodes.length == 4) {
document.getElementsByClassName('plyr__menu__container')[0].childNodes[0].childNodes[3].insertAdjacentHTML('afterend', serverSelectSubmenu(streamServerIndex, streamServerCount));
}
// Event Listeners
document.getElementById('server-select').addEventListener('click', serverSelectMenuClickToggle);
document.getElementById('server-select-list-back-btn').addEventListener('click', serverSelectMenuClickToggle);
let serverSelects = document.getElementsByClassName('change_server');
for (let i = 0; i < serverSelects.length; i++) {
serverSelects[i].addEventListener('click', function() {
streamServerIndex = Number(this.value);
streamServer = streamServers[streamServerIndex];
console.log('Selected Server: ' + streamServer);
if (player) {
clearInterval(saveInterval);
player.destroy();
}
initPlayer();
});
}
}
// Periodically save last timestamp
saveInterval = setInterval(function () {
lastTime = player.currentTime;
console.log("Last Player Position: " + lastTime);
}, 10000);
}
async function initVidstackPlayer() {
const videoSource = streamServer + '/' + apiResponse.stream_url + '/x264.720p.mp4';
const videoThumbs = streamServer + '/' + apiResponse.stream_url + '/thumbs.vtt';
const videoCaption = streamServer + '/' + apiResponse.stream_url + '/eng.vtt';
player = await VidstackPlayer.create({
target: '#player',
title: apiResponse.title,
src: videoSource,
poster: apiResponse.poster,
layout: new VidstackPlayerLayout({
thumbnails: videoThumbs,
}),
tracks: [
{
src: videoCaption,
label: 'English',
language: 'en-US',
kind: 'subtitles',
type: 'vtt',
default: true,
}
]
});
// Ambient Mode
let canvas = document.getElementById("ambientVideo"), ctx = canvas.getContext("2d"), video = document.getElementsByTagName('video')[0];
setCanvasDimension(canvas, video);
paintStaticVideo(ctx, video);
player.addEventListener('play', () => {
setCanvasDimension(canvas, video);
console.log('Play => Function Loop()');
var $this = video;
(function loop() {
if (!player.paused && !player.ended && localStorage.theme == 'dark' && ambientMode) {
ctx.drawImage($this, 0, 0, $this.offsetWidth, $this.offsetHeight);
setTimeout(loop, 24000 / 1001); // drawing at 30fps
}
})();
});
}
// Get Data from API
window.axios.post('/player/api', {
episode_id: document.getElementById('e_id').value
}).then(function (response) {
if (response.status == 200) {
apiResponse = response.data;
streamServers = apiResponse.stream_domains;
if (serverFallback) {
streamServers = apiResponse.asia_stream_domains;
}
streamServerCount = streamServers.length;
streamServerIndex = Math.floor(Math.random() * streamServerCount);
streamServer = streamServers[streamServerIndex];
console.log('Selected Server: ' + streamServer + ' with Index: ' + streamServerIndex);
if (!isIOS()) {
initPlayer();
}
else {
console.log("Detected Apple Shit. Using different player.")
initVidstackPlayer();
}
}
}).catch(function (error) {
var alert = document.getElementById("player-alert");
alert.innerText = 'The player encountered a problem: ' + error;
alert.classList.remove("hidden");
});

120
resources/js/playlist.js Normal file
View File

@@ -0,0 +1,120 @@
export function playNextPlaylistVideo() {
console.log('Playing next episode');
if (!document.getElementById("playlist_id")) {
console.log('No playlist specified');
return;
}
var playlistId = document.getElementById("playlist_id").value;
var nextEpisode = document.getElementById("playlist_next_episode_slug").value;
if (nextEpisode === ""){
return;
}
window.location.href = '/hentai/' + nextEpisode + '?playlist=' + playlistId;
}
function deleteEntry(playlistId, episodeId) {
window.axios.post('/user/playlist-episode', {
playlist: playlistId,
episode: episodeId
}).then(function (response) {
if (response.status == 200) {
console.log(response);
if (response.data.message == 'success') {
Swal.fire({
title: "Deleted!",
text: "Removed entry from playlist!",
icon: "success",
confirmButtonText: "OK",
willClose: () => {
location.reload();
}
}).then((result) => {
if (result.isConfirmed) {
location.reload();
}
});
}
}
}).catch(function (error) {
Swal.fire({
title: "Error!",
text: error,
icon: "error"
});
console.log(error);
});
}
function addDesktopDeleteListener() {
const deleteButtons = document.querySelectorAll('[id^="delD"]');
deleteButtons.forEach(button => {
const playlist = button.id.split('-')[1];
const episode = button.id.split('-')[2];
console.log("Playlist: " + playlist + " Episode: " + episode);
button.addEventListener('click', () => deleteEntry(playlist, episode));
});
}
// Playlist Swipe (Delete)
document.addEventListener('DOMContentLoaded', () => {
const swipeContainers = document.querySelectorAll('.swipe-container');
var swipeOptions = {
dragLockToAxis: true,
dragBlockHorizontal: true
};
swipeContainers.forEach(container => {
const controls = new Hammer(container, swipeOptions);
const originalColor = container.style.backgroundColor;
const playlistId = container.id.split('-')[0];
const episodeId = container.id.split('-')[1];
const delIcon = document.getElementById('del-' + container.id);
// Set the initial position
let posX = 0;
// Listen for the pan gesture
controls.on('pan', (event) => {
// Update the X position based on the drag delta
posX = event.deltaX;
if (posX > 0) {
// Only allow left swipe
posX = 0;
}
// Apply the translation to the element
container.style.transform = `translateX(${posX}px)`;
container.style.backgroundColor = "rgba(159, 18, 18, 0.3)";
setTimeout(() => {
delIcon.classList.remove('fa-grip-lines-vertical');
delIcon.classList.add('fa-trash');
}, 300);
});
controls.on('panend', () => {
container.style.transition = 'transform 0.3s ease';
container.style.transform = 'translateX(0)';
setTimeout(() => {
container.style.transition = ''; // Reset transition for next drag
container.style.backgroundColor = originalColor;
delIcon.classList.remove('fa-trash');
delIcon.classList.add('fa-grip-lines-vertical');
}, 300);
});
controls.on('swipeleft', (event) => {
container.style.display = 'none';
console.log(playlistId, episodeId);
deleteEntry(playlistId, episodeId);
});
});
addDesktopDeleteListener();
});

56
resources/js/preview.js Normal file
View File

@@ -0,0 +1,56 @@
const sleep = (ms = 0) => new Promise(resolve => setTimeout(resolve, ms));
var old_timestamp = document.getElementById('ts_reference').value;
function initPreviews() {
var thumbs = document.querySelectorAll('div[data-thumbs]');
thumbs.forEach(function (thumb) {
var thumbsJSON = JSON.parse(thumb.dataset.thumbs);
var originalImage = thumb.children[0].children[1].src;
var interval;
var i = 1;
function clear() {
thumb.children[0].children[1].src = originalImage;
i = 1;
clearTimeout(interval);
}
function toggle() {
if (i == 0) {
clear();
return;
}
thumb.children[0].children[1].src = thumbsJSON[i];
i = (i + 1) % thumbsJSON.length;
}
function interval() {
// Start Preview
interval = setInterval(toggle, 700);
}
thumb.addEventListener('mouseenter', interval);
thumb.addEventListener('mouseleave', clear);
});
}
async function init() {
for (let i = 0; i < 9; i++) {
var new_timestamp = document.getElementById('ts_reference').value;
if (new_timestamp != old_timestamp) {
console.log('== Changed ==');
initPreviews();
break;
}
console.log('== Didnt Change ==');
await sleep(1000);
}
}
window.addEventListener('contentChanged', event => {
console.log('== Received contentChanged Event ==');
init();
});
initPreviews();

22
resources/js/theme.js Normal file
View File

@@ -0,0 +1,22 @@
function darkModeListener() {
document.querySelector("html").classList.toggle("dark");
if (localStorage.theme == 'light') {
localStorage.theme = 'dark';
} else {
localStorage.theme = 'light';
}
}
document.querySelector("input[type='checkbox']#toogleTheme").addEventListener("click", darkModeListener);
if(localStorage.theme) {
if (localStorage.theme == 'light') {
if (document.querySelector("html").classList.contains('dark')) {
document.querySelector("html").classList.toggle("dark");
}
document.getElementById("toogleTheme").checked = true;
}
} else {
// Default Dark Theme
localStorage.theme = 'dark';
}

113
resources/js/upload.js Normal file
View File

@@ -0,0 +1,113 @@
import Tagify from '@yaireo/tagify';
import '@yaireo/tagify/dist/tagify.css';
const taginput = document.querySelector("#tags");
const studioinput = document.querySelector("#studio");
// Get Tags from API
window.axios.get('/admin/tags').then(function (response) {
if (response.status != 200) {
return;
}
new Tagify(taginput, {
whitelist: response.data.tags,
dropdown: {
classname: "color-blue",
enabled: 0, // show the dropdown immediately on focus
maxItems: 10,
position: "text", // place the dropdown near the typed text
closeOnSelect: false, // keep the dropdown open after selecting a suggestion
highlightFirst: true
}
});
}).catch(function (error) {
console.log(error);
});
// Get Studios from API
window.axios.get('/admin/studios').then(function (response) {
if (response.status != 200) {
return;
}
new Tagify(studioinput, {
whitelist: response.data.studios,
dropdown: {
classname: "color-blue",
enabled: 0, // show the dropdown immediately on focus
maxItems: 10,
position: "text", // place the dropdown near the typed text
closeOnSelect: false, // keep the dropdown open after selecting a suggestion
highlightFirst: true
}
});
}).catch(function (error) {
console.log(error);
});
let eps = 1;
function dynEpisode() {
let amount = this.value;
if (amount > eps) {
eps += 1;
var episodeUploads = `
<div class="grid grid-cols-2" id="dynU` + eps + `">
<div class="p-4">
<label class="leading-tight text-gray-800 dark:text-gray-200 w-full" for="episodecover` + eps + `">Cover ` + eps + `:</label>
<input class="block w-full text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-rose-800 focus:border-rose-900 dark:bg-neutral-900 dark:border-neutral-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-rose-800 dark:focus:border-rose-900" type="file" name="episodecover` + eps + `" id="episodecover` + eps + `" required>
</div>
<div class="p-4">
<label class="leading-tight text-gray-800 dark:text-gray-200 w-full" for="episodegallery` + eps + `">Gallery ` + eps + `:</label>
<input class="block w-full text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-rose-800 focus:border-rose-900 dark:bg-neutral-900 dark:border-neutral-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-rose-800 dark:focus:border-rose-900" type="file" name="episodegallery` + eps + `[]" id="episodegallery` + eps + `" multiple="">
</div>
</div>
<div class="p-4 pt-0" id="dynB` + eps + `">
<label class="leading-tight text-gray-800 dark:text-gray-200 w-full" for="description` + eps + `">Description ` + eps + `:</label>
<textarea rows="4" cols="50" id="description` + eps + `" name="description` + eps + `" class="mt-1 block w-full border-gray-300 dark:border-gray-700 dark:bg-neutral-900 dark:text-gray-300 focus:border-rose-500 dark:focus:border-rose-600 focus:ring-rose-500 dark:focus:ring-rose-600 rounded-md shadow-sm" required>
</textarea>
</div>
<div class="p-4 pt-0" id="dynD` + eps + `">
<label class="leading-tight text-gray-800 dark:text-gray-200 w-full" for="episodedlurl` + eps + `">Download 1080p ` + eps + `:</label>
<input class="border-gray-300 dark:border-gray-700 dark:bg-neutral-900 dark:text-gray-300 focus:border-rose-500 dark:focus:border-rose-600 focus:ring-rose-500 dark:focus:ring-rose-600 rounded-md shadow-sm block w-full" id="episodedlurl` + eps + `" type="text" name="episodedlurl` + eps + `" required="required">
</div>
<div class="p-4 pt-0" id="dynD48fps` + eps + `">
<label class="leading-tight text-gray-800 dark:text-gray-200 w-full" for="episodedlurlinterpolated` + eps + `">Download 1080p48fps ` + eps + `:</label>
<input class="border-gray-300 dark:border-gray-700 dark:bg-neutral-900 dark:text-gray-300 focus:border-rose-500 dark:focus:border-rose-600 focus:ring-rose-500 dark:focus:ring-rose-600 rounded-md shadow-sm block w-full" id="episodedlurlinterpolated` + eps + `" type="text" name="episodedlurlinterpolated` + eps + `">
</div>
<div class="p-4 pt-0" id="dynD4k` + eps + `">
<label class="leading-tight text-gray-800 dark:text-gray-200 w-full" for="episodedlurl4k` + eps + `">Download 4k ` + eps + `:</label>
<input class="border-gray-300 dark:border-gray-700 dark:bg-neutral-900 dark:text-gray-300 focus:border-rose-500 dark:focus:border-rose-600 focus:ring-rose-500 dark:focus:ring-rose-600 rounded-md shadow-sm block w-full" id="episodedlurl4k` + eps + `" type="text" name="episodedlurl4k` + eps + `" required="required">
</div>
<div class="p-4 pt-0" id="dynDUHD48fps` + eps + `">
<label class="leading-tight text-gray-800 dark:text-gray-200 w-full" for="downloadUHDi` + eps + `">Download 4k 48fps ` + eps + `:</label>
<input class="border-gray-300 dark:border-gray-700 dark:bg-neutral-900 dark:text-gray-300 focus:border-rose-500 dark:focus:border-rose-600 focus:ring-rose-500 dark:focus:ring-rose-600 rounded-md shadow-sm block w-full" id="downloadUHDi` + eps + `" type="text" name="downloadUHDi` + eps + `">
</div>
`;
var element = document.getElementById('moreEpisodes');
element.innerHTML = element.innerHTML + episodeUploads;
} else if (amount < eps) {
if (amount == 0) {
this.value = 1;
return;
}
document.getElementById("dynU" + eps).remove();
document.getElementById("dynD" + eps).remove();
document.getElementById("dynD4k" + eps).remove();
document.getElementById("dynD48fps" + eps).remove();
document.getElementById("dynDUHD48fps" + eps).remove();
document.getElementById("dynB" + eps).remove();
eps -= 1;
}
}
document.getElementById("episodes").addEventListener('change', dynEpisode);

View File

@@ -0,0 +1,28 @@
import Tagify from '@yaireo/tagify';
import '@yaireo/tagify/dist/tagify.css';
const taginput = document.querySelector("#tags");
// Get Tags from API
window.axios.get('/user/blacklist').then(function (response) {
if (response.status != 200) {
return;
}
var tagify = new Tagify(taginput, {
whitelist: response.data.tags,
enforceWhitelist: true,
dropdown: {
classname: "color-blue",
enabled: 0, // show the dropdown immediately on focus
maxItems: 10,
position: "text", // place the dropdown near the typed text
closeOnSelect: false, // keep the dropdown open after selecting a suggestion
highlightFirst: true,
}
});
tagify.addTags(response.data.usertags);
}).catch(function (error) {
console.log(error);
});