diff --git a/materialious/android/app/build.gradle b/materialious/android/app/build.gradle index 77d92978..4e421263 100644 --- a/materialious/android/app/build.gradle +++ b/materialious/android/app/build.gradle @@ -7,8 +7,8 @@ android { applicationId "us.materialio.app" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 138 - versionName "1.9.21" + versionCode 139 + versionName "1.9.22" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. diff --git a/materialious/electron/materialious.metainfo.xml b/materialious/electron/materialious.metainfo.xml index d82b5c68..decdd37d 100644 --- a/materialious/electron/materialious.metainfo.xml +++ b/materialious/electron/materialious.metainfo.xml @@ -55,7 +55,11 @@ intense - + + + https://github.com/Materialious/Materialious/releases/tag/1.9.22 + + https://github.com/Materialious/Materialious/releases/tag/1.9.21 diff --git a/materialious/electron/package-lock.json b/materialious/electron/package-lock.json index e7b06988..9425e8d8 100644 --- a/materialious/electron/package-lock.json +++ b/materialious/electron/package-lock.json @@ -1,12 +1,12 @@ { "name": "Materialious", - "version": "1.9.21", + "version": "1.9.22", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "Materialious", - "version": "1.9.21", + "version": "1.9.22", "license": "MIT", "dependencies": { "@capacitor-community/electron": "^5.0.0", @@ -22,7 +22,7 @@ "jsonfile": "^6.1.0" }, "devDependencies": { - "electron": "^37.2.1", + "electron": "^37.2.3", "electron-builder": "^26.0.12", "electron-rebuild": "^3.2.9", "typescript": "^5.7.3" @@ -2495,9 +2495,9 @@ } }, "node_modules/electron": { - "version": "37.2.1", - "resolved": "https://registry.npmjs.org/electron/-/electron-37.2.1.tgz", - "integrity": "sha512-ae2EbzRNqIAHlftfCHtbbt6EgJUW8+zxWLONqNnn2iSrLF0O/pbxbff3xcpZYPpmFBs4uqjoi+s4QS7DQ+zZ/w==", + "version": "37.2.3", + "resolved": "https://registry.npmjs.org/electron/-/electron-37.2.3.tgz", + "integrity": "sha512-JRKKn8cRDXDfkC+oWISbYs+c+L6RA776JM0NiB9bn2yV8H/LnBUlVPzKKfsXgrUIokN4YcbCw694vfAdEJwtGw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -3093,15 +3093,16 @@ } }, "node_modules/form-data": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", - "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { diff --git a/materialious/electron/package.json b/materialious/electron/package.json index 70c81c57..6b4e98e8 100644 --- a/materialious/electron/package.json +++ b/materialious/electron/package.json @@ -1,6 +1,6 @@ { "name": "Materialious", - "version": "1.9.21", + "version": "1.9.22", "description": "Modern material design for Invidious.", "author": { "name": "Ward Pearce", @@ -35,7 +35,7 @@ "jsonfile": "^6.1.0" }, "devDependencies": { - "electron": "^37.2.1", + "electron": "^37.2.3", "electron-builder": "^26.0.12", "electron-rebuild": "^3.2.9", "typescript": "^5.7.3" @@ -44,4 +44,4 @@ "capacitor", "electron" ] -} +} \ No newline at end of file diff --git a/materialious/electron/src/index.ts b/materialious/electron/src/index.ts index b1a2e6a4..642bc1e3 100644 --- a/materialious/electron/src/index.ts +++ b/materialious/electron/src/index.ts @@ -3,8 +3,7 @@ import { getCapacitorElectronConfig, setupElectronDeepLinking } from '@capacitor-community/electron'; -import type { MenuItemConstructorOptions } from 'electron'; -import { app, ipcMain, MenuItem, session } from 'electron'; +import { app, ipcMain, session } from 'electron'; import electronIsDev from 'electron-is-dev'; import unhandled from 'electron-unhandled'; import { autoUpdater } from 'electron-updater'; @@ -16,25 +15,12 @@ import { ElectronCapacitorApp, setupContentSecurityPolicy, setupReloadWatcher } // Graceful handling of unhandled errors. unhandled(); -// Define our menu templates (these are optional) -const trayMenuTemplate: (MenuItemConstructorOptions | MenuItem)[] = [ - new MenuItem({ label: 'Quit App', role: 'quit' }) -]; -const appMenuBarMenuTemplate: (MenuItemConstructorOptions | MenuItem)[] = [ - { role: process.platform === 'darwin' ? 'appMenu' : 'fileMenu' }, - { role: 'viewMenu' } -]; - // Get Config options from capacitor.config const capacitorFileConfig: CapacitorElectronConfig = getCapacitorElectronConfig(); // Initialize our app. You can pass menu templates into the app here. // const myCapacitorApp = new ElectronCapacitorApp(capacitorFileConfig); -const myCapacitorApp = new ElectronCapacitorApp( - capacitorFileConfig, - trayMenuTemplate, - appMenuBarMenuTemplate -); +const myCapacitorApp = new ElectronCapacitorApp(capacitorFileConfig); // If deeplinking is enabled then we will set it up here. if (capacitorFileConfig.electron?.deepLinkingEnabled) { diff --git a/materialious/electron/src/setup.ts b/materialious/electron/src/setup.ts index d6169790..7f026b1f 100644 --- a/materialious/electron/src/setup.ts +++ b/materialious/electron/src/setup.ts @@ -1,13 +1,23 @@ import type { CapacitorElectronConfig } from '@capacitor-community/electron'; import { - CapacitorSplashScreen, - CapElectronEventEmitter, - setupCapacitorElectronPlugins, + CapacitorSplashScreen, + CapElectronEventEmitter, + setupCapacitorElectronPlugins } from '@capacitor-community/electron'; import { USER_AGENT } from 'bgutils-js'; import chokidar from 'chokidar'; import type { MenuItemConstructorOptions } from 'electron'; -import { app, BrowserWindow, Menu, MenuItem, nativeImage, session, shell, Tray } from 'electron'; +import { + app, + BrowserWindow, + Menu, + MenuItem, + nativeImage, + session, + shell, + Tray, + globalShortcut +} from 'electron'; import electronIsDev from 'electron-is-dev'; import electronServe from 'electron-serve'; import windowStateKeeper from 'electron-window-state'; @@ -15,222 +25,236 @@ import { join } from 'path'; // Define components for a watcher to detect when the webapp is changed so we can reload in Dev mode. const reloadWatcher = { - debouncer: null, - ready: false, - watcher: null, + debouncer: null, + ready: false, + watcher: null }; export function setupReloadWatcher(electronCapacitorApp: ElectronCapacitorApp): void { - reloadWatcher.watcher = chokidar - .watch(join(app.getAppPath(), 'app'), { - ignored: /[/\\]\./, - persistent: true, - }) - .on('ready', () => { - reloadWatcher.ready = true; - }) - .on('all', (_event, _path) => { - if (reloadWatcher.ready) { - clearTimeout(reloadWatcher.debouncer); - reloadWatcher.debouncer = setTimeout(async () => { - electronCapacitorApp.getMainWindow().webContents.reload(); - reloadWatcher.ready = false; - clearTimeout(reloadWatcher.debouncer); - reloadWatcher.debouncer = null; - reloadWatcher.watcher = null; - setupReloadWatcher(electronCapacitorApp); - }, 1500); - } - }); + reloadWatcher.watcher = chokidar + .watch(join(app.getAppPath(), 'app'), { + ignored: /[/\\]\./, + persistent: true + }) + .on('ready', () => { + reloadWatcher.ready = true; + }) + .on('all', (_event, _path) => { + if (reloadWatcher.ready) { + clearTimeout(reloadWatcher.debouncer); + reloadWatcher.debouncer = setTimeout(async () => { + electronCapacitorApp.getMainWindow().webContents.reload(); + reloadWatcher.ready = false; + clearTimeout(reloadWatcher.debouncer); + reloadWatcher.debouncer = null; + reloadWatcher.watcher = null; + setupReloadWatcher(electronCapacitorApp); + }, 1500); + } + }); } // Define our class to manage our app. export class ElectronCapacitorApp { - private MainWindow: BrowserWindow | null = null; - private SplashScreen: CapacitorSplashScreen | null = null; - private TrayIcon: Tray | null = null; - private CapacitorFileConfig: CapacitorElectronConfig; - private TrayMenuTemplate: (MenuItem | MenuItemConstructorOptions)[] = [ - new MenuItem({ label: 'Quit App', role: 'quit' }), - ]; - private AppMenuBarMenuTemplate: (MenuItem | MenuItemConstructorOptions)[] = [ - { role: process.platform === 'darwin' ? 'appMenu' : 'fileMenu' }, - { role: 'viewMenu' }, - ]; - private mainWindowState; - private loadWebApp; - private customScheme: string; + private MainWindow: BrowserWindow | null = null; + private SplashScreen: CapacitorSplashScreen | null = null; + private TrayIcon: Tray | null = null; + private CapacitorFileConfig: CapacitorElectronConfig; + private TrayMenuTemplate: (MenuItem | MenuItemConstructorOptions)[] = [ + new MenuItem({ label: 'Quit App', role: 'quit' }) + ]; + private mainWindowState; + private loadWebApp; + private customScheme: string; - constructor( - capacitorFileConfig: CapacitorElectronConfig, - trayMenuTemplate?: (MenuItemConstructorOptions | MenuItem)[], - appMenuBarMenuTemplate?: (MenuItemConstructorOptions | MenuItem)[] - ) { - this.CapacitorFileConfig = capacitorFileConfig; + constructor( + capacitorFileConfig: CapacitorElectronConfig, + trayMenuTemplate?: (MenuItemConstructorOptions | MenuItem)[] + ) { + this.CapacitorFileConfig = capacitorFileConfig; - this.customScheme = this.CapacitorFileConfig.electron?.customUrlScheme ?? 'capacitor-electron'; + this.customScheme = this.CapacitorFileConfig.electron?.customUrlScheme ?? 'capacitor-electron'; - if (trayMenuTemplate) { - this.TrayMenuTemplate = trayMenuTemplate; - } + if (trayMenuTemplate) { + this.TrayMenuTemplate = trayMenuTemplate; + } - if (appMenuBarMenuTemplate) { - this.AppMenuBarMenuTemplate = appMenuBarMenuTemplate; - } + // Setup our web app loader, this lets us load apps like react, vue, and angular without changing their build chains. + this.loadWebApp = electronServe({ + directory: join(app.getAppPath(), 'app'), + scheme: this.customScheme + }); + } - // Setup our web app loader, this lets us load apps like react, vue, and angular without changing their build chains. - this.loadWebApp = electronServe({ - directory: join(app.getAppPath(), 'app'), - scheme: this.customScheme, - }); - } + // Helper function to load in the app. + private async loadMainWindow(thisRef: any) { + await thisRef.loadWebApp(thisRef.MainWindow); + } - // Helper function to load in the app. - private async loadMainWindow(thisRef: any) { - await thisRef.loadWebApp(thisRef.MainWindow); - } + // Expose the mainWindow ref for use outside of the class. + getMainWindow(): BrowserWindow { + return this.MainWindow; + } - // Expose the mainWindow ref for use outside of the class. - getMainWindow(): BrowserWindow { - return this.MainWindow; - } + getCustomURLScheme(): string { + return this.customScheme; + } - getCustomURLScheme(): string { - return this.customScheme; - } + async init(): Promise { + const icon = nativeImage.createFromPath( + join(app.getAppPath(), 'assets', process.platform === 'win32' ? 'appIcon.ico' : 'appIcon.png') + ); + this.mainWindowState = windowStateKeeper({ + defaultWidth: 1000, + defaultHeight: 800 + }); + // Setup preload script path and construct our main window. + const preloadPath = join(app.getAppPath(), 'build', 'src', 'preload.js'); + this.MainWindow = new BrowserWindow({ + icon, + show: false, + x: this.mainWindowState.x, + y: this.mainWindowState.y, + width: this.mainWindowState.width, + height: this.mainWindowState.height, + webPreferences: { + nodeIntegration: true, + contextIsolation: true, + webSecurity: false, + // Use preload to inject the electron varriant overrides for capacitor plugins. + // preload: join(app.getAppPath(), "node_modules", "@capacitor-community", "electron", "dist", "runtime", "electron-rt.js"), + preload: preloadPath + } + }); + this.mainWindowState.manage(this.MainWindow); - async init(): Promise { - const icon = nativeImage.createFromPath( - join(app.getAppPath(), 'assets', process.platform === 'win32' ? 'appIcon.ico' : 'appIcon.png') - ); - this.mainWindowState = windowStateKeeper({ - defaultWidth: 1000, - defaultHeight: 800, - }); - // Setup preload script path and construct our main window. - const preloadPath = join(app.getAppPath(), 'build', 'src', 'preload.js'); - this.MainWindow = new BrowserWindow({ - icon, - show: false, - x: this.mainWindowState.x, - y: this.mainWindowState.y, - width: this.mainWindowState.width, - height: this.mainWindowState.height, - webPreferences: { - nodeIntegration: true, - contextIsolation: true, - webSecurity: false, - // Use preload to inject the electron varriant overrides for capacitor plugins. - // preload: join(app.getAppPath(), "node_modules", "@capacitor-community", "electron", "dist", "runtime", "electron-rt.js"), - preload: preloadPath, - }, - }); - this.mainWindowState.manage(this.MainWindow); + if (this.CapacitorFileConfig.backgroundColor) { + this.MainWindow.setBackgroundColor(this.CapacitorFileConfig.electron.backgroundColor); + } - if (this.CapacitorFileConfig.backgroundColor) { - this.MainWindow.setBackgroundColor(this.CapacitorFileConfig.electron.backgroundColor); - } + // If we close the main window with the splashscreen enabled we need to destory the ref. + this.MainWindow.on('closed', () => { + if ( + this.SplashScreen?.getSplashWindow() && + !this.SplashScreen.getSplashWindow().isDestroyed() + ) { + this.SplashScreen.getSplashWindow().close(); + } + }); - // If we close the main window with the splashscreen enabled we need to destory the ref. - this.MainWindow.on('closed', () => { - if (this.SplashScreen?.getSplashWindow() && !this.SplashScreen.getSplashWindow().isDestroyed()) { - this.SplashScreen.getSplashWindow().close(); - } - }); + // When the tray icon is enabled, setup the options. + if (this.CapacitorFileConfig.electron?.trayIconAndMenuEnabled) { + this.TrayIcon = new Tray(icon); + this.TrayIcon.on('double-click', () => { + if (this.MainWindow) { + if (this.MainWindow.isVisible()) { + this.MainWindow.hide(); + } else { + this.MainWindow.show(); + this.MainWindow.focus(); + } + } + }); + this.TrayIcon.on('click', () => { + if (this.MainWindow) { + if (this.MainWindow.isVisible()) { + this.MainWindow.hide(); + } else { + this.MainWindow.show(); + this.MainWindow.focus(); + } + } + }); + this.TrayIcon.setToolTip(app.getName()); + this.TrayIcon.setContextMenu(Menu.buildFromTemplate(this.TrayMenuTemplate)); + } - // When the tray icon is enabled, setup the options. - if (this.CapacitorFileConfig.electron?.trayIconAndMenuEnabled) { - this.TrayIcon = new Tray(icon); - this.TrayIcon.on('double-click', () => { - if (this.MainWindow) { - if (this.MainWindow.isVisible()) { - this.MainWindow.hide(); - } else { - this.MainWindow.show(); - this.MainWindow.focus(); - } - } - }); - this.TrayIcon.on('click', () => { - if (this.MainWindow) { - if (this.MainWindow.isVisible()) { - this.MainWindow.hide(); - } else { - this.MainWindow.show(); - this.MainWindow.focus(); - } - } - }); - this.TrayIcon.setToolTip(app.getName()); - this.TrayIcon.setContextMenu(Menu.buildFromTemplate(this.TrayMenuTemplate)); - } + // Disable Menu Bar at the top. + Menu.setApplicationMenu(null); - // Setup the main manu bar at the top of our window. - Menu.setApplicationMenu(Menu.buildFromTemplate(this.AppMenuBarMenuTemplate)); + // If the splashscreen is enabled, show it first while the main window loads then switch it out for the main window, or just load the main window from the start. + if (this.CapacitorFileConfig.electron?.splashScreenEnabled) { + this.SplashScreen = new CapacitorSplashScreen({ + imageFilePath: join( + app.getAppPath(), + 'assets', + this.CapacitorFileConfig.electron?.splashScreenImageName ?? 'splash.png' + ), + windowWidth: 400, + windowHeight: 400 + }); + this.SplashScreen.init(this.loadMainWindow, this); + } else { + this.loadMainWindow(this); + } - // If the splashscreen is enabled, show it first while the main window loads then switch it out for the main window, or just load the main window from the start. - if (this.CapacitorFileConfig.electron?.splashScreenEnabled) { - this.SplashScreen = new CapacitorSplashScreen({ - imageFilePath: join( - app.getAppPath(), - 'assets', - this.CapacitorFileConfig.electron?.splashScreenImageName ?? 'splash.png' - ), - windowWidth: 400, - windowHeight: 400, - }); - this.SplashScreen.init(this.loadMainWindow, this); - } else { - this.loadMainWindow(this); - } + globalShortcut.register('Control+Shift+I', () => { + this.MainWindow.webContents.toggleDevTools(); + }); - // Security - this.MainWindow.webContents.setWindowOpenHandler((details) => { - shell.openExternal(details.url); - return { action: 'deny' }; - }); - this.MainWindow.webContents.on('will-navigate', (event, _newURL) => { - if (!this.MainWindow.webContents.getURL().includes(this.customScheme)) { - event.preventDefault(); - } - }); - // Link electron plugins into the system. - setupCapacitorElectronPlugins(); + let zoomFactor = 1.0; + globalShortcut.register('Control+Plus', () => { + zoomFactor += 0.1; + this.MainWindow.webContents.setZoomFactor(zoomFactor); + }); - // When the web app is loaded we hide the splashscreen if needed and show the mainwindow. - this.MainWindow.webContents.on('dom-ready', () => { - if (this.CapacitorFileConfig.electron?.splashScreenEnabled) { - this.SplashScreen.getSplashWindow().hide(); - } - if (!this.CapacitorFileConfig.electron?.hideMainWindowOnLaunch) { - this.MainWindow.show(); - } - setTimeout(() => { - if (electronIsDev) { - this.MainWindow.webContents.openDevTools(); - } - CapElectronEventEmitter.emit('CAPELECTRON_DeeplinkListenerInitialized', ''); - }, 400); - }); - } + globalShortcut.register('Control+Minus', () => { + zoomFactor = Math.max(0.2, zoomFactor - 0.1); + this.MainWindow.webContents.setZoomFactor(zoomFactor); + }); + + globalShortcut.register('Control+0', () => { + zoomFactor = 1.0; + this.MainWindow.webContents.setZoomFactor(zoomFactor); + }); + + // Security + this.MainWindow.webContents.setWindowOpenHandler((details) => { + shell.openExternal(details.url); + return { action: 'deny' }; + }); + this.MainWindow.webContents.on('will-navigate', (event, _newURL) => { + if (!this.MainWindow.webContents.getURL().includes(this.customScheme)) { + event.preventDefault(); + } + }); + // Link electron plugins into the system. + setupCapacitorElectronPlugins(); + + // When the web app is loaded we hide the splashscreen if needed and show the mainwindow. + this.MainWindow.webContents.on('dom-ready', () => { + if (this.CapacitorFileConfig.electron?.splashScreenEnabled) { + this.SplashScreen.getSplashWindow().hide(); + } + if (!this.CapacitorFileConfig.electron?.hideMainWindowOnLaunch) { + this.MainWindow.show(); + } + setTimeout(() => { + if (electronIsDev) { + this.MainWindow.webContents.openDevTools(); + } + CapElectronEventEmitter.emit('CAPELECTRON_DeeplinkListenerInitialized', ''); + }, 400); + }); + } } // Set a CSP up for our application based on the custom scheme export function setupContentSecurityPolicy(customScheme: string): void { - session.defaultSession.webRequest.onHeadersReceived((details, callback) => { - callback({ - responseHeaders: { - ...details.responseHeaders, - }, - }); - }); + session.defaultSession.webRequest.onHeadersReceived((details, callback) => { + callback({ + responseHeaders: { + ...details.responseHeaders + } + }); + }); - session.defaultSession.webRequest.onBeforeSendHeaders((details, callback) => { - const uri = new URL(details.url); + session.defaultSession.webRequest.onBeforeSendHeaders((details, callback) => { + const uri = new URL(details.url); - details.requestHeaders['User-Agent'] = USER_AGENT; - details.requestHeaders['origin'] = uri.origin; - details.requestHeaders['host'] = uri.host; + details.requestHeaders['User-Agent'] = USER_AGENT; + details.requestHeaders['origin'] = uri.origin; + details.requestHeaders['host'] = uri.host; - callback({ requestHeaders: details.requestHeaders }); - }); + callback({ requestHeaders: details.requestHeaders }); + }); } diff --git a/materialious/package-lock.json b/materialious/package-lock.json index 66614d57..36c6332d 100644 --- a/materialious/package-lock.json +++ b/materialious/package-lock.json @@ -55,7 +55,7 @@ "@vite-pwa/sveltekit": "^0.6.6", "eslint": "^8.56.0", "eslint-config-prettier": "^9.1.0", - "eslint-plugin-svelte": "^2.45.1", + "eslint-plugin-svelte": "^3.11.0", "patch-package": "^8.0.0", "prettier": "^3.3.3", "prettier-plugin-svelte": "^3.2.7", @@ -3598,9 +3598,9 @@ "license": "BSD-2-Clause" }, "node_modules/acorn": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -5699,22 +5699,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint-compat-utils": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.5.1.tgz", - "integrity": "sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.4" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "eslint": ">=6.0.0" - } - }, "node_modules/eslint-config-prettier": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", @@ -5729,32 +5713,31 @@ } }, "node_modules/eslint-plugin-svelte": { - "version": "2.46.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-2.46.1.tgz", - "integrity": "sha512-7xYr2o4NID/f9OEYMqxsEQsCsj4KaMy4q5sANaKkAb6/QeCjYFxRmDm2S3YC3A3pl1kyPZ/syOx/i7LcWYSbIw==", + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-3.11.0.tgz", + "integrity": "sha512-KliWlkieHyEa65aQIkRwUFfHzT5Cn4u3BQQsu3KlkJOs7c1u7ryn84EWaOjEzilbKgttT4OfBURA8Uc4JBSQIw==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@jridgewell/sourcemap-codec": "^1.4.15", - "eslint-compat-utils": "^0.5.1", + "@eslint-community/eslint-utils": "^4.6.1", + "@jridgewell/sourcemap-codec": "^1.5.0", "esutils": "^2.0.3", - "known-css-properties": "^0.35.0", - "postcss": "^8.4.38", + "globals": "^16.0.0", + "known-css-properties": "^0.37.0", + "postcss": "^8.4.49", "postcss-load-config": "^3.1.4", - "postcss-safe-parser": "^6.0.0", - "postcss-selector-parser": "^6.1.0", - "semver": "^7.6.2", - "svelte-eslint-parser": "^0.43.0" + "postcss-safe-parser": "^7.0.0", + "semver": "^7.6.3", + "svelte-eslint-parser": "^1.3.0" }, "engines": { - "node": "^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://github.com/sponsors/ota-meshi" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0-0 || ^9.0.0-0", + "eslint": "^8.57.1 || ^9.0.0", "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" }, "peerDependenciesMeta": { @@ -5763,6 +5746,19 @@ } } }, + "node_modules/eslint-plugin-svelte/node_modules/globals": { + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.3.0.tgz", + "integrity": "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/eslint-scope": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", @@ -6225,14 +6221,15 @@ } }, "node_modules/form-data": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", - "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { @@ -7966,9 +7963,9 @@ } }, "node_modules/known-css-properties": { - "version": "0.35.0", - "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.35.0.tgz", - "integrity": "sha512-a/RAk2BfKk+WFGhhOCAYqSiFLc34k8Mt/6NWRI4joER0EYUzXIcFivjjnoD3+XU1DggLn/tZc3DOAgke7l8a4A==", + "version": "0.37.0", + "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.37.0.tgz", + "integrity": "sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==", "dev": true, "license": "MIT" }, @@ -9438,20 +9435,30 @@ } }, "node_modules/postcss-safe-parser": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz", - "integrity": "sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-7.0.1.tgz", + "integrity": "sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==", "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss-safe-parser" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", "engines": { - "node": ">=12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" + "node": ">=18.0" }, "peerDependencies": { - "postcss": "^8.3.3" + "postcss": "^8.4.31" } }, "node_modules/postcss-scss": { @@ -9482,9 +9489,9 @@ } }, "node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", + "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", "dev": true, "license": "MIT", "dependencies": { @@ -11469,20 +11476,21 @@ } }, "node_modules/svelte-eslint-parser": { - "version": "0.43.0", - "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.43.0.tgz", - "integrity": "sha512-GpU52uPKKcVnh8tKN5P4UZpJ/fUDndmq7wfsvoVXsyP+aY0anol7Yqo01fyrlaWGMFfm4av5DyrjlaXdLRJvGA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-1.3.0.tgz", + "integrity": "sha512-VCgMHKV7UtOGcGLGNFSbmdm6kEKjtzo5nnpGU/mnx4OsFY6bZ7QwRF5DUx+Hokw5Lvdyo8dpk8B1m8mliomrNg==", "dev": true, "license": "MIT", "dependencies": { - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "postcss": "^8.4.39", - "postcss-scss": "^4.0.9" + "eslint-scope": "^8.2.0", + "eslint-visitor-keys": "^4.0.0", + "espree": "^10.0.0", + "postcss": "^8.4.49", + "postcss-scss": "^4.0.9", + "postcss-selector-parser": "^7.0.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://github.com/sponsors/ota-meshi" @@ -11496,6 +11504,54 @@ } } }, + "node_modules/svelte-eslint-parser/node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/svelte-eslint-parser/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/svelte-eslint-parser/node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/svelte-infinite-loading": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/svelte-infinite-loading/-/svelte-infinite-loading-1.4.0.tgz", diff --git a/materialious/package.json b/materialious/package.json index 840cbc4b..3ba92222 100644 --- a/materialious/package.json +++ b/materialious/package.json @@ -1,6 +1,6 @@ { "name": "materialious", - "version": "1.9.21", + "version": "1.9.22", "private": true, "scripts": { "dev": "vite dev", @@ -28,7 +28,7 @@ "@vite-pwa/sveltekit": "^0.6.6", "eslint": "^8.56.0", "eslint-config-prettier": "^9.1.0", - "eslint-plugin-svelte": "^2.45.1", + "eslint-plugin-svelte": "^3.11.0", "patch-package": "^8.0.0", "prettier": "^3.3.3", "prettier-plugin-svelte": "^3.2.7", @@ -70,4 +70,4 @@ "svelte-persisted-store": "^0.12.0", "youtubei.js": "^15.0.0" } -} +} \ No newline at end of file diff --git a/materialious/src/lib/api/model.ts b/materialious/src/lib/api/model.ts index 55feb8dc..bd1cdc60 100644 --- a/materialious/src/lib/api/model.ts +++ b/materialious/src/lib/api/model.ts @@ -100,7 +100,7 @@ export interface VideoPlay extends Video { genre: string; genreUrl: string; hlsUrl?: string; - dashUrl: string; + dashUrl?: string; adaptiveFormats: AdaptiveFormats[]; formatStreams: FormatStreams[]; recommendedVideos: VideoBase[]; diff --git a/materialious/src/lib/components/ErrorMsg.svelte b/materialious/src/lib/components/ErrorMsg.svelte new file mode 100644 index 00000000..48abe3b0 --- /dev/null +++ b/materialious/src/lib/components/ErrorMsg.svelte @@ -0,0 +1,21 @@ + + +
+ +
+ + + {#if page.error} +
+ {page.error.message} +
+ {/if} +
diff --git a/materialious/src/lib/components/Player.svelte b/materialious/src/lib/components/Player.svelte index 3144cab9..7ab39c19 100644 --- a/materialious/src/lib/components/Player.svelte +++ b/materialious/src/lib/components/Player.svelte @@ -8,7 +8,7 @@ import { ScreenOrientation, type ScreenOrientationResult } from '@capacitor/screen-orientation'; import { StatusBar, Style } from '@capacitor/status-bar'; import { NavigationBar } from '@hugotomazi/capacitor-navigation-bar'; - import { type Page } from '@sveltejs/kit'; + import { error, type Page } from '@sveltejs/kit'; import { HttpFetchPlugin } from '$lib/sabr/shakaHttpPlugin'; import ui from 'beercss'; import ISO6391 from 'iso-639-1'; @@ -385,7 +385,7 @@ watchProgressTimeout = setInterval(() => savePlayerPos(), 60000); setupSponsorSkip(); - let dashUrl: string = ''; + let dashUrl: string; // Due to CORs issues with redirects, hosted instances of Materialious // dirctly provide the companion instance @@ -393,6 +393,10 @@ if (import.meta.env.VITE_DEFAULT_COMPANION_INSTANCE && Capacitor.getPlatform() === 'web') { dashUrl = `${import.meta.env.VITE_DEFAULT_COMPANION_INSTANCE}/api/manifest/dash/id/${data.video.videoId}`; } else { + if (!data.video.dashUrl) { + error(500, 'No dash manifest found'); + return; + } dashUrl = data.video.dashUrl; } @@ -403,6 +407,10 @@ await player.load(dashUrl, await getLastPlayPos()); } else { if (data.video.fallbackPatch === 'youtubejs') { + if (!data.video.dashUrl) { + error(500, 'No dash manifest found'); + return; + } await player.load(data.video.dashUrl); } else { await player.load(data.video.hlsUrl + '?local=true'); @@ -590,22 +598,22 @@ playerElement.playbackRate = playerElement.playbackRate + 0.25; return false; }); - + Mousetrap.bind(',', () => { - if (!playerElement) return - - const currentTrack = player.getVariantTracks().find(track => track.active) - const frameTime = 1/(currentTrack?.frameRate || 30) - playerElement.currentTime -= frameTime - }) + if (!playerElement) return; + + const currentTrack = player.getVariantTracks().find((track) => track.active); + const frameTime = 1 / (currentTrack?.frameRate || 30); + playerElement.currentTime -= frameTime; + }); Mousetrap.bind('.', () => { - if (!playerElement) return - - const currentTrack = player.getVariantTracks().find(track => track.active) - const frameTime = 1/(currentTrack?.frameRate || 30) - playerElement.currentTime += frameTime - }) + if (!playerElement) return; + + const currentTrack = player.getVariantTracks().find((track) => track.active); + const frameTime = 1 / (currentTrack?.frameRate || 30); + playerElement.currentTime += frameTime; + }); playerElement.addEventListener('pause', async () => { savePlayerPos(); diff --git a/materialious/src/lib/components/Thumbnail.svelte b/materialious/src/lib/components/Thumbnail.svelte index 9d3c5de7..b0de978a 100644 --- a/materialious/src/lib/components/Thumbnail.svelte +++ b/materialious/src/lib/components/Thumbnail.svelte @@ -270,6 +270,7 @@ .video-title { word-wrap: break-word; overflow: hidden; + border-radius: 0px; } .thumbnail-details { diff --git a/materialious/src/lib/i18n/index.ts b/materialious/src/lib/i18n/index.ts index e62b5313..e6e27826 100644 --- a/materialious/src/lib/i18n/index.ts +++ b/materialious/src/lib/i18n/index.ts @@ -7,6 +7,7 @@ export const locale: Writable = writable(defaultLocale); export const _: Writable<(key: string, options?: any) => string> = writable(() => ''); const resources: Record Promise>> = { + ar: () => import('./locales/ar.json'), en: () => import('./locales/en.json'), ru: () => import('./locales/ru.json'), 'zh-CN': () => import('./locales/zh-CN.json'), @@ -14,6 +15,7 @@ const resources: Record Promise>> = { nl: () => import('./locales/nl.json'), de: () => import('./locales/de.json'), es: () => import('./locales/es.json'), + gsw: () => import('./locales/gsw.json'), sh: () => import('./locales/sh.json'), 'pt-BR': () => import('./locales/pt-BR.json'), lv: () => import('./locales/lv.json'), diff --git a/materialious/src/lib/i18n/locales/de.json b/materialious/src/lib/i18n/locales/de.json index 599bb757..21b3ada6 100644 --- a/materialious/src/lib/i18n/locales/de.json +++ b/materialious/src/lib/i18n/locales/de.json @@ -124,7 +124,7 @@ "previewVideoOnHover": "Video-Vorschau beim Hovern", "expandDescription": "Beschreibung standardmäßig erweitern", "expandComments": "Kommentare standardmäßig erweitern", - "disableAutoUpdate": "Automatische Installation von Updates deaktivieren", + "disableAutoUpdate": "Automatische Updates deaktivieren", "dataPreferences": { "content": "Du möchtest Abonnements importieren/exportieren, dein Passwort ändern oder dein Konto löschen? Klicke hier und scrolle bis zum Ende der Seite.", "dataPreferences": "Dateneinstellungen" diff --git a/materialious/src/lib/i18n/locales/en.json b/materialious/src/lib/i18n/locales/en.json index e081e293..7f697f11 100644 --- a/materialious/src/lib/i18n/locales/en.json +++ b/materialious/src/lib/i18n/locales/en.json @@ -1,6 +1,8 @@ { "enabled": "Enabled", "disabled": "Disabled", + "premium": "Premium YouTube content can't be watched on Materialious.", + "isUpcoming": "Video is not in a watchable state yet.", "copyUrl": "Copy URL", "loadMore": "Load more", "views": "views", diff --git a/materialious/src/lib/i18n/locales/gsw.json b/materialious/src/lib/i18n/locales/gsw.json index df5e4678..25f71674 100644 --- a/materialious/src/lib/i18n/locales/gsw.json +++ b/materialious/src/lib/i18n/locales/gsw.json @@ -85,7 +85,8 @@ }, "syncPartyWarning": "Bitte denk dra, dass dini IP für die vo dir iigladne Benutzer sichtbar isch.", "displayThumbnailAvatars": "Vorschaubild-Avatar (Langsam)", - "expandDescription": "Beschriibig standardmässig erwiitere" + "expandDescription": "Beschriibig standardmässig erwiitere", + "disableAutoUpdate": "Automatischi Updates deaktiviere" }, "videos": "Videos", "cancel": "Abbreche", diff --git a/materialious/src/lib/patches/youtubejs.ts b/materialious/src/lib/patches/youtubejs.ts index c77086db..135b6824 100644 --- a/materialious/src/lib/patches/youtubejs.ts +++ b/materialious/src/lib/patches/youtubejs.ts @@ -7,7 +7,6 @@ import type { VideoBase, VideoPlay } from '$lib/api/model'; -import { numberWithCommas } from '$lib/numbers'; import { fromFormat } from '$lib/sabr/formatKeyUtils'; import { interfaceRegionStore, poTokenCacheStore } from '$lib/store'; import { Capacitor } from '@capacitor/core'; @@ -77,8 +76,6 @@ export async function patchYoutubeJs(videoId: string): Promise { throw new Error('Unable to pull video info from youtube.js'); } - console.log(video); - let dashUri: string | undefined; if (video.streaming_data) { @@ -103,8 +100,6 @@ export async function patchYoutubeJs(videoId: string): Promise { } } - if (!dashUri) throw Error('Unable to find suitable dash manifest'); - const descString = video.secondary_info.description?.toString() || ''; let authorThumbnails: Image[]; @@ -206,9 +201,10 @@ export async function patchYoutubeJs(videoId: string): Promise { premiereTimestamp: 0, hlsUrl: video.streaming_data?.hls_manifest_url || undefined, liveNow: video.basic_info.is_live || false, - premium: false, + // @ts-expect-error Type does have offer_id + premium: video?.playability_status?.error_screen?.offer_id === 'sponsors_only_video', storyboards: storyboard, - isUpcoming: false, + isUpcoming: video?.playability_status?.status !== 'OK', videoId: videoId, videoThumbnails: video.basic_info.thumbnail as Thumbnail[], author: video.basic_info.author || 'Unknown', diff --git a/materialious/src/lib/store.ts b/materialious/src/lib/store.ts index 48b32947..27a0b60c 100644 --- a/materialious/src/lib/store.ts +++ b/materialious/src/lib/store.ts @@ -67,7 +67,7 @@ export const returnYTDislikesInstanceStore: Writable ) ); -export const synciousStore = persisted('syncious', true); +export const synciousStore = persisted('syncious', false); export const synciousInstanceStore: Writable = persisted( 'synciousInstance', platformDependentDefault( @@ -92,7 +92,7 @@ export const interfaceDisplayThumbnailAvatars = persisted('disableThumbnailAvata export const interfaceDefaultPage = persisted('defaultPage', '/'); export const interfaceSearchHistoryEnabled = persisted('searchHistoryEnabled', false); export const interfaceAllowInsecureRequests = persisted('allowInsecureRequests', false); -export const interfaceDisableAutoUpdate = persisted('disableAutoUpdate', false) +export const interfaceDisableAutoUpdate = persisted('disableAutoUpdate', false); export const sponsorBlockStore = persisted('sponsorBlock', true); export const sponsorBlockUrlStore: Writable = persisted( diff --git a/materialious/src/lib/watch.ts b/materialious/src/lib/watch.ts index d0979319..d9ff42d8 100644 --- a/materialious/src/lib/watch.ts +++ b/materialious/src/lib/watch.ts @@ -16,6 +16,7 @@ import { import { phaseDescription } from '$lib/timestamps'; import { error } from '@sveltejs/kit'; import { get } from 'svelte/store'; +import { _ } from './i18n'; export async function getWatchDetails(videoId: string, url: URL) { let video; @@ -25,6 +26,14 @@ export async function getWatchDetails(videoId: string, url: URL) { error(500, errorMessage); } + if (video.premium) { + error(400, get(_)('premium')); + } + + if (video.isUpcoming) { + error(400, get(_)('isUpcoming')); + } + let personalPlaylists; if (get(authStore)) { postHistory(video.videoId); diff --git a/materialious/src/routes/(app)/+error.svelte b/materialious/src/routes/(app)/+error.svelte index 9ebfd8a5..bee7c6b6 100644 --- a/materialious/src/routes/(app)/+error.svelte +++ b/materialious/src/routes/(app)/+error.svelte @@ -1,29 +1,5 @@ -
- -
- -

Oh no! You've encountered an error.

- -

Report it here

- - {#if $page.error} -
- {$page.error.message} -
- {/if} -
+ diff --git a/materialious/src/routes/(no-layout)/tv/[slug]/+error.svelte b/materialious/src/routes/(no-layout)/tv/[slug]/+error.svelte new file mode 100644 index 00000000..bee7c6b6 --- /dev/null +++ b/materialious/src/routes/(no-layout)/tv/[slug]/+error.svelte @@ -0,0 +1,5 @@ + + + diff --git a/materialious/src/routes/+error.svelte b/materialious/src/routes/+error.svelte new file mode 100644 index 00000000..bee7c6b6 --- /dev/null +++ b/materialious/src/routes/+error.svelte @@ -0,0 +1,5 @@ + + + diff --git a/update_versions.py b/update_versions.py index 34ce3310..831a58de 100644 --- a/update_versions.py +++ b/update_versions.py @@ -3,7 +3,7 @@ import os import re from datetime import datetime -LATEST_VERSION = "1.9.21" +LATEST_VERSION = "1.9.22" RELEASE_DATE = datetime.now().strftime("%Y-%-m-%d") # Format: YYYY-M-D WORKING_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "materialious")