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 };