From 5f9dfa12f452d2b6b468bd8a3794da5efb564a30 Mon Sep 17 00:00:00 2001 From: itdominator <1itdominator@gmail.com> Date: Fri, 31 Jan 2025 23:24:30 -0600 Subject: [PATCH] Initial srt support setup; ntfy bind change, seek event alterations; js-cookie file upgraded --- src/core/__builtins__.py | 2 +- src/core/routes/Routes.py | 19 +- src/core/static/css/webfm/main.css | 9 + .../backgrounds/{000.png => background.png} | Bin src/core/static/js/libs/js.cookie.js | 265 +++++++++--------- src/core/static/js/libs/srt-support.js | 2 + src/core/static/js/webfm/context-menu.js | 32 +++ src/core/static/js/webfm/ui-logic.js | 5 +- src/core/static/js/webfm/video-events.js | 85 +++++- src/core/templates/context-menu.html | 1 + src/core/templates/layout.html | 1 + src/core/templates/modals/file-modal.html | 5 +- 12 files changed, 272 insertions(+), 154 deletions(-) rename src/core/static/imgs/backgrounds/{000.png => background.png} (100%) create mode 100644 src/core/static/js/libs/srt-support.js diff --git a/src/core/__builtins__.py b/src/core/__builtins__.py index 86cfa8f..f8330b8 100644 --- a/src/core/__builtins__.py +++ b/src/core/__builtins__.py @@ -96,7 +96,7 @@ def _start_rtsp_and_ntfy_server(): @daemon_threaded def _start_ntfy_server_threaded(): os.chdir(NTFY_PATH) - command = ["./ntfy", "serve", "--behind-proxy", "--listen-http", ":7777"] + command = ["./ntfy", "serve", "--behind-proxy", "--listen-http", "127.0.0.1:7777"] process = subprocess.Popen(command) process.wait() diff --git a/src/core/routes/Routes.py b/src/core/routes/Routes.py index eac711d..03a1512 100644 --- a/src/core/routes/Routes.py +++ b/src/core/routes/Routes.py @@ -4,6 +4,7 @@ import requests import uuid # Lib imports +from flask import abort from flask import make_response from flask import redirect from flask import request @@ -42,6 +43,17 @@ def home(): message = 'Must use GET request type...') +# NOTE: Yeah, not exactly logged but meh. +@app.route('/log-client-exception', methods=['GET', 'POST']) +def ui_failure_exception_tracker(): + if request.method == 'POST': + DATA = str(request.values['exception_data']).strip() + print(f"\n\n{DATA}") + return json_message.create("success", "UI Exception logged...") + + return json_message.create("danger", "Must use POST request type...") + + @app.route('/api/list-files/<_hash>', methods=['GET', 'POST']) def list_files(_hash = None): if request.method == 'POST': @@ -93,7 +105,12 @@ def file_manager_action(_type, _hash = None): folder = view.get_current_directory() file = view.get_path_part_from_hash(_hash) - fpath = os.path.join(folder, file) + fpath = None; + try: + fpath = os.path.join(folder, file) + except Exception as e: + return abort(404) + logger.debug(fpath) if _type == "files": diff --git a/src/core/static/css/webfm/main.css b/src/core/static/css/webfm/main.css index 7a27d4a..4dd08a1 100644 --- a/src/core/static/css/webfm/main.css +++ b/src/core/static/css/webfm/main.css @@ -46,6 +46,15 @@ margin: 0 auto; } +#selectedFile, +#video-controls { + font-size: x-large; +} + +#seek-slider { + width: -moz-available !important; +} + /* CLASSES */ .scroller { diff --git a/src/core/static/imgs/backgrounds/000.png b/src/core/static/imgs/backgrounds/background.png similarity index 100% rename from src/core/static/imgs/backgrounds/000.png rename to src/core/static/imgs/backgrounds/background.png diff --git a/src/core/static/js/libs/js.cookie.js b/src/core/static/js/libs/js.cookie.js index 6d0965a..872e936 100644 --- a/src/core/static/js/libs/js.cookie.js +++ b/src/core/static/js/libs/js.cookie.js @@ -1,163 +1,148 @@ -/*! - * JavaScript Cookie v2.2.0 - * https://github.com/js-cookie/js-cookie - * - * Copyright 2006, 2015 Klaus Hartl & Fagner Brack - * Released under the MIT license - */ -;(function (factory) { - var registeredInModuleLoader; - if (typeof define === 'function' && define.amd) { - define(factory); - registeredInModuleLoader = true; - } - if (typeof exports === 'object') { - module.exports = factory(); - registeredInModuleLoader = true; - } - if (!registeredInModuleLoader) { - var OldCookies = window.Cookies; - var api = window.Cookies = factory(); - api.noConflict = function () { - window.Cookies = OldCookies; - return api; - }; - } -}(function () { - function extend () { - var i = 0; - var result = {}; - for (; i < arguments.length; i++) { - var attributes = arguments[ i ]; - for (var key in attributes) { - result[key] = attributes[key]; - } - } - return result; - } - function decode (s) { - return s.replace(/(%[0-9A-Z]{2})+/g, decodeURIComponent); - } +/*! js-cookie v3.0.4 | MIT */ +; +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = typeof globalThis !== 'undefined' ? globalThis : global || self, (function () { + var current = global.Cookies; + var exports = global.Cookies = factory(); + exports.noConflict = function () { global.Cookies = current; return exports; }; + })()); +})(this, (function () { 'use strict'; - function init (converter) { - function api() {} +/* eslint-disable no-var */ +function assign (target) { + for (var i = 1; i < arguments.length; i++) { + var source = arguments[i]; + for (var key in source) { + target[key] = source[key]; + } + } + return target +} +/* eslint-enable no-var */ - function set (key, value, attributes) { - if (typeof document === 'undefined') { - return; - } +/* eslint-disable no-var */ +var defaultConverter = { + read: function (value) { + if (value[0] === '"') { + value = value.slice(1, -1); + } + return value.replace(/(%[\dA-F]{2})+/gi, decodeURIComponent) + }, + write: function (value) { + return encodeURIComponent(value).replace( + /%(2[346BF]|3[AC-F]|40|5[BDE]|60|7[BCD])/g, + decodeURIComponent + ) + } +}; +/* eslint-enable no-var */ - attributes = extend({ - path: '/' - }, api.defaults, attributes); +/* eslint-disable no-var */ - if (typeof attributes.expires === 'number') { - attributes.expires = new Date(new Date() * 1 + attributes.expires * 864e+5); - } +function init (converter, defaultAttributes) { + function set (name, value, attributes) { + if (typeof document === 'undefined') { + return + } - // We're using "expires" because "max-age" is not supported by IE - attributes.expires = attributes.expires ? attributes.expires.toUTCString() : ''; + attributes = assign({}, defaultAttributes, attributes); - try { - var result = JSON.stringify(value); - if (/^[\{\[]/.test(result)) { - value = result; - } - } catch (e) {} + if (typeof attributes.expires === 'number') { + attributes.expires = new Date(Date.now() + attributes.expires * 864e5); + } + if (attributes.expires) { + attributes.expires = attributes.expires.toUTCString(); + } - value = converter.write ? - converter.write(value, key) : - encodeURIComponent(String(value)) - .replace(/%(23|24|26|2B|3A|3C|3E|3D|2F|3F|40|5B|5D|5E|60|7B|7D|7C)/g, decodeURIComponent); + name = encodeURIComponent(name) + .replace(/%(2[346B]|5E|60|7C)/g, decodeURIComponent) + .replace(/[()]/g, escape); - key = encodeURIComponent(String(key)) - .replace(/%(23|24|26|2B|5E|60|7C)/g, decodeURIComponent) - .replace(/[\(\)]/g, escape); + var stringifiedAttributes = ''; + for (var attributeName in attributes) { + if (!attributes[attributeName]) { + continue + } - var stringifiedAttributes = ''; - for (var attributeName in attributes) { - if (!attributes[attributeName]) { - continue; - } - stringifiedAttributes += '; ' + attributeName; - if (attributes[attributeName] === true) { - continue; - } + stringifiedAttributes += '; ' + attributeName; - // Considers RFC 6265 section 5.2: - // ... - // 3. If the remaining unparsed-attributes contains a %x3B (";") - // character: - // Consume the characters of the unparsed-attributes up to, - // not including, the first %x3B (";") character. - // ... - stringifiedAttributes += '=' + attributes[attributeName].split(';')[0]; - } + if (attributes[attributeName] === true) { + continue + } - return (document.cookie = key + '=' + value + stringifiedAttributes); - } + // Considers RFC 6265 section 5.2: + // ... + // 3. If the remaining unparsed-attributes contains a %x3B (";") + // character: + // Consume the characters of the unparsed-attributes up to, + // not including, the first %x3B (";") character. + // ... + stringifiedAttributes += '=' + attributes[attributeName].split(';')[0]; + } - function get (key, json) { - if (typeof document === 'undefined') { - return; - } + return (document.cookie = + name + '=' + converter.write(value, name) + stringifiedAttributes) + } - var jar = {}; - // To prevent the for loop in the first place assign an empty array - // in case there are no cookies at all. - var cookies = document.cookie ? document.cookie.split('; ') : []; - var i = 0; + function get (name) { + if (typeof document === 'undefined' || (arguments.length && !name)) { + return + } - for (; i < cookies.length; i++) { - var parts = cookies[i].split('='); - var cookie = parts.slice(1).join('='); + // To prevent the for loop in the first place assign an empty array + // in case there are no cookies at all. + var cookies = document.cookie ? document.cookie.split('; ') : []; + var jar = {}; + for (var i = 0; i < cookies.length; i++) { + var parts = cookies[i].split('='); + var value = parts.slice(1).join('='); - if (!json && cookie.charAt(0) === '"') { - cookie = cookie.slice(1, -1); - } + try { + var found = decodeURIComponent(parts[0]); + jar[found] = converter.read(value, found); - try { - var name = decode(parts[0]); - cookie = (converter.read || converter)(cookie, name) || - decode(cookie); + if (name === found) { + break + } + } catch (e) {} + } - if (json) { - try { - cookie = JSON.parse(cookie); - } catch (e) {} - } + return name ? jar[name] : jar + } - jar[name] = cookie; + return Object.create( + { + set, + get, + remove: function (name, attributes) { + set( + name, + '', + assign({}, attributes, { + expires: -1 + }) + ); + }, + withAttributes: function (attributes) { + return init(this.converter, assign({}, this.attributes, attributes)) + }, + withConverter: function (converter) { + return init(assign({}, this.converter, converter), this.attributes) + } + }, + { + attributes: { value: Object.freeze(defaultAttributes) }, + converter: { value: Object.freeze(converter) } + } + ) + } - if (key === name) { - break; - } - } catch (e) {} - } + var api = init(defaultConverter, { path: '/' }); + /* eslint-enable no-var */ - return key ? jar[key] : jar; - } + return api; - api.set = set; - api.get = function (key) { - return get(key, false /* read as raw */); - }; - api.getJSON = function (key) { - return get(key, true /* read as json */); - }; - api.remove = function (key, attributes) { - set(key, '', extend(attributes, { - expires: -1 - })); - }; - - api.defaults = {}; - - api.withConverter = init; - - return api; - } - - return init(function () {}); })); diff --git a/src/core/static/js/libs/srt-support.js b/src/core/static/js/libs/srt-support.js new file mode 100644 index 0000000..9cffb72 --- /dev/null +++ b/src/core/static/js/libs/srt-support.js @@ -0,0 +1,2 @@ +(function(){"use strict";class i{number;startTime;endTime;text;constructor(t,e,s,r){this.number=t,this.startTime=e,this.endTime=s,this.text=r}}function c(n){try{if(!n||n===" ")return!1;const t={number:parseInt(n.match(/^\d+/g)[0]),timing:{start:n.match(/(\d+:){2}\d+,\d+/g)[0].replace(",","."),end:n.match(/(\d+:){2}\d+,\d+/g)[1].replace(",",".")},text:n.split(/\r?\n/g).slice(2,n.split(/\r?\n/g).length).join(` +`)};return new i(t.number,a(t.timing.start),a(t.timing.end),t.text)}catch{return!1}}function a(n){let t=n.split(":"),e=0,s=1;for(;t.length>0;)e+=s*parseFloat(t.pop(),10),s*=60;return e}async function o(n,t="utf-8"){return(!t||t==="")&&(t="utf-8"),fetch(n).then(e=>e.arrayBuffer()).then(e=>new TextDecoder(t).decode(e))}class d{src;encoding;lang;kind;label;default;body;needsTransform;cues=[];constructor(t){this.src=t.src,this.encoding=t.dataset.encoding,this.lang=t.srclang,this.kind=t.kind,this.label=t.label,this.default=t.default,this.needsTransform=!this.src.toLowerCase().endsWith(".vtt")}async parse(){this.body=await o(this.src,this.encoding),this.cues=this.body.split(/\r?\n\r?\n/g).map(c).filter(Boolean)}}async function l(n){const t=[...n.querySelectorAll("track")].map(e=>new d(e));for(const e of t){if(!e.needsTransform)continue;await e.parse();const s=n.addTextTrack(e.kind,e.label,e.lang);e.cues.forEach(r=>s.addCue(new VTTCue(r.startTime,r.endTime,r.text))),e.default&&(s.mode="showing")}}document.addEventListener("DOMContentLoaded",()=>[...document.querySelectorAll("video")].forEach(l))})(); diff --git a/src/core/static/js/webfm/context-menu.js b/src/core/static/js/webfm/context-menu.js index 90fa7bf..7f27f1d 100644 --- a/src/core/static/js/webfm/context-menu.js +++ b/src/core/static/js/webfm/context-menu.js @@ -1,17 +1,41 @@ const img2TabElm = document.getElementById("img2Tab"); const ctxDownloadElm = document.getElementById("ctxDownload"); +const txt2copy = document.getElementById("txt2copy"); const menu = document.querySelector(".menu"); +let text2copy = ""; let menuVisible = false; let img2TabSrc = null; let active_card = null; + const img2Tab = () => { if (img2TabSrc !== null) { window.open(img2TabSrc,'_blank'); } }; +const text2Clipboard = () => { + try { + navigator.clipboard.writeText(text2copy); + } catch (err) { + console.error('Failed to copy: ', err); + } +} + +const getSelectedText = () => { + let text = ""; + + if (typeof window.getSelection != "undefined") { + text = window.getSelection().toString(); + } else if (typeof document.selection != "undefined" && document.selection.type == "Text") { + text = document.selection.createRange().text; + } + + return text; +} + + const toggleMenu = command => { menu.style.display = command === "show" ? "block" : "none"; menu.style.zIndex = "9999"; @@ -25,6 +49,13 @@ const setPosition = ({ top, left }) => { }; +document.body.addEventListener("mouseup", e => { + const data = getSelectedText(); + if (e.which !== 3 && e.target.innerText !== "Copy") { + text2copy = data; + } +}); + document.body.addEventListener("click", e => { if(menuVisible) toggleMenu("hide"); }); @@ -42,6 +73,7 @@ document.body.addEventListener("contextmenu", e => { elm.getAttribute("ftype") === "image") )) ? "block" : "none"; img2TabElm.style.display = (elm.nodeName === "IMG") ? "block" : "none"; + txt2copy.style.display = (text2copy !== "") ? "block" : "none"; img2TabSrc = (elm.nodeName === "IMG") ? elm.src : null; while (elm.nodeName != "BODY") { diff --git a/src/core/static/js/webfm/ui-logic.js b/src/core/static/js/webfm/ui-logic.js index a498b37..123b7fc 100644 --- a/src/core/static/js/webfm/ui-logic.js +++ b/src/core/static/js/webfm/ui-logic.js @@ -52,15 +52,16 @@ const scrollFilesToTop = () => { const closeFile = async () => { const trailerPlayer = document.getElementById("trailerPlayer") let title = document.getElementById("selectedFile"); + let player = document.getElementById("video"); document.getElementById("video-container").style.display = "node"; document.getElementById("image-viewer").style.display = "none"; document.getElementById("text-viewer").style.display = "none"; document.getElementById("pdf-viewer").style.display = "none"; - player.jPlayer("pause") title.innerText = ""; trailerPlayer.src = "#"; + player.pause(); trailerPlayer.style.display = "none"; // FIXME: Yes, a wasted call every time there is no stream. @@ -386,4 +387,4 @@ const getSHA256Hash = async (input) => { const hash = hashArray.map((item) => item.toString(16).padStart(2, "0")).join(""); return hash.substring(0, 18); -}; +}; \ No newline at end of file diff --git a/src/core/static/js/webfm/video-events.js b/src/core/static/js/webfm/video-events.js index 49ef97c..4e25ebd 100644 --- a/src/core/static/js/webfm/video-events.js +++ b/src/core/static/js/webfm/video-events.js @@ -4,16 +4,23 @@ let shouldPlay = null; let controlsTimeout = null; let playListMode = false; let videoPlaylist = []; +let seekto = null; + const getTimeFormatted = (duration = null) => { - if (duration == null) { return "00:00"; } + if (duration == null) { return "00:00:00"; } - const hours = (duration / 3600).toFixed(2).split(".")[0]; - const minutes = (duration / 60).toFixed(2).split(".")[0]; - const time = (duration / 60).toFixed(2) - const seconds = Math.floor( (time - Math.floor(time) ) * 60); + const hours = (duration / 3600).toFixed(2).split(".")[0]; + let _duration = (duration / 60).toFixed(2).split(".")[0] + const minutes = (_duration - (hours * 60)).toFixed(2).split(".")[0]; + const time = (duration / 60).toFixed(2) + const seconds = Math.floor( (time - Math.floor(time) ) * 60); - return hours + ":" + minutes + ":" + seconds; + return padnum(hours) + " : " + padnum(minutes) + " : " + padnum(seconds); +} + +const padnum = (value = 0) => { + return (value > 9) ? value : "0" + value; } @@ -115,9 +122,49 @@ const loadMediaToPlayer = (title = "", video_path = "") => { const modal = new bootstrap.Modal(document.getElementById('file-view-modal'), { keyboard: false }); const player = document.getElementById("video"); player.src = video_path; + + loadSubtitles(title); modal.show(); } +const insertSubtitle = (path = "", label = "English Track: NONE") => { + fetch(path).then((response) => { + if(response.status == 200) { + const player = document.getElementById("video"); + let track = document.createElement("TRACK"); + + track.srclang = "en"; + track.kind = "subtitles"; + track.label = label; + track.src = path; + track.setAttribute("default", ""); + + player.appendChild(track); + } + }).catch(function(error) { + let subStr1 = 'There has been a problem with your fetch operation: ' + error.message; + msg = "[Error] Status Code: 000\n[Message] -->" + subStr1; + return {'message': { 'type': "error", 'text': msg} } + }); +} + +const loadSubtitles = (title = "") => { + const player = document.getElementById("video"); + const subtitle_stub = title.split(".")[0]; + const subs = [`${subtitle_stub}.vtt`, `${subtitle_stub}.en.vtt`, `${subtitle_stub}.srt`, `${subtitle_stub}.en.srt`]; + + clearChildNodes(player) + let k = 0; + for (var i = 0; i < subs.length; i++) { + getSHA256Hash(subs[i]).then((_hash) => { + const data = "empty=NULL"; + subtitle_path = "api/file-manager-action/files/" + _hash; + insertSubtitle(subtitle_path, subs[k]); + k += 1; + }); + } +} + @@ -221,12 +268,32 @@ $( "#video").bind( "stalled", async function(eve) { }); $( "#seek-slider").bind( "change", async function(eve) { - const slider = eve.target; - let video = document.getElementById("video"); - let seekto = video.duration * (slider.value / 100); + const video = document.getElementById("video"); video.currentTime = seekto; }); +$( "#seek-slider").bind( "mousemove", async function(eve) { + const slider = eve.target; + const video = document.getElementById("video"); + const seek = document.getElementById("seek-time"); + const rect = slider.getBoundingClientRect() + const offset = rect.right - rect.width; + + const slider_val = Math.floor( + ( + (eve.pageX - offset) / (rect.right - offset) + ) * 100 + ) + + seekto = video.duration * (slider_val / 100); + seek.innerText = getTimeFormatted(seekto); +}); + +$( "#seek-slider").bind( "mouseleave", async function(eve) { + const seekto = document.getElementById("seek-time"); + seekto.innerText = ""; +}); + $( "#volume-slider").bind( "change", async function(eve) { const slider = eve.target; let video = document.getElementById("video"); diff --git a/src/core/templates/context-menu.html b/src/core/templates/context-menu.html index a6dc49c..6ad281e 100644 --- a/src/core/templates/context-menu.html +++ b/src/core/templates/context-menu.html @@ -4,6 +4,7 @@