Updated the new loader (Images still missing since they're not yet finished)

canary
WolverinDEV 2020-07-13 11:29:16 +02:00
parent 4365e03dbe
commit 8757a46b91
28 changed files with 2216 additions and 719 deletions

3
.gitignore vendored
View File

@ -25,9 +25,10 @@ node_modules/
/todo.txt /todo.txt
/tmp/ /tmp/
# All out config files are .ts files # All our config files are .ts files
/*.js /*.js
/*.js.map /*.js.map
!babel.config.js
/webpack/*.js /webpack/*.js
/webpack/*.js.map /webpack/*.js.map

View File

@ -1,4 +1,8 @@
# Changelog: # Changelog:
* **12.07.20**
- Made the loader compatible with ES5 to support older browsers
- Updated the loader animation
* **15.06.20** * **15.06.20**
- Recoded the permission editor with react - Recoded the permission editor with react
- Fixed sever permission editor display bugs - Fixed sever permission editor display bugs

27
babel.config.js Normal file
View File

@ -0,0 +1,27 @@
module.exports = function (api) {
api.cache(false);
const presets = [
[
"@babel/preset-env",
{
"corejs": { "version":3 },
"useBuiltIns": "usage",
"targets": {
"edge": "17",
"firefox": "60",
"chrome": "67",
"safari": "11.1",
"ie": "11"
}
}
]
];
const plugins = [
["@babel/transform-runtime"],
["@babel/plugin-transform-modules-commonjs"]
];
return {
presets,
plugins
};
};

View File

@ -121,7 +121,7 @@ const APP_FILE_LIST_SHARED_SOURCE: ProjectResource[] = [
}, },
{ /* shared image files */ { /* shared image files */
"type": "img", "type": "img",
"search-pattern": /.*\.(svg|png)/, "search-pattern": /.*\.(svg|png|gif)/,
"build-target": "dev|rel", "build-target": "dev|rel",
"path": "img/", "path": "img/",

31
loader/IndexGenerator.ts Normal file
View File

@ -0,0 +1,31 @@
import * as path from "path";
import EJSGenerator = require("../webpack/EJSGenerator");
class IndexGenerator extends EJSGenerator {
constructor(options: {
buildTarget: string;
output: string,
isDevelopment: boolean
}) {
super({
variables: {
build_target: options.buildTarget
},
output: options.output,
initialJSEntryChunk: "loader",
input: path.join(__dirname, "html/index.html.ejs"),
minify: !options.isDevelopment,
embedInitialJSEntryChunk: !options.isDevelopment,
embedInitialCSSFile: !options.isDevelopment,
initialCSSFile: {
localFile: path.join(__dirname, "css/index.css"),
publicFile: "css/index.css"
}
});
}
}
export = IndexGenerator;

128
loader/app/animation.ts Normal file
View File

@ -0,0 +1,128 @@
import * as loader from "./loader/loader";
import {Stage} from "./loader/loader";
let overlay: HTMLDivElement;
let setupContainer: HTMLDivElement;
let idleContainer: HTMLDivElement;
let idleSteamContainer: HTMLDivElement;
let loaderStageContainer: HTMLDivElement;
let finalizing = false;
let initializeTimestamp;
let verbose = false;
let apngSupport = undefined;
async function detectAPNGSupport() {
const image = new Image();
const ctx = document.createElement("canvas").getContext("2d");
// frame 1 (skipped on apng-supporting browsers): [0, 0, 0, 255]
// frame 2: [0, 0, 0, 0]
image.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACGFjVEwAAAABAAAAAcMq2TYAAAANSURBVAiZY2BgYPgPAAEEAQB9ssjfAAAAGmZjVEwAAAAAAAAAAQAAAAEAAAAAAAAAAAD6A+gBAbNU+2sAAAARZmRBVAAAAAEImWNgYGBgAAAABQAB6MzFdgAAAABJRU5ErkJggg==";
await new Promise(resolve => image.onload = resolve);
ctx.drawImage(image, 0, 0);
apngSupport = ctx.getImageData(0, 0, 1, 1).data[3] === 0;
console.log("Browser APNG support: %o", apngSupport);
}
function initializeElements() {
overlay = document.getElementById("loader-overlay") as HTMLDivElement;
if(!overlay)
throw "missing loader overlay";
for(const lazyImage of [...overlay.getElementsByTagName("lazy-img")]) {
const image = document.createElement("img");
image.alt = lazyImage.getAttribute("alt");
image.src = lazyImage.getAttribute(apngSupport ? "src-apng" : "src-gif") || lazyImage.getAttribute("src");
image.className = lazyImage.className;
lazyImage.replaceWith(image);
}
setupContainer = overlay.getElementsByClassName("setup")[0] as HTMLDivElement;
if(!setupContainer)
throw "missing setup container";
idleContainer = overlay.getElementsByClassName("idle")[0] as HTMLDivElement;
if(!idleContainer)
throw "missing idle container";
idleSteamContainer = idleContainer.getElementsByClassName("steam")[0] as HTMLDivElement;
if(!idleSteamContainer)
throw "missing idle steam container";
loaderStageContainer = overlay.getElementsByClassName("loader-stage")[0] as HTMLDivElement;
if(!loaderStageContainer)
throw "missing loader stage container";
setupContainer.onanimationend = setupAnimationFinished;
idleSteamContainer.onanimationiteration = idleSteamAnimationLooped;
overlay.onanimationend = overlayAnimationFinished;
}
export async function initialize() {
await detectAPNGSupport();
try {
initializeElements();
} catch (error) {
console.error("Failed to setup animations: %o", error);
loader.critical_error("Animation setup failed", error);
return false;
}
StageNames[Stage.SETUP] = "starting app";
StageNames[Stage.TEMPLATES] = "loading templates";
StageNames[Stage.STYLE] = "loading styles";
StageNames[Stage.JAVASCRIPT] = "loading app";
StageNames[Stage.JAVASCRIPT_INITIALIZING] = "initializing";
StageNames[Stage.FINALIZING] = "rounding up";
StageNames[Stage.LOADED] = "starting app";
overlay.classList.add("initialized");
setupContainer.classList.add("visible");
initializeTimestamp = Date.now();
return true;
}
export function abort() {
overlay?.remove();
}
export function finalize() {
finalizing = true;
if(loaderStageContainer)
loaderStageContainer.innerText = "app loaded successfully (" + (Date.now() - initializeTimestamp) + "ms)";
}
const StageNames = {};
export function updateState(state: Stage, tasks: string[]) {
if(loaderStageContainer)
loaderStageContainer.innerText = StageNames[state] + (tasks.length === 1 ? " (task: " + tasks[0] + ")" : " (tasks: " + tasks.join(",") + ")");
}
function setupAnimationFinished() {
verbose && console.log("Entering idle animation");
setupContainer.classList.remove("visible");
idleContainer.classList.add("visible");
}
function idleSteamAnimationLooped() {
verbose && console.log("Idle animation looped. Should finalize: %o", finalizing);
if(!finalizing)
return;
overlay.classList.add("finishing");
}
function overlayAnimationFinished(event: AnimationEvent) {
/* the text animation is the last one */
if(event.animationName !== "swipe-out-text")
return;
verbose && console.log("Animation finished");
overlay.remove();
}

View File

@ -1,8 +1,15 @@
import * as loader from "./targets/app"; import "core-js/stable";
import * as loader_base from "./loader/loader"; import "./polifill";
window["loader"] = loader_base;
/* let the loader register himself at the window first */ import * as loader from "./loader/loader";
setTimeout(loader.run, 0); window["loader"] = loader;
/* let the loader register himself at the window first */
import * as AppLoader from "./targets/app";
setTimeout(AppLoader.run, 0);
import * as EmptyLoader from "./targets/empty";
//setTimeout(EmptyLoader.run, 0);
export {}; export {};

View File

@ -1,6 +1,7 @@
import * as script_loader from "./script_loader"; import * as script_loader from "./script_loader";
import * as style_loader from "./style_loader"; import * as style_loader from "./style_loader";
import * as template_loader from "./template_loader"; import * as template_loader from "./template_loader";
import * as Animation from "../animation";
declare global { declare global {
interface Window { interface Window {
@ -31,7 +32,7 @@ export let config: Config = {
export type Task = { export type Task = {
name: string, name: string,
priority: number, /* tasks with the same priority will be executed in sync */ priority: number, /* tasks with the same priority will be executed in sync */
function: () => Promise<void> function: (taskId?: number) => Promise<void>
}; };
export enum Stage { export enum Stage {
@ -39,30 +40,37 @@ export enum Stage {
loading loader required files (incl this) loading loader required files (incl this)
*/ */
INITIALIZING, INITIALIZING,
/* /*
setting up the loading process setting up the loading process
*/ */
SETUP, SETUP,
/* /*
loading all style sheet files loading all style sheet files
*/ */
STYLE, STYLE,
/* /*
loading all javascript files loading all javascript files
*/ */
JAVASCRIPT, JAVASCRIPT,
/* /*
loading all template files loading all template files
*/ */
TEMPLATES, TEMPLATES,
/* /*
initializing static/global stuff initializing static/global stuff
*/ */
JAVASCRIPT_INITIALIZING, JAVASCRIPT_INITIALIZING,
/* /*
finalizing load process finalizing load process
*/ */
FINALIZING, FINALIZING,
/* /*
invoking main task invoking main task
*/ */
@ -72,7 +80,7 @@ export enum Stage {
} }
let cache_tag: string | undefined; let cache_tag: string | undefined;
let current_stage: Stage = undefined; let currentStage: Stage = undefined;
const tasks: {[key:number]:Task[]} = {}; const tasks: {[key:number]:Task[]} = {};
/* test if all files shall be load from cache or fetch again */ /* test if all files shall be load from cache or fetch again */
@ -109,12 +117,12 @@ export function module_mapping() : ModuleMapping[] { return module_mapping_; }
export function get_cache_version() { return cache_tag; } export function get_cache_version() { return cache_tag; }
export function finished() { export function finished() {
return current_stage == Stage.DONE; return currentStage == Stage.DONE;
} }
export function running() { return typeof(current_stage) !== "undefined"; } export function running() { return typeof(currentStage) !== "undefined"; }
export function register_task(stage: Stage, task: Task) { export function register_task(stage: Stage, task: Task) {
if(current_stage > stage) { if(currentStage > stage) {
if(config.error) if(config.error)
console.warn("Register loading task, but it had already been finished. Executing task anyways!"); console.warn("Register loading task, but it had already been finished. Executing task anyways!");
@ -139,20 +147,41 @@ export function register_task(stage: Stage, task: Task) {
tasks[stage] = task_array.sort((a, b) => a.priority - b.priority); tasks[stage] = task_array.sort((a, b) => a.priority - b.priority);
} }
type RunningTask = {
taskId: number,
name: string,
promise: Promise<void> | undefined
};
let runningTasks: RunningTask[] = [];
let runningTaskIdIndex = 1;
export function setCurrentTaskName(taskId: number, name: string) {
const task = runningTasks.find(e => e.taskId === taskId);
if(!task) {
console.warn("Tried to set task name of unknown task %d", taskId);
return;
}
task.name = name;
Animation.updateState(currentStage, runningTasks.map(e => e.name));
}
export async function execute() { export async function execute() {
document.getElementById("loader-overlay").classList.add("started"); if(!await Animation.initialize())
return;
loader_cache_tag(); loader_cache_tag();
const load_begin = Date.now(); const load_begin = Date.now();
let begin: number = 0; let begin: number = 0;
let end: number = Date.now(); let end: number = Date.now();
while(current_stage <= Stage.LOADED || typeof(current_stage) === "undefined") { while(currentStage <= Stage.LOADED || typeof(currentStage) === "undefined") {
let current_tasks: Task[] = []; let pendingTasks: Task[] = [];
while((tasks[current_stage] || []).length > 0) { while((tasks[currentStage] || []).length > 0) {
if(current_tasks.length == 0 || current_tasks[0].priority == tasks[current_stage][0].priority) { if(pendingTasks.length == 0 || pendingTasks[0].priority == tasks[currentStage][0].priority) {
current_tasks.push(tasks[current_stage].pop()); pendingTasks.push(tasks[currentStage].pop());
} else break; } else break;
} }
@ -161,23 +190,45 @@ export async function execute() {
task: Task task: Task
}[] = []; }[] = [];
const promises: Promise<void>[] = []; for(const task of pendingTasks) {
for(const task of current_tasks) { const rTask = {
taskId: ++runningTaskIdIndex,
name: task.name,
promise: undefined
} as RunningTask;
try { try {
if(config.verbose) console.debug("Executing loader %s (%d)", task.name, task.priority); if(config.verbose)
const promise = task.function(); console.debug("Executing loader %s (%d)", task.name, task.priority);
runningTasks.push(rTask);
const promise = task.function(rTask.taskId);
if(!promise) { if(!promise) {
runningTasks.splice(runningTasks.indexOf(rTask), 1);
console.error("Loading task %s hasn't returned a promise!", task.name); console.error("Loading task %s hasn't returned a promise!", task.name);
continue; continue;
} }
promises.push(promise.catch(error => {
rTask.promise = promise.catch(error => {
errors.push({ errors.push({
task: task, task: task,
error: error error: error
}); });
return Promise.resolve(); return Promise.resolve();
})); }).then(() => {
const index = runningTasks.indexOf(rTask);
if(index === -1) {
console.warn("Running task (%s) finished, but it has been unregistered already!", task.name);
return;
}
runningTasks.splice(index, 1);
});
} catch(error) { } catch(error) {
const index = runningTasks.indexOf(rTask);
if(index !== -1)
runningTasks.splice(index, 1);
errors.push({ errors.push({
task: task, task: task,
error: error error: error
@ -185,41 +236,47 @@ export async function execute() {
} }
} }
if(promises.length > 0) { if(runningTasks.length > 0) {
await Promise.all([...promises]); Animation.updateState(currentStage, runningTasks.map(e => e.name));
await Promise.all(runningTasks.map(e => e.promise));
} }
if(errors.length > 0) { if(errors.length > 0) {
if(config.loader_groups) console.groupEnd(); if(config.loader_groups)
console.groupEnd();
console.error("Failed to execute loader. The following tasks failed (%d):", errors.length); console.error("Failed to execute loader. The following tasks failed (%d):", errors.length);
for(const error of errors) for(const error of errors)
console.error(" - %s: %o", error.task.name, error.error); console.error(" - %s: %o", error.task.name, error.error);
throw "failed to process step " + Stage[current_stage]; throw "failed to process step " + Stage[currentStage];
} }
if(current_tasks.length == 0) { if(pendingTasks.length == 0) {
if(typeof(current_stage) === "undefined") { if(typeof(currentStage) === "undefined") {
current_stage = -1; currentStage = -1;
if(config.verbose) console.debug("[loader] Booting app"); if(config.verbose) console.debug("[loader] Booting app");
} else if(current_stage < Stage.INITIALIZING) { } else if(currentStage < Stage.INITIALIZING) {
if(config.loader_groups) console.groupEnd(); if(config.loader_groups) console.groupEnd();
if(config.verbose) console.debug("[loader] Entering next state (%s). Last state took %dms", Stage[current_stage + 1], (end = Date.now()) - begin); if(config.verbose) console.debug("[loader] Entering next state (%s). Last state took %dms", Stage[currentStage + 1], (end = Date.now()) - begin);
} else { } else {
if(config.loader_groups) console.groupEnd(); if(config.loader_groups) console.groupEnd();
if(config.verbose) console.debug("[loader] Finish invoke took %dms", (end = Date.now()) - begin); if(config.verbose) console.debug("[loader] Finish invoke took %dms", (end = Date.now()) - begin);
} }
begin = end; begin = end;
current_stage += 1; currentStage += 1;
if(current_stage != Stage.DONE && config.loader_groups) if(currentStage != Stage.DONE && config.loader_groups)
console.groupCollapsed("Executing loading stage %s", Stage[current_stage]); console.groupCollapsed("Executing loading stage %s", Stage[currentStage]);
} }
} }
if(config.verbose) console.debug("[loader] finished loader. (Total time: %dms)", Date.now() - load_begin); if(config.verbose)
console.debug("[loader] finished loader. (Total time: %dms)", Date.now() - load_begin);
Animation.finalize();
} }
export function execute_managed() { export function execute_managed() {
execute().then(() => { execute().then(() => {
if(config.verbose) { if(config.verbose) {
@ -242,28 +299,12 @@ export function execute_managed() {
}); });
} }
let _fadeout_warned;
export function hide_overlay() {
if(typeof($) === "undefined") {
if(!_fadeout_warned)
console.warn("Could not fadeout loader screen. Missing jquery functions.");
_fadeout_warned = true;
return;
}
const animation_duration = 750;
$(".loader .bookshelf_wrapper").animate({top: 0, opacity: 0}, animation_duration);
$(".loader .half").animate({width: 0}, animation_duration, () => {
$(".loader").detach();
});
}
/* critical error handler */ /* critical error handler */
export type ErrorHandler = (message: string, detail: string) => void; export type ErrorHandler = (message: string, detail: string) => void;
let _callback_critical_error: ErrorHandler; let _callback_critical_error: ErrorHandler;
let _callback_critical_called: boolean = false; let _callback_critical_called: boolean = false;
export function critical_error(message: string, detail?: string) { export function critical_error(message: string, detail?: string) {
document.getElementById("loader-overlay").classList.add("started"); Animation.abort();
if(_callback_critical_called) { if(_callback_critical_called) {
console.warn("[CRITICAL] %s", message); console.warn("[CRITICAL] %s", message);
@ -312,7 +353,6 @@ export const templates = template_loader;
/* Hello World message */ /* Hello World message */
{ {
const clog = console.log; const clog = console.log;
const print_security = () => { const print_security = () => {
{ {

View File

@ -1,5 +1,5 @@
import {config, critical_error, SourcePath} from "./loader"; import {config, critical_error, SourcePath} from "./loader";
import {load_parallel, LoadSyntaxError, ParallelOptions, script_name} from "./utils"; import {load_parallel, LoadCallback, LoadSyntaxError, ParallelOptions, script_name} from "./utils";
let _script_promises: {[key: string]: Promise<void>} = {}; let _script_promises: {[key: string]: Promise<void>} = {};
@ -88,13 +88,14 @@ export async function load(path: SourcePath, options: Options) : Promise<void> {
throw "Missing dependency " + depend; throw "Missing dependency " + depend;
await _script_promises[depend]; await _script_promises[depend];
} }
await load_script_url(source.url + (options.cache_tag || "")); await load_script_url(source.url + (options.cache_tag || ""));
} }
} }
type MultipleOptions = Options | ParallelOptions; type MultipleOptions = Options | ParallelOptions;
export async function load_multiple(paths: SourcePath[], options: MultipleOptions) : Promise<void> { export async function load_multiple(paths: SourcePath[], options: MultipleOptions, callback?: LoadCallback<SourcePath>) : Promise<void> {
const result = await load_parallel<SourcePath>(paths, e => load(e, options), e => script_name(e, false), options); const result = await load_parallel<SourcePath>(paths, e => load(e, options), e => script_name(e, false), options, callback);
if(result.failed.length > 0) { if(result.failed.length > 0) {
if(config.error) { if(config.error) {
console.error("Failed to load the following scripts:"); console.error("Failed to load the following scripts:");

View File

@ -1,5 +1,5 @@
import {config, critical_error, SourcePath} from "./loader"; import {config, critical_error, SourcePath} from "./loader";
import {load_parallel, LoadSyntaxError, ParallelOptions, script_name} from "./utils"; import {load_parallel, LoadCallback, LoadSyntaxError, ParallelOptions, script_name} from "./utils";
let _style_promises: {[key: string]: Promise<void>} = {}; let _style_promises: {[key: string]: Promise<void>} = {};
@ -121,8 +121,8 @@ export async function load(path: SourcePath, options: Options) : Promise<void> {
} }
export type MultipleOptions = Options | ParallelOptions; export type MultipleOptions = Options | ParallelOptions;
export async function load_multiple(paths: SourcePath[], options: MultipleOptions) : Promise<void> { export async function load_multiple(paths: SourcePath[], options: MultipleOptions, callback?: LoadCallback<SourcePath>) : Promise<void> {
const result = await load_parallel<SourcePath>(paths, e => load(e, options), e => script_name(e, false), options); const result = await load_parallel<SourcePath>(paths, e => load(e, options), e => script_name(e, false), options, callback);
if(result.failed.length > 0) { if(result.failed.length > 0) {
if(config.error) { if(config.error) {
console.error("Failed to load the following style sheets:"); console.error("Failed to load the following style sheets:");

View File

@ -1,5 +1,5 @@
import {config, critical_error, SourcePath} from "./loader"; import {config, critical_error, SourcePath} from "./loader";
import {load_parallel, LoadSyntaxError, ParallelOptions, script_name} from "./utils"; import {load_parallel, LoadCallback, LoadSyntaxError, ParallelOptions, script_name} from "./utils";
let _template_promises: {[key: string]: Promise<void>} = {}; let _template_promises: {[key: string]: Promise<void>} = {};
@ -69,8 +69,8 @@ export async function load(path: SourcePath, options: Options) : Promise<void> {
} }
export type MultipleOptions = Options | ParallelOptions; export type MultipleOptions = Options | ParallelOptions;
export async function load_multiple(paths: SourcePath[], options: MultipleOptions) : Promise<void> { export async function load_multiple(paths: SourcePath[], options: MultipleOptions, callback?: LoadCallback<SourcePath>) : Promise<void> {
const result = await load_parallel<SourcePath>(paths, e => load(e, options), e => script_name(e, false), options); const result = await load_parallel<SourcePath>(paths, e => load(e, options), e => script_name(e, false), options, callback);
if(result.failed.length > 0) { if(result.failed.length > 0) {
if(config.error) { if(config.error) {
console.error("Failed to load the following template files:"); console.error("Failed to load the following template files:");

View File

@ -10,11 +10,7 @@ export class LoadSyntaxError {
export function script_name(path: SourcePath, html: boolean) { export function script_name(path: SourcePath, html: boolean) {
if(Array.isArray(path)) { if(Array.isArray(path)) {
let buffer = ""; return path.filter(e => !!e).map(e => script_name(e, html)).join(" or ");
let _or = " or ";
for(let entry of path)
buffer += _or + script_name(entry, html);
return buffer.slice(_or.length);
} else if(typeof(path) === "string") } else if(typeof(path) === "string")
return html ? "<code>" + path + "</code>" : path; return html ? "<code>" + path + "</code>" : path;
else else
@ -35,31 +31,40 @@ export interface ParallelResult<T> {
skipped: T[]; skipped: T[];
} }
export async function load_parallel<T>(requests: T[], executor: (_: T) => Promise<void>, stringify: (_: T) => string, options: ParallelOptions) : Promise<ParallelResult<T>> { export type LoadCallback<T> = (entry: T, state: "loading" | "loaded") => void;
export async function load_parallel<T>(requests: T[], executor: (_: T) => Promise<void>, stringify: (_: T) => string, options: ParallelOptions, callback?: LoadCallback<T>) : Promise<ParallelResult<T>> {
const result: ParallelResult<T> = { failed: [], succeeded: [], skipped: [] }; const result: ParallelResult<T> = { failed: [], succeeded: [], skipped: [] };
const pending_requests = requests.slice(0).reverse(); /* we're only able to pop from the back */ const pendingRequests = requests.slice(0).reverse(); /* we're only able to pop from the back */
const current_requests = {}; const currentRequests = {};
while (pending_requests.length > 0) { if(typeof callback === "undefined")
while(typeof options.max_parallel_requests !== "number" || options.max_parallel_requests <= 0 || Object.keys(current_requests).length < options.max_parallel_requests) { callback = () => {};
const script = pending_requests.pop();
const name = stringify(script);
current_requests[name] = executor(script).catch(e => result.failed.push({ request: script, error: e })).then(() => { options.max_parallel_requests = 1;
delete current_requests[name]; while (pendingRequests.length > 0) {
while(typeof options.max_parallel_requests !== "number" || options.max_parallel_requests <= 0 || Object.keys(currentRequests).length < options.max_parallel_requests) {
const element = pendingRequests.pop();
const name = stringify(element);
callback(element, "loading");
currentRequests[name] = executor(element).catch(e => result.failed.push({ request: element, error: e })).then(() => {
delete currentRequests[name];
callback(element, "loaded");
}); });
if(pending_requests.length == 0) break; if(pendingRequests.length == 0)
break;
} }
/* /*
* Wait 'till a new "slot" for downloading is free. * Wait 'till a new "slot" for downloading is free.
* This should also not throw because any errors will be caught before. * This should also not throw because any errors will be caught before.
*/ */
await Promise.race(Object.keys(current_requests).map(e => current_requests[e])); await Promise.race(Object.keys(currentRequests).map(e => currentRequests[e]));
if(result.failed.length > 0) if(result.failed.length > 0)
break; /* finish loading the other requests and than show the error */ break; /* finish loading the other requests and than show the error */
} }
await Promise.all(Object.keys(current_requests).map(e => current_requests[e])); await Promise.all(Object.keys(currentRequests).map(e => currentRequests[e]));
result.skipped.push(...pending_requests); result.skipped.push(...pendingRequests);
return result; return result;
} }

57
loader/app/polifill.ts Normal file
View File

@ -0,0 +1,57 @@
/* IE11 */
if(!Element.prototype.remove)
Element.prototype.remove = function() {
this.parentElement?.removeChild(this);
};
function ReplaceWithPolyfill() {
let parent = this.parentNode, i = arguments.length, currentNode;
if (!parent) return;
if (!i) { parent.removeChild(this); }
while (i--) { // i-- decrements i and returns the value of i before the decrement
currentNode = arguments[i];
if (typeof currentNode !== 'object'){
currentNode = this.ownerDocument.createTextNode(currentNode);
} else if (currentNode.parentNode){
currentNode.parentNode.removeChild(currentNode);
}
// the value of "i" below is after the decrement
if (!i) // if currentNode is the first argument (currentNode === arguments[0])
parent.replaceChild(currentNode, this);
else // if currentNode isn't the first
parent.insertBefore(currentNode, this.nextSibling);
}
}
if (!Element.prototype.replaceWith)
Element.prototype.replaceWith = ReplaceWithPolyfill;
if (!CharacterData.prototype.replaceWith)
CharacterData.prototype.replaceWith = ReplaceWithPolyfill;
if (!DocumentType.prototype.replaceWith)
DocumentType.prototype.replaceWith = ReplaceWithPolyfill;
// Source: https://github.com/jserz/js_piece/blob/master/DOM/ParentNode/append()/append().md
(function (arr) {
arr.forEach(function (item) {
if (item.hasOwnProperty('append')) {
return;
}
Object.defineProperty(item, 'append', {
configurable: true,
enumerable: true,
writable: true,
value: function append() {
var argArr = Array.prototype.slice.call(arguments),
docFrag = document.createDocumentFragment();
argArr.forEach(function (argItem) {
var isNode = argItem instanceof Node;
docFrag.appendChild(isNode ? argItem : document.createTextNode(String(argItem)));
});
this.appendChild(docFrag);
}
});
});
})([Element.prototype, Document.prototype, DocumentFragment.prototype]);

View File

@ -1,5 +1,8 @@
import "./shared";
import * as loader from "../loader/loader"; import * as loader from "../loader/loader";
import {config} from "../loader/loader"; import {config, SourcePath} from "../loader/loader";
import {script_name} from "../loader/utils";
import { detect as detectBrowser } from "detect-browser";
declare global { declare global {
interface Window { interface Window {
@ -44,21 +47,18 @@ interface Manifest {
}}; }};
} }
const LoaderTaskCallback = taskId => (script: SourcePath, state) => {
if(state !== "loading")
return;
loader.setCurrentTaskName(taskId, script_name(script, false));
};
/* all javascript loaders */ /* all javascript loaders */
const loader_javascript = { const loader_javascript = {
load_scripts: async () => { load_scripts: async taskId => {
if(!window.require) { if(!window.require) {
await loader.scripts.load(["vendor/jquery/jquery.min.js"], { cache_tag: cache_tag() }); await loader.scripts.load_multiple(["vendor/jquery/jquery.min.js"], { cache_tag: cache_tag() }, LoaderTaskCallback(taskId));
} else {
/*
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
name: "forum sync",
priority: 10,
function: async () => {
forum.sync_main();
}
});
*/
} }
await loader.scripts.load_multiple([ await loader.scripts.load_multiple([
@ -71,8 +71,9 @@ const loader_javascript = {
], { ], {
cache_tag: cache_tag(), cache_tag: cache_tag(),
max_parallel_requests: -1 max_parallel_requests: -1
}); }, LoaderTaskCallback(taskId));
loader.setCurrentTaskName(taskId, "manifest");
let manifest: Manifest; let manifest: Manifest;
try { try {
const response = await fetch(config.baseUrl + "js/manifest.json"); const response = await fetch(config.baseUrl + "js/manifest.json");
@ -99,7 +100,7 @@ const loader_javascript = {
await loader.scripts.load_multiple(manifest.chunks[chunk_name].files.map(e => "js/" + e.file), { await loader.scripts.load_multiple(manifest.chunks[chunk_name].files.map(e => "js/" + e.file), {
cache_tag: undefined, cache_tag: undefined,
max_parallel_requests: -1 max_parallel_requests: -1
}); }, LoaderTaskCallback(taskId));
} }
}; };
@ -125,7 +126,7 @@ const loader_webassembly = {
}; };
const loader_style = { const loader_style = {
load_style: async () => { load_style: async taskId => {
const options = { const options = {
cache_tag: cache_tag(), cache_tag: cache_tag(),
max_parallel_requests: -1 max_parallel_requests: -1
@ -133,22 +134,24 @@ const loader_style = {
await loader.style.load_multiple([ await loader.style.load_multiple([
"vendor/xbbcode/src/xbbcode.css" "vendor/xbbcode/src/xbbcode.css"
], options); ], options, LoaderTaskCallback(taskId));
await loader.style.load_multiple([ await loader.style.load_multiple([
"vendor/emoji-picker/src/jquery.lsxemojipicker.css" "vendor/emoji-picker/src/jquery.lsxemojipicker.css"
], options); ], options, LoaderTaskCallback(taskId));
await loader.style.load_multiple([ await loader.style.load_multiple([
["vendor/highlight/styles/darcula.css", ""], /* empty string means not required */ ["vendor/highlight/styles/darcula.css", ""], /* empty string means not required */
], options); ], options, LoaderTaskCallback(taskId));
if(__build.mode === "debug") { if(__build.mode === "debug") {
await loader_style.load_style_debug(); await loader_style.load_style_debug(taskId);
} else { } else {
await loader_style.load_style_release(); await loader_style.load_style_release(taskId);
} }
}, },
load_style_debug: async () => { load_style_debug: async taskId => {
await loader.style.load_multiple([ await loader.style.load_multiple([
"css/static/main.css", "css/static/main.css",
"css/static/main-layout.css", "css/static/main-layout.css",
@ -196,17 +199,17 @@ const loader_style = {
], { ], {
cache_tag: cache_tag(), cache_tag: cache_tag(),
max_parallel_requests: -1 max_parallel_requests: -1
}); }, LoaderTaskCallback(taskId));
}, },
load_style_release: async () => { load_style_release: async taskId => {
await loader.style.load_multiple([ await loader.style.load_multiple([
"css/static/base.css", "css/static/base.css",
"css/static/main.css", "css/static/main.css",
], { ], {
cache_tag: cache_tag(), cache_tag: cache_tag(),
max_parallel_requests: -1 max_parallel_requests: -1
}); }, LoaderTaskCallback(taskId));
} }
}; };
@ -228,36 +231,12 @@ loader.register_task(loader.Stage.INITIALIZING, {
priority: 50 priority: 50
}); });
loader.register_task(loader.Stage.INITIALIZING, {
name: "Browser detection",
function: async () => {
navigator.browserSpecs = (function(){
let ua = navigator.userAgent, tem, M = ua.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i) || [];
if(/trident/i.test(M[1])){
tem = /\brv[ :]+(\d+)/g.exec(ua) || [];
return {name:'IE',version:(tem[1] || '')};
}
if(M[1]=== 'Chrome'){
tem = ua.match(/\b(OPR|Edge)\/(\d+)/);
if(tem != null) return {name:tem[1].replace('OPR', 'Opera'),version:tem[2]};
}
M = M[2]? [M[1], M[2]]: [navigator.appName, navigator.appVersion, '-?'];
if((tem = ua.match(/version\/(\d+)/i))!= null)
M.splice(1, 1, tem[1]);
return {name:M[0], version:M[1]};
})();
console.log("Resolved browser manufacturer to \"%s\" version \"%s\"", navigator.browserSpecs.name, navigator.browserSpecs.version);
},
priority: 30
});
loader.register_task(loader.Stage.INITIALIZING, { loader.register_task(loader.Stage.INITIALIZING, {
name: "secure tester", name: "secure tester",
function: async () => { function: async () => {
/* we need https or localhost to use some things like the storage API */ /* we need https or localhost to use some things like the storage API */
if(typeof isSecureContext === "undefined") if(typeof isSecureContext === "undefined")
(<any>window)["isSecureContext"] = location.protocol !== 'https:' && location.hostname !== 'localhost'; (<any>window)["isSecureContext"] = location.protocol !== 'https:' || location.hostname === 'localhost';
if(!isSecureContext) { if(!isSecureContext) {
loader.critical_error("TeaWeb cant run on unsecured sides.", "App requires to be loaded via HTTPS!"); loader.critical_error("TeaWeb cant run on unsecured sides.", "App requires to be loaded via HTTPS!");
@ -274,7 +253,7 @@ loader.register_task(loader.Stage.INITIALIZING, {
}); });
loader.register_task(loader.Stage.JAVASCRIPT, { loader.register_task(loader.Stage.JAVASCRIPT, {
name: "javascript", name: "scripts",
function: loader_javascript.load_scripts, function: loader_javascript.load_scripts,
priority: 10 priority: 10
}); });
@ -287,7 +266,7 @@ loader.register_task(loader.Stage.STYLE, {
loader.register_task(loader.Stage.TEMPLATES, { loader.register_task(loader.Stage.TEMPLATES, {
name: "templates", name: "templates",
function: async () => { function: async taskId => {
await loader.templates.load_multiple([ await loader.templates.load_multiple([
"templates.html", "templates.html",
"templates/modal/musicmanage.html", "templates/modal/musicmanage.html",
@ -295,17 +274,11 @@ loader.register_task(loader.Stage.TEMPLATES, {
], { ], {
cache_tag: cache_tag(), cache_tag: cache_tag(),
max_parallel_requests: -1 max_parallel_requests: -1
}); }, LoaderTaskCallback(taskId));
}, },
priority: 10 priority: 10
}); });
loader.register_task(loader.Stage.LOADED, {
name: "loaded handler",
function: async () => loader.hide_overlay(),
priority: 5
});
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, { loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
name: "lsx emoji picker setup", name: "lsx emoji picker setup",
function: async () => await (window as any).setup_lsx_emoji_picker({twemoji: typeof(window.twemoji) !== "undefined"}), function: async () => await (window as any).setup_lsx_emoji_picker({twemoji: typeof(window.twemoji) !== "undefined"}),
@ -368,8 +341,6 @@ loader.register_task(loader.Stage.SETUP, {
}); });
export function run() { export function run() {
window["Module"] = (window["Module"] || {}) as any; /* Why? */
/* TeaClient */ /* TeaClient */
if(node_require) { if(node_require) {
if(__build.target !== "client") { if(__build.target !== "client") {

View File

@ -71,12 +71,6 @@ loader.register_task(loader.Stage.STYLE, {
priority: 10 priority: 10
}); });
loader.register_task(loader.Stage.LOADED, {
name: "loaded handler",
function: async () => loader.hide_overlay(),
priority: 0
});
/* register tasks */ /* register tasks */
loader.register_task(loader.Stage.INITIALIZING, { loader.register_task(loader.Stage.INITIALIZING, {
name: "safari fix", name: "safari fix",

View File

@ -0,0 +1,27 @@
import "./shared";
import * as loader from "../loader/loader";
import {Stage} from "../loader/loader";
export function run() {
loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
name: "doing nothing",
priority: 1,
function: async taskId => {
console.log("Doing nothing");
for(let index of [1, 2, 3]) {
await new Promise(resolve => {
const callback = () => {
document.removeEventListener("click", resolve);
resolve();
};
document.addEventListener("click", callback);
});
loader.setCurrentTaskName(taskId, "try again (" + index + ")");
}
}
});
loader.execute_managed();
}

View File

@ -0,0 +1,38 @@
import * as loader from "../loader/loader";
import {Stage} from "../loader/loader";
import {detect as detectBrowser} from "detect-browser";
if(__build.target === "web") {
loader.register_task(Stage.SETUP, {
name: "outdated browser checker",
function: async () => {
const browser = detectBrowser();
navigator.browserSpecs = browser;
if(!browser)
return;
console.log("Resolved browser manufacturer to \"%s\" version \"%s\" on %s", browser.name, browser.version, browser.os);
if(browser.type && browser.type !== "browser") {
loader.critical_error("Your device isn't supported.", "User agent type " + browser.type + " isn't supported.");
throw "unsupported user type";
}
switch (browser?.name) {
case "aol":
case "bot":
case "crios":
case "ie":
loader.critical_error("Browser not supported", "We're sorry, but your browser isn't supported.");
throw "unsupported browser";
}
},
priority: 50
});
}
/* directly disable all context menus */ //disableGlobalContextMenu
if(!location.search.match(/(.*[?&]|^)disableGlobalContextMenu=1($|&.*)/)) {
document.addEventListener("contextmenu", event => event.preventDefault());
}

7
loader/css/index.scss Normal file
View File

@ -0,0 +1,7 @@
body {
padding: 0;
margin: 0;
}
@import "loader";
@import "overlay";

View File

@ -1,331 +1,186 @@
$thickness: 5px; $setup-time: 80s / 24; /* 24 frames / sec; the initial sequence is 80 seconds */
$duration: 2500;
$delay: $duration/6;
$background: #222222;
@mixin polka($size, $dot, $base, $accent) { #loader-overlay {
background: $base;
background-image: radial-gradient($accent $dot, transparent 0);
background-size: $size $size;
background-position: 0 -2.5px;
}
.loader {
margin: 0;
display: block;
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 900;
text-align: center;
}
.loader .half {
position: fixed;
background: #222222;
top: 0;
bottom: 0;
width: 50%;
height: 100%;
}
.loader .half.right {
right: 0;
}
.loader .half.left {
left: 0;
}
.bookshelf_wrapper {
position: relative;
top: 40%;
left: 50%;
transform: translate(-50%, -50%);
}
.books_list {
margin: 0 auto;
width: 300px;
padding: 0;
}
.book_item {
position: absolute; position: absolute;
top: -120px; overflow: hidden;
box-sizing: border-box;
list-style: none;
width: 40px;
height: 120px;
opacity: 0;
background-color: #1e6cc7;
border: $thickness solid white;
transform-origin: bottom left;
transform: translateX(300px);
animation: travel #{$duration}ms linear infinite;
&.first { top: 0;
top: -140px; left: 0;
height: 140px; right: 0;
bottom: 0;
&:before, background: #1e1e1e;
&:after {
content: '';
position: absolute;
top: 10px;
left: 0;
width: 100%;
height: $thickness;
background-color: white;
}
&:after { user-select: none;
top: initial;
bottom: 10px; z-index: 10000;
}
display: flex;
flex-direction: column;
justify-content: center;
.container {
flex-shrink: 0;
display: block;
position: relative;
width: 1000px;
height: 1000px;
align-self: center;
margin-bottom: 10vh;
transition-duration: .5s;
} }
&.second, .setup, .idle {
&.fifth {
&:before,
&:after {
box-sizing: border-box;
content: '';
position: absolute;
top: 10px;
left: 0;
width: 100%;
height: $thickness*3.5;
border-top: $thickness solid white;
border-bottom: $thickness solid white;
}
&:after {
top: initial;
bottom: 10px;
}
}
&.third {
&:before,
&:after {
box-sizing: border-box;
content: '';
position: absolute;
top: 10px;
left: 9px;
width: 12px;
height: 12px;
border-radius: 50%;
border: $thickness solid white;
}
&:after {
top: initial;
bottom: 10px;
}
}
&.fourth {
top: -130px;
height: 130px;
&:before {
box-sizing: border-box;
content: '';
position: absolute;
top: 46px;
left: 0;
width: 100%;
height: $thickness*3.5;
border-top: $thickness solid white;
border-bottom: $thickness solid white;
}
}
&.fifth {
top: -100px;
height: 100px;
}
&.sixth {
top: -140px;
height: 140px;
&:before {
box-sizing: border-box;
content: '';
position: absolute;
bottom: 31px;
left: 0px;
width: 100%;
height: $thickness;
background-color: white;
}
&:after {
box-sizing: border-box;
content: '';
position: absolute;
bottom: 10px;
left: 9px;
width: 12px;
height: 12px;
border-radius: 50%;
border: $thickness solid white;
}
}
&:nth-child(2) {
animation-delay: #{$delay*1}ms;
}
&:nth-child(3) {
animation-delay: #{$delay*2}ms;
}
&:nth-child(4) {
animation-delay: #{$delay*3}ms;
}
&:nth-child(5) {
animation-delay: #{$delay*4}ms;
}
&:nth-child(6) {
animation-delay: #{$delay*5}ms;
}
}
.shelf {
width: 300px;
height: $thickness;
margin: 0 auto;
background-color: white;
position: relative;
&:before,
&:after {
content: '';
position: absolute; position: absolute;
width: 100%;
height: 100%; top: 0;
@include polka(10px, 30%, $background, rgba(255, 255, 255, 0.5)); left: 0;
top: 200%; right: 0;
left: 5%; bottom: 0;
animation: move #{$duration/10}ms linear infinite;
display: none;
&.visible {
display: block;
}
} }
&:after { .setup.visible {
top: 400%; animation: loader-initial-sequence 0s cubic-bezier(.81,.01,.65,1.16) $setup-time forwards;
left: 7.5%;
} }
.idle {
img {
position: absolute;
}
.steam {
position: absolute;
top: 282px;
left: 380px;
width: 249px;
height: 125px;
background: url("../img/loader/steam.png") 0 0;
animation: sprite-steam 2.5s steps(50) forwards infinite;
}
}
&.finishing {
.idle {
.steam {
display: none;
}
.bowl {
animation: swipe-out-bowl .5s both;
}
.text {
animation: swipe-out-text .5s .12s both;
}
}
pointer-events: none;
animation: overlay-fade .3s .2s both;
}
.loader-stage {
position: absolute;
left: 5px;
bottom: 5px;
font-size: 12px;
font-family: monospace;
color: #999;
}
} }
@keyframes move { @media all and (max-width: 850px) {
#loader-overlay .container {
transform: scale(.5);
}
}
@media all and (max-height: 700px) {
#loader-overlay .container {
transform: scale(.5);
}
}
@media all and (max-width: 400px) {
#loader-overlay .container {
transform: scale(.3);
}
}
@keyframes loader-initial-sequence {
to {
display: none;
}
}
@keyframes sprite-steam {
to {
background-position: 0 -6250px;
}
}
@keyframes swipe-out-bowl {
from { from {
background-position-x: 0; transform: translate3d(0, 0, 0);
}
40% {
opacity: 1;
transform: translate3d(-60px, 0, 0) skew(-5deg, 0) rotateY(-6deg);
} }
to { to {
background-position-x: 10px; opacity: 0;
transform: translate3d(700px, 0, 0) skew(30deg, 0) rotateZ(-6deg);
} }
} }
@keyframes travel { @keyframes swipe-out-text {
from {
0% { transform: translate3d(0, 0, 0);
opacity: 0;
transform: translateX(300px) rotateZ(0deg) scaleY(1);
} }
6.5% { 40% {
transform: translateX(279.5px) rotateZ(0deg) scaleY(1.1);
}
8.8% {
transform: translateX(273.6px) rotateZ(0deg) scaleY(1);
}
10% {
opacity: 1; opacity: 1;
transform: translateX(270px) rotateZ(0deg); transform: translate3d(-30px, 20px, 0) skew(-5deg, 0);
} }
17.6% { to {
transform: translateX(247.2px) rotateZ(-30deg); opacity: 0;
transform: translate3d(550px, 0, 0) skew(30deg, 0) scale(.96, 1.25) rotateZ(6deg);
} }
}
45% { @keyframes overlay-fade {
transform: translateX(165px) rotateZ(-30deg); to {
}
49.5% {
transform: translateX(151.5px) rotateZ(-45deg);
}
61.5% {
transform: translateX(115.5px) rotateZ(-45deg);
}
67% {
transform: translateX(99px) rotateZ(-60deg);
}
76% {
transform: translateX(72px) rotateZ(-60deg);
}
83.5% {
opacity: 1;
transform: translateX(49.5px) rotateZ(-90deg);
}
90% {
opacity: 0; opacity: 0;
} }
100% {
opacity: 0;
transform: translateX(0px) rotateZ(-90deg);
}
} }
/* Automated loader timeout */ /* Automated loader timeout */
$loader_timeout: 2.5s; #loader-overlay:not(.initialized) + #critical-load {
.loader:not(.started) {
&, & > .half, & > .bookshelf_wrapper {
-moz-animation: _loader_hide 0s ease-in $loader_timeout forwards;
-webkit-animation: _loader_hide 0s ease-in $loader_timeout forwards;
-o-animation: _loader_hide 0s ease-in $loader_timeout forwards;
animation: _loader_hide 0s ease-in $loader_timeout forwards;
-webkit-animation-fill-mode: forwards;
animation-fill-mode: forwards;
}
}
.loader:not(.started) + #critical-load {
display: block !important; display: block !important;
opacity: 0; opacity: 0;
-moz-animation: _loader_show 0s ease-in $loader_timeout forwards; animation: loader-setup-timeout 0s ease-in $setup-time forwards;
-webkit-animation: _loader_show 0s ease-in $loader_timeout forwards;
-o-animation: _loader_show 0s ease-in $loader_timeout forwards;
animation: _loader_show 0s ease-in $loader_timeout forwards;
-webkit-animation-fill-mode: forwards;
animation-fill-mode: forwards;
.error::before { .error::before {
content: 'Failed to startup app loader!'; content: 'Failed to startup loader!';
} }
.detail::before { .detail::before {
@ -333,29 +188,8 @@ $loader_timeout: 2.5s;
} }
} }
@keyframes _loader_hide {
to {
width: 0;
height: 0;
overflow: hidden;
}
}
@-webkit-keyframes _loader_hide { @keyframes loader-setup-timeout {
to {
width: 0;
height: 0;
visibility: hidden;
}
}
@keyframes _loader_show {
to {
opacity: 1;
}
}
@-webkit-keyframes _loader_show {
to { to {
opacity: 1; opacity: 1;
} }

58
loader/css/overlay.scss Normal file
View File

@ -0,0 +1,58 @@
#overlay-no-js, #critical-load {
z-index: 10000;
display: none;
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
background: #1e1e1e;
text-align: center;
.container {
position: relative;
display: inline-block;
top: 20%;
}
&.shown {
display: block;
}
}
#critical-load {
img {
height: 12em
}
.error {
color: #bd1515;
margin-bottom: 0
}
.detail {
color: #696363;
margin-top: .5em
}
}
@media (max-height: 750px) {
#critical-load .container {
top: unset;
}
#critical-load {
font-size: .8rem;
flex-direction: column;
justify-content: center;
}
#critical-load.shown {
display: flex;
}
}

111
loader/html/index.html.ejs Normal file
View File

@ -0,0 +1,111 @@
<%
/* given on compile time */
var build_target;
var initial_script;
var initial_css;
%>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, min-zoom=1, max-zoom: 1, user-scalable=no">
<meta name="description" content="The TeaSpeak Web client is a in the browser running client for the VoIP communication software TeaSpeak." />
<meta name="keywords" content="TeaSpeak, TeaWeb, TeaSpeak-Web,Web client TeaSpeak, веб клієнт TeaSpeak, TSDNS, багатомовність, мультимовність, теми, функціонал"/>
<meta name="og:description" content="The TeaSpeak Web client is a in the browser running client for the VoIP communication software TeaSpeak." />
<meta name="og:url" content="https://web.teaspeak.de/">
<%# TODO: Put in an appropirate image <meta name="og:image" content="https://www.whatsapp.com/img/whatsapp-promo.png"> %>
<%# Using an absolute path here since the manifest.json works only with such. %>
<link rel="manifest" href="/manifest.json">
<% if(build_target === "client") { %>
<title>TeaClient</title>
<meta name='og:title' content='TeaClient'>
<% } else { %>
<title>TeaSpeak-Web</title>
<meta name='og:title' content='TeaSpeak-Web'>
<link rel='shortcut icon' href='img/favicon/teacup.png' type='image/x-icon'>
<%# <link rel="apple-touch-icon" sizes="194x194" href="/apple-touch-icon.png" type="image/png"> %>
<% } %>
<x-properties id="properties" style="display: none">
<%#
We don't need to put any properties down here.
But this tag is here to not brick the settings class.
But it will be removed quite soonly as soon this class has been fixed
%>
</x-properties>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="format-detection" content="telephone=no">
<!-- Global site tag (gtag.js) - Google Analytics -->
<script defer async src="https://www.googletagmanager.com/gtag/js?id=UA-113151733-4"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'UA-113151733-4');
</script>
<%# We're preloading the PNG file by default since most browser have APNG support %>
<link rel="preload" as="image" href="img/loader/initial-sequence.png">
<link rel="preload" as="image" href="img/loader/bowl.png">
<%# We don't preload the bowl since it's only a div background %>
<link rel="preload" as="image" href="img/loader/text.png">
<%- initial_css %>
</head>
<body>
<!-- No javascript error -->
<noscript>
<div id="overlay-no-js">
<div class="container">
<img src="img/script.svg" height="128px" alt="no script" >
<h1>Please enable JavaScript</h1>
<h3>TeaSpeak web could not run without it!</h3>
<h3>Its like you, without coffee</h3>
</div>
</div>
</noscript>
<!-- loader setup -->
<div id="style"></div>
<div id="scripts"></div>
<!-- Loading screen -->
<div class="loader" id="loader-overlay">
<div class="container">
<div class="setup">
<lazy-img src-apng="img/loader/initial-sequence.png" src-gif="img/loader/initial-sequence.gif" alt="initial loading sequence"></lazy-img>
</div>
<div class="idle">
<lazy-img class="bowl" src="img/loader/bowl.png" alt="bowl"></lazy-img>
<lazy-img class="text" src="img/loader/text.png" alt="TeaSpeak"></lazy-img>
<div class="steam"></div>
</div>
</div>
<div class="loader-stage"></div>
</div>
<!-- Critical load error -->
<div class="fulloverlay" id="critical-load">
<div class="container">
<img src="img/loading_error_right.svg" alt="load error" />
<h1 class="error"></h1>
<h3 class="detail"></h3>
</div>
</div>
<%- initial_script %>
</body>
</html>

1340
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -24,6 +24,9 @@
"author": "TeaSpeak (WolverinDEV)", "author": "TeaSpeak (WolverinDEV)",
"license": "ISC", "license": "ISC",
"devDependencies": { "devDependencies": {
"@babel/core": "^7.10.4",
"@babel/plugin-transform-runtime": "^7.10.4",
"@babel/preset-env": "^7.10.4",
"@google-cloud/translate": "^5.3.0", "@google-cloud/translate": "^5.3.0",
"@types/dompurify": "^2.0.1", "@types/dompurify": "^2.0.1",
"@types/ejs": "^3.0.2", "@types/ejs": "^3.0.2",
@ -38,6 +41,7 @@
"@types/react-dom": "^16.9.5", "@types/react-dom": "^16.9.5",
"@types/sha256": "^0.2.0", "@types/sha256": "^0.2.0",
"@types/websocket": "0.0.40", "@types/websocket": "0.0.40",
"babel-loader": "^8.1.0",
"chunk-manifest-webpack-plugin": "^1.1.2", "chunk-manifest-webpack-plugin": "^1.1.2",
"circular-dependency-plugin": "^5.2.0", "circular-dependency-plugin": "^5.2.0",
"clean-css": "^4.2.1", "clean-css": "^4.2.1",
@ -81,6 +85,7 @@
}, },
"homepage": "https://www.teaspeak.de", "homepage": "https://www.teaspeak.de",
"dependencies": { "dependencies": {
"detect-browser": "^5.1.1",
"dompurify": "^2.0.8", "dompurify": "^2.0.8",
"moment": "^2.24.0", "moment": "^2.24.0",
"react": "^16.13.1", "react": "^16.13.1",

View File

@ -1,220 +0,0 @@
<%
/* given build compile */
var build_target;
var initial_script;
var initial_css;
%>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<!-- App min width: 450px -->
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, min-zoom=1, max-zoom: 1, user-scalable=no">
<meta name="description" content="The TeaSpeak Web client is a in the browser running client for the VoIP communication software TeaSpeak." />
<meta name="keywords" content="TeaSpeak, TeaWeb, TeaSpeak-Web,Web client TeaSpeak, веб клієнт TeaSpeak, TSDNS, багатомовність, мультимовність, теми, функціонал"/>
<meta name="og:description" content="The TeaSpeak Web client is a in the browser running client for the VoIP communication software TeaSpeak." />
<meta name="og:url" content="https://web.teaspeak.de/">
<!-- WHAT THE HELL? <meta name="og:image" content="https://www.whatsapp.com/img/whatsapp-promo.png"> -->
<!-- TODO Needs some fix -->
<link rel="manifest" href="manifest.json">
<% if(build_target === "client") { %>
<title>TeaClient</title>
<meta name='og:title' content='TeaClient'>
<% } else { %>
<title>TeaSpeak-Web</title>
<meta name='og:title' content='TeaSpeak-Web'>
<link rel='shortcut icon' href='img/favicon/teacup.png' type='image/x-icon'>
<!-- <link rel="apple-touch-icon" sizes="194x194" href="/apple-touch-icon.png" type="image/png"> -->
<% } %>
<x-properties id="properties" style="display: none">
<!--
We don't need to put any properties down here.
But this tag is here to not brick the settings class.
But it will be removed quite soonly as soon this class has been fixed
-->
</x-properties>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="format-detection" content="telephone=no">
<!-- Global site tag (gtag.js) - Google Analytics -->
<script defer async src="https://www.googletagmanager.com/gtag/js?id=UA-113151733-4"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'UA-113151733-4');
</script>
<!-- required static style for the critical page and the enable javascript page -->
<style>
.fulloverlay {
z-index: 10000;
display: none;
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: gray;
text-align: center;
}
.fulloverlay .container {
position: relative;
display: inline-block;
top: 20%;
}
#critical-load.shown {
display: block;
}
@media (max-height: 750px) {
#critical-load .container {
top: unset;
}
#critical-load {
font-size: .8rem;
flex-direction: column;
justify-content: center;
}
#critical-load.shown {
display: flex;
}
}
.no-js {
display: block;
}
</style>
<%- initial_css %>
</head>
<body>
<!-- No javascript error -->
<noscript>
<div class="fulloverlay no-js">
<div class="container">
<img src="img/script.svg" height="128px">
<h1>Please enable JavaScript</h1>
<h3>TeaSpeak web could not run without it!</h3>
<h3>Its like you, without coffee</h3>
</div>
</div>
</noscript>
<!-- loader setup -->
<div id="style"></div>
<div id="scripts"></div>
<!-- Loading screen -->
<div class="loader" id="loader-overlay">
<div class="half right"></div>
<div class="half left"></div>
<div class="bookshelf_wrapper">
<ul class="books_list">
<li class="book_item first"></li>
<li class="book_item second"></li>
<li class="book_item third"></li>
<li class="book_item fourth"></li>
<li class="book_item fifth"></li>
<li class="book_item sixth"></li>
</ul>
<div class="shelf"></div>
</div>
</div>
<!-- Critical load error -->
<div class="fulloverlay" id="critical-load">
<div class="container">
<img src="img/loading_error_right.svg" style="height: 12em" />
<h1 class="error" style="color: red; margin-bottom: 0"></h1>
<h3 class="detail" style="margin-top: .5em"></h3>
</div>
</div>
<%# <!-- Old debgging stuff back for the days where we designed the client -->
<?php if($localhost && false) { ?>
<div id="spoiler-style" style="z-index: 1000000; position: absolute; display: block; background: white; right: 5px; left: 5px; top: 34px;">
<!-- <img src="https://www.chromatic-solutions.de/teaspeak/window/connect_opened.png"> -->
<!-- <img src="http://puu.sh/DZDgO/9149c0a1aa.png"> -->
<!-- <img src="http://puu.sh/E0QUb/ce5e3f93ae.png"> -->
<!-- <img src="img/style/default.png"> -->
<!-- <img src="img/style/user-selected.png"> -->
<!-- <img src="img/style/privat_chat.png"> -->
<!-- <img src="http://puu.sh/E1aBL/3c40ae3c2c.png"> -->
<!-- <img src="http://puu.sh/E2qb2/b27bb2fde5.png"> -->
<!-- <img src="http://puu.sh/E2UQR/1e0d7e03a3.png"> -->
<!-- <img src="http://puu.sh/E38yX/452e27864c.png"> -->
<!-- <img src="http://puu.sh/E3fjq/e2b4447bcd.png"> -->
<!-- <img src="http://puu.sh/E3WlW/f791a9e7b1.png"> -->
<!-- <img src="http://puu.sh/E4lHJ/1a4afcdf0b.png"> -->
<!-- <img src="http://puu.sh/E4HKK/5ee74d4cc7.png"> -->
<!-- <img src="http://puu.sh/E6LN1/8518c10898.png"> -->
<!--
http://puu.sh/E8IoF/242ed5ca3a.png
http://puu.sh/E8Ip9/9632d33591.png
http://puu.sh/E8Ips/6c314253e5.png
http://puu.sh/E8IpG/015a38b184.png
http://puu.sh/E8IpY/5be454a15e.png
Identity imporve: http://puu.sh/E9jTp/380a734677.png
Identity select: http://puu.sh/E9jYi/3003c58a2f.png
Server Info Bandwidth: http://puu.sh/E9jTe/b41f6386de.png
Server Info: http://puu.sh/E9jT6/302912ae34.png
Bookmarks: http://puu.sh/Eb5w4/8d38fe5b8f.png
serveredit_1.png https://www.hypixel-koo.cf/tsapoijdsadpoijsadsapj.png
serveredit_2.png https://www.hypixel-koo.cf/tsandljsandljsamndoj3oiwejlkjmnlksandljsadmnlmsadnlsa.png
serveredit_3.png https://www.hypixel-koo.cf/toiuhsadouhgdsapoiugdsapouhdsapouhdsaouhwouhwwouhwwoiuhwoihwwoihwoijhwwoknw.png
Query accounts: https://puu.sh/EhvkJ/7551f548e3.png
Channel info: https://puu.sh/EhuVH/1e21540589.png
-->
<!-- <img src="http://puu.sh/E6NXv/eb2f19c7c3.png"> -->
<!-- <img src="http://puu.sh/E9jT6/302912ae34.png"> -->
<!-- <img src="http://puu.sh/E9jTe/b41f6386de.png"> -->
<!-- <img src="img/style/ban-list.png"> -->
<!-- <img src="http://puu.sh/E9jTe/b41f6386de.png"> -->
<!-- <img src="https://puu.sh/EhuVH/1e21540589.png"> -->
<img src="https://puu.sh/EhvkJ/7551f548e3.png">
</div>
<button class="toggle-spoiler-style" style="height: 30px; width: 100px; z-index: 100000000; position: absolute; bottom: 2px;">toggle style</button>
<script>
const init = (jQuery) => {
if(typeof jQuery === "undefined") {
setTimeout(() => init($), 1000);
return;
}
jQuery("#spoiler-style").hide();
jQuery(".toggle-spoiler-style").on('click', () => {
jQuery("#spoiler-style").toggle();
});
};
setTimeout(() => init($), 1000);
</script>
%>
<%- initial_script %>
</body>
</html>

View File

@ -34,8 +34,6 @@ import {spawnFileTransferModal} from "tc-shared/ui/modal/transfer/ModalFileTrans
import {MenuEntryType, spawn_context_menu} from "tc-shared/ui/elements/ContextMenu"; import {MenuEntryType, spawn_context_menu} from "tc-shared/ui/elements/ContextMenu";
import {copy_to_clipboard} from "tc-shared/utils/helpers"; import {copy_to_clipboard} from "tc-shared/utils/helpers";
import ContextMenuEvent = JQuery.ContextMenuEvent; import ContextMenuEvent = JQuery.ContextMenuEvent;
import {spawnPermissionEditorModal} from "tc-shared/ui/modal/permission/ModalPermissionEditor";
import {spawnGroupCreate} from "tc-shared/ui/modal/ModalGroups";
/* required import for init */ /* required import for init */
require("./proto").initialize(); require("./proto").initialize();

View File

@ -18,9 +18,11 @@
"webpack/WatLoader.ts", "webpack/WatLoader.ts",
"webpack/DevelBlocks.ts", "webpack/DevelBlocks.ts",
"loader/IndexGenerator.ts",
"file.ts" "file.ts"
], ],
"exclude": [ "exclude": [
"node_modules", "node_modules"
] ]
} }

View File

@ -3,7 +3,7 @@ import * as fs from "fs";
import trtransformer from "./tools/trgen/ts_transformer"; import trtransformer from "./tools/trgen/ts_transformer";
import {exec} from "child_process"; import {exec} from "child_process";
import * as util from "util"; import * as util from "util";
import EJSGenerator = require("./webpack/EJSGenerator"); import LoaderIndexGenerator = require("./loader/IndexGenerator");
const path = require('path'); const path = require('path');
const webpack = require("webpack"); const webpack = require("webpack");
@ -44,6 +44,16 @@ const generate_definitions = async (target: string) => {
} as any; } as any;
}; };
const isLoaderFile = (file: string) => {
if(file.startsWith(__dirname)) {
const path = file.substr(__dirname.length).replace(/\\/g, "/");
if(path.startsWith("/loader/")) {
return true;
}
}
return false;
};
export const config = async (target: "web" | "client") => { return { export const config = async (target: "web" | "client") => { return {
entry: { entry: {
"loader": "./loader/app/index.ts" "loader": "./loader/app/index.ts"
@ -77,21 +87,10 @@ export const config = async (target: "web" | "client") => { return {
}), }),
new webpack.DefinePlugin(await generate_definitions(target)), new webpack.DefinePlugin(await generate_definitions(target)),
new EJSGenerator({ new LoaderIndexGenerator({
variables: { buildTarget: target,
build_target: target
},
input: path.join(__dirname, "shared/html/index.html.ejs"),
output: path.join(__dirname, "dist/index.html"), output: path.join(__dirname, "dist/index.html"),
initialJSEntryChunk: "loader", isDevelopment: isDevelopment
minify: !isDevelopment,
embedInitialJSEntryChunk: !isDevelopment,
embedInitialCSSFile: !isDevelopment,
initialCSSFile: {
localFile: path.join(__dirname, "loader/css/loader.css"),
publicFile: "css/loader.css"
}
}) })
].filter(e => !!e), ].filter(e => !!e),
module: { module: {
@ -127,7 +126,7 @@ export const config = async (target: "web" | "client") => { return {
] ]
}, },
{ {
test: /\.tsx?$/, test: (module: string) => module.match(/\.tsx?$/) && !isLoaderFile(module),
exclude: /node_modules/, exclude: /node_modules/,
loader: [ loader: [
@ -154,6 +153,25 @@ export const config = async (target: "web" | "client") => { return {
} }
] ]
}, },
{
test: (module: string) => module.match(/\.tsx?$/) && isLoaderFile(module),
exclude: /(node_modules|bower_components)/,
loader: [
{
loader: "babel-loader",
options: {
presets: ["@babel/preset-env"] //Preset used for env setup
}
},
{
loader: "ts-loader",
options: {
transpileOnly: true
}
}
]
},
{ {
test: /\.was?t$/, test: /\.was?t$/,
loader: [ loader: [

View File

@ -31,31 +31,34 @@ class EJSGenerator {
this.options = options; this.options = options;
} }
private async generate_entry_js_tag(compilation: Compilation) { private async generateEntryJsTag(compilation: Compilation) {
const entry_group = compilation.chunkGroups.find(e => e.options.name === this.options.initialJSEntryChunk); const entry_group = compilation.chunkGroups.find(e => e.options.name === this.options.initialJSEntryChunk);
if(!entry_group) return; /* not the correct compilation */ if(!entry_group) return; /* not the correct compilation */
if(entry_group.chunks.length !== 1) throw "Unsupported entry chunk size. We only support one at the moment.";
if(entry_group.chunks[0].files.length !== 1)
throw "Entry chunk has too many files. We only support to inline one!";
const file = entry_group.chunks[0].files[0];
if(path.extname(file) !== ".js")
throw "Entry chunk file has unknown extension";
if(!this.options.embedInitialJSEntryChunk) { const tags = entry_group.chunks.map(chunk => {
return '<script type="application/javascript" src=' + compilation.compiler.options.output.publicPath + file + ' async defer></script>'; if(chunk.files.length !== 1)
} else { throw "invalid chunk file count";
const script = await util.promisify(fs.readFile)(path.join(compilation.compiler.outputPath, file));
return `<script type="application/javascript">${script}</script>`; const file = chunk.files[0];
} if(path.extname(file) !== ".js")
throw "Entry chunk file has unknown extension";
if(!this.options.embedInitialJSEntryChunk) {
return '<script type="application/javascript" src=' + compilation.compiler.options.output.publicPath + file + ' async defer></script>';
} else {
const script = fs.readFileSync(path.join(compilation.compiler.outputPath, file));
return `<script type="application/javascript">${script}</script>`;
}
});
return tags.join("\n");
} }
private async generate_entry_css_tag() { private async generateEntryCssTag() {
if(this.options.embedInitialCSSFile) { if(this.options.embedInitialCSSFile) {
const style = await util.promisify(fs.readFile)(this.options.initialCSSFile.localFile); const style = await util.promisify(fs.readFile)(this.options.initialCSSFile.localFile);
return `<style>${style}</style>` return `<style>${style}</style>`
} else { } else {
//<link rel="preload" href="mystyles.css" as="style" onload="this.rel='stylesheet'"> return `<link rel="stylesheet" href="${this.options.initialCSSFile.publicFile}">`
return `<link rel="preload" as="style" onload="this.rel='stylesheet'" href="${this.options.initialCSSFile.publicFile}">`
} }
} }
@ -64,11 +67,12 @@ class EJSGenerator {
const input = await util.promisify(fs.readFile)(this.options.input); const input = await util.promisify(fs.readFile)(this.options.input);
const variables = Object.assign({}, this.options.variables); const variables = Object.assign({}, this.options.variables);
variables["initial_script"] = await this.generate_entry_js_tag(compilation); variables["initial_script"] = await this.generateEntryJsTag(compilation);
variables["initial_css"] = await this.generate_entry_css_tag(); variables["initial_css"] = await this.generateEntryCssTag();
let generated = await ejs.render(input.toString(), variables, { let generated = await ejs.render(input.toString(), variables, {
beautify: false /* uglify is a bit dump and does not understands ES6 */ beautify: false, /* uglify is a bit dump and does not understands ES6 */
context: this
}); });
if(this.options.minify) { if(this.options.minify) {
@ -82,12 +86,21 @@ class EJSGenerator {
removeTagWhitespace: true, removeTagWhitespace: true,
minifyCSS: true, minifyCSS: true,
minifyJS: true, minifyJS: true,
minifyURLs: true minifyURLs: true,
}); });
} }
await util.promisify(fs.writeFile)(this.options.output, generated); await util.promisify(fs.writeFile)(this.options.output, generated);
}); });
compiler.hooks.afterCompile.tapPromise(this.constructor.name, async compilation => {
const file = path.resolve(this.options.input);
if(compilation.fileDependencies.has(file))
return;
console.log("Adding additional watch to %s", file);
compilation.fileDependencies.add(file);
});
} }
} }