From 257dd0c071c91eae4bd1c5a9a6599f946dfc9679 Mon Sep 17 00:00:00 2001 From: Edenware Date: Mon, 11 Nov 2024 16:04:06 -0300 Subject: [PATCH] v17.5.5 - Replaced moment.js with day.js for improved performance. - Switched from Jimp to HTML5 OffscreenCanvas for image processing to enhance performance. - Added scripts to assist with project preparation and building. - Improved the busy indicator, adjusted the default font size calculation and minimized interference with scrolling to provide a smoother user experience. --- android/app/build.gradle | 6 +- android/app/src/main/AndroidManifest.xml | 21 +- android/gradle.properties | 3 +- android/variables.gradle | 2 +- bin.js | 1 - build.mjs | 146 ++ package-lock.json | 1572 +++-------------- package.json | 20 +- prepare.mjs | 52 + rollup.config.mjs | 42 +- www/nodejs/main.mjs | 17 +- www/nodejs/modules/bookmarks/bookmarks.js | 4 +- www/nodejs/modules/bridge/renderer.js | 2 + www/nodejs/modules/channels/channels.js | 134 +- www/nodejs/modules/downloads/downloads.js | 2 +- www/nodejs/modules/ffmpeg/ffmpeg.js | 2 +- www/nodejs/modules/history/epg-history.js | 10 +- www/nodejs/modules/history/history.js | 4 +- www/nodejs/modules/icon-server/icon-server.js | 77 +- www/nodejs/modules/icon-server/icon.js | 4 +- www/nodejs/modules/lang/lang.js | 2 +- www/nodejs/modules/lists/epg-worker.js | 346 ++-- www/nodejs/modules/lists/lists.js | 154 +- www/nodejs/modules/lists/manager.js | 19 +- www/nodejs/modules/lists/test.mjs | 9 +- www/nodejs/modules/menu/menu.js | 86 +- www/nodejs/modules/menu/renderer.js | 411 ++--- .../modules/multi-worker/multi-worker.js | 28 +- www/nodejs/modules/multi-worker/worker.mjs | 9 +- www/nodejs/modules/omni/renderer.js | 57 +- www/nodejs/modules/options/options.js | 5 +- www/nodejs/modules/paths/paths.js | 2 + .../recommendations/recommendations.js | 108 +- www/nodejs/modules/search/search.js | 22 +- .../modules/stream-state/stream-state.js | 8 +- www/nodejs/modules/streamer/renderer.js | 77 +- www/nodejs/modules/streamer/streamer.js | 41 +- .../streamer/utils/mpegts-processor.js | 2 +- www/nodejs/modules/theme/renderer.js | 43 +- www/nodejs/modules/theme/theme.js | 17 +- www/nodejs/modules/utils/utils.js | 43 +- www/nodejs/package.json | 6 +- www/nodejs/renderer/assets/js/electron.js | 7 +- www/nodejs/renderer/index.html | 6 +- www/nodejs/renderer/src/App.svelte | 14 +- www/nodejs/renderer/src/Menu.svelte | 60 +- www/nodejs/renderer/src/Player.svelte | 4 +- www/nodejs/renderer/src/scripts/app.js | 133 +- www/nodejs/renderer/src/scripts/clock.js | 34 +- .../renderer/src/scripts/hotkeys-actions.js | 5 +- www/nodejs/renderer/src/scripts/hotkeys.js | 2 +- .../renderer/src/scripts/image-processor.js | 307 ++++ .../src/scripts/mediaplayer-adapter.js | 2 +- www/nodejs/renderer/src/scripts/utils.js | 24 +- 54 files changed, 1743 insertions(+), 2471 deletions(-) create mode 100644 build.mjs create mode 100644 prepare.mjs create mode 100644 www/nodejs/renderer/src/scripts/image-processor.js diff --git a/android/app/build.gradle b/android/app/build.gradle index 2c9cb9a2..373b32ea 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -11,11 +11,12 @@ static def getVersionCodeFromVersionName(String versionName) { android { namespace "tv.megacubo.app" compileSdk rootProject.ext.compileSdkVersion + compileSdkVersion 34 defaultConfig { applicationId "tv.megacubo.app" minSdkVersion 24 - targetSdkVersion 33 - versionName '17.5.4' + targetSdkVersion 34 + versionName '17.5.5' versionCode getVersionCodeFromVersionName(versionName) testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { @@ -35,7 +36,6 @@ android { abi { enable true reset() - //include 'armeabi-v7a'_64' include 'armeabi-v7a' universalApk false } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index b69c03cf..f9e47217 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,8 +1,7 @@ + android:windowSoftInputMode="adjustResize" + android:windowLayoutInDisplayCutoutMode="always"> @@ -50,23 +50,32 @@ android:grantUriPermissions="true"> - + + + + + + + - - + { stdio: 'ignore', }; const child = spawn(electronPath, params, opts); - console.log({electronPath, params, opts}) if(debug) { child.stdout.on('data', (data) => { process.stdout.write(data); diff --git a/build.mjs b/build.mjs new file mode 100644 index 00000000..49d4312c --- /dev/null +++ b/build.mjs @@ -0,0 +1,146 @@ +import fs from 'fs'; +import path from 'path'; +import { execSync } from 'child_process'; +import { fileURLToPath } from 'url'; + +console.log('This script will build the Megacubo APKs for ARM and ARM64 architectures, building PC installers is not covered yet. Remember to run \'npm run prepare\' before running this script.'); + +// Get __dirname in ESM +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +// Constants +const RELEASE_DIRECTORY = path.join(__dirname, "releases"); +const APK_OUTPUT_DIRECTORY = path.join(__dirname, "android", "app", "build", "outputs", "apk", "release"); +const BUILD_GRADLE_FILE_PATH = path.join(__dirname, "android", "app", "build.gradle"); +const DISTRIBUTION_DIRECTORY = path.join(__dirname, "android", "app", "src", "main", "assets", "public", "nodejs", "dist"); +const PACKAGE_JSON_PATH = path.join(__dirname, "package.json"); +const SIGNING_PROPERTIES_PATH = path.join(__dirname, "release-signing.properties"); + +// Function to retrieve the application version from package.json +const getApplicationVersion = async () => { + const { default: { version } } = await import("file://" + PACKAGE_JSON_PATH, { assert: { type: "json" } }); + return version || ""; +}; + +// Function to read signing properties from the properties file +const readSigningProperties = () => { + const properties = {}; + if (fs.existsSync(SIGNING_PROPERTIES_PATH)) { + const content = fs.readFileSync(SIGNING_PROPERTIES_PATH, "utf-8").split("\n"); + content.forEach(line => { + const [key, value] = line.split("="); + if (key && value) { + properties[key.trim()] = value.trim(); + } + }); + } + if(properties.storeFile && (properties.storeFile.startsWith(".") || !(properties.storeFile.includes("/") || properties.storeFile.includes("\\")))) { + properties.storeFile = path.join(__dirname, properties.storeFile); + } + return properties; +}; + +// Update build.gradle file to include specified ABI +const updateBuildGradleWithABI = (abi) => { + let gradleContent = fs.readFileSync(BUILD_GRADLE_FILE_PATH, "utf-8"); + gradleContent = gradleContent.replace(/include \x27[a-z0-9\- ,\x27]+/g, `include '${abi}'`); + fs.writeFileSync(BUILD_GRADLE_FILE_PATH, gradleContent); +}; + +// Execute shell command with error handling +const executeCommand = (command) => { + try { + execSync(command, { stdio: "inherit" }); + } catch (error) { + console.error(`Command failed: ${command}`); + process.exit(error.status); + } +}; + +// Main build process +const buildApplication = async () => { + const applicationVersion = await getApplicationVersion(); + const signingProperties = readSigningProperties(); + + if (!applicationVersion || !/^[0-9]+(\.[0-9]+)*$/.test(applicationVersion)) { + console.error(`Application version is invalid or empty: ${applicationVersion}`); + process.exit(1); + } + + console.log(`Application Version: ${applicationVersion}`); + + const signedApkPath = path.join(APK_OUTPUT_DIRECTORY, "app-release-signed.apk"); + const unsignedApkPath = path.join(APK_OUTPUT_DIRECTORY, "app-release-unsigned.apk"); + + if (fs.existsSync(signedApkPath)) { + fs.unlinkSync(signedApkPath); + } + + if (fs.existsSync(unsignedApkPath)) { + fs.unlinkSync(unsignedApkPath); + } + + // ARM64 build process + updateBuildGradleWithABI("arm64-v8a"); + if (fs.existsSync(path.join(DISTRIBUTION_DIRECTORY, "premium.js"))) { + fs.unlinkSync(path.join(DISTRIBUTION_DIRECTORY, "premium.js")); + } + + const arm64PremiumFilePath = path.join(__dirname, "compiled_premium", "premium-arm64.jsc"); + const destinationPremiumFilePath = path.join(DISTRIBUTION_DIRECTORY, "premium.jsc"); + + // Copy ARM64 premium file if it exists + if (fs.existsSync(arm64PremiumFilePath)) { + fs.copyFileSync(arm64PremiumFilePath, destinationPremiumFilePath); + } + + // Build command + let buildCommand = `npx cap build android`; + + if (signingProperties.storeFile && signingProperties.storePassword && signingProperties.keyAlias && signingProperties.keyPassword) { + console.log("Signing properties found. Signing APK..."); + buildCommand += ` --keystorepath ${signingProperties.storeFile} --keystorepass ${signingProperties.storePassword} --keystorealias ${signingProperties.keyAlias} --keystorealiaspass ${signingProperties.keyPassword}`; + } else { + console.log("Signing properties not found. Building unsigned APK..."); +} + + buildCommand += ` --androidreleasetype APK --signing-type apksigner`; + executeCommand(buildCommand); + + const signedApkMtime = fs.existsSync(signedApkPath) ? fs.statSync(signedApkPath).mtime : 0; + const unsignedApkMtime = fs.existsSync(unsignedApkPath) ? fs.statSync(unsignedApkPath).mtime : 0; + + const outputApkPath = signedApkMtime > unsignedApkMtime ? signedApkPath : unsignedApkPath; + fs.renameSync(outputApkPath, path.join(RELEASE_DIRECTORY, `Megacubo_${applicationVersion}_android_arm64-v8a.apk`)); + + // ARM build process + updateBuildGradleWithABI("armeabi-v7a"); + + const armPremiumFilePath = path.join(__dirname, "compiled_premium", "premium-arm.jsc"); + + // Copy ARM premium file if it exists + if (fs.existsSync(armPremiumFilePath)) { + fs.copyFileSync(armPremiumFilePath, destinationPremiumFilePath); + } + + console.log("Building ARM as last to keep project files with ARM as default instead of ARM64..."); + + // Resetting buildCommand for ARM build + buildCommand = `npx cap build android`; + + if (signingProperties.storeFile && signingProperties.storePassword && signingProperties.keyAlias && signingProperties.keyPassword) { + buildCommand += ` --keystorepath ${signingProperties.storeFile} --keystorepass ${signingProperties.storePassword} --keystorealias ${signingProperties.keyAlias} --keystorealiaspass ${signingProperties.keyPassword}`; + } + + buildCommand += ` --androidreleasetype APK --signing-type apksigner`; + executeCommand(buildCommand); + + fs.renameSync( + outputApkPath, + path.join(RELEASE_DIRECTORY, `Megacubo_${applicationVersion}_android_armeabi-v7a.apk`) + ); + + console.log(`Finished: ${new Date().toLocaleString()}`); +}; + +buildApplication().catch(error => console.error("Build failed:", error)); diff --git a/package-lock.json b/package-lock.json index 70e1e337..d876b109 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,6 @@ "bytenode": "github:EdenwareApps/bytenode", "capacitor-nodejs": "https://github.com/EdenwareApps/Capacitor-NodeJS/releases/download/v1.0.0-beta.7/capacitor6-nodejs.tgz", "check-disk-space": "^3.4.0", - "color-thief-jimp": "github:EdenwareApps/color-thief-jimp", "cordova-androidx-build": "^1.0.4", "cordova-plugin-android-permissions": "^1.1.5", "cordova-plugin-background-mode": "bitbucket:TheBosZ/cordova-plugin-run-in-background", @@ -30,6 +29,7 @@ "create-desktop-shortcuts": "^1.11.0", "cross-dirname": "^0.1.0", "dashjs": "^4.7.4", + "dayjs": "^1.11.13", "decode-entities": "^1.0.7", "electron": "^9.1.1", "env-paths": "^3.0.0", @@ -43,10 +43,7 @@ "iconv-lite": "^0.6.3", "ionic-plugin-deeplinks": "^1.0.24", "jexidb": "^1.0.2", - "jimp": "^0.22.12", "m3u8-parser": "^7.1.0", - "moment": "^2.30.1", - "moment-timezone": "^0.5.45", "mpegts.js": "github:xqq/mpegts.js", "node-cleanup": "^2.1.2", "opensubtitles.com": "^1.1.0", @@ -60,13 +57,12 @@ "tv.megacubo.ffmpeg": "github:EdenwareApps/tv.megacubo.ffmpeg", "tv.megacubo.pip": "github:EdenwareApps/tv.megacubo.pip", "tv.megacubo.player": "github:EdenwareApps/tv.megacubo.player", - "xmltv": "github:EdenwareApps/node-xmltv", + "xmltv-stream": "^0.4.1", "ytdl-core": "^4.11.5", "ytsr": "^3.8.4" }, "bin": { - "megacubo": "bin.js", - "megacubo-debug": "bin-debug.js" + "megacubo": "bin.js" }, "devDependencies": { "@babel/plugin-syntax-import-attributes": "^7.25.6", @@ -83,6 +79,7 @@ "postcss": "^8.4.43", "rimraf": "^5.0.10", "rollup": "^4.21.2", + "rollup-plugin-copy": "^3.5.0", "rollup-plugin-import-css": "^3.5.1", "rollup-plugin-node-builtins": "^2.1.2", "rollup-plugin-svelte": "^7.2.2", @@ -1688,25 +1685,6 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/polyfill": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/polyfill/-/polyfill-7.12.1.tgz", - "integrity": "sha512-X0pi0V6gxLi6lFZpGmeNa4zxtwEmCs42isWLNjZZDE0Y8yVfgu0T2OAHlzBbdYlqbW/YXVvoBHpATEM+goCj8g==", - "deprecated": "🚨 This package has been deprecated in favor of separate inclusion of a polyfill and regenerator-runtime (when needed). See the @babel/polyfill docs (https://babeljs.io/docs/en/babel-polyfill) for more information.", - "license": "MIT", - "dependencies": { - "core-js": "^2.6.5", - "regenerator-runtime": "^0.13.4" - } - }, - "node_modules/@babel/polyfill/node_modules/core-js": { - "version": "2.6.12", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", - "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==", - "deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.", - "hasInstallScript": true, - "license": "MIT" - }, "node_modules/@babel/preset-env": { "version": "7.25.4", "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.25.4.tgz", @@ -2716,449 +2694,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/@jimp/bmp": { - "version": "0.22.12", - "resolved": "https://registry.npmjs.org/@jimp/bmp/-/bmp-0.22.12.tgz", - "integrity": "sha512-aeI64HD0npropd+AR76MCcvvRaa+Qck6loCOS03CkkxGHN5/r336qTM5HPUdHKMDOGzqknuVPA8+kK1t03z12g==", - "license": "MIT", - "dependencies": { - "@jimp/utils": "^0.22.12", - "bmp-js": "^0.1.0" - }, - "peerDependencies": { - "@jimp/custom": ">=0.3.5" - } - }, - "node_modules/@jimp/core": { - "version": "0.22.12", - "resolved": "https://registry.npmjs.org/@jimp/core/-/core-0.22.12.tgz", - "integrity": "sha512-l0RR0dOPyzMKfjUW1uebzueFEDtCOj9fN6pyTYWWOM/VS4BciXQ1VVrJs8pO3kycGYZxncRKhCoygbNr8eEZQA==", - "license": "MIT", - "dependencies": { - "@jimp/utils": "^0.22.12", - "any-base": "^1.1.0", - "buffer": "^5.2.0", - "exif-parser": "^0.1.12", - "file-type": "^16.5.4", - "isomorphic-fetch": "^3.0.0", - "pixelmatch": "^4.0.2", - "tinycolor2": "^1.6.0" - } - }, - "node_modules/@jimp/core/node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/@jimp/custom": { - "version": "0.22.12", - "resolved": "https://registry.npmjs.org/@jimp/custom/-/custom-0.22.12.tgz", - "integrity": "sha512-xcmww1O/JFP2MrlGUMd3Q78S3Qu6W3mYTXYuIqFq33EorgYHV/HqymHfXy9GjiCJ7OI+7lWx6nYFOzU7M4rd1Q==", - "license": "MIT", - "dependencies": { - "@jimp/core": "^0.22.12" - } - }, - "node_modules/@jimp/gif": { - "version": "0.22.12", - "resolved": "https://registry.npmjs.org/@jimp/gif/-/gif-0.22.12.tgz", - "integrity": "sha512-y6BFTJgch9mbor2H234VSjd9iwAhaNf/t3US5qpYIs0TSbAvM02Fbc28IaDETj9+4YB4676sz4RcN/zwhfu1pg==", - "license": "MIT", - "dependencies": { - "@jimp/utils": "^0.22.12", - "gifwrap": "^0.10.1", - "omggif": "^1.0.9" - }, - "peerDependencies": { - "@jimp/custom": ">=0.3.5" - } - }, - "node_modules/@jimp/jpeg": { - "version": "0.22.12", - "resolved": "https://registry.npmjs.org/@jimp/jpeg/-/jpeg-0.22.12.tgz", - "integrity": "sha512-Rq26XC/uQWaQKyb/5lksCTCxXhtY01NJeBN+dQv5yNYedN0i7iYu+fXEoRsfaJ8xZzjoANH8sns7rVP4GE7d/Q==", - "license": "MIT", - "dependencies": { - "@jimp/utils": "^0.22.12", - "jpeg-js": "^0.4.4" - }, - "peerDependencies": { - "@jimp/custom": ">=0.3.5" - } - }, - "node_modules/@jimp/plugin-blit": { - "version": "0.22.12", - "resolved": "https://registry.npmjs.org/@jimp/plugin-blit/-/plugin-blit-0.22.12.tgz", - "integrity": "sha512-xslz2ZoFZOPLY8EZ4dC29m168BtDx95D6K80TzgUi8gqT7LY6CsajWO0FAxDwHz6h0eomHMfyGX0stspBrTKnQ==", - "license": "MIT", - "dependencies": { - "@jimp/utils": "^0.22.12" - }, - "peerDependencies": { - "@jimp/custom": ">=0.3.5" - } - }, - "node_modules/@jimp/plugin-blur": { - "version": "0.22.12", - "resolved": "https://registry.npmjs.org/@jimp/plugin-blur/-/plugin-blur-0.22.12.tgz", - "integrity": "sha512-S0vJADTuh1Q9F+cXAwFPlrKWzDj2F9t/9JAbUvaaDuivpyWuImEKXVz5PUZw2NbpuSHjwssbTpOZ8F13iJX4uw==", - "license": "MIT", - "dependencies": { - "@jimp/utils": "^0.22.12" - }, - "peerDependencies": { - "@jimp/custom": ">=0.3.5" - } - }, - "node_modules/@jimp/plugin-circle": { - "version": "0.22.12", - "resolved": "https://registry.npmjs.org/@jimp/plugin-circle/-/plugin-circle-0.22.12.tgz", - "integrity": "sha512-SWVXx1yiuj5jZtMijqUfvVOJBwOifFn0918ou4ftoHgegc5aHWW5dZbYPjvC9fLpvz7oSlptNl2Sxr1zwofjTg==", - "license": "MIT", - "dependencies": { - "@jimp/utils": "^0.22.12" - }, - "peerDependencies": { - "@jimp/custom": ">=0.3.5" - } - }, - "node_modules/@jimp/plugin-color": { - "version": "0.22.12", - "resolved": "https://registry.npmjs.org/@jimp/plugin-color/-/plugin-color-0.22.12.tgz", - "integrity": "sha512-xImhTE5BpS8xa+mAN6j4sMRWaUgUDLoaGHhJhpC+r7SKKErYDR0WQV4yCE4gP+N0gozD0F3Ka1LUSaMXrn7ZIA==", - "license": "MIT", - "dependencies": { - "@jimp/utils": "^0.22.12", - "tinycolor2": "^1.6.0" - }, - "peerDependencies": { - "@jimp/custom": ">=0.3.5" - } - }, - "node_modules/@jimp/plugin-contain": { - "version": "0.22.12", - "resolved": "https://registry.npmjs.org/@jimp/plugin-contain/-/plugin-contain-0.22.12.tgz", - "integrity": "sha512-Eo3DmfixJw3N79lWk8q/0SDYbqmKt1xSTJ69yy8XLYQj9svoBbyRpSnHR+n9hOw5pKXytHwUW6nU4u1wegHNoQ==", - "license": "MIT", - "dependencies": { - "@jimp/utils": "^0.22.12" - }, - "peerDependencies": { - "@jimp/custom": ">=0.3.5", - "@jimp/plugin-blit": ">=0.3.5", - "@jimp/plugin-resize": ">=0.3.5", - "@jimp/plugin-scale": ">=0.3.5" - } - }, - "node_modules/@jimp/plugin-cover": { - "version": "0.22.12", - "resolved": "https://registry.npmjs.org/@jimp/plugin-cover/-/plugin-cover-0.22.12.tgz", - "integrity": "sha512-z0w/1xH/v/knZkpTNx+E8a7fnasQ2wHG5ze6y5oL2dhH1UufNua8gLQXlv8/W56+4nJ1brhSd233HBJCo01BXA==", - "license": "MIT", - "dependencies": { - "@jimp/utils": "^0.22.12" - }, - "peerDependencies": { - "@jimp/custom": ">=0.3.5", - "@jimp/plugin-crop": ">=0.3.5", - "@jimp/plugin-resize": ">=0.3.5", - "@jimp/plugin-scale": ">=0.3.5" - } - }, - "node_modules/@jimp/plugin-crop": { - "version": "0.22.12", - "resolved": "https://registry.npmjs.org/@jimp/plugin-crop/-/plugin-crop-0.22.12.tgz", - "integrity": "sha512-FNuUN0OVzRCozx8XSgP9MyLGMxNHHJMFt+LJuFjn1mu3k0VQxrzqbN06yIl46TVejhyAhcq5gLzqmSCHvlcBVw==", - "license": "MIT", - "dependencies": { - "@jimp/utils": "^0.22.12" - }, - "peerDependencies": { - "@jimp/custom": ">=0.3.5" - } - }, - "node_modules/@jimp/plugin-displace": { - "version": "0.22.12", - "resolved": "https://registry.npmjs.org/@jimp/plugin-displace/-/plugin-displace-0.22.12.tgz", - "integrity": "sha512-qpRM8JRicxfK6aPPqKZA6+GzBwUIitiHaZw0QrJ64Ygd3+AsTc7BXr+37k2x7QcyCvmKXY4haUrSIsBug4S3CA==", - "license": "MIT", - "dependencies": { - "@jimp/utils": "^0.22.12" - }, - "peerDependencies": { - "@jimp/custom": ">=0.3.5" - } - }, - "node_modules/@jimp/plugin-dither": { - "version": "0.22.12", - "resolved": "https://registry.npmjs.org/@jimp/plugin-dither/-/plugin-dither-0.22.12.tgz", - "integrity": "sha512-jYgGdSdSKl1UUEanX8A85v4+QUm+PE8vHFwlamaKk89s+PXQe7eVE3eNeSZX4inCq63EHL7cX580dMqkoC3ZLw==", - "license": "MIT", - "dependencies": { - "@jimp/utils": "^0.22.12" - }, - "peerDependencies": { - "@jimp/custom": ">=0.3.5" - } - }, - "node_modules/@jimp/plugin-fisheye": { - "version": "0.22.12", - "resolved": "https://registry.npmjs.org/@jimp/plugin-fisheye/-/plugin-fisheye-0.22.12.tgz", - "integrity": "sha512-LGuUTsFg+fOp6KBKrmLkX4LfyCy8IIsROwoUvsUPKzutSqMJnsm3JGDW2eOmWIS/jJpPaeaishjlxvczjgII+Q==", - "license": "MIT", - "dependencies": { - "@jimp/utils": "^0.22.12" - }, - "peerDependencies": { - "@jimp/custom": ">=0.3.5" - } - }, - "node_modules/@jimp/plugin-flip": { - "version": "0.22.12", - "resolved": "https://registry.npmjs.org/@jimp/plugin-flip/-/plugin-flip-0.22.12.tgz", - "integrity": "sha512-m251Rop7GN8W0Yo/rF9LWk6kNclngyjIJs/VXHToGQ6EGveOSTSQaX2Isi9f9lCDLxt+inBIb7nlaLLxnvHX8Q==", - "license": "MIT", - "dependencies": { - "@jimp/utils": "^0.22.12" - }, - "peerDependencies": { - "@jimp/custom": ">=0.3.5", - "@jimp/plugin-rotate": ">=0.3.5" - } - }, - "node_modules/@jimp/plugin-gaussian": { - "version": "0.22.12", - "resolved": "https://registry.npmjs.org/@jimp/plugin-gaussian/-/plugin-gaussian-0.22.12.tgz", - "integrity": "sha512-sBfbzoOmJ6FczfG2PquiK84NtVGeScw97JsCC3rpQv1PHVWyW+uqWFF53+n3c8Y0P2HWlUjflEla2h/vWShvhg==", - "license": "MIT", - "dependencies": { - "@jimp/utils": "^0.22.12" - }, - "peerDependencies": { - "@jimp/custom": ">=0.3.5" - } - }, - "node_modules/@jimp/plugin-invert": { - "version": "0.22.12", - "resolved": "https://registry.npmjs.org/@jimp/plugin-invert/-/plugin-invert-0.22.12.tgz", - "integrity": "sha512-N+6rwxdB+7OCR6PYijaA/iizXXodpxOGvT/smd/lxeXsZ/empHmFFFJ/FaXcYh19Tm04dGDaXcNF/dN5nm6+xQ==", - "license": "MIT", - "dependencies": { - "@jimp/utils": "^0.22.12" - }, - "peerDependencies": { - "@jimp/custom": ">=0.3.5" - } - }, - "node_modules/@jimp/plugin-mask": { - "version": "0.22.12", - "resolved": "https://registry.npmjs.org/@jimp/plugin-mask/-/plugin-mask-0.22.12.tgz", - "integrity": "sha512-4AWZg+DomtpUA099jRV8IEZUfn1wLv6+nem4NRJC7L/82vxzLCgXKTxvNvBcNmJjT9yS1LAAmiJGdWKXG63/NA==", - "license": "MIT", - "dependencies": { - "@jimp/utils": "^0.22.12" - }, - "peerDependencies": { - "@jimp/custom": ">=0.3.5" - } - }, - "node_modules/@jimp/plugin-normalize": { - "version": "0.22.12", - "resolved": "https://registry.npmjs.org/@jimp/plugin-normalize/-/plugin-normalize-0.22.12.tgz", - "integrity": "sha512-0So0rexQivnWgnhacX4cfkM2223YdExnJTTy6d06WbkfZk5alHUx8MM3yEzwoCN0ErO7oyqEWRnEkGC+As1FtA==", - "license": "MIT", - "dependencies": { - "@jimp/utils": "^0.22.12" - }, - "peerDependencies": { - "@jimp/custom": ">=0.3.5" - } - }, - "node_modules/@jimp/plugin-print": { - "version": "0.22.12", - "resolved": "https://registry.npmjs.org/@jimp/plugin-print/-/plugin-print-0.22.12.tgz", - "integrity": "sha512-c7TnhHlxm87DJeSnwr/XOLjJU/whoiKYY7r21SbuJ5nuH+7a78EW1teOaj5gEr2wYEd7QtkFqGlmyGXY/YclyQ==", - "license": "MIT", - "dependencies": { - "@jimp/utils": "^0.22.12", - "load-bmfont": "^1.4.1" - }, - "peerDependencies": { - "@jimp/custom": ">=0.3.5", - "@jimp/plugin-blit": ">=0.3.5" - } - }, - "node_modules/@jimp/plugin-resize": { - "version": "0.22.12", - "resolved": "https://registry.npmjs.org/@jimp/plugin-resize/-/plugin-resize-0.22.12.tgz", - "integrity": "sha512-3NyTPlPbTnGKDIbaBgQ3HbE6wXbAlFfxHVERmrbqAi8R3r6fQPxpCauA8UVDnieg5eo04D0T8nnnNIX//i/sXg==", - "license": "MIT", - "dependencies": { - "@jimp/utils": "^0.22.12" - }, - "peerDependencies": { - "@jimp/custom": ">=0.3.5" - } - }, - "node_modules/@jimp/plugin-rotate": { - "version": "0.22.12", - "resolved": "https://registry.npmjs.org/@jimp/plugin-rotate/-/plugin-rotate-0.22.12.tgz", - "integrity": "sha512-9YNEt7BPAFfTls2FGfKBVgwwLUuKqy+E8bDGGEsOqHtbuhbshVGxN2WMZaD4gh5IDWvR+emmmPPWGgaYNYt1gA==", - "license": "MIT", - "dependencies": { - "@jimp/utils": "^0.22.12" - }, - "peerDependencies": { - "@jimp/custom": ">=0.3.5", - "@jimp/plugin-blit": ">=0.3.5", - "@jimp/plugin-crop": ">=0.3.5", - "@jimp/plugin-resize": ">=0.3.5" - } - }, - "node_modules/@jimp/plugin-scale": { - "version": "0.22.12", - "resolved": "https://registry.npmjs.org/@jimp/plugin-scale/-/plugin-scale-0.22.12.tgz", - "integrity": "sha512-dghs92qM6MhHj0HrV2qAwKPMklQtjNpoYgAB94ysYpsXslhRTiPisueSIELRwZGEr0J0VUxpUY7HgJwlSIgGZw==", - "license": "MIT", - "dependencies": { - "@jimp/utils": "^0.22.12" - }, - "peerDependencies": { - "@jimp/custom": ">=0.3.5", - "@jimp/plugin-resize": ">=0.3.5" - } - }, - "node_modules/@jimp/plugin-shadow": { - "version": "0.22.12", - "resolved": "https://registry.npmjs.org/@jimp/plugin-shadow/-/plugin-shadow-0.22.12.tgz", - "integrity": "sha512-FX8mTJuCt7/3zXVoeD/qHlm4YH2bVqBuWQHXSuBK054e7wFRnRnbSLPUqAwSeYP3lWqpuQzJtgiiBxV3+WWwTg==", - "license": "MIT", - "dependencies": { - "@jimp/utils": "^0.22.12" - }, - "peerDependencies": { - "@jimp/custom": ">=0.3.5", - "@jimp/plugin-blur": ">=0.3.5", - "@jimp/plugin-resize": ">=0.3.5" - } - }, - "node_modules/@jimp/plugin-threshold": { - "version": "0.22.12", - "resolved": "https://registry.npmjs.org/@jimp/plugin-threshold/-/plugin-threshold-0.22.12.tgz", - "integrity": "sha512-4x5GrQr1a/9L0paBC/MZZJjjgjxLYrqSmWd+e+QfAEPvmRxdRoQ5uKEuNgXnm9/weHQBTnQBQsOY2iFja+XGAw==", - "license": "MIT", - "dependencies": { - "@jimp/utils": "^0.22.12" - }, - "peerDependencies": { - "@jimp/custom": ">=0.3.5", - "@jimp/plugin-color": ">=0.8.0", - "@jimp/plugin-resize": ">=0.8.0" - } - }, - "node_modules/@jimp/plugins": { - "version": "0.22.12", - "resolved": "https://registry.npmjs.org/@jimp/plugins/-/plugins-0.22.12.tgz", - "integrity": "sha512-yBJ8vQrDkBbTgQZLty9k4+KtUQdRjsIDJSPjuI21YdVeqZxYywifHl4/XWILoTZsjTUASQcGoH0TuC0N7xm3ww==", - "license": "MIT", - "dependencies": { - "@jimp/plugin-blit": "^0.22.12", - "@jimp/plugin-blur": "^0.22.12", - "@jimp/plugin-circle": "^0.22.12", - "@jimp/plugin-color": "^0.22.12", - "@jimp/plugin-contain": "^0.22.12", - "@jimp/plugin-cover": "^0.22.12", - "@jimp/plugin-crop": "^0.22.12", - "@jimp/plugin-displace": "^0.22.12", - "@jimp/plugin-dither": "^0.22.12", - "@jimp/plugin-fisheye": "^0.22.12", - "@jimp/plugin-flip": "^0.22.12", - "@jimp/plugin-gaussian": "^0.22.12", - "@jimp/plugin-invert": "^0.22.12", - "@jimp/plugin-mask": "^0.22.12", - "@jimp/plugin-normalize": "^0.22.12", - "@jimp/plugin-print": "^0.22.12", - "@jimp/plugin-resize": "^0.22.12", - "@jimp/plugin-rotate": "^0.22.12", - "@jimp/plugin-scale": "^0.22.12", - "@jimp/plugin-shadow": "^0.22.12", - "@jimp/plugin-threshold": "^0.22.12", - "timm": "^1.6.1" - }, - "peerDependencies": { - "@jimp/custom": ">=0.3.5" - } - }, - "node_modules/@jimp/png": { - "version": "0.22.12", - "resolved": "https://registry.npmjs.org/@jimp/png/-/png-0.22.12.tgz", - "integrity": "sha512-Mrp6dr3UTn+aLK8ty/dSKELz+Otdz1v4aAXzV5q53UDD2rbB5joKVJ/ChY310B+eRzNxIovbUF1KVrUsYdE8Hg==", - "license": "MIT", - "dependencies": { - "@jimp/utils": "^0.22.12", - "pngjs": "^6.0.0" - }, - "peerDependencies": { - "@jimp/custom": ">=0.3.5" - } - }, - "node_modules/@jimp/tiff": { - "version": "0.22.12", - "resolved": "https://registry.npmjs.org/@jimp/tiff/-/tiff-0.22.12.tgz", - "integrity": "sha512-E1LtMh4RyJsoCAfAkBRVSYyZDTtLq9p9LUiiYP0vPtXyxX4BiYBUYihTLSBlCQg5nF2e4OpQg7SPrLdJ66u7jg==", - "license": "MIT", - "dependencies": { - "utif2": "^4.0.1" - }, - "peerDependencies": { - "@jimp/custom": ">=0.3.5" - } - }, - "node_modules/@jimp/types": { - "version": "0.22.12", - "resolved": "https://registry.npmjs.org/@jimp/types/-/types-0.22.12.tgz", - "integrity": "sha512-wwKYzRdElE1MBXFREvCto5s699izFHNVvALUv79GXNbsOVqlwlOxlWJ8DuyOGIXoLP4JW/m30YyuTtfUJgMRMA==", - "license": "MIT", - "dependencies": { - "@jimp/bmp": "^0.22.12", - "@jimp/gif": "^0.22.12", - "@jimp/jpeg": "^0.22.12", - "@jimp/png": "^0.22.12", - "@jimp/tiff": "^0.22.12", - "timm": "^1.6.1" - }, - "peerDependencies": { - "@jimp/custom": ">=0.3.5" - } - }, - "node_modules/@jimp/utils": { - "version": "0.22.12", - "resolved": "https://registry.npmjs.org/@jimp/utils/-/utils-0.22.12.tgz", - "integrity": "sha512-yJ5cWUknGnilBq97ZXOyOS0HhsHOyAyjHwYfHxGbSyMTohgQI6sVyE8KPgDwH8HHW/nMKXk8TrSwAE71zt716Q==", - "license": "MIT", - "dependencies": { - "regenerator-runtime": "^0.13.3" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", @@ -3696,12 +3231,6 @@ "node": ">=6" } }, - "node_modules/@tokenizer/token": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", - "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", - "license": "MIT" - }, "node_modules/@trapezedev/gradle-parse": { "version": "7.0.10", "resolved": "https://registry.npmjs.org/@trapezedev/gradle-parse/-/gradle-parse-7.0.10.tgz", @@ -3802,6 +3331,17 @@ "@types/node": "*" } }, + "node_modules/@types/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/minimatch": "*", + "@types/node": "*" + } + }, "node_modules/@types/http-cache-semantics": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", @@ -3817,6 +3357,13 @@ "@types/node": "*" } }, + "node_modules/@types/minimatch": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/minimist": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", @@ -4048,12 +3595,6 @@ "node": ">=4" } }, - "node_modules/any-base": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/any-base/-/any-base-1.1.0.tgz", - "integrity": "sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg==", - "license": "MIT" - }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -4397,12 +3938,6 @@ "readable-stream": "^4.2.0" } }, - "node_modules/bmp-js": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/bmp-js/-/bmp-js-0.1.0.tgz", - "integrity": "sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw==", - "license": "MIT" - }, "node_modules/bn.js": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", @@ -4689,15 +4224,6 @@ "node": ">=8.0.0" } }, - "node_modules/buffer-equal": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-0.0.1.tgz", - "integrity": "sha512-RgSV6InVQ9ODPdLWJ5UAqBqJBOg370Nz6ZQtRzpt6nUjc8v0St97uJ4PYC6NztqIScrAXafKM3mZPMygSe1ggA==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/buffer-es6": { "version": "4.9.3", "resolved": "https://registry.npmjs.org/buffer-es6/-/buffer-es6-4.9.3.tgz", @@ -4848,15 +4374,6 @@ "@capacitor/core": "^6.0.0" } }, - "node_modules/centra": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/centra/-/centra-2.7.0.tgz", - "integrity": "sha512-PbFMgMSrmgx6uxCdm57RUos9Tc3fclMvhLSATYN39XsDV29B89zZ3KA89jmY0vwSGazyU+uerqwa6t+KaodPcg==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.6" - } - }, "node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -5058,509 +4575,17 @@ "simple-swizzle": "^0.2.2" } }, - "node_modules/color-thief-jimp": { - "version": "2.0.2", - "resolved": "git+ssh://git@github.com/EdenwareApps/color-thief-jimp.git#e5e53767d71b3bfcfc4ebad301949b9d870282fb", - "dependencies": { - "jimp": "^0.5.6" - } - }, - "node_modules/color-thief-jimp/node_modules/@jimp/bmp": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/@jimp/bmp/-/bmp-0.5.4.tgz", - "integrity": "sha512-P/ezH1FuoM3FwS0Dm2ZGkph4x5/rPBzFLEZor7KQkmGUnYEIEG4o0BUcAWFmJOp2HgzbT6O2SfrpJNBOcVACzQ==", + "node_modules/color/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "license": "MIT", "dependencies": { - "@jimp/utils": "^0.5.0", - "bmp-js": "^0.1.0", - "core-js": "^2.5.7" + "color-name": "~1.1.4" }, - "peerDependencies": { - "@jimp/custom": ">=0.3.5" - } - }, - "node_modules/color-thief-jimp/node_modules/@jimp/core": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/@jimp/core/-/core-0.5.4.tgz", - "integrity": "sha512-n3uvHy2ndUKItmbhnRO8xmU8J6KR+v6CQxO9sbeUDpSc3VXc1PkqrA8ZsCVFCjnDFcGBXL+MJeCTyQzq5W9Crw==", - "license": "MIT", - "dependencies": { - "@jimp/utils": "^0.5.0", - "any-base": "^1.1.0", - "buffer": "^5.2.0", - "core-js": "^2.5.7", - "exif-parser": "^0.1.12", - "file-type": "^9.0.0", - "load-bmfont": "^1.3.1", - "mkdirp": "0.5.1", - "phin": "^2.9.1", - "pixelmatch": "^4.0.2", - "tinycolor2": "^1.4.1" - } - }, - "node_modules/color-thief-jimp/node_modules/@jimp/custom": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/@jimp/custom/-/custom-0.5.4.tgz", - "integrity": "sha512-tLfyJoyouDl2J3RPFGfDzTtE+4S8ljqJUmLzy/cmx1n7+xS5TpLPdPskp7UaeAfNTqdF4CNAm94KYoxTZdj2mg==", - "license": "MIT", - "dependencies": { - "@jimp/core": "^0.5.4", - "core-js": "^2.5.7" - } - }, - "node_modules/color-thief-jimp/node_modules/@jimp/gif": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@jimp/gif/-/gif-0.5.0.tgz", - "integrity": "sha512-HVB4c7b8r/yCpjhCjVNPRFLuujTav5UPmcQcFJjU6aIxmne6e29rAjRJEv3UMamHDGSu/96PzOsPZBO5U+ZGww==", - "license": "MIT", - "dependencies": { - "@jimp/utils": "^0.5.0", - "core-js": "^2.5.7", - "omggif": "^1.0.9" - }, - "peerDependencies": { - "@jimp/custom": ">=0.3.5" - } - }, - "node_modules/color-thief-jimp/node_modules/@jimp/jpeg": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/@jimp/jpeg/-/jpeg-0.5.4.tgz", - "integrity": "sha512-YaPWm+YSGCThNE/jLMckM3Qs6uaMxd/VsHOnEaqu5tGA4GFbfVaWHjKqkNGAFuiNV+HdgKlNcCOF3of+elvzqQ==", - "license": "MIT", - "dependencies": { - "@jimp/utils": "^0.5.0", - "core-js": "^2.5.7", - "jpeg-js": "^0.3.4" - }, - "peerDependencies": { - "@jimp/custom": ">=0.3.5" - } - }, - "node_modules/color-thief-jimp/node_modules/@jimp/plugin-blit": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/@jimp/plugin-blit/-/plugin-blit-0.5.4.tgz", - "integrity": "sha512-WqDYOugv76hF1wnKy7+xPGf9PUbcm9vPW28/jHWn1hjbb2GnusJ2fVEFad76J/1SPfhrQ2Uebf2QCWJuLmOqZg==", - "license": "MIT", - "dependencies": { - "@jimp/utils": "^0.5.0", - "core-js": "^2.5.7" - }, - "peerDependencies": { - "@jimp/custom": ">=0.3.5" - } - }, - "node_modules/color-thief-jimp/node_modules/@jimp/plugin-blur": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@jimp/plugin-blur/-/plugin-blur-0.5.0.tgz", - "integrity": "sha512-5k0PXCA1RTJdITL7yMAyZ5tGQjKLHqFvwdXj/PCoBo5PuMyr0x6qfxmQEySixGk/ZHdDxMi80vYxHdKHjNNgjg==", - "license": "MIT", - "dependencies": { - "@jimp/utils": "^0.5.0", - "core-js": "^2.5.7" - }, - "peerDependencies": { - "@jimp/custom": ">=0.3.5" - } - }, - "node_modules/color-thief-jimp/node_modules/@jimp/plugin-color": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/@jimp/plugin-color/-/plugin-color-0.5.5.tgz", - "integrity": "sha512-hWeOqNCmLguGYLhSvBrpfCvlijsMEVaLZAOod62s1rzWnujozyKOzm2eZe+W3To6mHbp5RGJNVrIwHBWMab4ug==", - "license": "MIT", - "dependencies": { - "@jimp/utils": "^0.5.0", - "core-js": "^2.5.7", - "tinycolor2": "^1.4.1" - }, - "peerDependencies": { - "@jimp/custom": ">=0.3.5" - } - }, - "node_modules/color-thief-jimp/node_modules/@jimp/plugin-contain": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/@jimp/plugin-contain/-/plugin-contain-0.5.4.tgz", - "integrity": "sha512-8YJh4FI3S69unri0nJsWeqVLeVGA77N2R0Ws16iSuCCD/5UnWd9FeWRrSbKuidBG6TdMBaG2KUqSYZeHeH9GOQ==", - "license": "MIT", - "dependencies": { - "@jimp/utils": "^0.5.0", - "core-js": "^2.5.7" - }, - "peerDependencies": { - "@jimp/custom": ">=0.3.5", - "@jimp/plugin-blit": ">=0.3.5", - "@jimp/plugin-resize": ">=0.3.5", - "@jimp/plugin-scale": ">=0.3.5" - } - }, - "node_modules/color-thief-jimp/node_modules/@jimp/plugin-cover": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/@jimp/plugin-cover/-/plugin-cover-0.5.4.tgz", - "integrity": "sha512-2Rur7b44WiDDgizUI2M2uYWc1RmfhU5KjKS1xXruobjQ0tXkf5xlrPXSushq0hB6Ne0Ss6wv0+/6eQ8WeGHU2w==", - "license": "MIT", - "dependencies": { - "@jimp/utils": "^0.5.0", - "core-js": "^2.5.7" - }, - "peerDependencies": { - "@jimp/custom": ">=0.3.5", - "@jimp/plugin-crop": ">=0.3.5", - "@jimp/plugin-resize": ">=0.3.5", - "@jimp/plugin-scale": ">=0.3.5" - } - }, - "node_modules/color-thief-jimp/node_modules/@jimp/plugin-crop": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/@jimp/plugin-crop/-/plugin-crop-0.5.4.tgz", - "integrity": "sha512-6t0rqn4VazquGk48tO6hFBrQ+nkvC+A1RnR6UM/m8ZtG2/yjpwF0MXcpgJI1Fb+a4Ug7BY1fu2GPcZOhnAVK/g==", - "license": "MIT", - "dependencies": { - "@jimp/utils": "^0.5.0", - "core-js": "^2.5.7" - }, - "peerDependencies": { - "@jimp/custom": ">=0.3.5" - } - }, - "node_modules/color-thief-jimp/node_modules/@jimp/plugin-displace": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@jimp/plugin-displace/-/plugin-displace-0.5.0.tgz", - "integrity": "sha512-Bec7SQvnmKia4hOXEDjeNVx7vo/1bWqjuV6NO8xbNQcAO3gaCl91c9FjMDhsfAVb0Ou6imhbIuFPrLxorXsecQ==", - "license": "MIT", - "dependencies": { - "@jimp/utils": "^0.5.0", - "core-js": "^2.5.7" - }, - "peerDependencies": { - "@jimp/custom": ">=0.3.5" - } - }, - "node_modules/color-thief-jimp/node_modules/@jimp/plugin-dither": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@jimp/plugin-dither/-/plugin-dither-0.5.0.tgz", - "integrity": "sha512-We2WJQsD/Lm8oqBFp/vUv9/5r2avyenL+wNNu/s2b1HqA5O4sPGrjHy9K6vIov0NroQGCQ3bNznLkTmjiHKBcg==", - "license": "MIT", - "dependencies": { - "@jimp/utils": "^0.5.0", - "core-js": "^2.5.7" - }, - "peerDependencies": { - "@jimp/custom": ">=0.3.5" - } - }, - "node_modules/color-thief-jimp/node_modules/@jimp/plugin-flip": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@jimp/plugin-flip/-/plugin-flip-0.5.0.tgz", - "integrity": "sha512-D/ehBQxLMNR7oNd80KXo4tnSET5zEm5mR70khYOTtTlfti/DlLp3qOdjPOzfLyAdqO7Ly4qCaXrIsnia+pfPrA==", - "license": "MIT", - "dependencies": { - "@jimp/utils": "^0.5.0", - "core-js": "^2.5.7" - }, - "peerDependencies": { - "@jimp/custom": ">=0.3.5", - "@jimp/plugin-rotate": ">=0.3.5" - } - }, - "node_modules/color-thief-jimp/node_modules/@jimp/plugin-gaussian": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@jimp/plugin-gaussian/-/plugin-gaussian-0.5.0.tgz", - "integrity": "sha512-Ln4kgxblv0/YzLBDb/J8DYPLhDzKH87Y8yHh5UKv3H+LPKnLaEG3L4iKTE9ivvdocnjmrtTFMYcWv2ERSPeHcg==", - "license": "MIT", - "dependencies": { - "@jimp/utils": "^0.5.0", - "core-js": "^2.5.7" - }, - "peerDependencies": { - "@jimp/custom": ">=0.3.5" - } - }, - "node_modules/color-thief-jimp/node_modules/@jimp/plugin-invert": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@jimp/plugin-invert/-/plugin-invert-0.5.0.tgz", - "integrity": "sha512-/vyKeIi3T7puf+8ruWovTjzDC585EnTwJ+lGOOUYiNPsdn4JDFe1B3xd+Ayv9aCQbXDIlPElZaM9vd/+wqDiIQ==", - "license": "MIT", - "dependencies": { - "@jimp/utils": "^0.5.0", - "core-js": "^2.5.7" - }, - "peerDependencies": { - "@jimp/custom": ">=0.3.5" - } - }, - "node_modules/color-thief-jimp/node_modules/@jimp/plugin-mask": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/@jimp/plugin-mask/-/plugin-mask-0.5.4.tgz", - "integrity": "sha512-mUJ04pCrUWaJGXPjgoVbzhIQB8cVobj2ZEFlGO3BEAjyylYMrdJlNlsER8dd7UuJ2L/a4ocWtFDdsnuicnBghQ==", - "license": "MIT", - "dependencies": { - "@jimp/utils": "^0.5.0", - "core-js": "^2.5.7" - }, - "peerDependencies": { - "@jimp/custom": ">=0.3.5" - } - }, - "node_modules/color-thief-jimp/node_modules/@jimp/plugin-normalize": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/@jimp/plugin-normalize/-/plugin-normalize-0.5.4.tgz", - "integrity": "sha512-Q5W0oEz9wxsjuhvHAJynI/OqXZcmqEAuRONQId7Aw5ulCXSOg9C4y2a67EO7aZAt55T+zMVxI9UpVUpzVvO6hw==", - "license": "MIT", - "dependencies": { - "@jimp/utils": "^0.5.0", - "core-js": "^2.5.7" - }, - "peerDependencies": { - "@jimp/custom": ">=0.3.5" - } - }, - "node_modules/color-thief-jimp/node_modules/@jimp/plugin-print": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/@jimp/plugin-print/-/plugin-print-0.5.4.tgz", - "integrity": "sha512-DOZr5TY9WyMWFBD37oz7KpTEBVioFIHQF/gH5b3O5jjFyj4JPMkw7k3kVBve9lIrzIYrvLqe0wH59vyAwpeEFg==", - "license": "MIT", - "dependencies": { - "@jimp/utils": "^0.5.0", - "core-js": "^2.5.7", - "load-bmfont": "^1.4.0" - }, - "peerDependencies": { - "@jimp/custom": ">=0.3.5", - "@jimp/plugin-blit": ">=0.3.5" - } - }, - "node_modules/color-thief-jimp/node_modules/@jimp/plugin-resize": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/@jimp/plugin-resize/-/plugin-resize-0.5.4.tgz", - "integrity": "sha512-lXNprNAT0QY1D1vG/1x6urUTlWuZe2dfL29P81ApW2Yfcio471+oqo45moX5FLS0q24xU600g7cHGf2/TzqSfA==", - "license": "MIT", - "dependencies": { - "@jimp/utils": "^0.5.0", - "core-js": "^2.5.7" - }, - "peerDependencies": { - "@jimp/custom": ">=0.3.5" - } - }, - "node_modules/color-thief-jimp/node_modules/@jimp/plugin-rotate": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/@jimp/plugin-rotate/-/plugin-rotate-0.5.4.tgz", - "integrity": "sha512-SIdUpMc8clObMchy8TnjgHgcXEQM992z5KavgiuOnCuBlsmSHtE3MrXTOyMW0Dn3gqapV9Y5vygrLm/BVtCCsg==", - "license": "MIT", - "dependencies": { - "@jimp/utils": "^0.5.0", - "core-js": "^2.5.7" - }, - "peerDependencies": { - "@jimp/custom": ">=0.3.5", - "@jimp/plugin-blit": ">=0.3.5", - "@jimp/plugin-crop": ">=0.3.5", - "@jimp/plugin-resize": ">=0.3.5" - } - }, - "node_modules/color-thief-jimp/node_modules/@jimp/plugin-scale": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@jimp/plugin-scale/-/plugin-scale-0.5.0.tgz", - "integrity": "sha512-5InIOr3cNtrS5aQ/uaosNf28qLLc0InpNGKFmGFTv8oqZqLch6PtDTjDBZ1GGWsPdA/ljy4Qyy7mJO1QBmgQeQ==", - "license": "MIT", - "dependencies": { - "@jimp/utils": "^0.5.0", - "core-js": "^2.5.7" - }, - "peerDependencies": { - "@jimp/custom": ">=0.3.5", - "@jimp/plugin-resize": ">=0.3.5" - } - }, - "node_modules/color-thief-jimp/node_modules/@jimp/plugins": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/@jimp/plugins/-/plugins-0.5.5.tgz", - "integrity": "sha512-9oF6LbSM/K7YkFCcxaPaD8NUkL/ZY8vT8NIGfQ/NpX+tKQtcsLHcRavHpUC+M1xXShv/QGx9OdBV/jgiu82QYg==", - "license": "MIT", - "dependencies": { - "@jimp/plugin-blit": "^0.5.4", - "@jimp/plugin-blur": "^0.5.0", - "@jimp/plugin-color": "^0.5.5", - "@jimp/plugin-contain": "^0.5.4", - "@jimp/plugin-cover": "^0.5.4", - "@jimp/plugin-crop": "^0.5.4", - "@jimp/plugin-displace": "^0.5.0", - "@jimp/plugin-dither": "^0.5.0", - "@jimp/plugin-flip": "^0.5.0", - "@jimp/plugin-gaussian": "^0.5.0", - "@jimp/plugin-invert": "^0.5.0", - "@jimp/plugin-mask": "^0.5.4", - "@jimp/plugin-normalize": "^0.5.4", - "@jimp/plugin-print": "^0.5.4", - "@jimp/plugin-resize": "^0.5.4", - "@jimp/plugin-rotate": "^0.5.4", - "@jimp/plugin-scale": "^0.5.0", - "core-js": "^2.5.7", - "timm": "^1.6.1" - }, - "peerDependencies": { - "@jimp/custom": ">=0.3.5" - } - }, - "node_modules/color-thief-jimp/node_modules/@jimp/png": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/@jimp/png/-/png-0.5.4.tgz", - "integrity": "sha512-J2NU7368zihF1HUZdmpXsL/Hhyf+I3ubmK+6Uz3Uoyvtk1VS7dO3L0io6fJQutfWmPZ4bvu6Ry022oHjbi6QCA==", - "license": "MIT", - "dependencies": { - "@jimp/utils": "^0.5.0", - "core-js": "^2.5.7", - "pngjs": "^3.3.3" - }, - "peerDependencies": { - "@jimp/custom": ">=0.3.5" - } - }, - "node_modules/color-thief-jimp/node_modules/@jimp/tiff": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/@jimp/tiff/-/tiff-0.5.4.tgz", - "integrity": "sha512-hr7Zq3eWjAZ+itSwuAObIWMRNv7oHVM3xuEDC2ouP7HfE7woBtyhCyfA7u12KlgtM57gKWeogXqTlewRGVzx6g==", - "license": "MIT", - "dependencies": { - "core-js": "^2.5.7", - "utif": "^2.0.1" - }, - "peerDependencies": { - "@jimp/custom": ">=0.3.5" - } - }, - "node_modules/color-thief-jimp/node_modules/@jimp/types": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/@jimp/types/-/types-0.5.4.tgz", - "integrity": "sha512-nbZXM6TsdpnYHIBd8ZuoxGpvmxc2SqiggY30/bhOP/VJQoDBzm2v/20Ywz5M0snpIK2SdYG52eZPNjfjqUP39w==", - "license": "MIT", - "dependencies": { - "@jimp/bmp": "^0.5.4", - "@jimp/gif": "^0.5.0", - "@jimp/jpeg": "^0.5.4", - "@jimp/png": "^0.5.4", - "@jimp/tiff": "^0.5.4", - "core-js": "^2.5.7", - "timm": "^1.6.1" - }, - "peerDependencies": { - "@jimp/custom": ">=0.3.5" - } - }, - "node_modules/color-thief-jimp/node_modules/@jimp/utils": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@jimp/utils/-/utils-0.5.0.tgz", - "integrity": "sha512-7H9RFVU+Li2XmEko0GGyzy7m7JjSc7qa+m8l3fUzYg2GtwASApjKF/LSG2AUQCUmDKFLdfIEVjxvKvZUJFEmpw==", - "license": "MIT", - "dependencies": { - "core-js": "^2.5.7" - } - }, - "node_modules/color-thief-jimp/node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/color-thief-jimp/node_modules/core-js": { - "version": "2.6.12", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", - "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==", - "deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.", - "hasInstallScript": true, - "license": "MIT" - }, - "node_modules/color-thief-jimp/node_modules/file-type": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-9.0.0.tgz", - "integrity": "sha512-Qe/5NJrgIOlwijpq3B7BEpzPFcgzggOTagZmkXQY4LA6bsXKTUstK7Wp12lEJ/mLKTpvIZxmIuRcLYWT6ov9lw==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/color-thief-jimp/node_modules/jimp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/jimp/-/jimp-0.5.6.tgz", - "integrity": "sha512-H0nHTu6KgAgQzDxa38ew2dXbnRzKm1w5uEyhMIxqwCQVjwgarOjjkV/avbNLxfxRHAFaNp4rGIc/qm8P+uhX9A==", - "license": "MIT", - "dependencies": { - "@babel/polyfill": "^7.0.0", - "@jimp/custom": "^0.5.4", - "@jimp/plugins": "^0.5.5", - "@jimp/types": "^0.5.4", - "core-js": "^2.5.7" - } - }, - "node_modules/color-thief-jimp/node_modules/jpeg-js": { - "version": "0.3.7", - "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.3.7.tgz", - "integrity": "sha512-9IXdWudL61npZjvLuVe/ktHiA41iE8qFyLB+4VDTblEsWBzeg8WQTlktdUK4CdncUqtUgUg0bbOmTE2bKBKaBQ==", - "license": "BSD-3-Clause" - }, - "node_modules/color-thief-jimp/node_modules/minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha512-miQKw5Hv4NS1Psg2517mV4e4dYNaO3++hjAvLOAzKqZ61rH8NS1SK+vbfBWZ5PY/Me/bEWhUwqMghEW5Fb9T7Q==", - "license": "MIT" - }, - "node_modules/color-thief-jimp/node_modules/mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha512-SknJC52obPfGQPnjIkXbmA6+5H15E+fR+E4iR2oQ3zzCLbd7/ONua69R/Gw7AgkTLsRG+r5fzksYwWe1AgTyWA==", - "deprecated": "Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.)", - "license": "MIT", - "dependencies": { - "minimist": "0.0.8" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/color-thief-jimp/node_modules/phin": { - "version": "2.9.3", - "resolved": "https://registry.npmjs.org/phin/-/phin-2.9.3.tgz", - "integrity": "sha512-CzFr90qM24ju5f88quFC/6qohjC144rehe5n6DH900lgXmUe86+xCKc10ev56gRKC4/BkHUoG4uSiQgBiIXwDA==", - "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", - "license": "MIT" - }, - "node_modules/color-thief-jimp/node_modules/pngjs": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz", - "integrity": "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==", - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/color/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" + "engines": { + "node": ">=7.0.0" } }, "node_modules/color/node_modules/color-name": { @@ -5570,6 +4595,13 @@ "dev": true, "license": "MIT" }, + "node_modules/colorette": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", + "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", + "dev": true, + "license": "MIT" + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -6195,6 +5227,25 @@ "ua-parser-js": "^1.0.37" } }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/date-fns-tz": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-3.2.0.tgz", + "integrity": "sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==", + "license": "MIT", + "peerDependencies": { + "date-fns": "^3.0.0 || ^4.0.0" + } + }, "node_modules/dateformat": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz", @@ -6205,6 +5256,12 @@ "node": "*" } }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", @@ -6917,11 +5974,6 @@ "p-limit": "^3.1.0" } }, - "node_modules/exif-parser": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/exif-parser/-/exif-parser-0.1.12.tgz", - "integrity": "sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw==" - }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -7017,23 +6069,6 @@ "pend": "~1.2.0" } }, - "node_modules/file-type": { - "version": "16.5.4", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-16.5.4.tgz", - "integrity": "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==", - "license": "MIT", - "dependencies": { - "readable-web-to-node-stream": "^3.0.0", - "strtok3": "^6.2.4", - "token-types": "^4.1.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/file-type?sponsor=1" - } - }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -7060,26 +6095,6 @@ "node": ">=4" } }, - "node_modules/follow-redirects": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", - "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, "node_modules/foreach": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.6.tgz", @@ -7430,16 +6445,6 @@ "node": ">=6" } }, - "node_modules/gifwrap": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/gifwrap/-/gifwrap-0.10.1.tgz", - "integrity": "sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw==", - "license": "MIT", - "dependencies": { - "image-q": "^4.0.0", - "omggif": "^1.0.10" - } - }, "node_modules/git-raw-commits": { "version": "2.0.11", "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-2.0.11.tgz", @@ -8027,21 +7032,6 @@ "dev": true, "license": "ISC" }, - "node_modules/image-q": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/image-q/-/image-q-4.0.0.tgz", - "integrity": "sha512-PfJGVgIfKQJuq3s0tTDOKtztksibuUEbJQIYT3by6wctQo+Rdlh7ef4evJ5NCdxY4CfMbvFkocEwbl4BF8RlJw==", - "license": "MIT", - "dependencies": { - "@types/node": "16.9.1" - } - }, - "node_modules/image-q/node_modules/@types/node": { - "version": "16.9.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.1.tgz", - "integrity": "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==", - "license": "MIT" - }, "node_modules/immediate": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", @@ -8254,12 +7244,6 @@ "node": ">=8" } }, - "node_modules/is-function": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.2.tgz", - "integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==", - "license": "MIT" - }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -8336,6 +7320,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-plain-object": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-3.0.1.tgz", + "integrity": "sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-reference": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", @@ -8404,16 +7398,6 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "license": "ISC" }, - "node_modules/isomorphic-fetch": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", - "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", - "license": "MIT", - "dependencies": { - "node-fetch": "^2.6.1", - "whatwg-fetch": "^3.4.1" - } - }, "node_modules/jackspeak": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", @@ -8466,24 +7450,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/jimp": { - "version": "0.22.12", - "resolved": "https://registry.npmjs.org/jimp/-/jimp-0.22.12.tgz", - "integrity": "sha512-R5jZaYDnfkxKJy1dwLpj/7cvyjxiclxU3F4TrI/J4j2rS0niq6YDUMoPn5hs8GDpO+OZGo7Ky057CRtWesyhfg==", - "license": "MIT", - "dependencies": { - "@jimp/custom": "^0.22.12", - "@jimp/plugins": "^0.22.12", - "@jimp/types": "^0.22.12", - "regenerator-runtime": "^0.13.3" - } - }, - "node_modules/jpeg-js": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz", - "integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==", - "license": "BSD-3-Clause" - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -8882,22 +7848,6 @@ "dev": true, "license": "MIT" }, - "node_modules/load-bmfont": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/load-bmfont/-/load-bmfont-1.4.2.tgz", - "integrity": "sha512-qElWkmjW9Oq1F9EI5Gt7aD9zcdHb9spJCW1L/dmPf7KzCCEJxq8nhHz5eCgI9aMf7vrG/wyaCqdsI+Iy9ZTlog==", - "license": "MIT", - "dependencies": { - "buffer-equal": "0.0.1", - "mime": "^1.3.4", - "parse-bmfont-ascii": "^1.0.3", - "parse-bmfont-binary": "^1.0.5", - "parse-bmfont-xml": "^1.1.4", - "phin": "^3.7.1", - "xhr": "^2.0.1", - "xtend": "^4.0.0" - } - }, "node_modules/load-json-file": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", @@ -9376,18 +8326,6 @@ "dev": true, "license": "MIT" }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -9575,27 +8513,6 @@ "node": ">=0.10.0" } }, - "node_modules/moment": { - "version": "2.30.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", - "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/moment-timezone": { - "version": "0.5.45", - "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.45.tgz", - "integrity": "sha512-HIWmqA86KcmCAhnMAN0wuDOARV/525R2+lOLotuGFzn4HO+FH+/645z2wx0Dt3iDv6/p61SIvKnDstISainhLQ==", - "license": "MIT", - "dependencies": { - "moment": "^2.29.4" - }, - "engines": { - "node": "*" - } - }, "node_modules/mpegts.js": { "version": "1.7.3", "resolved": "git+ssh://git@github.com/xqq/mpegts.js.git#07b57663fe7810075d48ae744d0025685223b8ca", @@ -9733,6 +8650,7 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, "license": "MIT", "dependencies": { "whatwg-url": "^5.0.0" @@ -9962,12 +8880,6 @@ "dev": true, "license": "MIT" }, - "node_modules/omggif": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/omggif/-/omggif-1.0.10.tgz", - "integrity": "sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==", - "license": "MIT" - }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -10300,12 +9212,6 @@ "dev": true, "license": "BlueOak-1.0.0" }, - "node_modules/pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "license": "(MIT AND Zlib)" - }, "node_modules/parse-asn1": { "version": "5.1.7", "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.7.tgz", @@ -10345,34 +9251,6 @@ ], "license": "MIT" }, - "node_modules/parse-bmfont-ascii": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/parse-bmfont-ascii/-/parse-bmfont-ascii-1.0.6.tgz", - "integrity": "sha512-U4RrVsUFCleIOBsIGYOMKjn9PavsGOXxbvYGtMOEfnId0SVNsgehXh1DxUdVPLoxd5mvcEtvmKs2Mmf0Mpa1ZA==", - "license": "MIT" - }, - "node_modules/parse-bmfont-binary": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/parse-bmfont-binary/-/parse-bmfont-binary-1.0.6.tgz", - "integrity": "sha512-GxmsRea0wdGdYthjuUeWTMWPqm2+FAd4GI8vCvhgJsFnoGhTrLhXDDupwTo7rXVAgaLIGoVHDZS9p/5XbSqeWA==", - "license": "MIT" - }, - "node_modules/parse-bmfont-xml": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/parse-bmfont-xml/-/parse-bmfont-xml-1.1.6.tgz", - "integrity": "sha512-0cEliVMZEhrFDwMh4SxIyVJpqYoOWDJ9P895tFuS+XuNzI5UBmBk5U5O4KuJdTnZpSBI4LFA2+ZiJaiwfSwlMA==", - "license": "MIT", - "dependencies": { - "xml-parse-from-string": "^1.0.0", - "xml2js": "^0.5.0" - } - }, - "node_modules/parse-headers": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.5.tgz", - "integrity": "sha512-ft3iAoLOB/MlwbNXgzy43SWGP6sQki2jQvAyBg/zDFAgr9bfNWZIUj42Kw2eJIl8kEi4PbgE6U1Zau/HwI75HA==", - "license": "MIT" - }, "node_modules/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", @@ -10494,19 +9372,6 @@ "node": ">=0.12" } }, - "node_modules/peek-readable": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-4.1.0.tgz", - "integrity": "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -10548,18 +9413,6 @@ "@types/estree": "*" } }, - "node_modules/phin": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/phin/-/phin-3.7.1.tgz", - "integrity": "sha512-GEazpTWwTZaEQ9RhL7Nyz0WwqilbqgLahDM3D0hxWwmVDI52nXEybHqiN6/elwpkJBhcuj+WbBu+QfT0uhPGfQ==", - "license": "MIT", - "dependencies": { - "centra": "^2.7.0" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/picocolors": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", @@ -10590,27 +9443,6 @@ "node": ">=0.10.0" } }, - "node_modules/pixelmatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-4.0.2.tgz", - "integrity": "sha512-J8B6xqiO37sU/gkcMglv6h5Jbd9xNER7aHzpfRdNmV4IbQBzBpe4l9XmbG+xPF/znacgu2jfEw+wHffaq/YkXA==", - "license": "ISC", - "dependencies": { - "pngjs": "^3.0.0" - }, - "bin": { - "pixelmatch": "bin/pixelmatch" - } - }, - "node_modules/pixelmatch/node_modules/pngjs": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz", - "integrity": "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==", - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, "node_modules/plist": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", @@ -11143,36 +9975,6 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/readable-web-to-node-stream": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz", - "integrity": "sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==", - "license": "MIT", - "dependencies": { - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/readable-web-to-node-stream/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -11232,12 +10034,6 @@ "node": ">=4" } }, - "node_modules/regenerator-runtime": { - "version": "0.13.11", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", - "license": "MIT" - }, "node_modules/regenerator-transform": { "version": "0.15.2", "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", @@ -11716,6 +10512,124 @@ "fsevents": "~2.3.2" } }, + "node_modules/rollup-plugin-copy": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-copy/-/rollup-plugin-copy-3.5.0.tgz", + "integrity": "sha512-wI8D5dvYovRMx/YYKtUNt3Yxaw4ORC9xo6Gt9t22kveWz1enG9QrhVlagzwrxSC455xD1dHMKhIJkbsQ7d48BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/fs-extra": "^8.0.1", + "colorette": "^1.1.0", + "fs-extra": "^8.1.0", + "globby": "10.0.1", + "is-plain-object": "^3.0.0" + }, + "engines": { + "node": ">=8.3" + } + }, + "node_modules/rollup-plugin-copy/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/rollup-plugin-copy/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/rollup-plugin-copy/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup-plugin-copy/node_modules/globby": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/globby/-/globby-10.0.1.tgz", + "integrity": "sha512-sSs4inE1FB2YQiymcmTv6NWENryABjUNPeWhOvmn4SjtKybglsyPZxFB3U1/+L1bYi0rNZDqCLlHyLYDl1Pq5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/glob": "^7.1.1", + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.0.3", + "glob": "^7.1.3", + "ignore": "^5.1.1", + "merge2": "^1.2.3", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/rollup-plugin-copy/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/rollup-plugin-copy/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/rollup-plugin-copy/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/rollup-plugin-import-css": { "version": "3.5.1", "resolved": "https://registry.npmjs.org/rollup-plugin-import-css/-/rollup-plugin-import-css-3.5.1.tgz", @@ -12641,23 +11555,6 @@ "node": ">=0.10.0" } }, - "node_modules/strtok3": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.3.0.tgz", - "integrity": "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==", - "license": "MIT", - "dependencies": { - "@tokenizer/token": "^0.3.0", - "peek-readable": "^4.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, "node_modules/sumchecker": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", @@ -13000,18 +11897,6 @@ "node": ">= 6" } }, - "node_modules/timm": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/timm/-/timm-1.7.1.tgz", - "integrity": "sha512-IjZc9KIotudix8bMaBW6QvMuq64BrJWFs1+4V0lXwWGQZwH+LnX87doAYhem4caOEusRP9/g6jVDQmZ8XOk1nw==", - "license": "MIT" - }, - "node_modules/tinycolor2": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", - "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", - "license": "MIT" - }, "node_modules/tmp": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", @@ -13054,23 +11939,6 @@ "node": ">=8.0" } }, - "node_modules/token-types": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/token-types/-/token-types-4.2.1.tgz", - "integrity": "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==", - "license": "MIT", - "dependencies": { - "@tokenizer/token": "^0.3.0", - "ieee754": "^1.2.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, "node_modules/touch": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", @@ -13109,6 +11977,7 @@ "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true, "license": "MIT" }, "node_modules/tree-kill": { @@ -13471,24 +12340,6 @@ "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", "license": "(WTFPL OR MIT)" }, - "node_modules/utif": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/utif/-/utif-2.0.1.tgz", - "integrity": "sha512-Z/S1fNKCicQTf375lIP9G8Sa1H/phcysstNrrSdZKj1f9g58J4NMgb5IgiEZN9/nLMPDwF0W7hdOe9Qq2IYoLg==", - "license": "MIT", - "dependencies": { - "pako": "^1.0.5" - } - }, - "node_modules/utif2": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/utif2/-/utif2-4.1.0.tgz", - "integrity": "sha512-+oknB9FHrJ7oW7A2WZYajOcv4FcDR4CfoGB0dPNfxbi4GO05RRnFmt5oa23+9w32EanrYcSJWspUiJkLMs+37w==", - "license": "MIT", - "dependencies": { - "pako": "^1.0.11" - } - }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -13527,6 +12378,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true, "license": "BSD-2-Clause" }, "node_modules/webworkify-webpack": { @@ -13534,16 +12386,11 @@ "resolved": "git+ssh://git@github.com/xqq/webworkify-webpack.git#24d1e719b4a6cac37a518b2bb10fe124527ef4ef", "license": "MIT" }, - "node_modules/whatwg-fetch": { - "version": "3.6.20", - "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", - "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", - "license": "MIT" - }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, "license": "MIT", "dependencies": { "tr46": "~0.0.3", @@ -13708,18 +12555,6 @@ "node": ">=10.0.0" } }, - "node_modules/xhr": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/xhr/-/xhr-2.6.0.tgz", - "integrity": "sha512-/eCGLb5rxjx5e3mF1A7s+pLlR6CGyqWN91fv1JgER5mVWg1MZmlhBvy9kjcsOdRk8RrIujotWyJamfyrp+WIcA==", - "license": "MIT", - "dependencies": { - "global": "~4.4.0", - "is-function": "^1.0.1", - "parse-headers": "^2.0.0", - "xtend": "^4.0.0" - } - }, "node_modules/xml-js": { "version": "1.6.11", "resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz", @@ -13740,12 +12575,6 @@ "dev": true, "license": "ISC" }, - "node_modules/xml-parse-from-string": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/xml-parse-from-string/-/xml-parse-from-string-1.0.1.tgz", - "integrity": "sha512-ErcKwJTF54uRzzNMXq2X5sMIy88zJvfN2DmdoQvy7PAFJ+tPRU6ydWuOKNMyfmOjdyBQTFREi60s0Y0SyI0G0g==", - "license": "MIT" - }, "node_modules/xml2js": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", @@ -13777,16 +12606,18 @@ "node": ">=8.0" } }, - "node_modules/xmltv": { - "version": "0.3.0", - "resolved": "git+ssh://git@github.com/EdenwareApps/node-xmltv.git#b56676af8fed67ef4ce0597280907922ce88f302", + "node_modules/xmltv-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/xmltv-stream/-/xmltv-stream-0.4.1.tgz", + "integrity": "sha512-Z+lYZPXQO8yuNk3hgRkz9BZ3zLJ8m2D70RGb6E9aU0Z0onvvaZabJ1pJ4IRav0JAemm7qh4sJnQv9xvJSDnLRA==", "dependencies": { + "date-fns": "^4.1.0", + "date-fns-tz": "^3.2.0", "lodash": "^4.17.20", - "moment": "^2.29.1", "sax": "^1.2.4" } }, - "node_modules/xmltv/node_modules/sax": { + "node_modules/xmltv-stream/node_modules/sax": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", @@ -13806,6 +12637,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.4" diff --git a/package.json b/package.json index fb67c25f..294bf849 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "url": "https://megacubo.tv" }, "icon": "./default_icon.png", - "version": "17.5.4", + "version": "17.5.5", "dependencies": { "@capacitor-community/keep-awake": "^5.0.1", "@capacitor/android": "^6.1.2", @@ -31,7 +31,6 @@ "bytenode": "github:EdenwareApps/bytenode", "capacitor-nodejs": "https://github.com/EdenwareApps/Capacitor-NodeJS/releases/download/v1.0.0-beta.7/capacitor6-nodejs.tgz", "check-disk-space": "^3.4.0", - "color-thief-jimp": "github:EdenwareApps/color-thief-jimp", "cordova-androidx-build": "^1.0.4", "cordova-plugin-android-permissions": "^1.1.5", "cordova-plugin-background-mode": "bitbucket:TheBosZ/cordova-plugin-run-in-background", @@ -39,6 +38,7 @@ "create-desktop-shortcuts": "^1.11.0", "cross-dirname": "^0.1.0", "dashjs": "^4.7.4", + "dayjs": "^1.11.13", "decode-entities": "^1.0.7", "electron": "^9.1.1", "env-paths": "^3.0.0", @@ -52,10 +52,7 @@ "iconv-lite": "^0.6.3", "ionic-plugin-deeplinks": "^1.0.24", "jexidb": "^1.0.2", - "jimp": "^0.22.12", "m3u8-parser": "^7.1.0", - "moment": "^2.30.1", - "moment-timezone": "^0.5.45", "mpegts.js": "github:xqq/mpegts.js", "node-cleanup": "^2.1.2", "opensubtitles.com": "^1.1.0", @@ -69,7 +66,7 @@ "tv.megacubo.ffmpeg": "github:EdenwareApps/tv.megacubo.ffmpeg", "tv.megacubo.pip": "github:EdenwareApps/tv.megacubo.pip", "tv.megacubo.player": "github:EdenwareApps/tv.megacubo.player", - "xmltv": "github:EdenwareApps/node-xmltv", + "xmltv-stream": "^0.4.1", "ytdl-core": "^4.11.5", "ytsr": "^3.8.4" }, @@ -88,6 +85,7 @@ "postcss": "^8.4.43", "rimraf": "^5.0.10", "rollup": "^4.21.2", + "rollup-plugin-copy": "^3.5.0", "rollup-plugin-import-css": "^3.5.1", "rollup-plugin-node-builtins": "^2.1.2", "rollup-plugin-svelte": "^7.2.2", @@ -99,13 +97,13 @@ }, "scripts": { "start": "node bin.js", - "debug": "node --inspect bin.js debug", + "start:capacitor": "cap serve", + "debug": "node bin.js debug", "assets": "npx @capacitor/assets generate --android --iconBackgroundColor #4E2F8F --iconBackgroundColorDark #4E2F8F --splashBackgroundColor #200040 --splashBackgroundColorDark #200040", "cap": "node configure.mjs && cap sync", "capacitor:sync:after": "shx rm -rf android/app/src/main/assets/public/nodejs/node_modules/levelup/node_modules/semver/semver*.js.gz && shx rm -rf android/app/src/main/assets/public/nodejs/modules && shx rm -rf android/app/src/main/assets/public/nodejs/node_modules && shx rm -rf android/app/src/main/assets/public/nodejs/renderer/src && shx rm -f android/app/src/main/assets/public/nodejs/dist/windows.vbs", - "prepare": "rollup -c", - "start:capacitor": "cap serve", - "build:capacitor": "cap build", + "prepare": "node prepare.mjs", + "build": "node build.mjs", "build:electron": "electron-packager ./www/nodejs" } -} \ No newline at end of file +} diff --git a/prepare.mjs b/prepare.mjs new file mode 100644 index 00000000..28d873ec --- /dev/null +++ b/prepare.mjs @@ -0,0 +1,52 @@ +import { execSync } from 'child_process' +import fs from 'fs' +import path from 'path' +import { fileURLToPath } from 'url' + +// Get the current directory in ESM +const __dirname = path.dirname(fileURLToPath(import.meta.url)) + +// Dynamically import the version from package.json +const { default: { version: appVersion } } = await import('file://' + path.join(__dirname, 'package.json'), { assert: { type: 'json' } }) + +// Exit if appVersion is empty or invalid +if (!appVersion || !/^[0-9]+(\.[0-9]+)*$/.test(appVersion)) { + console.error(`Invalid or empty appVersion: ${appVersion}`) + process.exit(1) +} + +console.log(`App version: ${appVersion}`) + +// Helper to run commands and handle errors +const runCommand = (command, description) => { + try { + execSync(command, { stdio: 'inherit' }) + } catch (error) { + console.error(`${description} failed with code ${error.status}`) + process.exit(error.status) + } +} + +// Run Rollup build +runCommand('npx rollup -c', 'Rollup') + +// Remove .portable directory if it exists +const portableDir = path.join(__dirname, 'www', 'nodejs', '.portable') +if (fs.existsSync(portableDir)) fs.rmSync(portableDir, { recursive: true, force: true }) + +// Update versionName in android/app/build.gradle +const gradlePath = path.join(__dirname, 'android', 'app', 'build.gradle') +let buildGradle = fs.readFileSync(gradlePath, 'utf-8') +buildGradle = buildGradle.replace(/versionName\s+'.*'/, `versionName '${appVersion}'`) +fs.writeFileSync(gradlePath, buildGradle) + +// Update version in www/nodejs/package.json +const nodePackagePath = path.join(__dirname, 'www', 'nodejs', 'package.json') +const nodePackageJson = JSON.parse(fs.readFileSync(nodePackagePath, 'utf-8')) +nodePackageJson.version = appVersion +fs.writeFileSync(nodePackagePath, JSON.stringify(nodePackageJson, null, 2)) + +// Sync with Capacitor +runCommand('npx cap sync', 'Capacitor sync') + +console.log(`Finished: ${new Date().toLocaleString()}`) diff --git a/rollup.config.mjs b/rollup.config.mjs index 214d4e7c..05f4c719 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -11,6 +11,7 @@ import { getBabelOutputPlugin } from '@rollup/plugin-babel'; import babelConfig from './babel.config.json' with { type: 'json'}; import babelRendererConfig from './babel.config.renderer.json' with { type: 'json'}; import replace from '@rollup/plugin-replace'; +import copy from 'rollup-plugin-copy'; // import { visualizer } from 'rollup-plugin-visualizer'; const plugins = [ @@ -52,6 +53,18 @@ const pluginsMain = [ preferBuiltins: false // form-data }), commonjs({sourceMap: false}), + copy({ + targets: [ + { + src: 'node_modules/dayjs/locale/*.js', + dest: 'www/nodejs/dist/dayjs-locale' + }, + { + src: 'node_modules/create-desktop-shortcuts/src/windows.vbs', + dest: 'www/nodejs/dist' + } + ] + }), json({compact: true}), getBabelOutputPlugin(babelConfig), // transform esm to cjs here replace({ @@ -206,16 +219,6 @@ const outputs = [ plugins, external: [] }, - { - input: 'www/nodejs/modules/jimp-worker/jimp-worker.js', - output: { - format: 'cjs', - file: 'www/nodejs/dist/jimp-worker.js', - inlineDynamicImports: true - }, - plugins, - external: [] - }, { input: 'www/nodejs/modules/lists/epg-worker.js', output: { @@ -263,23 +266,4 @@ if(fs.existsSync('www/nodejs/modules/premium/premium.js')) { }) } -async function copyFiles() { - await fs.promises.copyFile('node_modules/create-desktop-shortcuts/src/windows.vbs', 'www/nodejs/dist/windows.vbs').catch(console.error) - await fs.promises.mkdir('www/nodejs/dist/moment-locale', {recursive: true}).catch(console.error) - for(const file of (await fs.promises.readdir('node_modules/moment/dist/locale'))) { - let err - const content = await fs.promises.readFile('node_modules/moment/dist/locale/'+ file).catch(e => err = e) - if(err) { - console.error(err) - } else { - await fs.promises.writeFile('www/nodejs/dist/moment-locale/'+ file, String(content). - replace('import moment from \'../moment\';', ''). // will use global moment - replace('export default', 'module.exports =') - ).catch(console.error) - } - } -} - -await copyFiles().catch(console.error) - export default outputs \ No newline at end of file diff --git a/www/nodejs/main.mjs b/www/nodejs/main.mjs index 50d63067..75401ea6 100644 --- a/www/nodejs/main.mjs +++ b/www/nodejs/main.mjs @@ -8,7 +8,6 @@ import streamer from './modules/streamer/main.js' import lang from './modules/lang/lang.js' import lists from './modules/lists/lists.js' import Theme from './modules/theme/theme.js' -import moment from 'moment-timezone' import options from './modules/options/options.js' import recommendations from './modules/recommendations/recommendations.js' import icons from './modules/icon-server/icon-server.js' @@ -27,12 +26,11 @@ import channels from './modules/channels/channels.js' import { getFilename } from 'cross-dirname' import { createRequire } from 'module' import menu from './modules/menu/menu.js' -import { clone, rmdirSync } from './modules/utils/utils.js' +import { moment, rmdirSync, ucWords } from './modules/utils/utils.js' import osd from './modules/osd/osd.js' import ffmpeg from './modules/ffmpeg/ffmpeg.js' import promo from './modules/promoter/promoter.js' import mega from './modules/mega/mega.js' -import EPGManager from './modules/lists/epg-worker.js' /* Preload script variables */ Object.assign(global, { @@ -46,10 +44,12 @@ Object.assign(global, { lang, lists, menu, + moment, options, osd, paths, promo, + recommendations, renderer, storage, streamer @@ -153,7 +153,10 @@ const init = async (language, timezone) => { initialized = true await lang.load(language, config.get('locale'), paths.cwd + '/lang', timezone).catch(e => menu.displayErr(e)) console.log('Language loaded.') - moment.locale(lang.locale) + moment.locale([ + lang.locale +'-'+ lang.countryCode, + lang.locale + ]) global.theme = new Theme() @@ -179,7 +182,8 @@ const init = async (language, timezone) => { menu.addOutputFilter(recommendations.hook.bind(recommendations)) renderer.ui.on('menu-update-range', icons.renderRange.bind(icons)) menu.on('render', icons.render.bind(icons)) - menu.on('action', async (e) => { + menu.on('action', async e => { + const busy = menu.setBusy(e.path) if (typeof(e.type) == 'undefined') { if (typeof(e.url) == 'string') { e.type = 'stream' @@ -229,6 +233,7 @@ const init = async (language, timezone) => { } break } + busy.release() }) renderer.ui.on('config-set', (k, v) => config.set(k, v)) renderer.ui.on('crash', (...args) => crashlog.save(...args)) @@ -288,7 +293,7 @@ const init = async (language, timezone) => { opts.push({ template: 'option', text: lang.OPEN_EXTERNAL_PLAYER, fa: 'fas fa-window-restore', id: 'external' }) } if (typeof(streamer.active.transcode) == 'function' && !streamer.active.isTranscoding()) { - opts.push({ template: 'option', text: lang.FIX_AUDIO_OR_VIDEO + ' · ' + lang.TRANSCODE, fa: 'fas fa-film', id: 'transcode' }) + opts.push({ template: 'option', text: lang.FIX_AUDIO_OR_VIDEO + ' · ' + lang.TRANSCODE, fa: 'fas fa-wrench', id: 'transcode' }) } if (opts.length > 2) { let ret = await menu.dialog(opts, def) diff --git a/www/nodejs/modules/bookmarks/bookmarks.js b/www/nodejs/modules/bookmarks/bookmarks.js index d739d183..720ff956 100644 --- a/www/nodejs/modules/bookmarks/bookmarks.js +++ b/www/nodejs/modules/bookmarks/bookmarks.js @@ -10,7 +10,7 @@ import fs from 'fs' import * as iconv from 'iconv-lite' import { exec } from 'child_process' import icons from '../icon-server/icon-server.js' -import jimp from '../jimp-worker/main.js' +import imp from '../icon-server/image-processor.js' import createShortcut from 'create-desktop-shortcuts' import config from '../config/config.js' import renderer from '../bridge/bridge.js' @@ -426,7 +426,7 @@ class Bookmarks extends EntriesGroup { } } if (nicon.file) { - const file = await jimp.iconize(nicon.file).catch(e => err = e); + const file = await imp.iconize(nicon.file).catch(e => err = e); if (!err) { icon = file; const cachedFile = await icons.saveDefaultIcon(global.channels.entryTerms(entry, true), file, true).catch(e => err = e); diff --git a/www/nodejs/modules/bridge/renderer.js b/www/nodejs/modules/bridge/renderer.js index 16156920..c0c2fdea 100644 --- a/www/nodejs/modules/bridge/renderer.js +++ b/www/nodejs/modules/bridge/renderer.js @@ -1,9 +1,11 @@ import EventEmitter from 'events' import { Idle } from '../../renderer/src/scripts/idle' +import { css } from '../../renderer/src/scripts/utils' class BridgeClient extends EventEmitter { constructor() { super() + this.css = css this.config = {} this.lang = {} this.isReady = {renderer: false, main: false} diff --git a/www/nodejs/modules/channels/channels.js b/www/nodejs/modules/channels/channels.js index 609dc5dd..b4a78c0b 100644 --- a/www/nodejs/modules/channels/channels.js +++ b/www/nodejs/modules/channels/channels.js @@ -6,7 +6,6 @@ import lang from "../lang/lang.js"; import storage from '../storage/storage.js' import { EventEmitter } from 'events'; import cloud from "../cloud/cloud.js"; -import moment from "moment-timezone"; import mega from "../mega/mega.js"; import Trending from "../trending/trending.js"; import Bookmarks from "../bookmarks/bookmarks.js"; @@ -16,7 +15,7 @@ import icons from '../icon-server/icon-server.js'; import config from "../config/config.js" import renderer from '../bridge/bridge.js' import paths from '../paths/paths.js' -import { clone, insertEntry, parseCommaDelimitedURIs, parseJSON, ts2clock } from "../utils/utils.js"; +import { clone, insertEntry, parseCommaDelimitedURIs, parseJSON, moment, time, ts2clock } from "../utils/utils.js"; import Search from '../search/search.js'; class ChannelsList extends EventEmitter { @@ -353,7 +352,7 @@ class ChannelsEPG extends ChannelsData { }, async data => { const name = data.originalName || data.name; const category = this.getChannelCategory(name) - if (!this.isEPGLoaded()) { + if (!global.lists.epg.loaded) { menu.displayErr(lang.EPG_DISABLED) } else if (category) { let err; @@ -363,7 +362,10 @@ class ChannelsEPG extends ChannelsData { }); if (!err) { const entries = await this.epgChannelEntries({ name }, null, true) - menu.render(entries, lang.EPG, 'fas fa-plus', '/') + menu.render(entries, lang.EPG, { + icon: 'fas fa-plus', + backTo: '/' + }) renderer.ui.emit('menu-playing') } } else { @@ -378,16 +380,12 @@ class ChannelsEPG extends ChannelsData { return Array.isArray(activeEPG) ? activeEPG.filter(r => r.active).length : parseCommaDelimitedURIs(activeEPG).length } } - isEPGLoaded() { - return lists.loadedEPGs && lists.loadedEPGs.length - } - clock(start, data, includeEnd) { - let t = this.clockIcon; - t += ts2clock(start); + clock(data, includeEnd) { + let t = this.clockIcon + ts2clock(data.start) if (includeEnd) { - t += ' - ' + ts2clock(data.e); + t += ' - ' + ts2clock(data.e) } - return t; + return t } epgSearchEntry() { return { @@ -399,10 +397,13 @@ class ChannelsEPG extends ChannelsData { if (value) { this.epgSearch(value).then(entries => { let path = menu.path.split('/').filter(s => s != lang.SEARCH).join('/'); - entries.unshift(this.epgSearchEntry()); - menu.render(entries, path + '/' + lang.SEARCH, 'fas fa-search', path); - this.search.history.add(value); - }).catch(e => menu.displayErr(e)); + entries.unshift(this.epgSearchEntry()) + menu.render(entries, path + '/' + lang.SEARCH, { + icon: 'fas fa-search', + backTo: path + }) + this.search.history.add(value) + }).catch(e => menu.displayErr(e)) } }, value: () => { @@ -427,27 +428,24 @@ class ChannelsEPG extends ChannelsData { }) } epgDataToEntries(epgData, ch, terms) { - let now = (Date.now() / 1000); - let at = start => { - if (start <= now && epgData[start].e > now) { - return lang.LIVE; - } - return this.clock(start, epgData[start], true); - }; - return Object.keys(epgData).filter(start => epgData[start].e > now).map((start, i) => { - let epgIcon = ''; - if (epgData[start].i && epgData[start].i.includes('//')) { - epgIcon = epgData[start].i; + const now = (Date.now() / 1000) + const at = p => { + if (p.start <= now && p.e >= now) { + return lang.LIVE } + return this.clock(p, true) + } + return epgData.filter(p => p.e > now).map(p => { + const epgIcon = (p.i && p.i.includes('//')) ? p.i : '' return { - name: epgData[start].t, - details: ch + ' | ' + at(start), + name: p.t, + details: ch + ' | ' + at(p), type: 'action', fa: 'fas fa-play-circle', - programme: { start, ch, i: epgIcon }, - action: this.epgProgramAction.bind(this, start, ch, epgData[start], terms, epgIcon) - }; - }); + programme: { start: p.start, ch, i: epgIcon }, + action: this.epgProgramAction.bind(this, p.start, ch, p, terms, epgIcon) + } + }) } epgPrepareSearch(e) { let ret = { name: e.originalName || e.name }, map = config.get('epg-map'); @@ -468,26 +466,26 @@ class ChannelsEPG extends ChannelsData { if (typeof(limit) != 'number') { limit = 72 } - console.log('EPG', {e, limit, detached}) + // console.log('EPG', {e, limit, detached}) let data = this.epgPrepareSearch(e) - console.log('EPG', {data}) + // console.log('EPG', {data}) const epgData = await global.lists.epgChannelsList(data, limit) let centries = [] - console.log('EPG', {epgData}) + // console.log('EPG', {epgData}) if (epgData) { if (typeof(epgData[0]) != 'string') { centries = this.epgDataToEntries(epgData, data.name, data.terms) if (!centries.length) { centries.push(menu.emptyEntry(lang.NOT_FOUND)) } - console.log('EPG', {centries}) + // console.log('EPG', {centries}) } } centries.unshift(this.adjustEPGChannelEntry(e, detached)) return centries } async epgChannelLiveNow(entry) { - if (!this.isEPGLoaded()) throw 'epg not loaded'; + if (!global.lists.epg.loaded) throw 'epg not loaded'; let channel = this.epgPrepareSearch(entry) let epgData = await global.lists.epgChannelsList(channel, 1) let ret = Object.values(epgData).shift() @@ -503,46 +501,44 @@ class ChannelsEPG extends ChannelsData { return ret; } async epgChannelLiveNowAndNextInfo(entry) { - if (!this.isEPGLoaded()) throw 'epg not loaded' - let channel = this.epgPrepareSearch(entry); - let epgData = await global.lists.epgChannelsList(channel, 2); - if (typeof(epgData) == 'string') throw 'epg is loading'; - if (Array.isArray(epgData)) throw 'not found 1'; - let now = Object.values(epgData).shift() + if (!global.lists.epg.loaded) throw 'epg not loaded' + let channel = this.epgPrepareSearch(entry) + let epgData = await global.lists.epgChannelsList(channel, 2) + if (typeof(epgData) == 'string') throw 'epg is loading' + if (!Array.isArray(epgData)) throw 'not found 1' + let now = epgData.shift() if (now && now.t) { - let ret = { now }; - let ks = Object.keys(epgData); - if (ks.length > 1) { - let start = ks.pop(); - let next = epgData[start]; - start = moment(start * 1000).fromNow(); - start = start.charAt(0).toUpperCase() + start.slice(1); - ret[start] = next; + let ret = { now } + if (epgData.length) { + let s = time() + let next = epgData.filter(n => n.start > s).shift() + let start = moment(next.start * 1000).fromNow() + start = start.charAt(0).toUpperCase() + start.slice(1) + ret[start] = next } - return ret; + return ret } else { - throw 'not found 2'; + throw 'not found 2' } } async epgChannelsLiveNow(entries) { - let ret = {}; - if(!entries.length) { + let ret = {} + if(!entries.length || !global.lists.epg.loaded) { return ret - } - if (!this.isEPGLoaded()) throw 'epg not loaded'; - let chs = entries.map(e => this.epgPrepareSearch(e)); - let epgData = await global.lists.epgChannelsList(chs, 1); + } + let chs = entries.map(e => this.epgPrepareSearch(e)) + let epgData = await global.lists.epgChannelsList(chs, 1) Object.keys(epgData).forEach(ch => { - ret[ch] = epgData[ch] ? Object.values(epgData[ch]).shift() : false; - if (!ret[ch] && ret[ch] !== false) - ret[ch] = false; - }); - return ret; + ret[ch] = epgData[ch] ? epgData[ch].shift() : false + if (!ret[ch] && ret[ch] !== false) ret[ch] = false + }) + return ret } async epgChannelsAddLiveNow(entries) { + if (!global.lists.epg.loaded) return entries let err const cs = entries.filter(e => e.terms || e.type == 'select').map(e => this.isChannel(e)).filter(e => e) - const epg = await this.epgChannelsLiveNow(cs).catch(e => err = e); + const epg = await this.epgChannelsLiveNow(cs).catch(e => err = e) if (!err && epg) { //console.warn('epgChannelsAddLiveNow', cs, entries, epg) entries.forEach((e, i) => { @@ -693,13 +689,11 @@ class ChannelsEditing extends ChannelsEPG { fa: 'fa-mega spin-x-alt', iconFallback: 'fas fa-exclamation-triangle', action: async () => { - menu.setLoading(true); let err; const r = await icons.fetchURL(image); const ret = await icons.adjust(r.file, { shouldBeAlpha: false }).catch(e => menu.displayErr(e)); const destFile = await icons.saveDefaultFile(terms, ret.file).catch(e => err = e); this.emit('edited', 'icon', e, destFile); - menu.setLoading(false); if (err) throw err; console.log('icon changed', terms, destFile); @@ -916,7 +910,6 @@ class ChannelsAutoWatchNow extends ChannelsEditing { this.watchNowAuto = false; this.disableWatchNowAuto = false; renderer.ready(() => { - moment.locale(lang.locale) menu.on('render', (_, path) => { if (path != this.watchNowAuto) { this.watchNowAuto = false; @@ -1508,6 +1501,9 @@ class Channels extends ChannelsKids { if (!global.lists.loaded()) { return [global.lists.manager.updatingListsEntry()]; } + if(!this.channelList) { + throw new Error('Channel list not loaded') + } let list const publicMode = config.get('public-lists') && !(paths.ALLOW_ADDING_LISTS && global.lists.loaded(true)); // no list available on index beyound public lists const type = publicMode ? 'public' : config.get('channel-grid'); diff --git a/www/nodejs/modules/downloads/downloads.js b/www/nodejs/modules/downloads/downloads.js index 506c3b88..a1a8917b 100644 --- a/www/nodejs/modules/downloads/downloads.js +++ b/www/nodejs/modules/downloads/downloads.js @@ -248,7 +248,7 @@ class Downloads extends EventEmitter { async serve(file, triggerDownload, doImport, name) { await this.prepare(); if (!name) { - name = path.basename(file) + name = path.basename(file) || String(Math.random()).substr(2) } let url if (doImport) { diff --git a/www/nodejs/modules/ffmpeg/ffmpeg.js b/www/nodejs/modules/ffmpeg/ffmpeg.js index b992ecb5..ea7368a0 100644 --- a/www/nodejs/modules/ffmpeg/ffmpeg.js +++ b/www/nodejs/modules/ffmpeg/ffmpeg.js @@ -604,7 +604,7 @@ class FFmpegDownloader { if (err) { osd.show(String(err), 'fas fa-exclamation-triangle faclr-red', 'ffmpeg-dl', 'normal'); } else { - osd.show(mask.replace('{0}', '100%'), 'fas fa-circle-notch faclr-green', 'ffmpeg-dl', 'normal'); + osd.show(mask.replace('{0}', '100%'), 'fas fa-check-circle faclr-green', 'ffmpeg-dl', 'normal'); this.executableDir = path.dirname(file); this.executable = path.basename(file); return true; diff --git a/www/nodejs/modules/history/epg-history.js b/www/nodejs/modules/history/epg-history.js index 5d1d50f3..5cdd5b0a 100644 --- a/www/nodejs/modules/history/epg-history.js +++ b/www/nodejs/modules/history/epg-history.js @@ -1,9 +1,8 @@ import menu from '../menu/menu.js' import lang from "../lang/lang.js"; import EntriesGroup from "../entries-group/entries-group.js"; -import moment from "moment-timezone"; import { ready } from '../bridge/bridge.js' -import { ts2clock } from "../utils/utils.js"; +import { moment, ts2clock } from "../utils/utils.js"; class EPGHistory extends EntriesGroup { constructor(channels) { @@ -16,7 +15,6 @@ class EPGHistory extends EntriesGroup { this.session = null; this.allowDupes = true; ready(() => { - moment.locale(global.lang.locale) global.streamer.on('commit', async () => { await this.busy() const data = this.currentStreamData() @@ -42,7 +40,7 @@ class EPGHistory extends EntriesGroup { console.warn('Session finished') this.finishSession() }) - global.lists.epg.ready().then(() => { + global.lists.epg.ready().then(() => { // we need to wait for the epg to be ready, it can be slow so do it as last if (this.inSection()) menu.refresh() }).catch(console.error) }) @@ -61,11 +59,11 @@ class EPGHistory extends EntriesGroup { } async finishSession() { if (this.session) { - this.setBusy(true) + const busy = global.menu.setBusy(true) clearInterval(this.session.timer) await this.check().catch(console.error) this.session = null - this.setBusy(false) + busy.release() } } startSessionTimer() { diff --git a/www/nodejs/modules/history/history.js b/www/nodejs/modules/history/history.js index d41470ea..f25757bb 100644 --- a/www/nodejs/modules/history/history.js +++ b/www/nodejs/modules/history/history.js @@ -2,10 +2,9 @@ import menu from '../menu/menu.js' import lang from "../lang/lang.js"; import EntriesGroup from "../entries-group/entries-group.js"; import mega from "../mega/mega.js"; -import moment from "moment-timezone"; import EPGHistory from './epg-history.js' import { ready } from '../bridge/bridge.js' -import { ucFirst, insertEntry } from '../utils/utils.js' +import { ucFirst, insertEntry, moment } from '../utils/utils.js' class History extends EntriesGroup { constructor(channels) { @@ -98,7 +97,6 @@ class History extends EntriesGroup { } async entries(e) { const epgAddLiveNowMap = {} - moment.locale(global.lang.locale) let gentries = this.get().map((e, i) => { e.details = ucFirst(moment(e.historyTime * 1000).fromNow(), true); const isMega = e.url && mega.isMega(e.url); diff --git a/www/nodejs/modules/icon-server/icon-server.js b/www/nodejs/modules/icon-server/icon-server.js index 24e2cc48..c28ab149 100644 --- a/www/nodejs/modules/icon-server/icon-server.js +++ b/www/nodejs/modules/icon-server/icon-server.js @@ -6,7 +6,7 @@ import storage from '../storage/storage.js' import crypto from 'crypto'; import lists from '../lists/lists.js'; import fs from 'fs'; -import jimp from '../jimp-worker/main.js'; +import imp from '../icon-server/image-processor.js'; import crashlog from '../crashlog/crashlog.js'; import paths from '../paths/paths.js'; import path from 'path'; @@ -89,17 +89,14 @@ class IconDefault { } async adjust(file, options) { return await this.limiter.adjust(async () => { - return await this.doAdjust(file, options); - }); - } - async doAdjust(file, options) { - let opts = { - autocrop: config.get('autocrop-logos') - }; - if (options) { - Object.assign(opts, options); - } - return await jimp.transform(file, opts); + let opts = { + autocrop: config.get('autocrop-logos') + }; + if (options) { + Object.assign(opts, options); + } + return await imp.transform(file, opts) + }) } } class IconSearch extends IconDefault { @@ -190,8 +187,8 @@ class IconSearch extends IconDefault { class IconServerStore extends IconSearch { constructor() { super(); - this.ttlHTTPCache = 24 * 3600 - this.ttlBadHTTPCache = 1800 + this.ttlCache = 24 * 3600 + this.ttlBadCache = 600 this.activeDownloads = {} this.downloadErrors = {} } @@ -245,42 +242,23 @@ class IconServerStore extends IconSearch { if(err) throw err return this.validate(content.slice(0, bytesRead)) } - resolveHTTPCache(key) { + resolve(key) { return storage.resolve('icons-cache-' + key) } - async checkHTTPCache(key) { + async checkCache(key) { const has = await storage.exists('icons-cache-' + key) if (has !== false) { - return this.resolveHTTPCache(key) + return this.resolve(key) } throw 'no http cache' } - async getHTTPCache(key) { - const data = await storage.get('icons-cache-' + key) - if (data) { - return { data } - } - throw 'no cache*' - } - async saveHTTPCache(key, data) { - if (!cb) - cb = () => {}; - const ttl = data && data.length ? this.ttlHTTPCache : this.ttlBadHTTPCache; - await storage.set('icons-cache-' + key, data, { raw: true, ttl }); - } - async saveHTTPCacheExpiration(key, size) { - let stat; + async saveCacheExpiration(key, valid) { const file = storage.resolve('icons-cache-' + key); - if (typeof(size) != 'number') { - let err; - - stat = await fs.promises.stat(file).catch(e => err = e); - err || (size = stat.size); - } - let time = this.ttlBadHTTPCache; - if (stat && stat.size) { - time = this.ttlHTTPCache; + if (typeof(valid) != 'boolean') { + const stat = await fs.promises.stat(file).catch(() => false); + valid = stat && stat.size && stat.size > 25 } + const time = valid ? this.ttlCache : this.ttlBadCache; storage.setTTL('icons-cache-' + key, time); } async fetchURL(url) { @@ -291,7 +269,7 @@ class IconServerStore extends IconSearch { const suffix = 'data:image/png;base64,'; if (String(url).startsWith(suffix)) { const key = this.key(url); - const file = this.resolveHTTPCache(key); + const file = this.resolve(key); await fs.promises.writeFile(file, Buffer.from(url.substr(suffix.length), 'base64')); this.opts.debug && console.log('FETCHED ' + url + ' => ' + file); const ret = await this.validateFile(file); @@ -305,7 +283,7 @@ class IconServerStore extends IconSearch { console.warn('WILLFETCH', url); } let err; - const cfile = await this.checkHTTPCache(key).catch(e => err = e); + const cfile = await this.checkCache(key).catch(e => err = e); if (!err) { // has cache if (this.opts.debug) { console.log('fetchURL', url, 'cached'); @@ -318,7 +296,7 @@ class IconServerStore extends IconSearch { if (this.opts.debug) { console.log('fetchURL', url, 'request', err); } - const file = this.resolveHTTPCache(key); + const file = this.resolve(key); err = null; await this.limiter.download(async () => { if(!this.activeDownloads[url]) { @@ -330,7 +308,8 @@ class IconServerStore extends IconSearch { downloadLimit: this.opts.downloadLimit, headers: { 'content-encoding': 'identity' - } + }, + cacheTTL: this.ttlBadCache }).catch(e => err = e) } await this.activeDownloads[url] @@ -341,7 +320,7 @@ class IconServerStore extends IconSearch { await fs.promises.unlink(file).catch(console.error) throw err } - await this.saveHTTPCacheExpiration(key); + await this.saveCacheExpiration(key, true); const ret2 = await this.validateFile(file); const atts = { key, file, isAlpha: ret2 == 2 }; if (this.opts.debug) { @@ -372,7 +351,7 @@ class IconServer extends IconServerStore { this.server = false; this.limiter = { download: pLimit(20), - adjust: pLimit(1) + adjust: pLimit(2) }; this.rendering = {}; this.renderingPath = null; @@ -483,7 +462,7 @@ class IconServer extends IconServerStore { if ((!this.rendering[j] || this.rendering[j].entry.name != e.name) && this.qualifyEntry(e)) { this.rendering[j] = this.get(e); // do not use then directly to avoid losing destroy method } - }); + }) } } } @@ -549,7 +528,7 @@ class IconServer extends IconServerStore { response.end(err + ' - ' + req.url.split('#')[0]); }; if (this.isHashKey(key)) { - this.checkHTTPCache(key).then(send).catch(onerr); + this.checkCache(key).then(send).catch(onerr); } else { this.getDefaultFile(decodeURIComponentSafe(key).split(',')).then(send).catch(onerr); } diff --git a/www/nodejs/modules/icon-server/icon.js b/www/nodejs/modules/icon-server/icon.js index 44f5d7ab..b2d94a00 100644 --- a/www/nodejs/modules/icon-server/icon.js +++ b/www/nodejs/modules/icon-server/icon.js @@ -74,7 +74,7 @@ class IconFetcher extends EventEmitter { return false; } const ret2 = await this.master.adjust(ret.file, { shouldBeAlpha: true, minWidth: 75, minHeight: 75 }); - await this.master.saveHTTPCacheExpiration(key); + await this.master.saveCacheExpiration(key, true) if (!done || this.hasPriority(done.image, image, images)) { done = ret2; if (!done.key) done.key = key @@ -83,7 +83,7 @@ class IconFetcher extends EventEmitter { this.succeeded = true; this.result = done; results[image.icon] = 'OK' - this.emit('result', done); + this.emit('result', done) } }; }).map(limit); diff --git a/www/nodejs/modules/lang/lang.js b/www/nodejs/modules/lang/lang.js index d638cdb7..3f5aaf84 100644 --- a/www/nodejs/modules/lang/lang.js +++ b/www/nodejs/modules/lang/lang.js @@ -20,7 +20,7 @@ class Language extends EventEmitter { } else { this.on('ready', resolve); } - }); + }) } async findLanguages() { let files = await fs.promises.readdir(this.folder).catch(e => menu.displayErr(e)); diff --git a/www/nodejs/modules/lists/epg-worker.js b/www/nodejs/modules/lists/epg-worker.js index 733b80d9..80ebcf1a 100644 --- a/www/nodejs/modules/lists/epg-worker.js +++ b/www/nodejs/modules/lists/epg-worker.js @@ -2,18 +2,88 @@ import fs from 'fs' import pLimit from 'p-limit'; import { moveFile, parseCommaDelimitedURIs, textSimilarity, time, ucWords } from '../utils/utils.js' import Download from '../download/download.js' -import paths from '../paths/paths.js' import config from '../config/config.js'; import { EventEmitter } from 'events' import listsTools from '../lists/tools.js' import setupUtils from '../multi-worker/utils.js' import Mag from './mag.js' -import xmltv from 'xmltv' +import { Parser } from 'xmltv-stream' import { getFilename } from 'cross-dirname' import { Database } from 'jexidb' import { workerData } from 'worker_threads' const utils = setupUtils(getFilename()) +const DBOPTS = { + indexes: {ch: 'string', start: 'number', e: 'number', c: 'number'}, + index: {channels: {}, terms: {}}, + compressIndex: false, + v8: false +} + +class EPGDataRefiner { + constructor(){ + this.data = {} + } + format(t){ + return t.trim().toLowerCase() + } + learn(programme){ + const key = this.format(programme.t) + if(!this.data[key]){ + this.data[key] = { + title: programme.t, + c: new Set(programme.c), + i: programme.i, + programmes: [ + {ch: programme.ch, start: programme.start} + ] + } + } else { + if(programme.c && programme.c.length){ + programme.c.forEach(c => { + this.data[key].updated = true + this.data[key].c.has(c) || this.data[key].c.add(c) + }) + } + if(programme.i && !this.data[key].i){ + this.data[key].updated = true + this.data[key].i = programme.i + } + this.data[key].programmes.push({ch: programme.ch, start: programme.start}) + } + } + async apply(db) { + const start = time() + try { + const tmpFile = db.fileHandler.filePath +'.refine' + const rdb = new Database(tmpFile, Object.assign({clear: true}, DBOPTS)) + await rdb.init() + for await (const programme of db.walk()) { + delete programme._ + const key = this.format(programme.t) + if(key && this.data[key] && this.data[key].updated) { + if(!programme.c.length && this.data[key].c.size) { + programme.c = [...this.data[key].c] + } + if(!programme.i && this.data[key].i) { + programme.i = this.data[key].i + } + } + await rdb.insert(programme) + } + rdb.indexManager.index = db.indexManager.index + await rdb.save() + await rdb.destroy() + await fs.promises.unlink(db.fileHandler.filePath) + await moveFile(tmpFile, db.fileHandler.filePath) + console.error('REFINER APPLIED IN '+ parseInt(time() - start) +'s', tmpFile) + } catch(e) { + console.error('REFINER APPLY ERROR', e) + } finally { + this.data = {} + } + } +} class EPGPaginateChannelsList extends EventEmitter { constructor(){ @@ -92,20 +162,11 @@ class EPGPaginateChannelsList extends EventEmitter { class EPGUpdater extends EventEmitter { constructor(url){ super() + this.refiner = new EPGDataRefiner() } fixSlashes(txt){ return txt.replaceAll('/', '|') // this character will break internal app navigation } - prepareProgrammeData(programme, end){ - if(!end){ - end = time(programme.end) - } - let t = programme.title.shift() || 'No title' - if(t.includes('/')) { - t = this.fixSlashes(t) - } - return {e: end, t, c: programme.category || [], i: programme.icon || ''} - } channel(channel){ if(!channel) return let name = channel.displayName || channel.name @@ -122,6 +183,10 @@ class EPGUpdater extends EventEmitter { }) } async update(){ + if(this.parser) { + console.error('already updating') + return + } await this.db.init().catch(console.error) const now = time() const lastFetchedAt = this.db.index.fetchCtrlKey @@ -134,7 +199,7 @@ class EPGUpdater extends EventEmitter { if(!this.loaded){ this.state = 'connecting' } - let validEPG, failed, hasErr, newLastModified, received = 0, errorCount = 0, initialBuffer = [] + let validEPG, failed, hasErr, newLastModified, received = 0, errorCount = 0 this.error = null console.log('epg updating...') const onErr = err => { @@ -142,7 +207,7 @@ class EPGUpdater extends EventEmitter { return } hasErr = true - //console.error('EPG FAILED DEBUG', initialBuffer) + //console.error('EPG FAILED DEBUG') errorCount++ console.error(err) if(errorCount >= 128){ @@ -153,8 +218,7 @@ class EPGUpdater extends EventEmitter { this.request = null } if(this.parser){ - this.parser.destroy() - this.parser = null + this.parser.end() } this.state = 'error' this.error = 'EPG_BAD_FORMAT' @@ -182,7 +246,7 @@ class EPGUpdater extends EventEmitter { cacheTTL: this.ttl - 30, responseType: 'text' } - this.parser = new xmltv.Parser() + this.parser = new Parser() this.request = new Download(req) this.request.on('error', err => { console.warn(err) @@ -206,10 +270,11 @@ class EPGUpdater extends EventEmitter { }) this.request.on('data', chunk => { received += chunk.length - if(!hasErr) initialBuffer.push(chunk) try { this.parser.write(chunk) - } catch(e) {} + } catch(e) { + console.error(e) + } if(!validEPG && chunk.toLowerCase().includes(' { this.request.destroy() this.request = null - console.log('EPG REQUEST ENDED', validEPG, received, Object.keys(this.data).length) - this.parser && this.parser.end() + console.log('EPG REQUEST ENDED', validEPG, received, this.udb?.length) + this.parser.end() }) this.request.start() } - this.udb = new Database(this.tmpFile, { - clear: true, - indexes: {channel: 'string', start: 'number', c: 'number'}, - index: {channels: {}, terms: {}}, - v8: false, - compressIndex: false - }) + this.udb = new Database(this.tmpFile, Object.assign({clear: true}, DBOPTS)) await this.udb.init() this.parser.on('programme', this.programme.bind(this)) this.parser.on('channel', this.channel.bind(this)) - this.parser.on('error', () => {}) - await new Promise(resolve => this.parser.once('end', resolve)) + this.parser.on('error', onErr) + console.log('EPG UPDATE START') + await (new Promise(resolve => { + this.parser.once('close', resolve) + this.parser.once('end', resolve) + })).catch(console.error) + console.log('EPG UPDATE END 0', this.udb.length) this.parser && this.parser.destroy() // TypeError: Cannot read property 'destroy' of null this.parser = null this.scheduleNextUpdate() + console.log('EPG UPDATE END', this.udb.length) if(this.udb.length){ if(newLastModified){ this.udb.index.lastmCtrlKey = newLastModified @@ -247,16 +312,16 @@ class EPGUpdater extends EventEmitter { this.error = null await this.udb.save() + + console.log('EPG apply 1') + await this.refiner.apply(this.udb).catch(console.error) + + console.log('EPG apply 2') await this.udb.destroy() await this.db.destroy() await moveFile(this.tmpFile, this.file) - this.db = new Database(this.file, { - indexes: {channel: 'string', start: 'number', c: 'number'}, - index: {channels: {}, terms: {}}, - v8: false, - compressIndex: false - }) + this.db = new Database(this.file, DBOPTS) await this.db.init() this.emit('load') } else { @@ -280,18 +345,47 @@ class EPGUpdater extends EventEmitter { this.udb.index.channels[cid].name } programme(programme){ - if(programme && programme.channel && programme.title.length){ - const now = time(), start = time(programme.start), end = time(programme.end) - programme.channel = this.cidToDisplayName(programme.channel) - if(end >= now && end <= (now + this.dataLiveWindow)){ - this.indexate(programme.channel, start, this.prepareProgrammeData(programme, end)) + if(programme && programme.channel && programme.title.length) { + const now = time() + const start = parseInt(programme.start.getTime() / 1000) + const end = parseInt(programme.end.getTime() / 1000) + if(end >= now && end <= (now + this.dataLiveWindow)) { + const ch = this.cidToDisplayName(programme.channel) + let t = programme.title.shift() || 'Untitled' + if(t.includes('/')) { + t = this.fixSlashes(t) + } + let i + if(programme.icon) { + i = programme.icon + } else if(programme.images.length) { + const weight = { + 'large': 1, + 'medium': 0, + 'small': 2 + } + programme.images.sort((a, b) => { + return weight[a.size] - weight[b.size] + }).some(a => { + i = a.url + return true + }) + } else { + i = '' + } + this.indexate({ + start, e: end, + t, i, ch, + c: programme.category || [] + }) } } } - indexate(channel, start, data){ - this.udb.insert({channel, start, ...data}).catch(console.error) - if(!this.udb.index.terms[channel] || !Array.isArray(this.udb.index.terms[channel])){ - this.udb.index.terms[channel] = listsTools.terms(channel) + indexate(data){ + this.udb.insert(data).catch(console.error) + this.refiner.learn(data) + if(!this.udb.index.terms[data.ch] || !Array.isArray(this.udb.index.terms[data.ch])){ + this.udb.index.terms[data.ch] = listsTools.terms(data.ch) } } extractTerms(c){ @@ -337,12 +431,7 @@ class EPG extends EPGUpdater { this.minExpectedEntries = 72 this.state = 'uninitialized' this.error = null - this.db = new Database(this.file, { - fields: {channel: 'string', start: 'number', c: 'number'}, - index: {channels: {}, terms: {}}, - v8: false, - compressIndex: false - }) + this.db = new Database(this.file, DBOPTS) } ready(){ return new Promise((resolve, reject) => { @@ -370,12 +459,14 @@ class EPG extends EPGUpdater { async start(){ if(!this.loaded){ // initialize this.state = 'loading' + console.log('START EPG', this.url) await this.db.init().catch(err => { this.error = err }) const updatePromise = this.update().catch(err => { this.error = err }) + console.log('START EPG2', this.url) if(this.db.length < this.minExpectedEntries) { await updatePromise // will update anyway, but only wait for if it has few programmes } @@ -384,9 +475,6 @@ class EPG extends EPGUpdater { this.emit('load') } } - async getTerms(){ - return this.db.index.terms - } async getState(){ return { progress: this.request ? this.request.progress : (this.state == 'loaded' ? 100 : 0), @@ -525,15 +613,26 @@ class EPGManager extends EPGPaginateChannelsList { } return cs } + async liveNow(ch) { + const now = time() + const data = await this.get(ch, 1) + if(data && data.length) { + const p = data[0] + if(p.e > now && parseInt(p.start) <= now){ + return p + } + } + return false + } async liveNowChannelsList(){ const categories = {}, now = time() let updateAfter = 600 for(const url in this.epgs) { if(this.epgs[url].state !== 'loaded') continue const db = this.epgs[url].db - for(const channel of db.indexManager.readColumnIndex('channel')) { - const name = this.prepareChannelName(channel) - const programmes = await db.query({channel, start: {'<=': now}, e: {'>': now}}) + for(const ch of db.indexManager.readColumnIndex('ch')) { + const name = this.prepareChannelName(ch) + const programmes = await db.query({ch, start: {'<=': now}, e: {'>': now}}) for(const programme of programmes) { if(programme.e > now && parseInt(programme.start) <= now){ if(Array.isArray(programme.c)){ @@ -564,9 +663,9 @@ class EPGManager extends EPGPaginateChannelsList { for(const url in this.epgs) { if(this.epgs[url].state !== 'loaded') continue const db = this.epgs[url].db - for(const channel of db.indexManager.readColumnIndex('channel')) { - const name = this.prepareChannelName(channel) - const programmes = await db.query({channel, start: {'<=': now}, e: {'>': now}}) + for(const ch of db.indexManager.readColumnIndex('ch')) { + const name = this.prepareChannelName(ch) + const programmes = await db.query({ch, start: {'<=': now}, e: {'>': now}}) for(const programme of programmes) { if(programme.e > now && parseInt(programme.start) <= now){ if(typeof(categories[category]) == 'undefined'){ @@ -643,8 +742,8 @@ class EPGManager extends EPGPaginateChannelsList { for(const url in this.epgs) { if(this.epgs[url].state !== 'loaded') continue const db = this.epgs[url].db - for(const channel of db.indexManager.readColumnIndex('channel')) { - const programmes = await db.query({channel, start: {'<=': until}, e: {'>': now}}) + for(const ch of db.indexManager.readColumnIndex('ch')) { + const programmes = await db.query({ch, start: {'<=': until}, e: {'>': now}}) for(const programme of programmes) { const start = programme.start if(programme.e > now && parseInt(start) <= until){ @@ -652,11 +751,11 @@ class EPGManager extends EPGPaginateChannelsList { if(Array.isArray(programme.c)){ for(const c of programme.c) { if(lcCategories.includes(c)){ - if(typeof(results[channel]) == 'undefined'){ - results[channel] = {} + if(typeof(results[ch]) == 'undefined'){ + results[ch] = {} } const row = programme - row.meta = {channel, start, score: 0} + row.meta = {ch, start, score: 0} results.push(row) added = true break @@ -668,7 +767,7 @@ class EPGManager extends EPGPaginateChannelsList { for(const l of lcCategories) { if(lct.includes(l)){ const row = programme - row.meta = {channel, start, score: 0} + row.meta = {ch, start, score: 0} results.push(row) break } @@ -701,11 +800,11 @@ class EPGManager extends EPGPaginateChannelsList { }) const ret = {} for(const row of results) { - if(typeof(ret[row.meta.channel]) == 'undefined'){ - ret[row.meta.channel] = {} + if(typeof(ret[row.meta.ch]) == 'undefined'){ + ret[row.meta.ch] = {} } - ret[row.meta.channel][row.meta.start] = row - delete ret[row.meta.channel][row.meta.start].meta + ret[row.meta.ch][row.meta.start] = row + delete ret[row.meta.ch][row.meta.start].meta } return ret } @@ -737,21 +836,21 @@ class EPGManager extends EPGPaginateChannelsList { return maxData } async get(channel, limit){ - let data if(channel.searchName == '-'){ - data = {} + return [] } else { - const now = time() + const now = time(), query = {e: {'>': now}} + if (limit <= 1) { + query.start = {'<=': now} // will short-up the search + } if(channel.searchName) { for(const url in this.epgs) { if(this.epgs[url].state !== 'loaded') continue const db = this.epgs[url].db - const availables = db.indexManager.readColumnIndex('channel') + const availables = db.indexManager.readColumnIndex('ch') if(availables.has(channel.searchName)){ - return await db.query( - {channel: channel.searchName, e: {'>': now}}, - {orderBy: 'start', limit} - ) + query.ch = channel.searchName + return await db.query(query, {orderBy: 'start', limit}) } } } @@ -759,37 +858,15 @@ class EPGManager extends EPGPaginateChannelsList { for(const url in this.epgs) { if(this.epgs[url].state !== 'loaded') continue const db = this.epgs[url].db - const availables = db.indexManager.readColumnIndex('channel') + const availables = db.indexManager.readColumnIndex('ch') if(n && availables.has(n)){ - return await db.query( - {channel: n, e: {'>': now}}, - {orderBy: 'start', limit} - ) + query.ch = n + return await db.query(query, {orderBy: 'start', limit}) } } } return false } - async getData(){ - return Object.keys(this.epgs).map(url => { - return { - state: this.epgs[url].state, - error: this.epgs[url].error, - length: this.epgs[url].db.length, - } - }) - } - async getTerms(){ - let results = {} - for(const url in this.epgs) { - if(this.epgs[url].state !== 'loaded') continue - for(const name in this.epgs[url].db.index.terms) { - if(results[name] !== undefined) continue - results[name] = this.epgs[url].db.index.terms[name] - } - } - return results - } async getMulti(channelsList, limit){ let results = {} for(const ch of channelsList) { @@ -812,7 +889,7 @@ class EPGManager extends EPGPaginateChannelsList { } data = data.sortByProp('score', true).slice(0, 24) for(const r of data) { - results[r.name] = await this.epgs[r.url].db.query({channel: r.name, end: {'>': time()}}, {limit}) + results[r.name] = await this.epgs[r.url].db.query({ch: r.name, end: {'>': time()}}, {limit}) } return results } @@ -832,26 +909,11 @@ class EPGManager extends EPGPaginateChannelsList { } return results.unique() } - async validateChannelProgramme(channel, start, title){ - const cid = await this.findChannel(channel) - if(cid) { - for(const url in this.epgs) { - if(this.epgs[url].state !== 'loaded') continue - const db = this.epgs[url].db - const programmes = await db.query({channel: cid, start}, {orderBy: 'start asc', limit: 2}) - for(const programme of programmes) { - if(programme.t == title || textSimilarity(programme.t, title) > 0.75) { - return true - } - } - } - } - } async findChannel(data){ for(const url in this.epgs) { if(this.epgs[url].state !== 'loaded') continue const db = this.epgs[url].db - const availables = db.indexManager.readColumnIndex('channel') + const availables = db.indexManager.readColumnIndex('ch') if(data.searchName && data.searchName != '-' && availables.has(data.searchName)){ return data.searchName } else if(data.name && availables.has(data.name)){ @@ -863,6 +925,7 @@ class EPGManager extends EPGPaginateChannelsList { if(this.epgs[url].state !== 'loaded') continue const db = this.epgs[url].db for(const name in db.index.terms) { + if(!Array.isArray(db.index.terms[name])) continue score = listsTools.match(terms, db.index.terms[name], false) if(score && score >= maxScore){ maxScore = score @@ -875,12 +938,11 @@ class EPGManager extends EPGPaginateChannelsList { if(this.epgs[url].state !== 'loaded') continue const db = this.epgs[url].db for(const name in db.index.terms) { - if(Array.isArray(db.index.terms[name])){ - score = listsTools.match(db.index.terms[name], terms, false) - if(score && score >= maxScore){ - maxScore = score - candidates.push({name, score}) - } + if(!Array.isArray(db.index.terms[name])) continue + score = listsTools.match(db.index.terms[name], terms, false) + if(score && score >= maxScore){ + maxScore = score + candidates.push({name, score}) } } } @@ -893,9 +955,9 @@ class EPGManager extends EPGPaginateChannelsList { for(const url in this.epgs) { if(this.epgs[url].state !== 'loaded') continue const db = this.epgs[url].db - const availables = db.indexManager.readColumnIndex('channel') - for(const channel of availables) { - const query = {channel, e: {'>': now}} + const availables = db.indexManager.readColumnIndex('ch') + for(const ch of availables) { + const query = {ch, e: {'>': now}} if(nowLive) query['<='] = now for await (const programme of db.walk(query)) { let t = programme.t @@ -904,15 +966,35 @@ class EPGManager extends EPGPaginateChannelsList { } let pterms = listsTools.terms(t) if(listsTools.match(terms, pterms, true)){ - if(typeof(epgData[channel]) == 'undefined'){ - epgData[channel] = {} + if(typeof(epgData[ch]) == 'undefined'){ + epgData[ch] = {} } - epgData[channel][programme.start] = programme + epgData[ch][programme.start] = programme } } } } return epgData + } + async validateChannels(data) { + const processed = {} + for (const channel in data) { + let err + const cid = await this.findChannel(data[channel].terms || listsTools.terms(channel)) + const live = await this.liveNow(cid).catch(e => console.error(err = e)) + if (!err && live) { + for (const candidate of data[channel].candidates) { + if (live.t == candidate.t) { + processed[channel] = candidate.ch + break + } + } + } + } + return processed + } + async lang(code, timezone) { + return global.lang.countryCode || global.lang.locale } async terminate(){ config.removeListener('change', this.changeListener) diff --git a/www/nodejs/modules/lists/lists.js b/www/nodejs/modules/lists/lists.js index dfe7097b..2ff18598 100644 --- a/www/nodejs/modules/lists/lists.js +++ b/www/nodejs/modules/lists/lists.js @@ -10,7 +10,7 @@ import List from "./list.js"; import Discovery from "../discovery/discovery.js"; import config from "../config/config.js" import { ready } from '../bridge/bridge.js' -import paths from '../paths/paths.js' +import { inWorker } from '../paths/paths.js' import { forwardSlashes, parseCommaDelimitedURIs, LIST_DATA_KEY_MASK } from "../utils/utils.js"; import { getDirname } from 'cross-dirname' import MultiWorker from '../multi-worker/multi-worker.js'; @@ -20,9 +20,11 @@ class ListsEPGTools extends Index { super(opts) this.epgWorker = new MultiWorker() this.epg = this.epgWorker.load(path.join(getDirname(), 'epg-worker.js')) + this.epg.loaded = null this.epg.on('update', async () => { const states = await this.epg.getState() - this.loadedEPGs = states.info.filter(r => r.progress > 99).map(r => r.url) + const loaded = states.info.filter(r => r.progress > 99).map(r => r.url) + this.epg.loaded = loaded.length ? loaded : null this.emit('epg-update', states) }) ready(() => { @@ -48,37 +50,39 @@ class ListsEPGTools extends Index { let data, err let ret = await this.epg.getState().catch(e => err = e) if (err) return ['error', String(err)] - const { progress, state, error } = ret; - if (error) { + const { progress, state, error } = ret + if (error || !this.epg.loaded) { data = [state]; if (state == 'error') { - data.push(error); + data.push(error) } else { - data.push(progress); + data.push(progress) } - } else if (!this.epg) { // unset in the meantime - data = ['error', 'no epg'] } else { if (Array.isArray(channelsList)) { - channelsList = channelsList.map(c => this.tools.applySearchRedirectsOnObject(c)); - data = await this.epg.getMulti(channelsList, limit); + channelsList = channelsList.map(c => this.tools.applySearchRedirectsOnObject(c)) + data = await this.epg.getMulti(channelsList, limit) } else { - channelsList = this.tools.applySearchRedirectsOnObject(channelsList); - data = await this.epg.get(channelsList, limit); + channelsList = this.tools.applySearchRedirectsOnObject(channelsList) + data = await this.epg.get(channelsList, limit) } } - return data; + return data } async epgSearch(terms, nowLive) { + if (!this.epg.loaded) return [] return await this.epg.search(this.tools.applySearchRedirects(terms), nowLive); } async epgSearchChannel(terms, limit) { + if (!this.epg.loaded) return {} return await this.epg.searchChannel(this.tools.applySearchRedirects(terms), limit); } async epgSearchChannelIcon(terms) { + if (!this.epg.loaded) return [] return await this.epg.searchChannelIcon(this.tools.applySearchRedirects(terms)); } async epgLiveNowChannelsList() { + if (!this.epg.loaded) return {categories: {}} let data = await this.epg.liveNowChannelsList() if (data && data['categories'] && Object.keys(data['categories']).length) { let currentScore = this.epgChannelsListSanityScore(data['categories']); @@ -117,67 +121,6 @@ class ListsEPGTools extends Index { throw 'failed'; } } - async epgChannelsTermsList() { - if (!this.epg) { - throw 'no epg'; - } - let data = await this.epg.getTerms(); - if (data && Object.keys(data).length) { - return data; - } else { - throw 'failed'; - } - } -} -class Lists extends ListsEPGTools { - constructor(opts) { - super(opts) - if(paths.inWorker) throw new Error('Lists cannot be used in a worker') - this.setMaxListeners(256) - this.debug = false - this.lists = {} - this.activeLists = { - my: [], - community: [], - length: 0 - }; - this.epgs = {} - this.myLists = []; - this.communityLists = []; - this.processedLists = new Map(); - this.requesting = {}; - this.loadTimes = {}; - this.processes = []; - this.satisfied = false; - this.isFirstRun = !config.get('communitary-mode-lists-amount') && !config.get('lists').length; - this.queue = new PQueue({concurrency: 4}); - config.on('change', keys => { - keys.includes('lists') && this.configChanged(); - }); - ready(async () => { - global.channels.on('channel-grid-updated', keys => { - this._relevantKeywords = null - }) - }); - this.on('satisfied', () => { - if (this.activeLists.length) { - this.queue._concurrency = 1; // try to change pqueue concurrency dinamically - } - }); - this.discovery = new Discovery(this) - this.loader = new Loader(this) - this.manager = new Manager(this) - this.configChanged() - } - ready() { - return new Promise(resolve => { - if(this.isReady) { - resolve() - } else { - this.once('ready', resolve) - } - }) - } epgScore(url) { if(this.epgScoreCache[url] !== undefined) { return this.epgScoreCache[url] @@ -225,6 +168,14 @@ class Lists extends ListsEPGTools { await this.resetEPGScoreCache() } let epgs = Object.keys(this.epgs) + if (this.epg.loaded) { + epgs.push(...this.epg.loaded.filter(u => !epgs.includes(u))) + } + + const c = config.get('epg-'+ lang.locale) + if (Array.isArray(c) && c.length) { + epgs.push(...c.filter(e => e.active && !epgs.includes(e.url)).map(e => e.url)) + } if (!epgs.length) return [] // Precompute scores to avoid multiple scorify calls @@ -247,6 +198,56 @@ class Lists extends ListsEPGTools { } return result } +} +class Lists extends ListsEPGTools { + constructor(opts) { + super(opts) + if(inWorker) throw new Error('Lists cannot be used in a worker') + this.setMaxListeners(256) + this.debug = false + this.lists = {} + this.activeLists = { + my: [], + community: [], + length: 0 + }; + this.epgs = {} + this.myLists = []; + this.communityLists = []; + this.processedLists = new Map(); + this.requesting = {}; + this.loadTimes = {}; + this.processes = []; + this.satisfied = false; + this.isFirstRun = !config.get('communitary-mode-lists-amount') && !config.get('lists').length; + this.queue = new PQueue({concurrency: 4}); + config.on('change', keys => { + keys.includes('lists') && this.configChanged(); + }); + ready(async () => { + global.channels.on('channel-grid-updated', keys => { + this._relevantKeywords = null + }) + }); + this.on('satisfied', () => { + if (this.activeLists.length) { + this.queue._concurrency = 1; // try to change pqueue concurrency dinamically + } + }); + this.discovery = new Discovery(this) + this.loader = new Loader(this) + this.manager = new Manager(this) + this.configChanged() + } + ready() { + return new Promise(resolve => { + if(this.isReady) { + resolve() + } else { + this.once('ready', resolve) + } + }) + } getAuthURL(listUrl) { if (listUrl && this.lists[listUrl] && this.lists[listUrl].index && this.lists[listUrl].index.meta && this.lists[listUrl].index.meta['auth-url']) { return this.lists[listUrl].index.meta['auth-url']; @@ -924,9 +925,8 @@ class Lists extends ListsEPGTools { return list.slice(0); // clone it to not alter cache } } -if(paths.inWorker) { +if(inWorker) { console.error('!!!!!!! LISTS ON WORKER '+ global.file) - console.error(JSON.stringify(paths.workerData)) - console.error(JSON.stringify(paths.inWorker)) + console.error(JSON.stringify(inWorker)) } export default new Lists(); diff --git a/www/nodejs/modules/lists/manager.js b/www/nodejs/modules/lists/manager.js index 461bc8a8..b7674be4 100644 --- a/www/nodejs/modules/lists/manager.js +++ b/www/nodejs/modules/lists/manager.js @@ -10,7 +10,7 @@ import downloads from "../downloads/downloads.js"; import Mag from "./mag.js"; import { EventEmitter } from "events"; import { promises as fsp } from "fs"; -import { basename, clone, forwardSlashes, getDomain, insertEntry, kfmt, LIST_DATA_KEY_MASK, listNameFromURL, parseCommaDelimitedURIs, validateURL } from "../utils/utils.js"; +import { basename, clone, forwardSlashes, getDomain, insertEntry, kfmt, LIST_DATA_KEY_MASK, listNameFromURL, parseCommaDelimitedURIs, validateURL, ucWords } from "../utils/utils.js"; import config from "../config/config.js" import renderer from '../bridge/bridge.js' import paths from '../paths/paths.js' @@ -44,7 +44,10 @@ class ManagerEPG extends EventEmitter { const hash = entries.map(e => e.name + e.details + e.value).join('') if (hash !== currentHash) { currentHash = hash - menu.render(entries, this.epgSelectionPath(), global.channels ? global.channels.epgIcon : '') + menu.render(entries, this.epgSelectionPath(), { + icon: global.channels ? global.channels.epgIcon : '', + filter: true + }) } } this.epgStatusTimer = setTimeout(listener, 1000) @@ -67,10 +70,12 @@ class ManagerEPG extends EventEmitter { return lang.EPG + '/' + lang.SELECT } inEPGSelectionPath(path) { - return path.includes(lang.EPG + '/' + lang.SELECT) + if (path.startsWith(lang.EPG) || path.startsWith(lang.MY_LISTS)) { + return path.includes(lang.EPG + '/' + lang.SELECT) + } } EPGs(activeOnly=false, urlsOnly=false) { - let ret = [], activeEPG = config.get('epg-' + lang.locale) + let ret = [], activeEPG = config.get('epg-'+ lang.locale) if(activeEPG && activeEPG !== 'disabled') { if(Array.isArray(activeEPG)) { ret = activeEPG @@ -115,10 +120,10 @@ class ManagerEPG extends EventEmitter { return { name, type: 'check', - action: (e, checked) => { + action: (e, isChecked) => { const data = this.EPGs() const has = data.findIndex(r => r.url == url) - if(checked) { + if(isChecked) { if(has === -1) { data.push({url, active: true}) this.epgShowLoading(url).catch(console.error) @@ -697,7 +702,7 @@ class Manager extends ManagerEPG { } showUpdateProgress(m, fa, duration) { this.updateProgressVisible = true - osd.show(m, fa, duration) + osd.show(m, fa, 'update-progress', duration) } hideUpdateProgress() { if(!this.updateProgressVisible) return diff --git a/www/nodejs/modules/lists/test.mjs b/www/nodejs/modules/lists/test.mjs index 082b9419..35db9c7e 100644 --- a/www/nodejs/modules/lists/test.mjs +++ b/www/nodejs/modules/lists/test.mjs @@ -10,6 +10,7 @@ async function runTests() { // Teste 1: Criar uma instância de EPG const url = "http://app.megacubo.net/stats/data/epg.br.xml.gz"; + //const url = "https://epg.pw/xmltv/epg_BR.xml.gz" /* const epg = new EPG(url); @@ -26,16 +27,10 @@ async function runTests() { const epgManager = global.epgManager = new EPGManager(); console.assert(epgManager.epgs instanceof Object, "Teste 3 falhou: epgs não é um objeto"); - // Teste 4: Adicionar um EPG ao EPGManager - epgManager.add(url); - console.assert(epgManager.epgs[url] instanceof EPG, "Teste 4 falhou: EPG não foi adicionado ao EPGManager"); - - // Teste 5: Remover um EPG - epgManager.remove(url); console.assert(epgManager.epgs[url] === undefined, "Teste 5 falhou: EPG não foi removido do EPGManager"); // Teste 6: Verificar o estado do EPGManager - epgManager.add(url); + await epgManager.add(url); epgManager.ready().then(() => { console.assert(epgManager.epgs[url].loaded === true, "Teste 6 falhou: EPG não está carregado"); }).catch(e => console.error("Teste 6 falhou:", e)); diff --git a/www/nodejs/modules/menu/menu.js b/www/nodejs/modules/menu/menu.js index 9c569ff9..dfb01f74 100644 --- a/www/nodejs/modules/menu/menu.js +++ b/www/nodejs/modules/menu/menu.js @@ -6,7 +6,7 @@ import Limiter from '../limiter/limiter.js' import mega from '../mega/mega.js' import config from '../config/config.js' import renderer from '../bridge/bridge.js' -import paths from '../paths/paths.js' +import { inWorker } from '../paths/paths.js' class Menu extends EventEmitter { constructor(opts) { @@ -61,50 +61,43 @@ class Menu extends EventEmitter { }) }) renderer.ui.on('menu-open', async (path, tabindex) => { - const busy = this.setBusy() + const busy = this.setBusy(path, tabindex) this.opts.debug && console.log('menu-open', path, tabindex) await this.open(path, tabindex).catch(e => this.displayErr(e)) busy.release() }) renderer.ui.on('menu-action', async (path, tabindex) => { - const busy = this.setBusy() + const busy = this.setBusy(path, tabindex) await this.action(path, tabindex).catch(e => this.displayErr(e)) busy.release() }) renderer.ui.on('menu-back', async () => { - const busy = this.setBusy() + const busy = this.setBusy(this.dirname(this.path)) await this.back().catch(e => this.displayErr(e)) busy.release() }) renderer.ui.on('menu-check', async (path, val) => { - const busy = this.setBusy() + const busy = this.setBusy(path) await this.check(path, val) busy.release() }) renderer.ui.on('menu-input', async (path, val) => { - const busy = this.setBusy() + const busy = this.setBusy(path) this.opts.debug && console.log('menu-input', path, val) await this.input(path, val) busy.release() }) renderer.ui.on('menu-select', async (path, tabindex) => { - const busy = this.setBusy() + const busy = this.setBusy(path, tabindex) await this.select(path, tabindex).catch(e => this.displayErr(e)) busy.release() }) - this.applyFilters(this.pages[this.path], this.path).then(es => { - this.pages[this.path] = es - if (this.waitingRender) { - this.waitingRender = false - this.render(this.pages[this.path], this.path, 'fas fa-home') - } - }).catch(e => this.displayErr(e)) } - setBusy() { + setBusy(path) { const uid = 'busy-' + Date.now() - if(typeof(this.busies) == 'undefined') this.busies = new Set() - this.busies.size || renderer.ui.emit('menu-busy', true) - this.busies.add(uid) + if(typeof(this.busies) == 'undefined') this.busies = new Map() + this.busies.set(uid, path) + renderer.ui.emit('menu-busy', Array.from(this.busies.values())) return { release: () => { this.busies.delete(uid) @@ -173,11 +166,7 @@ class Menu extends EventEmitter { } } start() { - if (typeof(this.pages[this.path]) != 'undefined') { - this.render(this.pages[this.path], this.path, 'fas fa-home') - } else { - this.waitingRender = true - } + this.open('').catch(e => this.displayErr(e)) } refresh(deep=false, p) { if (typeof(p) != 'string') p = this.path @@ -219,8 +208,11 @@ class Menu extends EventEmitter { deepRefresh(p) { if (typeof(p) != 'string') p = this.path if (p != this.path || !this.rendering) return - this.deepRead(p).then(ret => { - this.render(ret.entries, p, (ret.parent ? ret.fa : '') || 'fas fa-box-open') + this.deepRead(p).then(async ret => { + await this.render(ret.entries, p, { + parent: ret.parent, + icon: (ret.parent ? ret.fa : '') || 'fas fa-box-open' + }) }).catch(e => this.displayErr(e)) } inSelect() { @@ -299,7 +291,7 @@ class Menu extends EventEmitter { entries[i].value = entries[i].value() } } - this.opts.debug && console.log('Menu filtering DONE* ', !!paths.inWorker) + this.opts.debug && console.log('Menu filtering DONE* ', !!inWorker) } return entries || [] } @@ -398,18 +390,6 @@ class Menu extends EventEmitter { } } } - setLoading(state) { - if(state) { - if(!this.loadingEntriesBusyLock) { - this.loadingEntriesBusyLock = this.setBusy() - } - } else { - if(this.loadingEntriesBusyLock) { - this.loadingEntriesBusyLock.release() - this.loadingEntriesBusyLock = null - } - } - } dirname(path) { let i = String(path).lastIndexOf('/') if (i <= 0) { @@ -555,7 +535,7 @@ class Menu extends EventEmitter { } es = this.addMetaEntries(es, destPath, parentPath) this.pages[this.path] = es - await this.render(this.pages[this.path], this.path, parentEntry) + await this.render(this.pages[this.path], this.path, {parent: parentEntry}) return true } if (name) { @@ -597,7 +577,7 @@ class Menu extends EventEmitter { } } else { this.path = destPath - await this.render(this.pages[this.path], this.path, parentEntry) + await this.render(this.pages[this.path], this.path, {parent: parentEntry}) return true } } @@ -742,11 +722,14 @@ class Menu extends EventEmitter { }) return nentries } - render(es, path, parentEntryOrIcon, backTo) { + async render(es, path, opts={}) { if (this.opts.debug) { - console.log('render', es, path, parentEntryOrIcon, backTo) + console.log('render', es, path, opts) } if (Array.isArray(es)) { + if(opts.filter === true) { + es = await this.applyFilters(es, path) + } for (let i = 0; i < es.length; i++) { if (!es[i].type) { es[i].type = 'stream' @@ -756,17 +739,16 @@ class Menu extends EventEmitter { } } this.currentEntries = es.slice(0) - this.currentEntries = this.addMetaEntries(this.currentEntries, path, backTo) + this.currentEntries = this.addMetaEntries(this.currentEntries, path, opts.backTo) this.pages[path] = this.currentEntries.slice(0) this.currentEntries = this.cleanEntries(this.currentEntries, 'renderer,entries,action') - if (path && this.path != path) - this.path = path - } - if (this.rendering) { - const icon = typeof(parentEntryOrIcon) == 'string' ? parentEntryOrIcon : (parentEntryOrIcon ? parentEntryOrIcon.fa : 'fas fa-home') - renderer.ui.emit('render', this.cleanEntries(this.checkFlags(this.currentEntries), 'checked,users,terms'), path, icon) - this.emit('render', this.currentEntries, path, parentEntryOrIcon, backTo) - this.syncPages() + if (typeof(path) === 'string') this.path = path + if (this.rendering) { + const icon = opts.icon || opts?.parent?.fa || 'fas fa-home' + renderer.ui.emit('render', this.cleanEntries(this.checkFlags(this.currentEntries), 'checked,users,terms'), path, icon) + this.emit('render', this.currentEntries, path) + this.syncPages() + } } } suspendRendering() { @@ -800,4 +782,4 @@ class Menu extends EventEmitter { } } -export default (global.menu || (paths.inWorker ? {} : (global.menu = new Menu({})))) +export default (global.menu || (inWorker ? {} : (global.menu = new Menu({})))) diff --git a/www/nodejs/modules/menu/renderer.js b/www/nodejs/modules/menu/renderer.js index 5dfa9b34..d6927298 100644 --- a/www/nodejs/modules/menu/renderer.js +++ b/www/nodejs/modules/menu/renderer.js @@ -1,7 +1,7 @@ import { EventEmitter } from 'events' import { Sounds } from './sound' import { main } from '../bridge/renderer' -import { css } from '../../renderer/src/scripts/utils' +import { traceback } from '../../renderer/src/scripts/utils' class MenuURLInputHelper { constructor(){ @@ -102,6 +102,16 @@ class MenuIcons extends MenuBase { let changed const isCover = !data.alpha + + if(!data.force) { + if(this.icons[fullPath].cover === false && isCover) { // prefer alpha icons + return + } + if(this.icons[fullPath].cover === isCover) { // ignore updates with same type + return + } + } + if(this.icons[fullPath].cover != isCover) { this.icons[fullPath].cover = isCover changed = true @@ -114,7 +124,7 @@ class MenuIcons extends MenuBase { if(changed) { let entries = this.currentEntries.map(e => this.prepareEntry(e)) this.applyCurrentEntries(entries) - this.uiUpdate(false) + this.emit('updated') } }) } @@ -123,72 +133,75 @@ class MenuIcons extends MenuBase { class MenuSelectionMemory extends MenuIcons { constructor(container){ super(container) - this.selectionMemory = {} - this.on('open', () => this.saveSelection()) - this.on('focus', () => this.saveSelection()) - this.on('scroll', () => this.saveSelection()) - this.on('pos-modal-end', this.restoreSelection.bind(this)) + this.selectionMemory = { + '': { + default: {scroll: 0, index: 0} + } + } + this.on('open', () => this.save()) + this.on('focus', () => this.save()) main.on('current-search', (terms, type) => { this.currentSearch = JSON.stringify({terms, type}) }) + this.once('render', () => { + this.scrollTop(0) + this.save() + this.on('scroll', () => this.selected(true)) + }) } - saveSelection(scrollTop) { + save(scrollTop) { if(this.rendering) return - this.debug && console.error('saveSelection', this.wrap.scrollTop, this.selectedIndex, this.path) - const n = this.currentViewName() + this.debug && console.error('save', this.wrap.scrollTop, this.selectedIndex, this.path) + const { level } = this.activeSpatialNavigationLayout() if(!this.selectionMemory[this.path]) this.selectionMemory[this.path] = {} - this.selectionMemory[this.path][n] = { - scroll: n == 'default' ? (typeof(scrollTop) == 'number' ? scrollTop : this.wrap.scrollTop) : 0, + this.selected(true) // force a entry to be selected in current scroll view + this.selectionMemory[this.path][level] = { + scroll: level == 'default' ? (typeof(scrollTop) == 'number' ? scrollTop : this.wrap.scrollTop) : 0, index: this.selectedIndex, search: this.currentSearch } } - currentViewName() { - return this.activeSpatialNavigationLayout().level - } - restoreSelection(){ + reset(){ if(this.rendering) return - this.debug && console.log('restoreSelection', this.path) - const n = this.currentViewName() + this.debug && console.log('reset', this.path) const selected = this.selected() if(selected && selected.id == 'menu-search') return - let data = {scroll: 0, index: this.path ? 1 : 0} + const { level } = this.activeSpatialNavigationLayout() + const i = (level == 'default' && this.path) ? 1 : 0 + let remembered, data = {scroll: 0, index: i} const selectables = this.selectables() - if(typeof(this.selectionMemory[this.path]) != 'undefined' && this.selectionMemory[this.path][n]) { + if(typeof(this.selectionMemory[this.path]) != 'undefined' && this.selectionMemory[this.path][level]) { const inSearch = this.path.includes(main.lang.SEARCH) || this.path.includes(main.lang.MORE_RESULTS) || this.path.includes(main.lang.SEARCH_MORE) - const inSameSearch = inSearch && this.currentSearch == this.selectionMemory[this.path][n].search + const inSameSearch = inSearch && this.currentSearch == this.selectionMemory[this.path][level].search if(!inSearch || inSameSearch) { - data = this.selectionMemory[this.path][n] - if(data.index == 0 && this.path){ - data.index = 1 - } + remembered = true + data = this.selectionMemory[this.path][level] } } let ret - if(this.activeSpatialNavigationLayout().level == 'default' && this.currentElements[data.index]){ - let range = this.viewportRange(data.scroll) + if(level == 'default' && this.currentElements[data.index] && remembered) { + const range = this.viewportRange(data.scroll) if(data.index < range.start || data.index >= range.end) { data.index = range.start } this.focus(this.currentElements[data.index], true) const start = this.wrap.scrollTop, end = this.wrap.scrollTop + this.wrap.clientHeight if(data.scroll < start || data.scroll > end) { - this.scrollTop(data.scroll, true) + this.scrollTop(data.scroll) } ret = true - } else { - selectables.includes(selected) || this.reset() + } else if(!selectables.includes(selected)) { + this.focus(selectables[i]) } return ret } - scrollTop(y, raw){ - if(typeof(y) == 'number') { + scrollTop(y, animate){ + if(typeof(y) == 'number' && this.wrap.scrollTop != y) { this.wrap.scroll({ top: y, left: 0, - behavior: raw ? 'instant' : 'smooth' + behavior: animate ? 'smooth' : 'instant' }) - this.saveSelection(y) } return this.wrap.scrollTop } @@ -207,7 +220,8 @@ class MenuSpatialNavigation extends MenuSelectionMemory { if(this.rendering) return setTimeout(scrollEndTrigger, 100) if(this.lastScrollTop !== this.wrap.scrollTop){ this.debug && console.log('menu.scroll', this.rendering, this.wrap.scrollTop) - this.lastScrollTop = this.wrap.scrollTop + this.lastScrollTop = this.wrap.scrollTop + this.updateRange(this.wrap.scrollTop) this.emit('scroll', this.wrap.scrollTop) } } @@ -225,11 +239,7 @@ class MenuSpatialNavigation extends MenuSelectionMemory { window.addEventListener('resize', resizeListener, { capture: true }) window.addEventListener('orientationchange', resizeListener, { capture: true }) screen.orientation && screen.orientation.addEventListener('change', resizeListener) - this.once('render', this.adjustIconSize.bind(this)) - setTimeout(() => { - this.setGridLayout(4, 3, 1, 8) - resizeListener() // to apply initial icons size - }, 0) + setTimeout(resizeListener, 0) } setGridLayout(x, y, px, py){ this._gridLayoutX = x @@ -253,44 +263,13 @@ class MenuSpatialNavigation extends MenuSelectionMemory { const verticalLayout = main.config['view-size'][portrait ? 'portrait' : 'landscape'].x == 1 document.body.classList[wide ? 'add' : 'remove']('menu-wide') document.body.classList[verticalLayout ? 'add' : 'remove']('portrait') - this.adjustIconSize() this.sideMenuSync(true) - window.capacitor && plugins.megacubo.updateScreenMetrics() - } - adjustIconSize(){ - let e = document.querySelector('a:not(.entry-2x) .entry-icon-image') - if(e){ - let metrics = e.getBoundingClientRect() - if(metrics && metrics.width){ - let min = Math.min(metrics.width, metrics.height) * 0.85 - css(` - - #menu content a .entry-icon-image i { - font-size: ${min}px; - line-height: ${metrics.height}px; - min-height: ${metrics.height}px; - } - - `, 'entry-icon-i') - } - } } isVisible(e) { return e.offsetParent !== null } selector(s){ - return Array.from(document.querySelectorAll(s)).filter(this.isVisible) - } - start(){ - document.body.addEventListener('focus', e => { // use addEventListener instead of on() here for capturing - setTimeout(() => { - if (document.activeElement == document.body) { - this.debug && console.log('body focus, menu.reset', e) - this.reset() - } - }, 100) - }, { passive: true }) - this.reset() + return [...document.querySelectorAll(s)].filter(this.isVisible) } updateElement(element){ // if layout has changed, find the actual corresponding element if(!element.parentNode){ @@ -327,65 +306,31 @@ class MenuSpatialNavigation extends MenuSelectionMemory { element.parentNode && element.parentNode.classList && element.parentNode.classList.add(this.parentClassName) } } - findSelected(deep){ - let elements, element = document.activeElement - if(element == document.body) { - element = document.querySelector('.'+ this.className) - } - if(!deep && element) return element - if(element && element != document.body){ // not explicitly selected - elements = this.selectables() // check if is any explicitly selected?? - let selected = elements.filter(e => e.classList.contains(this.className)) - if(selected.length){ - element = selected[0] // yes, that's one explicitly selected - } else { - element = this.updateElement(element) // find this element in current layout, if it's not - if(!element.parentNode || !elements.includes(element)){ // not found, we'll reset so - element = false - } - } - } - if(!element || element == document || element == document.body){ - if(typeof(elements) == 'undefined'){ - elements = this.selectables() - } - let selected - if(elements.length){ - try { // $.filter was triggering errors on some TV boxes, somehow - selected = elements.filter(e => e.classList.contains(this.className)) - } catch(e) {} - } - if(selected && selected.length){ - element = selected[0] // yes, that's one explicitly selected - } else { - element = elements[0] - } - } + selected(force){ + const selectables = this.selectables() // check if is any explicitly selected?? + let element = selectables.find(e => (e.classList.contains(this.className) || e == document.activeElement)) if(element && element.id == 'menu-omni-input') { element = element.parentNode + } else if(!element) { + const { level } = this.activeSpatialNavigationLayout() + if(this.selectionMemory[this.path] && this.selectionMemory[this.path][level]) { + const { index } = this.selectionMemory[this.path][level] + element = selectables.find(e => e.tabIndex == index) + } + if(!element) { + if (level !== 'default' || force) { + const i = (level == 'default' && this.path && !this.wrap.scrollTop) ? 1 : 0 + element = selectables[i] + } + } } - if(this.debug){ - console.log('findSelected', element, elements) - } - return element - } - selected(deep){ - let element = this.findSelected(deep) - if(element){ - element.classList.contains(this.className) || this.updateSelectedElementClasses(element) - element.focus({preventScroll: true}) - } + element && this.focus(element) return element } focus(a, preventScroll){ - if(this.rendering) return - let ret = null this.debug && console.error('focus', a, this.wrap.scrollTop) - if(!a) { - a = this.entries().shift() - } - const currentLayer = this.activeSpatialNavigationLayout().level - if(a && (a != document.querySelector('.'+ this.className))) { + if(!a) return + if(!a.classList.contains(this.className)) { if(this.debug){ console.log('FOCUSENTRY', a) } @@ -401,14 +346,13 @@ class MenuSpatialNavigation extends MenuSelectionMemory { } else { a.focus({preventScroll: true}) } - let n = a.querySelector('input:not([type="range"]), textarea') - n && n.focus({preventScroll: true}) preventScroll || a.scrollIntoViewIfNeeded({behavior: 'auto', block: 'nearest', inline: 'nearest'}) + const n = a.querySelector('input:not([type="range"]), textarea') + n && n.focus({preventScroll: true}) this.emit('focus', a, index) } else { - this.debug && console.log('Already focused', {currentLayer, a, selected: document.querySelector('.'+ this.className), scrollLeft: this.container.scrollLeft, scrollTop: this.wrap.scrollTop}) + this.debug && console.log('Already focused', {a, selected: document.querySelector('.'+ this.className), scrollLeft: this.container.scrollLeft, scrollTop: this.wrap.scrollTop}) } - return ret } activeSpatialNavigationLayout(){ let ret = {selector: 'body', level: 'default'} // placeholder while no views are added @@ -423,25 +367,6 @@ class MenuSpatialNavigation extends MenuSelectionMemory { }) return ret } - reset(){ - if(this.debug){ - console.log('reset') - } - let elements, layout = this.activeSpatialNavigationLayout() - if(typeof(layout.resetSelector) == 'function'){ - elements = layout.resetSelector() - } else { - elements = this.entries() - } - if(elements.length && !elements.includes(this.selected())){ - elements = elements.filter(e => e.getAttribute('data-type') != 'back').slice(0) - if(elements.length){ - let _sound = this.sounds - this.focus(elements[0]) - this.sounds = _sound - } - } - } entries(noAsides){ let e = [], layout = this.activeSpatialNavigationLayout(), sel = layout.selector if(typeof(sel)=='function'){ @@ -738,7 +663,7 @@ class MenuModal extends MenuBBCode { document.body.classList.add('modal') mandatory && document.body.classList.add('modal-mandatory') this.inputHelper.start() - this.reset() + this.emit('modal-start') } endModal(cancel){ if(this.inModal()){ @@ -792,20 +717,17 @@ class MenuPlayer extends MenuModal { isExploring(){ return !this.inModal() && (!this.inPlayer() || document.body.classList.contains('menu-playing')) } - showWhilePlaying(enable, ignoreFocus) { + showWhilePlaying(enable) { if (enable) { - main.emit('menu-playing', true) - document.body.classList.add('menu-playing') - if (!ignoreFocus) { - setTimeout(() => { - this.restoreSelection() || this.reset() - }, 100) + if(!document.body.classList.contains('menu-playing')) { + document.body.classList.add('menu-playing') + this.emit('menu-playing', true) } } else { - main.emit('menu-playing', false) - document.body.classList.remove('menu-playing') - main.idle.reset() - main.idle.lock(0.1) + if(document.body.classList.contains('menu-playing')) { + document.body.classList.remove('menu-playing') + this.emit('menu-playing', false) + } } } } @@ -1117,7 +1039,7 @@ class MenuDialog extends MenuDialogQueue { } } else if(e.template == 'message') { if(e.text.includes(' { + m.querySelectorAll('.modal-template-message i').forEach(s => { s.parentNode.style.display = 'block' }) } @@ -1302,13 +1224,9 @@ class MenuPrompt extends MenuOpenFile { } if(ret !== false){ this.endModal() - this.emit('prompt-end', id) } }, 'submit') this.inputHelper.stop() - - this.emit('prompt-start') - p = this.modalContent.querySelector('#modal-template-option-submit') if(p){ p.addEventListener('keypress', (event) => { @@ -1545,7 +1463,7 @@ class MenuStatusFlags extends MenuSlider { } processStatusFlags(){ this.currentEntries.map(e => this.statusAddHTML(e)) - this.uiUpdate(false) + this.emit('updated') } } @@ -1582,7 +1500,7 @@ class MenuNav extends MenuStatusFlags { const n = document.body.classList.contains('side-menu') if(c != n) { document.body.classList[c ? 'add' : 'remove']('side-menu') - this.selected(true) // update current selection + this.emit('side-menu', c) } } sideMenu(enable, behavior='smooth') { @@ -1618,19 +1536,6 @@ export class Menu extends MenuNav { console.error(e) } console.log('menu init') - main.on('menu-playing', () => { - if(!document.body.classList.contains('menu-playing')){ - document.body.classList.add('menu-playing') - main.emit('menu-playing', true) - setTimeout(() => this.reset(), 100) - } - }) - main.on('menu-playing-close', () => { - if(document.body.classList.contains('menu-playing')){ - document.body.classList.remove('menu-playing') - main.emit('menu-playing', false) - } - }) main.on('render', (entries, path, icon) => { this.render(entries, path, icon) }) @@ -1650,7 +1555,6 @@ export class Menu extends MenuNav { this.currentEntries = [] this.currentElements = [] this.range = {start: 0, end: 99} - this.ranging = false main.on('trigger', data => { if(this.debug){ console.warn('TRIGGER', data) @@ -1660,9 +1564,18 @@ export class Menu extends MenuNav { }) }) main.on('menu-busy', state => { - this.busy = state + this.busy = state !== false document.querySelector('.menu-busy').style.display = this.busy ? 'flex' : 'none' document.querySelector('.menu-time time').style.display = this.busy ? 'none' : 'flex' + if(state) { + for(const path of state) { + this.get({path}).forEach(e => { + e.classList.add('entry-busy') + }) + } + } else { + this.wrap.querySelectorAll('.entry-busy').forEach(e => e.classList.remove('entry-busy')) + } }) console.log('menu init') } @@ -1691,41 +1604,48 @@ export class Menu extends MenuNav { return diff } render(entries, path, icon){ - this.debug && console.log('menu render', path, icon, this.wrap.scrollTop) + this.debug && console.log('menu render1', path, this.wrap.scrollTop) this.rendering = true - let prevPath = this.path, navigated = path == this.path + let prevPath = this.path, navigated = path !== this.path + this.debug && console.log('menu render2', path, this.wrap.scrollTop) entries = entries.map(e => this.prepareEntry(e)) let changed = this.applyCurrentEntries(entries) + this.debug && console.log('menu render3', path, this.wrap.scrollTop) this.emit('pre-render', path, this.path) this.path = path - changed && this.uiUpdate(navigated, true) + this.debug && console.log('menu render4', path, this.wrap.scrollTop) + let scrollTop = this.wrap.scrollTop + if (changed) { + if(navigated) { + scrollTop = this.selectionMemory?.[this.path]?.default?.scroll || 0 + this.debug && console.log('menu render4.5', path, this.wrap.scrollTop) + this.scrollTop(scrollTop) + } + this.emit('updated') + this.updateRange(scrollTop) + } + const unscroll = event => { + if(event) { + event.stopPropagation() + event.preventDefault() + } + if (this.wrap.scrollTop != scrollTop) { + this.wrap.scrollTop = scrollTop + } + } + this.wrap.addEventListener('scroll', unscroll) // lock scrolling during html updates to prevent content jumping + this.debug && console.log('menu render5', path, this.wrap.scrollTop) setTimeout(() => { // wait a bit to truste the browser to render the elements - this.currentElements = Array.from(this.wrap.getElementsByTagName('a')) + this.wrap.removeEventListener('scroll', unscroll) + unscroll() + this.debug && console.log('menu render6', path, this.wrap.scrollTop) + this.currentElements = [...this.wrap.getElementsByTagName('a')] this.has2xEntry = this.currentElements.slice(0, 2).some(e => e.classList.contains('entry-2x')) this.rendering = false - if(navigated) { - this.restoreSelection() // keep it in this timer, or the hell gates will open up! - } else { - this.selected() // force finding and set it as 'selected' if needed - } this.emit('render', this.path, icon, prevPath) - this.debug && console.log('menu rendered', path, icon, this.wrap.scrollTop) + this.debug && console.log('menu rendered7', path, this.wrap.scrollTop) }, 0) } - uiUpdate(navigated, trusted){ - if(trusted !== true && this.rendering) return - let targetScrollTop = 0, path = this.path - if(!navigated){ - targetScrollTop = this.wrap.scrollTop - } else if(typeof(this.selectionMemory[path]) != 'undefined' && this.selectionMemory[path].default) { - targetScrollTop = this.selectionMemory[path].default.scroll - } - this.wrap.style.minHeight = (targetScrollTop + this.wrap.offsetHeight) + 'px' // avoid scrolling - this.emit('updated') - this.scrollTop(targetScrollTop, true) - this.wrap.style.minHeight = 0 - this.getRange(targetScrollTop) - } applyCurrentEntries(entries) { let changed if(this.currentEntries.length > entries.length) { @@ -1782,73 +1702,20 @@ export class Menu extends MenuNav { } return ret } - getRange(targetScrollTop){ + updateRange(targetScrollTop){ if(typeof(targetScrollTop) != 'number'){ targetScrollTop = this.wrap.scrollTop } this.ranging = false - let entries = [], tolerance = this.gridLayoutX, vs = Math.ceil(this.gridLayoutX * this.gridLayoutY), minLengthForRanging = vs + (tolerance * 2), shouldRange = main.config['show-logos'] && this.currentEntries.length >= minLengthForRanging + const tolerance = this.gridLayoutX, vs = Math.ceil(this.gridLayoutX * this.gridLayoutY) + const minLengthForRanging = vs + (tolerance * 2) + const shouldRange = main.config['show-logos'] && this.currentEntries.length >= minLengthForRanging + const prevRange = Object.assign({}, this.range || {}) this.range = this.viewportRange(targetScrollTop) - if(shouldRange){ - let trange = Object.assign({}, this.range) - trange.end += tolerance - if(trange.start >= tolerance){ - trange.start -= tolerance - } - this.currentEntries.forEach((e, i) => { - const lazy = i < trange.start || i > trange.end - entries[i] = Object.assign({lazy}, e) - if(lazy && !this.ranging) { - this.ranging = true - } - }) - } else { - entries = this.currentEntries.slice(0) + if(this.range.start != prevRange.start || this.range.end != prevRange.end) { + main.emit('menu-update-range', this.range, this.path) } - return entries } - updateRange(y){ - if(this.ranging){ - const changed = [], shouldUpdateRange = main.config['show-logos'] && this.currentEntries.length > (this.gridLayoutX * this.gridLayoutY) - if(shouldUpdateRange){ - const rgx = new RegExp(' { - if(!elements[e.tabindex]) return - if(this.debug){ - //console.warn(e.type, type, elements[e.tabindex], e.tabindex, this.selectedIndex) - } - const lazy = e.lazy && elements[e.tabindex].innerHTML.match(rgx) - if(lazy != elements[e.tabindex].getAttribute('data-lazy')){ - elements[e.tabindex].setAttribute('data-lazy', !!lazy) - changed.push(elements[e.tabindex]) - } else { - elements[e.tabindex].setAttribute('data-lazy', false) - } - }) - this.wrap.scrollTop = currentScrolltop // scroll was somehow being changed from function start to this point - main.emit('menu-update-range', this.range, this.path) - if(changed.length){ - this.debug && console.log('updateRange', changed, this.range, this.selectedIndex) - if(this.selectedIndex < this.range.start || this.selectedIndex >= this.range.end){ - this.focus(this.currentElements[this.range.start], true) - } else { - this.focus(this.currentElements[this.selectedIndex], true) - } - } - } - } - } - delayedFocus(element){ - setTimeout(() => { - if(this.debug){ - console.warn('DELAYED FOCUS', element) - } - this.focus(element) - }, 50) - } check(element){ this.sounds.play('switch', 65) const i = element.tabIndex @@ -1860,7 +1727,7 @@ export class Menu extends MenuNav { console.warn('NAVCHK', path, value) } main.emit('menu-check', path, value) - this.uiUpdate(false) + this.emit('updated') } setupSelect(entries, path, fa){ const element = this.wrap.querySelector('[data-path="'+ path.replaceAll('"', '"') +'"]') @@ -1897,8 +1764,10 @@ export class Menu extends MenuNav { element.setAttribute('data-default-value', retPath) } } - this.lastSelectTriggerer && this.delayedFocus(this.lastSelectTriggerer) - this.lastSelectTriggerer = null + this.lastSelectTriggerer && setTimeout(() => { + this.focus(this.lastSelectTriggerer) + this.lastSelectTriggerer = null + }, 50) }) } setupSlider(element){ @@ -1921,16 +1790,15 @@ export class Menu extends MenuNav { this.currentEntries[i].value = value this.prepareEntry(this.currentEntries[i], i) } - this.uiUpdate(false) + this.emit('updated') } setTimeout(() => { console.warn('DELAYED FOCUS ON', i, this.currentElements[i]) this.focus(this.currentElements[i]) - }, 200) + }, 50) }, fa) } open(element){ - this.focus(element) let timeToLock = 3, path = element.getAttribute('data-path'), type = element.getAttribute('data-type'), tabindex = element.tabIndex || 0 if(this.busy) { // multi-click prevention return @@ -1973,6 +1841,7 @@ export class Menu extends MenuNav { action(element){ let type = element.getAttribute('data-type') console.log('action', type, element) + this.focus(element) switch(type){ case 'slider': this.setupSlider(element) diff --git a/www/nodejs/modules/multi-worker/multi-worker.js b/www/nodejs/modules/multi-worker/multi-worker.js index 9c12a7ba..5b67421d 100644 --- a/www/nodejs/modules/multi-worker/multi-worker.js +++ b/www/nodejs/modules/multi-worker/multi-worker.js @@ -18,6 +18,16 @@ const setupConstructor = () => { const workerData = { paths } workerData.paths.android = !!paths.android workerData.bytenode = true + const getLangObject = () => { + const ret = {} + if (typeof(lang) != 'undefined' && typeof(lang.getTexts) == 'function') { + Object.assign(ret, lang.getTexts()) + } + ret.locale = lang.locale + ret.timezone = lang.timezone + ret.countryCode = lang.countryCode + return ret + } class WorkerDriver extends EventEmitter { constructor() { super(); @@ -137,9 +147,14 @@ const setupConstructor = () => { this.configChangeListener = () => { this.worker && this.worker.postMessage({ method: 'configChange', id: 0 }); }; + this.langChangeListener = () => { + const lang = getLangObject() + this.worker && this.worker.postMessage({ method: 'langChange', id: 0, data: lang }); + }; this.storageTouchListener = (key, entry) => { this.worker && this.worker.postMessage({ method: 'storageTouch', entry, key, id: 0 }); }; + lang.on('ready', this.langChangeListener); config.on('change', this.configChangeListener); storage.on('touch', this.storageTouchListener); } @@ -174,6 +189,7 @@ const setupConstructor = () => { console.error(e) } } + this.langChangeListener && lang.removeListener('ready', this.langChangeListener); this.configChangeListener && config.removeListener('change', this.configChangeListener); this.storageTouchListener && storage.removeListener('touch', this.storageTouchListener); this.removeAllListeners() @@ -186,16 +202,8 @@ const setupConstructor = () => { constructor() { super() //let file = paths.cwd +'/modules/multi-worker/worker.mjs' - const file = paths.cwd +'/dist/worker.js' - - if (typeof(lang) != 'undefined' && typeof(lang.getTexts) == 'function') { - workerData.lang = lang.getTexts() - } else { - workerData.lang = {} - } - workerData.lang.locale = lang.locale - workerData.lang.countryCode = lang.countryCode - + const file = paths.cwd +'/dist/worker.js' + workerData.lang = getLangObject() this.worker = new Worker(file, { type: 'commonjs', // (file == distFile ? 'commonjs' : 'module'), workerData // leave stdout/stderr undefined diff --git a/www/nodejs/modules/multi-worker/worker.mjs b/www/nodejs/modules/multi-worker/worker.mjs index 665c4fac..156dec25 100644 --- a/www/nodejs/modules/multi-worker/worker.mjs +++ b/www/nodejs/modules/multi-worker/worker.mjs @@ -1,4 +1,4 @@ -import '../utils/utils.js' +import { moment } from '../utils/utils.js' import utilsSetup from './utils.js' import config from '../config/config.js' import storage from '../storage/storage.js' @@ -17,6 +17,9 @@ global.config = config global.storage = storage global.crashlog = crashlog +lang.timezone && moment.tz.setDefault(lang.timezone.name) +lang.locale && moment.locale([lang.locale +'-'+ lang.countryCode, lang.locale]) + process.on('warning', e => console.warn(e, e.stack)) process.on('unhandledRejection', (reason, promise) => { const msg = 'Unhandled Rejection at: '+ String(promise)+ ', reason: '+ String(reason) + ' | ' + JSON.stringify(reason.stack) @@ -47,6 +50,10 @@ parentPort.on('message', msg => { config.removeListener('change', changeListener) config.reload(msg.args) config.on('change', changeListener) + } else if(msg.method == 'langChange'){ + global.lang = msg.data + global.lang.timezone && moment.tz.setDefault(global.lang.timezone.name) + global.lang.locale && moment.locale([global.lang.locale +'-'+ global.lang.countryCode, global.lang.locale]) } else if(msg.method == 'storageTouch'){ const changed = storage.validateTouchSync(msg.key, msg.entry) if (changed && changed.length) { diff --git a/www/nodejs/modules/omni/renderer.js b/www/nodejs/modules/omni/renderer.js index d72a8e36..cb12f189 100644 --- a/www/nodejs/modules/omni/renderer.js +++ b/www/nodejs/modules/omni/renderer.js @@ -35,8 +35,10 @@ export class OMNI extends OMNIUtils { this.button = document.querySelector('.menu-omni .menu-omni-submit') this.input = document.querySelector('.menu-omni input') this.rinput = this.input + this.visible = false this.setup() - this.bind() + this.bind() + this.element.style.display = 'none' document.addEventListener('keyup', this.eventHandler.bind(this)) } bind(){ @@ -54,20 +56,38 @@ export class OMNI extends OMNIUtils { return this.element.offsetParent !== null } show(focus) { - main.menu.sideMenu(false, 'instant') + if(this.visible) return + this.emit('before-show') + this.visible = true this.element.style.display = 'inline-flex' + if(window.innerHeight > window.innerWidth) { + document.body.classList.add('portrait-search') + this.input.addEventListener('blur', () => { + document.body.classList.remove('portrait-search') + }, { once: true }) + } + this.emit('show') focus && this.focus(true) } - showPortrait() { - document.body.classList.add('portrait-search') - this.focus(true) + focus(select){ + this.input.value = this.defaultValue + if(select){ + this.input.select() + } + this.input.focus() this.input.addEventListener('blur', () => { - document.body.classList.remove('portrait-search') + this.save() + this.hide() }, { once: true }) + if(!select) { // as last, move to the end + this.rinput.selectionStart = this.rinput.selectionEnd = this.rinput.value.length + } } hide() { + if(!this.visible) return + this.visible = false this.element.style.display = 'none' - main.menu.reset() + this.emit('hide') } submit(){ let val = this.save() @@ -80,8 +100,8 @@ export class OMNI extends OMNIUtils { } setup(){ this.hide() - this.button.addEventListener('click', event => { - if(!this.element.classList.contains('selected') || !this.submit()){ + this.button.addEventListener('click', () => { + if(!this.element.classList.contains('selected')){ this.focus(true) } }) @@ -95,23 +115,6 @@ export class OMNI extends OMNIUtils { if(event.key === 'Enter') this.submit() }) } - focus(select){ - this.show() - this.input.value = this.defaultValue - if(select){ - this.input.select() - } - main.menu.focus(this.element) - this.input.focus() - this.input.addEventListener('blur', () => { - main.menu.focus(main.menu.currentElements[0]) - this.save() - this.hide() - }, { once: true }) - if(!select) { // as last, move to the end - this.rinput.selectionStart = this.rinput.selectionEnd = this.rinput.value.length - } - } save(){ this.updateIcon('fas fa-search') let val = this.input.value @@ -177,7 +180,7 @@ export class OMNI extends OMNIUtils { if(evt.key && evt.key.length == 1 && evt.key != ' ') { this.defaultValue = evt.key if(main.menu.inPlayer() && !main.menu.isExploring()) { - main.menu.showWhilePlaying(true, true) + main.menu.showWhilePlaying(true) } this.focus(false) this.update() diff --git a/www/nodejs/modules/options/options.js b/www/nodejs/modules/options/options.js index f9bf5d30..fb3c79f7 100644 --- a/www/nodejs/modules/options/options.js +++ b/www/nodejs/modules/options/options.js @@ -4,7 +4,6 @@ import lang from '../lang/lang.js'; import storage from '../storage/storage.js' import { exec } from 'child_process'; import { EventEmitter } from 'events'; -import moment from 'moment-timezone'; import energy from '../energy/energy.js'; import fs from 'fs'; import downloads from '../downloads/downloads.js'; @@ -22,7 +21,7 @@ import config from '../config/config.js' import renderer from '../bridge/bridge.js' import paths from '../paths/paths.js' import Download from '../download/download.js' -import { insertEntry, kbfmt, kbsfmt, parseJSON, rmdirSync, ucFirst, ucWords } from '../utils/utils.js' +import { kbfmt, kbsfmt, parseJSON, moment, ucFirst, ucWords } from '../utils/utils.js' class Timer extends EventEmitter { constructor() { @@ -495,7 +494,7 @@ class Options extends OptionsExportImport { { template: 'question', text: lang.SELECT_LANGUAGE, fa: 'fas fa-language' } ].concat(options), def); if (locale == 'improve') { - renderer.ui.emit('open-external-url', 'https://github.com/efoxbr/megacubo/tree/master/www/nodejs-project/lang'); + renderer.ui.emit('open-external-url', 'https://github.com/EdenwareApps/megacubo/tree/master/www/nodejs-project/lang'); return await this.showLanguageEntriesDialog(); } const _def = config.get('locale') || lang.locale; diff --git a/www/nodejs/modules/paths/paths.js b/www/nodejs/modules/paths/paths.js index 61f749f8..1b7e2ded 100644 --- a/www/nodejs/modules/paths/paths.js +++ b/www/nodejs/modules/paths/paths.js @@ -75,3 +75,5 @@ if(paths.inWorker) { export default paths export const temp = paths.temp +export const inWorker = paths.inWorker + diff --git a/www/nodejs/modules/recommendations/recommendations.js b/www/nodejs/modules/recommendations/recommendations.js index 9ee46ef4..382f42ab 100644 --- a/www/nodejs/modules/recommendations/recommendations.js +++ b/www/nodejs/modules/recommendations/recommendations.js @@ -207,15 +207,15 @@ class Recommendations extends EventEmitter { this.epgLoaded = true this.scheduleUpdate() }) + global.lists.manager.ready().then(async () => { + this.listsLoaded = true + this.scheduleUpdate() + }).catch(console.error) global.lists.epg.ready().then(() => { this.epgLoaded || storage.delete(this.cacheKey) this.epgLoaded = true this.scheduleUpdate() }).catch(console.error) - global.lists.manager.ready().then(async () => { - this.listsLoaded = true - this.scheduleUpdate() - }).catch(console.error) }) } async scheduleUpdate() { @@ -223,54 +223,59 @@ class Recommendations extends EventEmitter { await this.update().catch(console.error) }) } - /* async validateChannels(data) { - let chs = {} - Object.keys(data).forEach(ch => { + const chs = {}, now = time() + for(const ch in data) { let channel = global.channels.isChannel(ch) if (channel) { - if (!chs[channel.name]) { + if(typeof(chs[channel.name]) == 'undefined') { chs[channel.name] = global.channels.epgPrepareSearch(channel) + chs[channel.name].candidates = [] + } + for(const p in data[ch]) { + if(parseInt(p) <= now && data[ch][p].e < now) { + chs[channel.name].candidates.push({ + t: data[ch][p].t, + ch + }) + break + } } } - }) - let alloweds = [] - await Promise.allSettled(Object.keys(chs).map(async name => { - return this.queue.add(async () => { - const channelMappedTo = await global.lists.epg.findChannel(chs[name]) - if (channelMappedTo) - alloweds.push(channelMappedTo) - }) - })) - Object.keys(data).forEach(ch => { - if (!alloweds.includes(ch)) { - delete data[ch] - } - }) - return data + } + const ret = {}, alloweds = await global.lists.epg.validateChannels(chs) + for(const ch in alloweds) { + ret[ch] = data[ch] + } + return ret } - */ - processEPGRecommendations(data) { + async processEPGRecommendations(data) { + data = await this.validateChannels(data) const results = [], already = new Set() - Object.keys(data).forEach(ch => { + for(const ch in data) { let channel = global.channels.isChannel(ch) if (channel) { - if(already.has(channel.name)) return - already.add(channel.name) - Object.keys(data[ch]).forEach(start => { + let t + for (const programme of data[ch]) { + if(!t) { + t = programme.t + if(already.has(t)) return // prevent same program on diff channels + already.add(t) + } results.push({ channel, - labels: data[ch][start].c, - programme: data[ch][start], - start: parseInt(start), + labels: programme.c, + programme, + start: parseInt(programme.start), och: ch }) - }) + } } - }) + } return results } async get(tags, amount=128) { + if(!global.lists.epg.loaded) return [] const now = (Date.now() / 1000) const timeRange = 3 * 3600 const timeRangeP = timeRange / 100 @@ -279,7 +284,7 @@ class Recommendations extends EventEmitter { tags = await this.tags.get() } let data = await global.lists.epg.getRecommendations(tags, until, amount * 4) - + const interests = new Set() global.channels.history.get().some(e => { const c = global.channels.isChannel(e) @@ -297,7 +302,7 @@ class Recommendations extends EventEmitter { } // console.log('suggestions.get', tags) - let maxScore = 0, results = this.processEPGRecommendations(data) + let maxScore = 0, results = await this.processEPGRecommendations(data) results = results.map(r => { let score = 0 @@ -326,19 +331,12 @@ class Recommendations extends EventEmitter { }) // remove repeated programmes - let nresults = [], already = new Set() - results = results.sortByProp('score', true) // sort before equilibrating - - for(const r of results) { - if (already.has(r.programme.t)) continue - already.add(r.programme.t) - const c = global.channels.epgPrepareSearch(r.channel) - const valid = await global.lists.epg.validateChannelProgramme(c, r.start, r.programme.t).catch(console.error) - valid === true && nresults.push(r) - } - results = nresults - nresults = [] - + let already = new Set() + results = results.sortByProp('score', true).filter(e => { + if (already.has(e.programme.t)) return false + already.add(e.programme.t) + return true + }) // equilibrate categories presence /* not yet mature piece of code, needs more testing @@ -406,10 +404,8 @@ class Recommendations extends EventEmitter { return entry }) } - hasEPG() { - return global.channels.isEPGLoaded() - } - async hasEPGChannel(ch, withIcon) { + async hasEPGChannel(ch, withIcon) { + if(!global.lists.epg.loaded) return false const terms = global.channels.entryTerms(ch).filter(t => !t.startsWith('-')) const results = await global.lists.epgSearchChannel(terms, 99) return Object.keys(results).some(name => { @@ -425,7 +421,7 @@ class Recommendations extends EventEmitter { } async getChannels(amount=5, _excludes=[]) { const excludes = new Set(_excludes) - const results = [], epgAvailable = this.hasEPG() + const results = [] const isChannelCache = {}, validateCache = {}, hasEPGCache = {} const channel = e => { const name = typeof(e) == 'string' ? e : (e.originalName || e.name) @@ -461,7 +457,7 @@ class Recommendations extends EventEmitter { return validateCache[name] } const hasEPG = async (e, icon) => { - if(!epgAvailable) return true + if(!global.lists.epg.loaded) return true const name = typeof(e) == 'string' ? e : (e.originalName || e.name) const key = name + (icon ? '-icon' : '') if (typeof(hasEPGCache[key]) == 'undefined') { @@ -540,7 +536,7 @@ class Recommendations extends EventEmitter { es = [] } if (!es.length) { - if (global.channels.isEPGLoaded()) { + if (global.lists.epg.loaded) { es.push({ name: lang.NO_RECOMMENDATIONS_YET, type: 'action', diff --git a/www/nodejs/modules/search/search.js b/www/nodejs/modules/search/search.js index 0257adf2..e800bdcd 100644 --- a/www/nodejs/modules/search/search.js +++ b/www/nodejs/modules/search/search.js @@ -99,7 +99,6 @@ class Search extends EventEmitter { mediaType = 'all'; } console.log('search-start', value); - global.menu.setLoading(true) osd.show(lang.SEARCHING, 'fas fa-search spin-x-alt', 'search', 'persistent'); this.searchMediaType = mediaType; let err; @@ -112,15 +111,17 @@ class Search extends EventEmitter { } this.emit('search', { query: value }); if (!menu.path) { - menu.path = lang.SEARCH; + menu.path = lang.SEARCH } - const resultsCount = rs.length; - menu.render(this.addFixedEntries(mediaType, rs), menu.path, 'fas fa-search', '/'); + const resultsCount = rs.length + menu.render(this.addFixedEntries(mediaType, rs), menu.path, { + icon: 'fas fa-search', + backTo: '/' + }) osd.show(lang.X_RESULTS.format(resultsCount), 'fas fa-check-circle', 'search', 'normal'); } else { menu.displayErr(err); } - global.menu.setLoading(false) this.history.add(value) } refresh() { @@ -166,10 +167,13 @@ class Search extends EventEmitter { let ret = await menu.dialog(opts, def); if (ret == 'epg') { this.channels.epgSearch(this.currentSearch.name).then(entries => { - entries.unshift(this.channels.epgSearchEntry()); - let path = menu.path.split('/').filter(s => s != lang.SEARCH).join('/'); - menu.render(entries, path + '/' + lang.SEARCH, 'fas fa-search', path); - this.history.add(this.currentSearch.name); + entries.unshift(this.channels.epgSearchEntry()) + let path = menu.path.split('/').filter(s => s != lang.SEARCH).join('/') + menu.render(entries, path + '/' + lang.SEARCH, { + icon: 'fas fa-search', + backTo: path + }) + this.history.add(this.currentSearch.name) }).catch(e => menu.displayErr(e)); } else { this.go(this.currentSearch.name, 'all'); diff --git a/www/nodejs/modules/stream-state/stream-state.js b/www/nodejs/modules/stream-state/stream-state.js index 4fc32ec1..ef09cfdc 100644 --- a/www/nodejs/modules/stream-state/stream-state.js +++ b/www/nodejs/modules/stream-state/stream-state.js @@ -238,10 +238,10 @@ class StreamState extends EventEmitter { const allowAutoTest = config.get('auto-test'); const manuallyTesting = force === true; const autoTesting = !manuallyTesting && allowAutoTest; - const nt = { name: lang.TEST_STREAMS }; + let busy if (manuallyTesting) { - global.menu.setLoading(true) - osd.show(lang.TESTING + ' 0%', 'fa-mega spin-x-alt', 'stream-state-tester', 'persistent'); + busy = global.menu.setBusy(global.menu.path +'/'+ lang.TESTING) + osd.show(lang.TESTING + ' 0%', 'fa-mega spin-x-alt', 'stream-state-tester', 'persistent') } const retest = [], syncData = {} entries = entries.filter(e => { @@ -314,7 +314,7 @@ class StreamState extends EventEmitter { if (this.debug) { console.warn('TESTER FINISH!', nt, this.testing.results, this.testing.states); } - global.menu.setLoading(false) + busy.release() manuallyTesting && osd.hide('stream-state-tester') this.testing.destroy() this.testing = null diff --git a/www/nodejs/modules/streamer/renderer.js b/www/nodejs/modules/streamer/renderer.js index ddaa7280..843beb24 100644 --- a/www/nodejs/modules/streamer/renderer.js +++ b/www/nodejs/modules/streamer/renderer.js @@ -422,7 +422,7 @@ class StreamerClientVideoAspectRatio extends StreamerState { this.activeAspectRatio = this.aspectRatioList[0] }) } - resize() { + resize() { let landscape = window.innerWidth > window.innerHeight if(landscape != this.landscape){ // orientation changed this.landscape = landscape @@ -430,6 +430,7 @@ class StreamerClientVideoAspectRatio extends StreamerState { } else { this.applyAspectRatio(this.activeAspectRatio) } + window.capacitor && plugins.megacubo.updateScreenMetrics() } generateAspectRatioMetrics(r){ let h = r, v = 1 @@ -664,6 +665,7 @@ class StreamerButtonActionFeedback extends StreamerSpeedo { this.on('draw', () => { this.buttonActionFeedbackLayer = document.querySelector('#button-action-feedback') this.buttonActionFeedbackLayer.style.visibility = 'visible' + this.buttonActionFeedbackLayer.style.display = 'none' this.buttonActionFeedbackLayerInner = this.buttonActionFeedbackLayer.querySelector('span') }) this.on('added-player-button', this.addedPlayerButton.bind(this)) @@ -689,10 +691,10 @@ class StreamerButtonActionFeedback extends StreamerSpeedo { } clearTimeout(this.buttonActionFeedbackTimer) this.buttonActionFeedbackLayerInner.innerHTML = '' - this.buttonActionFeedbackLayer.style.display = 'inline-block' + this.buttonActionFeedbackLayer.style.display = 'flex' const i = this.buttonActionFeedbackLayerInner.querySelector('i') i.style.transform = 'scale(1.5)' - i.style.opacity = '0.01' + i.style.opacity = '0.75' this.buttonActionFeedbackTimer = setTimeout(() => { this.buttonActionFeedbackLayer.style.display = 'none' }, 500) @@ -1145,31 +1147,19 @@ class StreamerAndroidNetworkIP extends StreamerClientTimeWarp { class StreamerClientVideoFullScreen extends StreamerAndroidNetworkIP { constructor(controls){ super(controls) + this.metricsCache = {} let b = this.controls.querySelector('button.fullscreen') - if(main.config['startup-window'] == 'fullscreen'){ + if(window.capacitor){ + if(b) b.style.display = 'none' + plugins.megacubo.on('nightmode', this.handleDarkModeInfoDialog.bind(this)) + } else if(main.config['startup-window'] == 'fullscreen') { this.inFullScreen = true document.body.classList.add('fullscreen') if(b) b.style.display = 'none' this.enterFullScreen() } else { - if(window.capacitor){ - if(b){ - b.style.display = 'none' - } - plugins.megacubo.on('metrics', this.updateAndroidScreenMetrics.bind(this)) - plugins.megacubo.on('nightmode', this.handleDarkModeInfoDialog.bind(this)) - this.updateAndroidScreenMetrics(plugins.megacubo.metrics) - } else { - this.inFullScreen = false - if(b) b.style.display = 'inline-flex' - } - this.on('fullscreenchange', fs => { - if(fs){ - document.body.classList.add('fullscreen') - } else { - document.body.classList.remove('fullscreen') - } - }) + this.inFullScreen = false + if(b) b.style.display = 'inline-flex' } } handleDarkModeInfoDialog(info){ @@ -1181,39 +1171,30 @@ class StreamerClientVideoFullScreen extends StreamerAndroidNetworkIP { ]) } } - updateAndroidScreenMetrics(metrics){ - if(metrics && typeof(metrics.bottom) != 'undefined') { - this.metrics = metrics - } - if(this.inFullScreen || !this.metrics){ - // keep as '0px' instead of '0' to avoid CSS calc() issues - css(' :root { --menu-padding-top: 0px; --menu-padding-bottom: 0.5vmin; --menu-padding-right: 0.5vmin; --menu-padding-left: 0.5vmin; } ', 'frameless-window') - } else { - css(' :root { --menu-padding-top: ' + this.metrics.top + 'px; --menu-padding-bottom: calc(0.5vmin + ' + this.metrics.bottom + 'px); --menu-padding-right: calc(0.5vmin + ' + this.metrics.right + 'px); --menu-padding-left: calc(0.5vmin + ' + this.metrics.left + 'px); } ', 'frameless-window') - } - } updateAfterLeaveAndroidMiniPlayer(){ if(screen.width == window.outerWidth && screen.height == window.outerHeight && this.active){ this.enterFullScreen() } } enterFullScreen(){ - if(window.capacitor){ - if(!this.pipLeaveListener){ - this.pipLeaveListener = () => { - console.log('LEAVING PIP', screen.width, screen.height, window.outerWidth, window.outerHeight, this.active) - this.updateAfterLeaveAndroidMiniPlayer() + if(!this.inFullScreen){ + this.inFullScreen = true + if(window.capacitor){ + if(!this.pipLeaveListener){ + this.pipLeaveListener = () => { + console.log('LEAVING PIP', screen.width, screen.height, window.outerWidth, window.outerHeight, this.active) + this.updateAfterLeaveAndroidMiniPlayer() + } } + plugins.megacubo.enterFullScreen() + if(!winActions.listeners('leave').includes(this.pipLeaveListener)){ + winActions.on('leave', this.pipLeaveListener) + } + } else { + parent.Manager.setFullScreen(true) } - plugins.megacubo.enterFullScreen() - if(!winActions.listeners('leave').includes(this.pipLeaveListener)){ - winActions.on('leave', this.pipLeaveListener) - } - } else { - parent.Manager.setFullScreen(true) + this.emit('fullscreenchange', this.inFullScreen) } - this.inFullScreen = true - this.emit('fullscreenchange', this.inFullScreen) } leaveFullScreen(){ if(this.inFullScreen){ @@ -1516,8 +1497,8 @@ class StreamerClientControls extends StreamerAudioUI { const bt = this.buildElementFromHTML(template) const bts = container.querySelectorAll('button') if(bts.length) { - let ptr, type = 'after' - Array.from(bts).some(e => { + let ptr, type = 'after'; + [...bts].some(e => { const btpos = parseInt(e.getAttribute('data-position')) if(btpos < position) { ptr = e diff --git a/www/nodejs/modules/streamer/streamer.js b/www/nodejs/modules/streamer/streamer.js index addefa1e..c4063f7e 100644 --- a/www/nodejs/modules/streamer/streamer.js +++ b/www/nodejs/modules/streamer/streamer.js @@ -585,7 +585,7 @@ class Streamer extends StreamerGoNext { { template: 'option', text: global.lang.YES, id: 'yes', fa: 'fas fa-check-circle' }, { template: 'option', text: global.lang.NO_THANKS, id: 'no', fa: 'fas fa-times-circle' }, { template: 'option', text: global.lang.RETRY, id: 'retry', fa: 'fas fa-redo' }, - { template: 'option', text: global.lang.TRANSCODE, id: 'transcode', fa: 'fas fa-cogs' } + { template: 'option', text: global.lang.FIX_AUDIO_OR_VIDEO, id: 'transcode', fa: 'fas fa-wrench' } ], 'yes') if (chosen == 'yes') { renderer.ui.emit('external-player', url) @@ -820,7 +820,7 @@ class Streamer extends StreamerGoNext { } const loadingEntriesData = [global.lang.AUTO_TUNING, name]; console.warn('playFromEntries', name, connectId, silent); - global.menu.setLoading(true); + const busy = global.menu.setBusy(this.path +'/'+ name); silent || (global.osd && global.osd.show(global.lang.TUNING_WAIT_X.format(name) + ' 0%', 'fa-mega spin-x-alt', 'streamer', 'persistent')) this.tuning && this.tuning.destroy(); if (this.connectId != connectId) { @@ -860,7 +860,7 @@ class Streamer extends StreamerGoNext { } else { this.setTuneable(true) } - global.menu.setLoading(false) + busy.release() return !hasErr } async playPromise(e, results, silent) { @@ -882,12 +882,12 @@ class Streamer extends StreamerGoNext { this.connectId = connectId; this.emit('connecting', connectId); - const isMega = mega.isMega(e.url), txt = isMega ? global.lang.TUNING : undefined; - const opts = isMega ? mega.parse(e.url) : { mediaType: 'live' }; - const loadingEntriesData = [e, global.lang.AUTO_TUNING]; - silent || global.menu.setLoading(true); - console.warn('STREAMER INTENT', e, results); - let succeeded; + const isMega = mega.isMega(e.url), txt = isMega ? global.lang.TUNING : undefined + const opts = isMega ? mega.parse(e.url) : { mediaType: 'live' } + const loadingEntriesData = [e, global.lang.AUTO_TUNING] + const busy = silent ? false : global.menu.setBusy(this.path +'/'+ e.name) + console.warn('STREAMER INTENT', e, results) + let succeeded this.emit('pre-play-entry', e); if (Array.isArray(results)) { let name = e.name; @@ -898,11 +898,11 @@ class Streamer extends StreamerGoNext { if (this.connectId == connectId) { this.connectId = false; if (!succeeded) { - this.emit('connecting-failure', e); + this.emit('connecting-failure', e) } } else { - silent || global.menu.setLoading(false); - throw 'another play intent in progress'; + busy && busy.release() + throw 'another play intent in progress' } } else if (isMega && !opts.url) { let name = e.name @@ -922,7 +922,7 @@ class Streamer extends StreamerGoNext { limit: 1024 }) if (this.connectId != connectId) { - silent || global.menu.setLoading(false) + busy && busy.release() throw 'another play intent in progress' } // console.error('ABOUT TO TUNE', terms, name, JSON.stringify(entries), opts) @@ -960,7 +960,7 @@ class Streamer extends StreamerGoNext { let hasErr, intent = await this.intent(e).catch(r => hasErr = r); if (typeof(hasErr) != 'undefined') { if (this.connectId != connectId) { - silent || global.menu.setLoading(false); + busy && busy.release() throw 'another play intent in progress'; } console.warn('STREAMER INTENT ERROR', hasErr); @@ -976,8 +976,8 @@ class Streamer extends StreamerGoNext { succeeded = true; } } - silent || global.menu.setLoading(false); - return succeeded; + busy && busy.release() + return succeeded } play(e, results, silent) { return this.playPromise(e, results, silent).catch(silent ? console.error : global.menu.displayErr); @@ -999,13 +999,14 @@ class Streamer extends StreamerGoNext { e.name = ch.name } const same = this.tuning && !this.tuning.finished && !this.tuning.destroyed && (this.tuning.has(e.url) || this.tuning.opts.megaURL == e.url); - const loadingEntriesData = [e, global.lang.AUTO_TUNING]; + const loadingEntriesData = [e, global.lang.AUTO_TUNING] + const busy = global.menu.setBusy(this.path +'/'+ e.name) console.log('tuneEntry', e, same); if (same) { let err; await this.tuning.tune().catch(e => err = e); - global.menu.setLoading(false); if (err) { + busy.release() if (err != 'cancelled by user') { this.emit('connecting-failure', e); console.error('tune() ERR', err); @@ -1014,8 +1015,7 @@ class Streamer extends StreamerGoNext { return; } this.setTuneable(true); - } else { - + } else { if (ch) { e.url = mega.build(ch.name, { terms: ch.terms }); } else { @@ -1026,6 +1026,7 @@ class Streamer extends StreamerGoNext { } this.play(e); } + busy.release() return true; } } diff --git a/www/nodejs/modules/streamer/utils/mpegts-processor.js b/www/nodejs/modules/streamer/utils/mpegts-processor.js index ecce42a2..d5b1616e 100644 --- a/www/nodejs/modules/streamer/utils/mpegts-processor.js +++ b/www/nodejs/modules/streamer/utils/mpegts-processor.js @@ -134,7 +134,7 @@ class MPEGTSProcessor extends EventEmitter { this.pcrMemo.set(pcr, 0); if (this.pcrMemoSize > this.maxPcrMemoSize) { const deleteCount = this.pcrMemoSize - (this.maxPcrMemoSize - this.pcrMemoNudgeSize); - const keysToDelete = Array.from(this.pcrMemo.keys()).slice(0, deleteCount); + const keysToDelete = [...this.pcrMemo.keys()].slice(0, deleteCount); keysToDelete.forEach((pcr) => this.pcrMemo.delete(pcr)); this.pcrMemoSize -= deleteCount; } diff --git a/www/nodejs/modules/theme/renderer.js b/www/nodejs/modules/theme/renderer.js index 667d1527..68a0adf4 100644 --- a/www/nodejs/modules/theme/renderer.js +++ b/www/nodejs/modules/theme/renderer.js @@ -1,4 +1,6 @@ import { main } from '../bridge/renderer' +import { detectFontSizeMultiplier } from '../../renderer/src/scripts/utils' +import { EventEmitter } from 'events' const colorChannelMixer = (colorChannelA, colorChannelB, amountToMix) => { var channelA = colorChannelA*amountToMix @@ -49,14 +51,20 @@ const hexToRgb = ohex => { } : ohex } -class Theme { +class Theme extends EventEmitter { constructor(){ - this.curtains = Array.from(document.querySelectorAll('.curtain')) + super() + this.curtains = [...document.querySelectorAll('.curtain')] this.splashStartTime = (new Date()).getTime() - window.addEventListener('resize', () => this.updateFontSize()) + window.addEventListener('resize', () => { + this.resizingTimer && clearTimeout(this.resizingTimer) + this.resizingTimer = setTimeout(() => { + this.emit('resized') + }, 150) + }) + this.on('resized', () => this.updateFontSize()) } renderBackground(data) { - console.warn('theming renderbackground', data) const bg = document.getElementById('background') if(data.video){ bg.style.backgroundImage = 'none'; @@ -305,7 +313,7 @@ class Theme { defaultFontSize() { const areaSize = window.innerHeight * window.innerWidth; const references = [ - [72720, 12], // miniplayer + [72720, 14], // miniplayer [921600, 18], // 720p [1927680, 24], // 1080p [3840000, 28], // 4K @@ -328,21 +336,24 @@ class Theme { } updateFontSize() { let fontSize = this.defaultFontSize() - if (window.innerHeight > window.innerWidth) { // portrait - fontSize *= 1.1 - } - - console.log({fontSize, resolution: window.innerWidth + 'x' + window.innerHeight}) - if(main.config && main.config['font-size']){ + if(main.config && main.config['font-size']) { if(main.config['font-size'] > 3) { - fontSize += (main.config['font-size'] - 3) * 0.5 + fontSize += (main.config['font-size'] - 3) } else if(main.config['font-size'] < 3) { - fontSize -= (3 - main.config['font-size']) * 0.5 + fontSize -= (3 - main.config['font-size']) } } - - let cssCode = ` :root { --menu-entry-name-font-size: ${fontSize}px; } ` - main.css(cssCode, 'theme-font-size') + if (window.innerHeight > window.innerWidth) { // portrait + fontSize *= 1.1 + } + const multiplier = detectFontSizeMultiplier() // may change on orientationchange or miniplayer + const cssText = ` + :root { + --font-size: ${fontSize}px; + --font-scaling: ${multiplier}; + } + ` + main.css(cssText, 'theme-font-size', window) } hideSplashScreen() { localStorage.setItem('splash-time-hint', String((new Date()).getTime() - this.splashStartTime)) diff --git a/www/nodejs/modules/theme/theme.js b/www/nodejs/modules/theme/theme.js index ccf2816b..88874721 100644 --- a/www/nodejs/modules/theme/theme.js +++ b/www/nodejs/modules/theme/theme.js @@ -6,7 +6,7 @@ import lang from "../lang/lang.js"; import storage from '../storage/storage.js' import { EventEmitter } from "events"; import fs from "fs"; -import jimp from "../jimp-worker/main.js"; +import imp from "../icon-server/image-processor.js"; import path from "path"; import downloads from "../downloads/downloads.js"; import options from "../options/options.js"; @@ -18,7 +18,7 @@ class Theme extends EventEmitter { constructor() { super(); const { data } = paths; - this.jimp = jimp + this.imp = imp this.backgroundVideoSizeLimit = 40 * (1024 * 1024); this.customBackgroundImagePath = data + '/background.png'; this.customBackgroundVideoPath = data + '/background'; @@ -50,7 +50,7 @@ class Theme extends EventEmitter { const key = 'colors-' + basename(file) + '-' + stat.size let colors = await storage.get(key) if (!Array.isArray(colors)) { - colors = await jimp.colors(file) + colors = await imp.colors(file) await storage.set(key, colors, { expiration: true }) } if (!Array.isArray(colors)) { @@ -97,7 +97,6 @@ class Theme extends EventEmitter { return (n / (255 * 3)) * 100; } async importBackgroundImage(file) { - global.menu.setLoading(true); osd.show(lang.PROCESSING, 'fas fa-cog fa-spin', 'theme-upload', 'persistent'); try { await fs.promises.copyFile(file, this.customBackgroundImagePath); @@ -110,11 +109,9 @@ class Theme extends EventEmitter { menu.displayErr(err) } console.warn('!!! IMPORT CUSTOM BACKGROUND FILE !!! ok', menu.path, file, this.customBackgroundImagePath); - global.menu.setLoading(false); osd.hide('theme-upload'); } async importBackgroundVideo(file) { - global.menu.setLoading(true); osd.show(lang.PROCESSING, 'fas fa-cog fa-spin', 'theme-upload', 'persistent'); try { @@ -137,7 +134,6 @@ class Theme extends EventEmitter { } catch (err) { menu.displayErr(err); } - global.menu.setLoading(false); osd.hide('theme-upload'); } cleanVideoBackgrounds(currentFile) { @@ -387,7 +383,7 @@ class Theme extends EventEmitter { renderer: async () => { let hasErr, colors = await this.colors(this.customBackgroundImagePath, c => this.colorLightLevel(c) < 40, 52).catch(err => hasErr = err); osd.hide('theme-upload'); - global.menu.setLoading(true) + const busy = global.menu.setBusy(global.menu.path +'/'+ lang.BACKGROUND_COLOR) if (!Array.isArray(colors)) colors = [] try { colors = this.colorsAddDefaults(colors, false).map(c => { @@ -431,8 +427,8 @@ class Theme extends EventEmitter { } catch (err) { console.error(err) } - global.menu.setLoading(false) - return colors; + busy.release() + return colors }, value: () => { return global.config.get('background-color'); @@ -510,7 +506,6 @@ class Theme extends EventEmitter { menu.back(); }).finally(() => { osd.hide('theme-upload') - global.menu.setLoading(false) }) }) }, diff --git a/www/nodejs/modules/utils/utils.js b/www/nodejs/modules/utils/utils.js index d432470b..569b1ca2 100644 --- a/www/nodejs/modules/utils/utils.js +++ b/www/nodejs/modules/utils/utils.js @@ -2,9 +2,46 @@ import sanitizeFilename from 'sanitize-filename' import np from '../network-ip/network-ip.js' import os from 'os'; import { URL } from 'url' +import { inWorker } from '../paths/paths.js' import fs from 'fs' -import moment from 'moment-timezone' +import path from 'path' +import dayjs from 'dayjs' import clone from 'fast-json-clone' +import utc from 'dayjs/plugin/utc' +import timezone from 'dayjs/plugin/timezone' // dependent on utc plugin +import relativeTime from 'dayjs/plugin/relativeTime' +import localizedFormat from 'dayjs/plugin/localizedFormat' +import { getDirname } from 'cross-dirname' + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(relativeTime) +dayjs.extend(localizedFormat) + +const originalLocale = dayjs.locale.bind(dayjs) +dayjs.locale = async locales => { + if (typeof(locales) == 'string') { + locales = [locales] + } + if(inWorker) return // fails to load language in worker (android) as it tries to require 'dayjs' again + for (const locale of locales) { + const file = path.join(getDirname(), 'dayjs-locale', locale +'.js') + try { + console.log(`Loading locale ${file}`) + const data = await import(file) + if(!data.default) { + throw new Error('No default export found') + } + const ret = originalLocale(data.default) + console.log(`Locale ${locale} loaded from ${file}`) + break + } catch(err) { + console.error(`Error loading locale ${locale} from ${file}:`, err) + } + } +} + +export const moment = dayjs if (!global.Promise.allSettled) { global.Promise.allSettled = ((promises) => Promise.all(promises.map(p => p @@ -15,6 +52,7 @@ if (!global.Promise.allSettled) { status: 'rejected', reason }))))) } + if (!global.String.prototype.format) { Object.defineProperty(global.String.prototype, 'format', { enumerable: false, @@ -343,7 +381,6 @@ export const ucFirst = (str, keepCase) => { }); } export const ts2clock = time => { - let locale = undefined, timezone = undefined; if (typeof(time) == 'string') { time = parseInt(time) } @@ -636,5 +673,3 @@ export const findSyncBytePosition = (buffer, from = 0) => { } return -1 // return -1 if no valid sync byte is found } - - diff --git a/www/nodejs/package.json b/www/nodejs/package.json index e6f62c46..7ca5f218 100644 --- a/www/nodejs/package.json +++ b/www/nodejs/package.json @@ -1,9 +1,9 @@ { "name": "megacubo", "icon": "./default_icon.png", - "version": "17.5.4", + "version": "17.5.5", "megacubo": { - "revision": "17521" + "revision": "17531" }, "main": "dist/main.js", "window": { @@ -13,4 +13,4 @@ "bugs": { "url": "https://github.com/EdenwareApps/Megacubo/issues" } -} +} \ No newline at end of file diff --git a/www/nodejs/renderer/assets/js/electron.js b/www/nodejs/renderer/assets/js/electron.js index c2ce9e1b..6fca90a6 100644 --- a/www/nodejs/renderer/assets/js/electron.js +++ b/www/nodejs/renderer/assets/js/electron.js @@ -190,7 +190,7 @@ class WindowManagerCommon { } this.openFileDialogChooser.onchange = evt => { if(this.openFileDialogChooser.value){ - const file = Array.from(evt.target.files).shift() + const file = [...evt.target.files].shift() if(file && file.path){ cb(null, file.path) } else { @@ -356,10 +356,7 @@ class WindowManager extends WindowManagerCommon { this.app.main.on('arguments', this.handleArgs.bind(this)) this.app.main.on('exit-page', url => this.exitPage.url = url) this.patch() - setTimeout(() => { - this.focusApp() - this.app.menu.reset() - }, 100) + setTimeout(() => this.focusApp(), 100) }) } getScreenSize(real){ diff --git a/www/nodejs/renderer/index.html b/www/nodejs/renderer/index.html index ddc6e255..c31a220c 100644 --- a/www/nodejs/renderer/index.html +++ b/www/nodejs/renderer/index.html @@ -2,7 +2,7 @@ - + Megacubo @@ -107,7 +107,7 @@ } - +
@@ -143,8 +143,6 @@ } } - -