const notifyChannel = new BroadcastChannel("tabsSync"); const SETTINGS_STORAGE_KEY = "pbSettings"; const COLOR_SCHEME_STORAGE_KEY = "pbColorScheme"; window.app = window.app || {}; window.app.store = store({ // flag used to track when the internal bootstrap process is done _ready: false, // the current authenticated superuser superuser: null, // used to force hiding the header even when authenticated showHeader: true, page: t.div({ className: "page" }, () => { if (!app.store._ready) { return t.span({ className: "loader lg m-auto", title: "Loading plugins..." }); } }), mainLogo: import.meta.env.BASE_URL + "images/logo.svg", headerLogo: import.meta.env.BASE_URL + "images/logo_white.svg", favicon: "", // leave empty to fallback to the default one title: "", _mediaColorScheme: "", userColorScheme: window.localStorage.getItem(COLOR_SCHEME_STORAGE_KEY) || "", get activeColorScheme() { // explicitly set if (app.store.userColorScheme) { return app.store.userColorScheme; } // fallback to the loaded browser preference return app.store._mediaColorScheme || "light"; }, // api response errors errors: null, creditLinks: [ { // optional: isActive href: import.meta.env.PB_DOCS_URL, icon: "ri-book-open-line", label: "Docs", }, { href: import.meta.env.PB_RELEASES, icon: "ri-github-line", label: `PocketBase ${import.meta.env.PB_VERSION}`, }, ], headerLinks: [ { // optional: isActive href: "#/collections", icon: "ri-database-2-line", label: "Collections", }, { href: "#/logs", icon: "ri-bar-chart-box-line", label: "Logs", }, { href: "#/settings", icon: "ri-settings-3-line", label: "Settings", }, ], settingsNavGroups: { System: [ { // optional: isActive href: "#/settings", icon: "ri-home-gear-line", label: "Application", }, { href: "#/settings/mail", icon: "ri-send-plane-2-line", label: "Mail settings", }, { href: "#/settings/storage", icon: "ri-archive-drawer-line", label: "Files storage", }, { href: "#/settings/backups", icon: "ri-archive-line", label: "Backups", }, { href: "#/settings/crons", icon: "ri-time-line", label: "Crons", }, ], Sync: [ { href: "#/settings/export-collections", icon: "ri-uninstall-line", label: "Export collections", }, { href: "#/settings/import-collections", icon: "ri-install-line", label: "Import collections", }, ], }, predefinedAccentColors: [ "#1055c9", "#a3142a", "#096d5c", "#e6620a", "#007d9c", "#3f3da9", ], settings: app.utils.getLocalHistory(SETTINGS_STORAGE_KEY, {}), isLoadingSettings: false, async loadSettings() { app.store.isLoadingSettings = true; try { const settings = await app.pb.settings.getAll({ requestKey: "appStore.loadSettings" }); app.store.settings = settings; app.store.isLoadingSettings = false; } catch (err) { if (!err.isAbort) { app.store.isLoadingSettings = false; app.checkApiError(err); } } }, collections: [], collectionScaffolds: {}, isLoadingCollections: false, _activeCollectionIdOrName: "", get activeCollection() { const idOrName = app.store._activeCollectionIdOrName; return app.store.collections.find((c) => c.id == idOrName || c.name == idOrName) || app.store.collections[0]; }, set activeCollection(collection) { if (typeof collection == "string") { app.store._activeCollectionIdOrName = collection; } else { app.store._activeCollectionIdOrName = collection?.id; } }, async silentlyReloadCollections() { try { let newCollections = await app.pb.collections.getFullList({ requestKey: "appStore.silentlyReloadCollections", }); newCollections = app.utils.sortedCollectionsByType(newCollections); if (JSON.stringify(newCollections) != JSON.stringify(app.store.collections)) { app.store.collections = newCollections; } } catch (err) { if (!err.isAbort) { console.warn("failed to reload app store collections:", err); } } }, async loadCollections(activeIdOrName = null) { app.store.isLoadingCollections = true; try { const [resultScaffolds, resultCollections] = await Promise.all([ app.pb.collections.getScaffolds({ requestKey: "appStore.loadCollections.getScaffolds" }), app.pb.collections.getFullList({ requestKey: "appStore.loadCollections.getFullList" }), ]); app.store.collections = app.utils.sortedCollectionsByType(resultCollections); app.store.collectionScaffolds = resultScaffolds; app.store._activeCollectionIdOrName = activeIdOrName || app.store._activeCollectionIdOrName || app.store.collections[0]?.id || ""; app.store.isLoadingCollections = false; } catch (err) { if (!err.isAbort) { app.store.isLoadingCollections = false; app.checkApiError(err); } } }, addOrUpdateCollection(collection) { const index = app.store.collections.findIndex((c) => c.id == collection.id); if (index >= 0) { if (app.store.activeCollection.id == collection.id) { app.store._activeCollectionIdOrName = collection.id; } app.store.collections[index] = collection; } else { app.store.collections.push(collection); } app.store.collections = app.utils.sortedCollectionsByType(app.store.collections); }, oauth2Providers: [], isLoadingOAuth2Providers: false, async loadOAuth2Providers() { app.store.isLoadingOAuth2Providers = true; try { // @todo replace with SDK call app.store.oauth2Providers = await app.pb.send("/api/collections/meta/oauth2-providers"); app.store.isLoadingOAuth2Providers = false; } catch (err) { if (!err.isAbort) { app.checkApiError(err); app.store.isLoadingOAuth2Providers = false; } } }, }); // reset title and errors on route change window.addEventListener("hashchange", () => { app.store.title = ""; app.store.errors = null; }); // append the app settings name to document.title. watch(() => { let titleParts = app.utils.toArray(app.store.title); const appName = app.store.settings?.meta?.appName || ""; if (appName) { titleParts.push(appName); } document.title = titleParts.join(" - "); }); // sync with the accent color let metaThemeColor; watch(() => app.store.settings?.meta?.accentColor, (newColor) => { if (!metaThemeColor) { metaThemeColor = t.meta({ name: "theme-color" }); document.head.appendChild(metaThemeColor); } if (newColor) { metaThemeColor?.setAttribute("content", newColor); document.documentElement.style.setProperty("--accentColor", newColor); } else { metaThemeColor?.removeAttribute("content"); document.documentElement.style.removeProperty("--accentColor"); } }); // sync favicon let linkFavicon; watch(() => app.store.favicon, (favicon) => { if (!linkFavicon) { linkFavicon = t.link({ rel: "icon" }); document.head.appendChild(linkFavicon); } if (favicon) { linkFavicon.href = favicon; } else { linkFavicon.href = window.location.href.startsWith("https://") ? "./images/favicon_prod.png" : "./images/favicon.png"; } }); // sync color scheme const colorSchemeMedia = window.matchMedia("(prefers-color-scheme: dark)"); app.store._mediaColorScheme = colorSchemeMedia.matches ? "dark" : "light"; colorSchemeMedia.addEventListener("change", ({ matches }) => { app.store._mediaColorScheme = matches ? "dark" : "light"; }); watch(() => app.store.userColorScheme, (colorScheme) => { if (!colorScheme) { window.localStorage.removeItem(COLOR_SCHEME_STORAGE_KEY); } else { window.localStorage.setItem(COLOR_SCHEME_STORAGE_KEY, colorScheme); } notifyChannel?.postMessage({ colorScheme }); }); // temporary disable animations on color scheme change to minimize flickering let tempNoAnimationTimeoutId; watch(() => app.store.activeColorScheme, (colorScheme) => { clearTimeout(tempNoAnimationTimeoutId); document.documentElement.style.setProperty("--animationSpeed", "0"); document.documentElement.setAttribute("data-color-scheme", colorScheme); // restore animation tempNoAnimationTimeoutId = setTimeout(() => { document.documentElement.style.removeProperty("--animationSpeed"); }, 100); }); // Errors handler // ------------------------------------------------------------------- function removeErrorState(input, container) { if (input.__errListener) { input.removeEventListener("input", input.__errListener); input.removeEventListener("change", input.__errListener); input.__errListener = null; } if (input.setCustomValidity) { input.setCustomValidity(""); if (input._oldTitle) { input.setAttribute("title", input._oldTitle); } else { input.removeAttribute("title"); } } input.removeAttribute("data-error"); const helpElem = container.nextSibling; if ( helpElem && helpElem.classList?.contains("generated-error") // remove only the error help text related to the input && helpElem.getAttribute("data-input-name") == input.getAttribute("name") ) { helpElem.remove(); } // no other error inputs if (!container.querySelector("[data-error]")) { container.classList.remove("error"); } } watch( () => JSON.stringify(app.store.errors) && app.store.errors, (errs) => { // search for input or other elements wiht "name" attribute const inputs = document.querySelectorAll(`[name]`); for (let input of inputs) { if (input.classList.contains("no-error")) { continue; } const container = input.closest(".fields") || input.closest(".field"); if (!container) { continue; } const name = input.getAttribute("name"); removeErrorState(input, container); const errMsg = app.utils.getByPath(errs, name)?.message; if (!errMsg) { continue; } container.classList.add("error"); input.__errListener = function() { removeErrorState(input, container); app.utils.deleteByPath(app.store.errors, name); }; input.addEventListener("input", input.__errListener); input.addEventListener("change", input.__errListener); input.setAttribute("data-error", true); if (input.setCustomValidity && input.reportValidity && input.classList.contains("inline-error")) { input.setCustomValidity(errMsg); input.reportValidity(); input._oldTitle = input.title; input.title = errMsg; } else { container.after( t.div({ "html-data-input-name": name, "className": "field-help error generated-error", "textContent": errMsg, }), ); } } }, ); // Tabs sync // ------------------------------------------------------------------- notifyChannel.onmessage = (e) => { if ( e.data?.collections // replace only if there are changes to minimize flickering && JSON.stringify(app.store.collections) != JSON.stringify(e.data.collections) ) { app.store.collections = e.data.collections; } if ( e.data?.settings // replace only if there are changes to minimize flickering && JSON.stringify(app.store.settings) != JSON.stringify(e.data.settings) ) { app.store.settings = e.data.settings; } if (e.data?.colorScheme) { app.store.userColorScheme = e.data.colorScheme; } }; watch( () => JSON.stringify(app.store.collections), (newHash, oldHash) => { if (newHash && newHash != "[]" && oldHash && oldHash != "[]" && newHash != oldHash) { notifyChannel?.postMessage({ collections: JSON.parse(newHash), }); } }, ); watch( () => JSON.stringify(app.store.settings), (newHash, oldHash) => { if (newHash && newHash != "{}" && oldHash && oldHash != "{}" && newHash != oldHash) { notifyChannel?.postMessage({ settings: JSON.parse(newHash), }); } window.localStorage.setItem(SETTINGS_STORAGE_KEY, newHash); }, );