wordRotator/src/js/lib/pwa-assets.js
2018-10-30 14:34:12 +01:00

656 lines
21 KiB
JavaScript
Executable File

import { Helper, InitPromise, MultipleShareButton, AndroidBridge, PauseSite, Fragment, Translator } from './pwa-lib.js';
class DelayPromise extends Promise {
static async delay(delay) {
return new Promise((resolve) => {
setTimeout(resolve, delay);
});
}
}
class MyStorageManager {
static getInstance() {
if (Helper.isNull(MyStorageManager.instance)) {
MyStorageManager.instance = new MyStorageManager();
}
return MyStorageManager.instance;
}
async estimate() {
if ('storage' in navigator && 'estimate' in navigator.storage) {
// We've got the real thing! Return its response.
return navigator.storage.estimate();
}
if ('webkitTemporaryStorage' in navigator &&
'queryUsageAndQuota' in navigator.webkitTemporaryStorage) {
// Return a promise-based wrapper that will follow the expected interface.
return new Promise(function (resolve, reject) {
navigator.webkitTemporaryStorage.queryUsageAndQuota(
function (usage, quota) {
resolve({usage: usage, quota: quota});
},
reject
);
});
}
// If we can't estimate the values, return a Promise that resolves with NaN.
return Promise.resolve({usage: NaN, quota: NaN});
}
canEstimateStorage() {
return ('storage' in navigator && 'estimate' in navigator.storage || 'webkitTemporaryStorage' in navigator &&
'queryUsageAndQuota' in navigator.webkitTemporaryStorage);
}
canPersist() {
return (navigator.storage && navigator.storage.persist);
}
persist(){
if (this.canPersist()){
return navigator.storage.persist();
}
return Promise.resolve(false);
}
}
MyStorageManager.instance = null;
class InstallManager {
static init() {
window.addEventListener('beforeinstallprompt', e => {
e.preventDefault();
this.setDeferredPrompt(e);
});
}
static setDeferredPrompt(e){
this.deferredPromt = e;
if (this.canInstallListener) {
this.canInstallListener(this.deferredPromt);
}
}
static async prompt(){
if (Helper.isNotNull(this.deferredPromt)){
this.deferredPromt["prompt"]();
return this.deferredPromt["userChoice"].then(r => {
MyStorageManager.getInstance().persist();
return r;
});
}
return Promise.resolve({
"outcome":"dismissed",
"platform":""
});
}
static setCanInstallListener(listener, callIfCanInstall) {
this.canInstallListener = listener;
callIfCanInstall = Helper.nonNull(callIfCanInstall, true);
if (callIfCanInstall && Helper.nonNull(this.deferredPromt)) {
this.canInstallListener(this.deferredPromt);
}
}
}
InstallManager.init();
class Matomo {
static init() {
Matomo.isTrackingPromise = new Promise(async (resolve) => {
let shouldTrack = Helper.nonNull(localStorage.getItem(Matomo.LOCAL_STORAGE_KEY), "1");
if (Helper.isNull(shouldTrack)) {
shouldTrack = await Matomo._askIsTracking();
localStorage.setItem(Matomo.LOCAL_STORAGE_KEY, shouldTrack);
}
else {
shouldTrack = (shouldTrack === "1");
Matomo.setTrack(shouldTrack);
}
resolve(shouldTrack);
});
Matomo.isTrackingPromise.then(() => {
Matomo.push(['trackPageView'], true);
Matomo.push(['enableLinkTracking'], true);
Matomo.push(['setTrackerUrl', Matomo.TRACK_SITE + '/piwik.php'], true);
Matomo.push(['setSiteId', Matomo.SIDE_ID + ""], true);
let d = document, g = d.createElement('script'), s = d.getElementsByTagName('head')[0];
g.type = 'text/javascript';
g.async = true;
g.defer = true;
g.src = Matomo.TRACK_SITE + '/piwik.js';
s.appendChild(g);
});
}
static update(title) {
if (Helper.nonNull(Matomo.currentUrl)) {
Matomo.push(['setReferrerUrl', Matomo.currentUrl]);
}
Matomo.currentUrl = window.location.pathname + window.location.search;
Matomo.push(['setCustomUrl', Matomo.currentUrl]);
Matomo.push(['setDocumentTitle', title]);
// remove all previously assigned custom variables, requires Matomo (formerly Piwik) 3.0.2
Matomo.push(['deleteCustomVariables', 'page']);
Matomo.push(['setGenerationTimeMs', 0]);
Matomo.push(['trackPageView']);
// make Matomo aware of newly added content
var content = document.getElementById('site-content');
Matomo.push(['MediaAnalytics::scanForMedia', content]);
Matomo.push(['FormAnalytics::scanForForms', content]);
Matomo.push(['trackContentImpressionsWithinNode', content]);
Matomo.push(['enableLinkTracking']);
}
static async _askIsTracking() {
Matomo.isTrackingPromise = new Promise(resolve => {
Matomo.push([function () {
resolve(!this["isUserOptedOut"]());
}]);
Matomo.push([function () {
resolve(!this["isUserOptedOut"]());
}]);
});
return Matomo.isTrackingPromise;
}
static async query(method) {
return fetch(Matomo.TRACK_SITE + Matomo.BASE_PATH + method, {
"mode": "cors",
"credentials": "include",
}).then(res => res.text()).then(text => (new window.DOMParser()).parseFromString(text, "text/xml"));
}
static getTrackingPromise() {
return Matomo.isTrackingPromise;
}
static async setTrack(shouldTrack) {
Matomo.isTrackingPromise = Promise.resolve(shouldTrack);
localStorage.setItem(Matomo.LOCAL_STORAGE_KEY, (shouldTrack === true) ? "1" : "0");
if (shouldTrack) {
Matomo.push(["forgetUserOptOut"], true);
}
else {
Matomo.push(["optUserOut"], true);
}
}
static async trackEvent(event, name, label, value){
let ev = ["trackEvent", event, name];
if (Helper.isNotNull(label)){
ev.push(label);
}
if (Helper.isNotNull(value) && !isNaN(parseFloat(value)) && isFinite(value)){
ev.push(value);
}
return this.push(ev);
}
static async push(arr, force) {
if (!Array.isArray(arr)) {
arr = [arr];
}
window["_paq"].push(arr);
}
}
Matomo.currentUrl = null;
Matomo.LOCAL_STORAGE_KEY = "matomoShouldTrack";
Matomo.TRACK_SITE = "//matomo.silas.link";
Matomo.BASE_PATH = "/index.php?module=API&method=AjaxOptOut.";
Matomo.SIDE_ID = "1";
InitPromise.addPromise(() => {
window["_paq"] = window["_paq"] || [];
Matomo.init();
});
class MatomoShareButton extends MultipleShareButton{
constructor(baseButton, platform, shouldLoadImg) {
super([baseButton, (url) => {
Matomo.trackEvent("shared", url, platform);
}], shouldLoadImg);
}
}
AndroidBridge.addDefinition("MatomoShareButton", MatomoShareButton);
class RotateHelper {
rotate(element, degrees){
let rotateText = element.innerText;
element.removeAllChildren();
let partDegree = degrees/rotateText.length;
for(let i = 0; i < rotateText.length; i++){
let child = document.createElement("span");
child.innerText = rotateText.charAt(i);
child.style.transform ="rotate("+(partDegree*i)+"deg)";
child.classList.add("rotated");
element.appendChild(child);
}
}
}
class ScaleHelper {
async scaleTo(scale, fontElement, container, ignoreHeight, ignoreWidth, margin, fontWeight, animationDelay, addListener) {
addListener = Helper.nonNull(addListener, true);
animationDelay = Helper.nonNull(animationDelay, 0);
let newFontSize = await this._getNewFontSize(scale, fontElement, container, ignoreHeight, ignoreWidth, margin, fontWeight, animationDelay === 0);
if (animationDelay > 0) {
await new Promise(r => {
setTimeout(r, animationDelay);
fontElement.style.fontSize = newFontSize + "px";
});
}
let self = this;
let listener = function () {
return new Promise(resolve => {
let timeout = (typeof addListener === 'number') ? addListener : 255;
setTimeout(() => {
resolve(self.scaleTo(scale, fontElement, container, ignoreHeight, ignoreWidth, margin, fontWeight, animationDelay, false));
}, timeout);
});
};
if (addListener !== false) {
window.addEventListener("resize", listener);
}
return listener;
}
async scaleToFull(fontElement, container, ignoreHeight, ignoreWidth, margin, fontWeight, animDelay, addListener) {
return this.scaleTo(1, fontElement, container, ignoreHeight, ignoreWidth, margin, fontWeight, animDelay, addListener);
}
async _getNewFontSize(scale, fontElement, container, ignoreHeight, ignoreWidth, margin, fontWeight, setFontSize) {
margin = Helper.nonNull(margin, 10);
ignoreHeight = Helper.nonNull(ignoreHeight, false);
ignoreWidth = Helper.nonNull(ignoreWidth, false);
fontWeight = Helper.nonNull(fontWeight, fontElement.innerHTML.length);
setFontSize = Helper.nonNull(setFontSize, true);
let hasNoTransitionClass = container.classList.contains("no-transition");
if (!hasNoTransitionClass) {
container.classList.add("no-transition");
}
const numChanged = 5;
let oldDiffIndex = 0;
let oldDiff = [];
for (let i = 0; i < numChanged; i++) {
oldDiff.push(0);
}
let beforeFontSize = fontElement.style.fontSize;
let currentFontSize = 1;
let widthDiff = 0;
let heightDiff = 0;
let containerWidth = 0;
let containerHeight = 0;
do {
currentFontSize += oldDiff[oldDiffIndex] / (fontWeight + 1);
fontElement.style.fontSize = currentFontSize + 'px';
let containerStyle = window.getComputedStyle(container);
containerWidth = containerStyle.getPropertyValue("width").replace('px', '');
containerHeight = containerStyle.getPropertyValue("height").replace('px', '');
widthDiff = containerWidth - fontElement.offsetWidth;
heightDiff = containerHeight - fontElement.offsetHeight;
oldDiffIndex = (oldDiffIndex+1)%numChanged;
let newDiff = (ignoreWidth ? heightDiff : (ignoreHeight ? widthDiff : Math.min(widthDiff, heightDiff)));
if (newDiff === oldDiff[(oldDiffIndex+1)%numChanged]) {
break;
}
oldDiff[oldDiffIndex] = newDiff;
} while ((widthDiff > (1 - scale) * containerWidth || ignoreWidth) && (heightDiff > (1 - scale) * containerHeight || ignoreHeight));
currentFontSize -= margin;
fontElement.style.fontSize = ((setFontSize) ? currentFontSize + "px" : beforeFontSize);
if (!hasNoTransitionClass) {
await new Promise((r) => {
setTimeout(r, 50);
});
container.classList.remove("no-transition");
}
return currentFontSize;
}
}
class AudioChain {
constructor(context, sourceBuffer, chainFunction) {
this.buffer = sourceBuffer;
this.shouldLoop = false;
this.loopStart = null;
this.loopEnd = null;
this.chainFunction = chainFunction;
this.context = context;
this.startTime = null;
this.pauseTime = null;
this.source = null;
this.running = false;
}
setBuffer(buffer) {
this.buffer = buffer;
}
setLooping(shouldLoop, loopStart, loopEnd) {
this.shouldLoop = shouldLoop;
if (Helper.isNotNull(loopStart)) {
this.loopStart = loopStart;
}
if (Helper.isNotNull(loopEnd)) {
this.loopEnd = loopEnd;
}
}
async start(delay, offset, duration) {
//sind sonst null, schmeißt in Android 5 einen fehler
delay = Helper.nonNull(delay, 0);
offset = Helper.nonNull(offset, 0);
//Duration darf nicht gesetzt werden
// duration = Helper.nonNull(duration, -1);
let source = this.context.createBufferSource();
source.loop = this.shouldLoop;
if (Helper.isNotNull(this.loopStart)) {
source.loopStart = this.loopStart;
}
if (Helper.isNotNull(this.loopEnd)) {
source.loopEnd = this.loopEnd;
}
source.buffer = this.buffer;
await this.chainFunction(source);
if (Helper.isNull(duration)){
source.start(delay, offset);
}
else{
source.start(delay, offset, duration);
}
this.startTime = (new Date()).getTime() - (Helper.nonNull(offset, 0) * 1000);
this.source = source;
this.running = true;
}
async stop(delay) {
if (Helper.isNotNull(this.source)) {
delay = Helper.nonNull(delay, 0);
this.pauseTime = ((new Date()).getTime()) - this.startTime;
this.running = false;
return this.source.stop(delay);
}
return null;
}
async resume() {
if (!this.running) {
return this.start(null, Helper.nonNull(this.pauseTime, 0) / 1000.0);
}
}
}
class SoundManager {
static getInstance() {
if (Helper.isNull(SoundManager.instance)) {
SoundManager.instance = new SoundManager();
}
return SoundManager.instance;
}
constructor() {
this.channels = {};
this.context = new AudioContext();
this.context.onstatechange = function () {
console.log("stateChange from context", arguments);
};
this.context.oncomplete = function () {
console.log("onComplete from context", arguments);
};
window.addEventListener("visibilitychange", () => {
this.handleVisibilityChange();
});
}
isNotSuspended(){
// return false;
return this.context.state !== "suspended";
}
set(options, channel) {
channel = Helper.nonNull(channel, SoundManager.CHANNELS.DEFAULT);
let audioObject = Helper.nonNull(this.channels[channel], {});
if (typeof options === "string") {
options = {audio: options};
}
let audio = options.audio;
if (Helper.isNotNull(audio)) {
audioObject.loadedPromise = fetch(audio).then(res => res.arrayBuffer()).then(arrayBuffer => {
return new Promise((r) => this.context.decodeAudioData(arrayBuffer, r));
}).catch(e => console.error(e));
this.stop(channel);
}
audioObject.muted = Helper.nonNull(options.muted, audioObject.muted, false);
audioObject.volume = Helper.nonNull(options.volume, audioObject.volume, 1);
audioObject.loop = Helper.nonNull(options.loop, audioObject.loop, false);
audioObject.timeOffset = Helper.nonNull(options.timeOffset, audioObject.timeOffset, 0);
this.channels[channel] = audioObject;
if (audioObject.muted) {
this.stop(channel);
}
return this.channels[channel];
}
async resumeContext(){
if (typeof this.context.resume === "function") {
return this.context.resume();
}
}
async play(channel, audioOrOptions) {
this.resumeContext();
channel = Helper.nonNull(channel, SoundManager.CHANNELS.DEFAULT);
if (Helper.isNull(audioOrOptions)) {
audioOrOptions = {};
} else if (!(typeof audioOrOptions === "object")) {
audioOrOptions = {
audio: audioOrOptions
};
}
audioOrOptions.timeOffset = Helper.nonNull(audioOrOptions.timeOffset, 0);
this.stop(channel);
this.set(audioOrOptions, channel);
if (!this.channels[channel].muted) {
let buffer = await this.channels[channel].loadedPromise;
let source = new AudioChain(this.context, buffer, (sourceNode) => {
let gain = this.context.createGain();
gain.gain.value = this.channels[channel].volume;
sourceNode.connect(gain);
gain.connect(this.context.destination);
});
source.setBuffer(buffer);
//to prevent gap in mp3-files
source.setLooping(this.channels[channel].loop, 0.3, buffer.duration - 0.3);
this.channels[channel].source = source;
await source.start();
}
return this.channels[channel];
}
stop(channel) {
channel = Helper.nonNull(channel, SoundManager.CHANNELS.DEFAULT);
let oldAudio = this.channels[channel];
if (Helper.isNotNull(oldAudio) && Helper.isNotNull(oldAudio.source)) {
oldAudio.source.stop();
}
}
get(channel) {
channel = Helper.nonNull(channel, SoundManager.CHANNELS.DEFAULT);
return this.channels[channel];
}
async resume(channel) {
channel = Helper.nonNull(channel, SoundManager.CHANNELS.DEFAULT);
if (Helper.isNotNull(this.channels[channel]) && !this.channels[channel].muted && Helper.isNotNull(this.channels[channel].source)) {
return this.channels[channel].source.resume();
}
}
stopAll(){
for (let k in this.channels) {
if (Helper.isNotNull(this.channels[k].source)) {
this.channels[k].source.stop();
}
}
}
resumeAllIfNotMuted(){
for (let k in this.channels) {
if (Helper.isNotNull(this.channels[k]) && !this.channels[k].muted && Helper.isNotNull(this.channels[k].source)) {
this.channels[k].source.resume();
}
}
}
handleVisibilityChange() {
if (document.hidden) {
this.stopAll();
}
else {
this.resumeAllIfNotMuted();
}
}
}
SoundManager.CHANNELS = {
MUSIC: "music",
SOUND: "sound",
DEFAULT: "default"
};
InitPromise.addPromise(() => {
PauseSite.onPauseListeners.push(() => {
SoundManager.getInstance().stopAll();
});
PauseSite.onStartListeners.push(() => {
SoundManager.getInstance().resumeAllIfNotMuted();
});
});
// AndroidBridge.addDefinition(() => {
// window["soundManagerInstance"] = SoundManager.getInstance();
// window["soundManagerInstance"]["stopAll"] = window["soundManagerInstance"].stopAll;
// window["soundManagerInstance"]["resumeAllIfNotMuted"] = window["soundManagerInstance"].resumeAllIfNotMuted;
// });
class TabbedFragment extends Fragment {
constructor(site) {
super(site, 'pwaAssets/html/fragment/tabbedFragment.html');
this.fragments = {};
this.tabHeadingTemplate = null;
this.tabHeadingContainer = null;
this.tabContainer = null;
this.tabKeys = [];
this.currentTabIndex = 0;
}
onFirstStart() {
super.onFirstStart();
this.tabContainer = this.findBy(".tab-container");
this.showTab(0);
}
addFragment(name, fragment, translationArgs, isTranslatable) {
isTranslatable = Helper.nonNull(isTranslatable, true);
if (translationArgs === false) {
isTranslatable = false;
}
this.fragments[name] = fragment;
const tabIndex = this.tabKeys.length;
this.tabKeys.push(name);
const _self = this;
this.inflatePromise = this.inflatePromise.then(function (siteContent) {
if (Helper.isNull(_self.tabHeadingTemplate)) {
_self.tabHeadingTemplate = siteContent.querySelector(".tab-header-template");
}
const newTabHeader = Helper.cloneNode(_self.tabHeadingTemplate);
newTabHeader.classList.add("tab-" + tabIndex);
newTabHeader.querySelector(".tab-name").appendChild((isTranslatable) ? Translator.makePersistentTranslation(name, translationArgs) : document.createTextNode(name));
newTabHeader.addEventListener("click", function(){
_self.showTab(tabIndex);
});
if (Helper.isNull(_self.tabHeadingContainer)) {
_self.tabHeadingContainer = siteContent.querySelector(".tab-header-container");
}
_self.tabHeadingContainer.appendChild(newTabHeader);
return siteContent;
});
}
showTab(index) {
if (index >= 0 && index < this.tabKeys.length) {
this.findBy(".tab-" + this.currentTabIndex).classList.remove("active");
this.findBy(".tab-" + index).classList.add("active");
this.tabContainer.removeAllChildren().appendChild(Helper.createLoadingSymbol());
this.currentTabIndex = index;
const _self = this;
this.fragments[this.tabKeys[index]].inflatePromise.then(tabView => {
if (_self.currentTabIndex === index) {
_self.tabContainer.removeAllChildren().appendChild(tabView);
}
});
}
}
}
export { DelayPromise, InstallManager, Matomo, MatomoShareButton, MyStorageManager, RotateHelper, ScaleHelper, AudioChain, SoundManager, TabbedFragment };