diff --git a/Privacy Policy.md b/Privacy Policy.md index 9623fc8..a64800c 100644 --- a/Privacy Policy.md +++ b/Privacy Policy.md @@ -1,14 +1,14 @@ -# Privacy Policy for AniList to AniWave +# Privacy Policy for AniList Watcher ## Introduction -I, Sans, am committed to maintaining the trust and confidence of all users of my AniList to AniWave browser extension. I want you to know that AniList to AniWave is not in the business of selling, renting or trading any personal information with other entities. +I, Sans, am committed to maintaining the trust and confidence of all users of my AniList Watcher browser extension. I want you to know that AniList Watcher is not in the business of selling, renting or trading any personal information with other entities. In this Privacy Policy, I provide detailed information on when and why I do not collect your personal information, how it would be used if it were collected, the limited conditions under which it may be disclosed to others and how it would be kept secure. ## Information That I Collect -AniList to AniWave does not collect or store any personal information. +AniList Watcher does not collect or store any personal information. ## Use of Your Information @@ -22,4 +22,4 @@ If I make a change to this policy that, in my sole discretion, is material, I wi ## Contact Me -If you have any questions about this policy or about how I handle your personal information, please contact me over on the Discord platform (sans._.) or via email (sansy3108@gmail.com). +If you have any questions about this policy or about how I handle your personal information, please contact me on Discord [`sans._.`](https://discord.com/users/366536353418182657). diff --git a/README.md b/README.md index 8987332..e1939de 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ -Simply adds a button to anilist anime pages that searches up the anime as best as it can on aniwave. +Simply adds a button to anilist anime & manga pages that searches up the media as best as it can on whatever site you want. + +Currently supports Yes, I'm lazy, how did you know? diff --git a/build.js b/build.js index 801ed12..4f3d002 100644 --- a/build.js +++ b/build.js @@ -1,26 +1,98 @@ -console.time('build'); - -const { exec } = require('child_process'); -const { existsSync, rmSync } = require('fs'); -const { copy } = require('fs-extra'); -const glob = require('glob'); -const path = require('path'); -const util = require('util'); - -const execAsync = util.promisify(exec); -if (existsSync('out')) rmSync('out', { recursive: true, force: true }); - -copy('src', 'out/extension').then(() => { - execAsync('tsc -p config/tsconfig.json').then(() => { - let tsFiles = glob.sync('out/extension/**/*.ts'); - tsFiles.forEach(tsFile => { - let jsFile = path.normalize(path.join('out', 'js', path.relative('out/extension', tsFile))).replace(/\.ts$/, '.js'); - if (existsSync(jsFile)) { - rmSync(tsFile, { force: true }); - copy(jsFile, tsFile.replace(/\.ts$/, '.js')); - } - }); - - console.timeEnd('build'); - }); +import { exec } from 'child_process'; +import CleanCSS from 'clean-css'; +import { existsSync, rmSync } from 'fs'; +import { copy, remove } from 'fs-extra'; +import { readFile, writeFile } from 'fs/promises'; +import { glob } from 'glob'; +import { minify as HtmlMinify } from 'html-minifier-terser'; +import { join, normalize, relative } from 'path'; +import { minify } from 'terser'; +import { promisify } from 'util'; +import manifest from './src/manifest.json' assert { type: 'json' }; + +const execAsync = promisify(exec); + +async function build() { + console.time(`- ${manifest.name} v${manifest.version} was built and minified in`); + + // Removing "out" if exists + if (existsSync('out')) { + rmSync('out', { recursive: true, force: true }); + console.log(`- Removed "out" folder`); + } + + // Copying everything from "src" to "out/extension" and compiling TS + await Promise.all([ + copy('src', 'out/extension').then(() => console.log(`- Copied contents of "src" to "out/extension"`)), + execAsync('tsc -p config/tsconfig.json').then(() => console.log(`- Compiled TypeScript`)) + ]); + + // Replacing TS files from "out/extension" with files from "out/js" + // Also minifying everying + const tsFiles = glob.sync('out/extension/**/*.ts'); + const cssFiles = glob.sync('out/extension/**/*.css'); + const htmlFiles = glob.sync('out/extension/**/*.html'); + + await Promise.all([ + ...tsFiles.map(async tsFile => { + if (tsFile.endsWith('global.d.ts')) return; + + const jsFile = normalize(join('out', 'js', relative('out/extension', tsFile))).replace(/\.ts$/, '.js'); + + // Read the JS file + const data = await readFile(jsFile, { encoding: 'utf8' }); + + // Minimize + const result = await minify(data); + if (!result.code) throw new Error(`Failed to minify ${jsFile}`); + + // Write the minified code to the new file + await writeFile(tsFile.replace(/\.ts$/, '.js'), result.code); + console.log(`- Written (minified) ${tsFile.replace(/\.ts$/, '.js')}`); + }), + ...cssFiles.map(async cssFile => { + // Read the CSS file + const data = await readFile(cssFile, { encoding: 'utf8' }); + + // Minimize + const result = new CleanCSS().minify(data).styles; + + // Write the minified CSS to the same file + await writeFile(cssFile, result); + console.log(`- Written (minified) ${cssFile}`); + }), + ...htmlFiles.map(async htmlFile => { + // Read the HTML file + const data = await readFile(htmlFile, { encoding: 'utf8' }); + + // Minimize + const result = await HtmlMinify(data, { collapseWhitespace: true, minifyCSS: true, minifyJS: true }); + + // Write the minified HTML to the same file + await writeFile(htmlFile, result); + console.log(`- Written (minified) ${htmlFile}`); + }), + // Removing "out/extension/**/*.ts" + ...tsFiles.map(tsFile => remove(tsFile).then(() => console.log(`- Removed ${tsFile}`))) + ]); + + // Removing "out/js" + await remove('out/js').then(() => console.log(`- Removed "out/js" folder`)); + + // Copy all files from "out/extension" to "out" + await copy('out/extension', 'out', { overwrite: true }).then(() => console.log(`- Moved everything from "out/extension" to "out"`)); + + // Remove the "out/extension" folder + await remove('out/extension').then(() => console.log(`- Removed "out/extenson"`)); + + console.log(); + console.timeEnd(`- ${manifest.name} v${manifest.version} was built and minified in`); +} + +build().catch(e => { + if (e.stdout) { + console.log(e.stdout); + } else { + console.log(e); + } }); diff --git a/config/nodemon.json b/config/nodemon.json index b102f7f..770731d 100644 --- a/config/nodemon.json +++ b/config/nodemon.json @@ -1,5 +1,5 @@ { - "watch": ["src"], + "watch": ["src", "build.js"], "ext": "*", - "exec": "node build.js" + "exec": "cls && node --disable-warning=ExperimentalWarning build.js" } diff --git a/config/tsconfig.json b/config/tsconfig.json index 5bb465a..25a5382 100644 --- a/config/tsconfig.json +++ b/config/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "ES2015", + "target": "ES2017", "moduleResolution": "node", "resolveJsonModule": true, "outDir": "../out/js", @@ -20,7 +20,7 @@ "skipDefaultLibCheck": true, "skipLibCheck": true, "sourceMap": false, - "lib": ["ES2016", "DOM"] + "lib": ["ES2017", "DOM"] }, "include": ["../src/**/*.ts"], "exclude": ["node_modules"] diff --git a/package.json b/package.json index 18ace93..5faf42f 100644 --- a/package.json +++ b/package.json @@ -1,21 +1,30 @@ { - "name": "anilist-to-aniwave", - "version": "7.0.0", - "description": "Adds a button on AniList anime pages that searches the anime on AniWave.", + "name": "anilist-watcher", + "version": "8.0.0", + "description": "Adds a button on AniList anime and manga pages to quickly open media on popular streaming and reading sites.", "scripts": { "watch": "nodemon --config config/nodemon.json", - "build": "node build.js" + "build": "node build.js --disable-warning ExperimentalWarning" }, "keywords": [], "author": "sans._.", "license": "ISC", + "type": "module", "devDependencies": { "@types/chrome": "^0.0.246", - "nodemon": "^3.0.1" + "@types/clean-css": "^4.2.11", + "@types/fs-extra": "^11.0.4", + "@types/html-minifier-terser": "^7.0.2", + "@types/node": "20.14.8", + "nodemon": "^3.1.4" }, "dependencies": { - "fs-extra": "^11.1.1", - "glob": "^10.3.10", - "typescript": "^5.2.2" + "clean-css": "^5.3.3", + "fs-extra": "^11.2.0", + "glob": "^10.4.5", + "html-minifier-terser": "^7.2.0", + "htmlparser2": "^9.1.0", + "terser": "^5.32.0", + "typescript": "^5.6.2" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d47f59e..27f8e25 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,527 +1,865 @@ -lockfileVersion: '6.0' +lockfileVersion: '9.0' settings: autoInstallPeers: true excludeLinksFromLockfile: false -dependencies: - fs-extra: - specifier: ^11.1.1 - version: 11.1.1 - glob: - specifier: ^10.3.10 - version: 10.3.10 - typescript: - specifier: ^5.2.2 - version: 5.2.2 - -devDependencies: - '@types/chrome': - specifier: ^0.0.246 - version: 0.0.246 - nodemon: - specifier: ^3.0.1 - version: 3.0.1 +importers: + + .: + dependencies: + clean-css: + specifier: ^5.3.3 + version: 5.3.3 + fs-extra: + specifier: ^11.2.0 + version: 11.2.0 + glob: + specifier: ^10.4.5 + version: 10.4.5 + html-minifier-terser: + specifier: ^7.2.0 + version: 7.2.0 + htmlparser2: + specifier: ^9.1.0 + version: 9.1.0 + terser: + specifier: ^5.32.0 + version: 5.32.0 + typescript: + specifier: ^5.6.2 + version: 5.6.2 + devDependencies: + '@types/chrome': + specifier: ^0.0.246 + version: 0.0.246 + '@types/clean-css': + specifier: ^4.2.11 + version: 4.2.11 + '@types/fs-extra': + specifier: ^11.0.4 + version: 11.0.4 + '@types/html-minifier-terser': + specifier: ^7.0.2 + version: 7.0.2 + '@types/node': + specifier: 20.14.8 + version: 20.14.8 + nodemon: + specifier: ^3.1.4 + version: 3.1.4 packages: - /@isaacs/cliui@8.0.2: + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} - dependencies: - string-width: 5.1.2 - string-width-cjs: /string-width@4.2.3 - strip-ansi: 7.1.0 - strip-ansi-cjs: /strip-ansi@6.0.1 - wrap-ansi: 8.1.0 - wrap-ansi-cjs: /wrap-ansi@7.0.0 - dev: false - /@pkgjs/parseargs@0.11.0: + '@jridgewell/gen-mapping@0.3.5': + resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} + engines: {node: '>=6.0.0'} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/set-array@1.2.1': + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} + engines: {node: '>=6.0.0'} + + '@jridgewell/source-map@0.3.6': + resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==} + + '@jridgewell/sourcemap-codec@1.5.0': + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + + '@jridgewell/trace-mapping@0.3.25': + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} - requiresBuild: true - dev: false - optional: true - /@types/chrome@0.0.246: + '@types/chrome@0.0.246': resolution: {integrity: sha512-MxGxEomGxsJiL9xe/7ZwVgwdn8XVKWbPvxpVQl3nWOjrS0Ce63JsfzxUc4aU3GvRcUPYsfufHmJ17BFyKxeA4g==} - dependencies: - '@types/filesystem': 0.0.34 - '@types/har-format': 1.2.14 - dev: true - /@types/filesystem@0.0.34: - resolution: {integrity: sha512-La4bGrgck8/rosDUA1DJJP8hrFcKq0BV6JaaVlNnOo1rJdJDcft3//slEbAmsWNUJwXRCc0DXpeO40yuATlexw==} - dependencies: - '@types/filewriter': 0.0.31 - dev: true + '@types/clean-css@4.2.11': + resolution: {integrity: sha512-Y8n81lQVTAfP2TOdtJJEsCoYl1AnOkqDqMvXb9/7pfgZZ7r8YrEyurrAvAoAjHOGXKRybay+5CsExqIH6liccw==} + + '@types/filesystem@0.0.36': + resolution: {integrity: sha512-vPDXOZuannb9FZdxgHnqSwAG/jvdGM8Wq+6N4D/d80z+D4HWH+bItqsZaVRQykAn6WEVeEkLm2oQigyHtgb0RA==} + + '@types/filewriter@0.0.33': + resolution: {integrity: sha512-xFU8ZXTw4gd358lb2jw25nxY9QAgqn2+bKKjKOYfNCzN4DKCFetK7sPtrlpg66Ywe3vWY9FNxprZawAh9wfJ3g==} + + '@types/fs-extra@11.0.4': + resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==} + + '@types/har-format@1.2.15': + resolution: {integrity: sha512-RpQH4rXLuvTXKR0zqHq3go0RVXYv/YVqv4TnPH95VbwUxZdQlK1EtcMvQvMpDngHbt13Csh9Z4qT9AbkiQH5BA==} + + '@types/html-minifier-terser@7.0.2': + resolution: {integrity: sha512-mm2HqV22l8lFQh4r2oSsOEVea+m0qqxEmwpc9kC1p/XzmjLWrReR9D/GRs8Pex2NX/imyEH9c5IU/7tMBQCHOA==} - /@types/filewriter@0.0.31: - resolution: {integrity: sha512-12df1utOvPC80+UaVoOO1d81X8pa5MefHNS+gWX9R2ucSESpMz9K5QwlTWDGKASrzCpSFwj7NPYh+nTsolgEGA==} - dev: true + '@types/jsonfile@6.1.4': + resolution: {integrity: sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==} - /@types/har-format@1.2.14: - resolution: {integrity: sha512-pEmBAoccWvO6XbSI8A7KvIDGEoKtlLWtdqVCKoVBcCDSFvR4Ijd7zGLu7MWGEqk2r8D54uWlMRt+VZuSrfFMzQ==} - dev: true + '@types/node@20.14.8': + resolution: {integrity: sha512-DO+2/jZinXfROG7j7WKFn/3C6nFwxy2lLpgLjEXJz+0XKphZlTLJ14mo8Vfg8X5BWN6XjyESXq+LcYdT7tR3bA==} - /abbrev@1.1.1: - resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} - dev: true + acorn@8.12.1: + resolution: {integrity: sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==} + engines: {node: '>=0.4.0'} + hasBin: true - /ansi-regex@5.0.1: + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} - dev: false - /ansi-regex@6.0.1: - resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} + ansi-regex@6.1.0: + resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} engines: {node: '>=12'} - dev: false - /ansi-styles@4.3.0: + ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} - dependencies: - color-convert: 2.0.1 - dev: false - /ansi-styles@6.2.1: + ansi-styles@6.2.1: resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} engines: {node: '>=12'} - dev: false - /anymatch@3.1.3: + anymatch@3.1.3: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} - dependencies: - normalize-path: 3.0.0 - picomatch: 2.3.1 - dev: true - /balanced-match@1.0.2: + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - /binary-extensions@2.2.0: - resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} - dev: true - /brace-expansion@1.1.11: + brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} - dependencies: - balanced-match: 1.0.2 - concat-map: 0.0.1 - dev: true - /brace-expansion@2.0.1: + brace-expansion@2.0.1: resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} - dependencies: - balanced-match: 1.0.2 - dev: false - /braces@3.0.2: - resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - dependencies: - fill-range: 7.0.1 - dev: true - /chokidar@3.5.3: - resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + camel-case@4.1.2: + resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} - dependencies: - anymatch: 3.1.3 - braces: 3.0.2 - glob-parent: 5.1.2 - is-binary-path: 2.1.0 - is-glob: 4.0.3 - normalize-path: 3.0.0 - readdirp: 3.6.0 - optionalDependencies: - fsevents: 2.3.3 - dev: true - /color-convert@2.0.1: + clean-css@5.3.3: + resolution: {integrity: sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==} + engines: {node: '>= 10.0'} + + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} - dependencies: - color-name: 1.1.4 - dev: false - /color-name@1.1.4: + color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - dev: false - /concat-map@0.0.1: + commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - dev: true - /cross-spawn@7.0.3: + cross-spawn@7.0.3: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - dev: false - /debug@3.2.7(supports-color@5.5.0): - resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + debug@4.3.7: + resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} + engines: {node: '>=6.0'} peerDependencies: supports-color: '*' peerDependenciesMeta: supports-color: optional: true - dependencies: - ms: 2.1.3 - supports-color: 5.5.0 - dev: true - /eastasianwidth@0.2.0: + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + domutils@3.1.0: + resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==} + + dot-case@3.0.4: + resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} + + eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - dev: false - /emoji-regex@8.0.0: + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - dev: false - /emoji-regex@9.2.2: + emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - dev: false - /fill-range@7.0.1: - resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} - dependencies: - to-regex-range: 5.0.1 - dev: true - /foreground-child@3.1.1: - resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==} + foreground-child@3.3.0: + resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} engines: {node: '>=14'} - dependencies: - cross-spawn: 7.0.3 - signal-exit: 4.1.0 - dev: false - /fs-extra@11.1.1: - resolution: {integrity: sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==} + fs-extra@11.2.0: + resolution: {integrity: sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==} engines: {node: '>=14.14'} - dependencies: - graceful-fs: 4.2.11 - jsonfile: 6.1.0 - universalify: 2.0.0 - dev: false - /fsevents@2.3.3: + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] - requiresBuild: true - dev: true - optional: true - /glob-parent@5.1.2: + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} - dependencies: - is-glob: 4.0.3 - dev: true - /glob@10.3.10: - resolution: {integrity: sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==} - engines: {node: '>=16 || 14 >=14.17'} + glob@10.4.5: + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} hasBin: true - dependencies: - foreground-child: 3.1.1 - jackspeak: 2.3.6 - minimatch: 9.0.3 - minipass: 7.0.4 - path-scurry: 1.10.1 - dev: false - /graceful-fs@4.2.11: + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - dev: false - /has-flag@3.0.0: + has-flag@3.0.0: resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} engines: {node: '>=4'} - dev: true - /ignore-by-default@1.0.1: + html-minifier-terser@7.2.0: + resolution: {integrity: sha512-tXgn3QfqPIpGl9o+K5tpcj3/MN4SfLtsx2GWwBC3SSd0tXQGyF3gsSqad8loJgKZGM3ZxbYDd5yhiBIdWpmvLA==} + engines: {node: ^14.13.1 || >=16.0.0} + hasBin: true + + htmlparser2@9.1.0: + resolution: {integrity: sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==} + + ignore-by-default@1.0.1: resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==} - dev: true - /is-binary-path@2.1.0: + is-binary-path@2.1.0: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} - dependencies: - binary-extensions: 2.2.0 - dev: true - /is-extglob@2.1.1: + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} - dev: true - /is-fullwidth-code-point@3.0.0: + is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} - dev: false - /is-glob@4.0.3: + is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} - dependencies: - is-extglob: 2.1.1 - dev: true - /is-number@7.0.0: + is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} - dev: true - /isexe@2.0.0: + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - dev: false - /jackspeak@2.3.6: - resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} - engines: {node: '>=14'} - dependencies: - '@isaacs/cliui': 8.0.2 - optionalDependencies: - '@pkgjs/parseargs': 0.11.0 - dev: false + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} - /jsonfile@6.1.0: + jsonfile@6.1.0: resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} - dependencies: - universalify: 2.0.0 - optionalDependencies: - graceful-fs: 4.2.11 - dev: false - /lru-cache@10.0.1: - resolution: {integrity: sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==} - engines: {node: 14 || >=16.14} - dev: false + lower-case@2.0.2: + resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} - /lru-cache@6.0.0: - resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} - engines: {node: '>=10'} - dependencies: - yallist: 4.0.0 - dev: true + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} - /minimatch@3.1.2: + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} - dependencies: - brace-expansion: 1.1.11 - dev: true - /minimatch@9.0.3: - resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} - dependencies: - brace-expansion: 2.0.1 - dev: false - /minipass@7.0.4: - resolution: {integrity: sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==} + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} - dev: false - /ms@2.1.3: + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - dev: true - /nodemon@3.0.1: - resolution: {integrity: sha512-g9AZ7HmkhQkqXkRc20w+ZfQ73cHLbE8hnPbtaFbFtCumZsjyMhKk9LajQ07U5Ux28lvFjZ5X7HvWR1xzU8jHVw==} - engines: {node: '>=10'} - hasBin: true - dependencies: - chokidar: 3.5.3 - debug: 3.2.7(supports-color@5.5.0) - ignore-by-default: 1.0.1 - minimatch: 3.1.2 - pstree.remy: 1.1.8 - semver: 7.5.4 - simple-update-notifier: 2.0.0 - supports-color: 5.5.0 - touch: 3.1.0 - undefsafe: 2.0.5 - dev: true + no-case@3.0.4: + resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} - /nopt@1.0.10: - resolution: {integrity: sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==} + nodemon@3.1.4: + resolution: {integrity: sha512-wjPBbFhtpJwmIeY2yP7QF+UKzPfltVGtfce1g/bB15/8vCGZj8uxD62b/b9M9/WVgme0NZudpownKN+c0plXlQ==} + engines: {node: '>=10'} hasBin: true - dependencies: - abbrev: 1.1.1 - dev: true - /normalize-path@3.0.0: + normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} - dev: true - /path-key@3.1.1: + package-json-from-dist@1.0.0: + resolution: {integrity: sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==} + + param-case@3.0.4: + resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==} + + pascal-case@3.1.2: + resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} + + path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} - dev: false - /path-scurry@1.10.1: - resolution: {integrity: sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==} - engines: {node: '>=16 || 14 >=14.17'} - dependencies: - lru-cache: 10.0.1 - minipass: 7.0.4 - dev: false + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} - /picomatch@2.3.1: + picomatch@2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} - dev: true - /pstree.remy@1.1.8: + pstree.remy@1.1.8: resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==} - dev: true - /readdirp@3.6.0: + readdirp@3.6.0: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} - dependencies: - picomatch: 2.3.1 - dev: true - /semver@7.5.4: - resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} + relateurl@0.2.7: + resolution: {integrity: sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==} + engines: {node: '>= 0.10'} + + semver@7.6.3: + resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} engines: {node: '>=10'} hasBin: true - dependencies: - lru-cache: 6.0.0 - dev: true - /shebang-command@2.0.0: + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} - dependencies: - shebang-regex: 3.0.0 - dev: false - /shebang-regex@3.0.0: + shebang-regex@3.0.0: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - dev: false - /signal-exit@4.1.0: + signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} - dev: false - /simple-update-notifier@2.0.0: + simple-update-notifier@2.0.0: resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} engines: {node: '>=10'} - dependencies: - semver: 7.5.4 - dev: true - /string-width@4.2.3: + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} - dependencies: - emoji-regex: 8.0.0 - is-fullwidth-code-point: 3.0.0 - strip-ansi: 6.0.1 - dev: false - /string-width@5.1.2: + string-width@5.1.2: resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} engines: {node: '>=12'} - dependencies: - eastasianwidth: 0.2.0 - emoji-regex: 9.2.2 - strip-ansi: 7.1.0 - dev: false - /strip-ansi@6.0.1: + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} - dependencies: - ansi-regex: 5.0.1 - dev: false - /strip-ansi@7.1.0: + strip-ansi@7.1.0: resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} engines: {node: '>=12'} - dependencies: - ansi-regex: 6.0.1 - dev: false - /supports-color@5.5.0: + supports-color@5.5.0: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} engines: {node: '>=4'} - dependencies: - has-flag: 3.0.0 - dev: true - /to-regex-range@5.0.1: + terser@5.32.0: + resolution: {integrity: sha512-v3Gtw3IzpBJ0ugkxEX8U0W6+TnPKRRCWGh1jC/iM/e3Ki5+qvO1L1EAZ56bZasc64aXHwRHNIQEzm6//i5cemQ==} + engines: {node: '>=10'} + hasBin: true + + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} - dependencies: - is-number: 7.0.0 - dev: true - /touch@3.1.0: - resolution: {integrity: sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==} + touch@3.1.1: + resolution: {integrity: sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==} hasBin: true - dependencies: - nopt: 1.0.10 - dev: true - /typescript@5.2.2: - resolution: {integrity: sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==} + tslib@2.7.0: + resolution: {integrity: sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==} + + typescript@5.6.2: + resolution: {integrity: sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==} engines: {node: '>=14.17'} hasBin: true - dev: false - /undefsafe@2.0.5: + undefsafe@2.0.5: resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==} - dev: true - /universalify@2.0.0: - resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==} + undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} - dev: false - /which@2.0.2: + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} hasBin: true - dependencies: - isexe: 2.0.0 - dev: false - /wrap-ansi@7.0.0: + wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + +snapshots: + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@jridgewell/gen-mapping@0.3.5': + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping': 0.3.25 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/set-array@1.2.1': {} + + '@jridgewell/source-map@0.3.6': + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + + '@jridgewell/sourcemap-codec@1.5.0': {} + + '@jridgewell/trace-mapping@0.3.25': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@types/chrome@0.0.246': + dependencies: + '@types/filesystem': 0.0.36 + '@types/har-format': 1.2.15 + + '@types/clean-css@4.2.11': + dependencies: + '@types/node': 20.14.8 + source-map: 0.6.1 + + '@types/filesystem@0.0.36': + dependencies: + '@types/filewriter': 0.0.33 + + '@types/filewriter@0.0.33': {} + + '@types/fs-extra@11.0.4': + dependencies: + '@types/jsonfile': 6.1.4 + '@types/node': 20.14.8 + + '@types/har-format@1.2.15': {} + + '@types/html-minifier-terser@7.0.2': {} + + '@types/jsonfile@6.1.4': + dependencies: + '@types/node': 20.14.8 + + '@types/node@20.14.8': + dependencies: + undici-types: 5.26.5 + + acorn@8.12.1: {} + + ansi-regex@5.0.1: {} + + ansi-regex@6.1.0: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.1: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + balanced-match@1.0.2: {} + + binary-extensions@2.3.0: {} + + brace-expansion@1.1.11: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.1: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + buffer-from@1.1.2: {} + + camel-case@4.1.2: + dependencies: + pascal-case: 3.1.2 + tslib: 2.7.0 + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + clean-css@5.3.3: + dependencies: + source-map: 0.6.1 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + commander@10.0.1: {} + + commander@2.20.3: {} + + concat-map@0.0.1: {} + + cross-spawn@7.0.3: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + debug@4.3.7(supports-color@5.5.0): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 5.5.0 + + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + domelementtype@2.3.0: {} + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + domutils@3.1.0: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + + dot-case@3.0.4: + dependencies: + no-case: 3.0.4 + tslib: 2.7.0 + + eastasianwidth@0.2.0: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + entities@4.5.0: {} + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + foreground-child@3.3.0: + dependencies: + cross-spawn: 7.0.3 + signal-exit: 4.1.0 + + fs-extra@11.2.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + + fsevents@2.3.3: + optional: true + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob@10.4.5: + dependencies: + foreground-child: 3.3.0 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.0 + path-scurry: 1.11.1 + + graceful-fs@4.2.11: {} + + has-flag@3.0.0: {} + + html-minifier-terser@7.2.0: + dependencies: + camel-case: 4.1.2 + clean-css: 5.3.3 + commander: 10.0.1 + entities: 4.5.0 + param-case: 3.0.4 + relateurl: 0.2.7 + terser: 5.32.0 + + htmlparser2@9.1.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.1.0 + entities: 4.5.0 + + ignore-by-default@1.0.1: {} + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + isexe@2.0.0: {} + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + jsonfile@6.1.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + lower-case@2.0.2: + dependencies: + tslib: 2.7.0 + + lru-cache@10.4.3: {} + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.11 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.1 + + minipass@7.1.2: {} + + ms@2.1.3: {} + + no-case@3.0.4: + dependencies: + lower-case: 2.0.2 + tslib: 2.7.0 + + nodemon@3.1.4: + dependencies: + chokidar: 3.6.0 + debug: 4.3.7(supports-color@5.5.0) + ignore-by-default: 1.0.1 + minimatch: 3.1.2 + pstree.remy: 1.1.8 + semver: 7.6.3 + simple-update-notifier: 2.0.0 + supports-color: 5.5.0 + touch: 3.1.1 + undefsafe: 2.0.5 + + normalize-path@3.0.0: {} + + package-json-from-dist@1.0.0: {} + + param-case@3.0.4: + dependencies: + dot-case: 3.0.4 + tslib: 2.7.0 + + pascal-case@3.1.2: + dependencies: + no-case: 3.0.4 + tslib: 2.7.0 + + path-key@3.1.1: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.2 + + picomatch@2.3.1: {} + + pstree.remy@1.1.8: {} + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + relateurl@0.2.7: {} + + semver@7.6.3: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + signal-exit@4.1.0: {} + + simple-update-notifier@2.0.0: + dependencies: + semver: 7.6.3 + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.0: + dependencies: + ansi-regex: 6.1.0 + + supports-color@5.5.0: + dependencies: + has-flag: 3.0.0 + + terser@5.32.0: + dependencies: + '@jridgewell/source-map': 0.3.6 + acorn: 8.12.1 + commander: 2.20.3 + source-map-support: 0.5.21 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + touch@3.1.1: {} + + tslib@2.7.0: {} + + typescript@5.6.2: {} + + undefsafe@2.0.5: {} + + undici-types@5.26.5: {} + + universalify@2.0.1: {} + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 string-width: 4.2.3 strip-ansi: 6.0.1 - dev: false - /wrap-ansi@8.1.0: - resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} - engines: {node: '>=12'} + wrap-ansi@8.1.0: dependencies: ansi-styles: 6.2.1 string-width: 5.1.2 strip-ansi: 7.1.0 - dev: false - - /yallist@4.0.0: - resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - dev: true diff --git a/src/assets/aw_button.css b/src/assets/aw_button.css deleted file mode 100644 index 1c3b974..0000000 --- a/src/assets/aw_button.css +++ /dev/null @@ -1,8 +0,0 @@ -#aw_button { - color: #8954d4; - transition-duration: 150ms; -} - -#aw_button:hover { - color: #eb3ac2; -} diff --git a/src/assets/daijobu.png b/src/assets/daijobu.png new file mode 100644 index 0000000..d14c142 Binary files /dev/null and b/src/assets/daijobu.png differ diff --git a/src/assets/grass_doge.png b/src/assets/grass_doge.png new file mode 100644 index 0000000..2649ad2 Binary files /dev/null and b/src/assets/grass_doge.png differ diff --git a/src/assets/icon128.png b/src/assets/icon128.png deleted file mode 100644 index 811fd5b..0000000 Binary files a/src/assets/icon128.png and /dev/null differ diff --git a/src/assets/icon256.png b/src/assets/icon256.png new file mode 100644 index 0000000..7f4c4b8 Binary files /dev/null and b/src/assets/icon256.png differ diff --git a/src/assets/kiana_shake.gif b/src/assets/kiana_shake.gif new file mode 100644 index 0000000..91b51f5 Binary files /dev/null and b/src/assets/kiana_shake.gif differ diff --git a/src/assets/laffey_shake.gif b/src/assets/laffey_shake.gif new file mode 100644 index 0000000..3f49599 Binary files /dev/null and b/src/assets/laffey_shake.gif differ diff --git a/src/assets/nod.gif b/src/assets/nod.gif new file mode 100644 index 0000000..6282770 Binary files /dev/null and b/src/assets/nod.gif differ diff --git a/src/manifest.json b/src/manifest.json index d1ee53a..63d94e6 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -1,15 +1,21 @@ { - "name": "AniList to AniWave", - "description": "Adds a button on AniList anime pages that searches the anime on AniWave.", - "version": "7.0.0", + "name": "AniList Watcher", + "description": "Adds a button on AniList anime and manga pages to quickly open media on popular streaming and reading sites.", + "version": "8.0.0", "manifest_version": 3, "background": { - "service_worker": "scripts/service-worker.js" + "service_worker": "scripts/background/service-worker.js", + "type": "module" + }, + "action": { + "default_popup": "views/popup.html" }, "permissions": ["scripting", "tabs", "storage"], - "host_permissions": ["https://anilist.co/*", "https://aniwave.to/*"], + "host_permissions": ["https://anilist.co/*", "https://api.myanimelist.net/*", "https://hianime.to/*", "https://mangafire.to/*", "https://mangareader.to/*"], "icons": { - "128": "assets/icon128.png" + "256": "assets/icon256.png" }, - "options_page": "views/options.html" + "options_page": "views/options.html", + "incognito": "split", + "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjEIfilATbN4nrvzwkA3Cdm3wv5CiTGh5/sFOr2qEcdzL2tGG5wpJVOHqj/IfZ++RBMUDM2XM9zjxyNG+SQw2ND8sM+9ztzDKgS6jua85s5s9FftkbFecxXr73kqd/Iq2WWjOSHryE7yxzav4lajiG+99HF+sWEmBMiXbynUAkG4RKBEFSM3+4AxAhgjOkCQAVDLyXck54pNdnQLuEZQVhAAvzuFszYFm7ogKxUZHWHLWQutwFeMsxN/7Ilm+zI6pfC7kHTlpUcmbRLOp5FxiEqmFkewc95ZmsGGwgPPkuVsKnocMb7PYsIDdEKjNCO8mp8Jbsu7sO0MxRzZAZHANCwIDAQAB" } diff --git a/src/scripts/background/service-worker.ts b/src/scripts/background/service-worker.ts new file mode 100644 index 0000000..0d99f6d --- /dev/null +++ b/src/scripts/background/service-worker.ts @@ -0,0 +1,1675 @@ +//#region Media Data +type Season = 'WINTER' | 'SPRING' | 'SUMMER' | 'FALL'; +type SeasonMalAnime = 'winter' | 'spring' | 'summer' | 'fall'; + +type Format = 'TV' | 'TV_SHORT' | 'MOVIE' | 'SPECIAL' | 'OVA' | 'ONA' | 'MUSIC' | 'MANGA' | 'NOVEL' | 'ONE_SHOT'; +type FormatMalAnime = 'unknown' | 'tv' | 'ova' | 'movie' | 'special' | 'ona' | 'music'; +type FormatMalManga = 'unknown' | 'manga' | 'novel' | 'one_shot' | 'doujinshi' | 'manhwa' | 'manhua' | 'oel'; + +type Genre = + | 'Action' + | 'Adventure' + | 'Comedy' + | 'Drama' + | 'Ecchi' + | 'Fantasy' + | 'Horror' + | 'Mahou Shoujo' + | 'Mecha' + | 'Music' + | 'Mystery' + | 'Psychological' + | 'Romance' + | 'Sci-Fi' + | 'Slice of Life' + | 'Sports' + | 'Supernatural' + | 'Thriller'; + +enum GenreMalAnime { + Action = 1, + Adventure = 2, + Racing = 3, + Comedy = 4, + 'Avant Garde' = 5, + Mythology = 6, + Mystery = 7, + Drama = 8, + Ecchi = 9, + Fantasy = 10, + 'Strategy Game' = 11, + Hentai = 12, + Historical = 13, + Horror = 14, + Kids = 15, + 'Martial Arts' = 17, + Mecha = 18, + Music = 19, + Parody = 20, + Samurai = 21, + Romance = 22, + School = 23, + 'Sci-Fi' = 24, + Shoujo = 25, + 'Girls Love' = 26, + Shounen = 27, + 'Boys Love' = 28, + Space = 29, + Sports = 30, + 'Super Power' = 31, + Vampire = 32, + Harem = 35, + 'Slice of Life' = 36, + Supernatural = 37, + Military = 38, + Detective = 39, + Psychological = 40, + Suspense = 41, + Seinen = 42, + Josei = 43, + 'Award Winning' = 46, + Gourmet = 47, + Workplace = 48, + Erotica = 49, + 'Adult Cast' = 50, + Anthropomorphic = 51, + CGDCT = 52, + Childcare = 53, + 'Combat Sports' = 54, + Delinquents = 55, + Educational = 56, + 'Gag Humor' = 57, + Gore = 58, + 'High Stakes Game' = 59, + 'Idols (Female)' = 60, + 'Idols (Male)' = 61, + Isekai = 62, + Iyashikei = 63, + 'Love Polygon' = 64, + 'Magical Sex Shift' = 65, + 'Mahou Shoujo' = 66, + Medical = 67, + 'Organized Crime' = 68, + 'Otaku Culture' = 69, + 'Performing Arts' = 70, + Pets = 71, + Reincarnation = 72, + 'Reverse Harem' = 73, + 'Romantic Subtext' = 74, + Showbiz = 75, + Survival = 76, + 'Team Sports' = 77, + 'Time Travel' = 78, + 'Video Game' = 79, + 'Visual Arts' = 80, + Crossdressing = 81 +} +enum GenreMalManga { + Action = 1, + Adventure = 2, + Racing = 3, + Comedy = 4, + 'Avant Garde' = 5, + Mythology = 6, + Mystery = 7, + Drama = 8, + Ecchi = 9, + Fantasy = 10, + 'Strategy Game' = 11, + Hentai = 12, + Historical = 13, + Horror = 14, + Kids = 15, + 'Martial Arts' = 17, + Mecha = 18, + Music = 19, + Parody = 20, + Samurai = 21, + Romance = 22, + School = 23, + 'Sci-Fi' = 24, + Shoujo = 25, + 'Girls Love' = 26, + Shounen = 27, + 'Boys Love' = 28, + Space = 29, + Sports = 30, + 'Super Power' = 31, + Vampire = 32, + Harem = 35, + 'Slice of Life' = 36, + Supernatural = 37, + Military = 38, + Detective = 39, + Psychological = 40, + Seinen = 41, + Josei = 42, + Crossdressing = 44, + Suspense = 45, + 'Award Winning' = 46, + Gourmet = 47, + Workplace = 48, + Erotica = 49, + 'Adult Cast' = 50, + Anthropomorphic = 51, + CGDCT = 52, + Childcare = 53, + 'Combat Sports' = 54, + Delinquents = 55, + Educational = 56, + 'Gag Humor' = 57, + Gore = 58, + 'High Stakes Game' = 59, + 'Idols (Female)' = 60, + 'Idols (Male)' = 61, + Isekai = 62, + Iyashikei = 63, + 'Love Polygon' = 64, + 'Magical Sex Shift' = 65, + 'Mahou Shoujo' = 66, + Medical = 67, + Memoir = 68, + 'Organized Crime' = 69, + 'Otaku Culture' = 70, + 'Performing Arts' = 71, + Pets = 72, + Reincarnation = 73, + 'Reverse Harem' = 74, + 'Romantic Subtext' = 75, + Showbiz = 76, + Survival = 77, + 'Team Sports' = 78, + 'Time Travel' = 79, + 'Video Game' = 80, + Villainess = 81, + 'Visual Arts' = 82 +} + +type Status = 'FINISHED' | 'RELEASING' | 'NOT_YET_RELEASED' | 'CANCELLED' | 'HIATUS'; +type StatusMalAnime = 'finished_airing' | 'currently_airing' | 'not_yet_aired'; +type StatusMalManga = 'finished' | 'currently_publishing' | 'not_yet_published'; + +interface MediaData { + id: number; + title: { + romaji: string | null; + english: string | null; + native: string | null; + } | null; + season: Season | null; + seasonYear: number | null; + startDate: { + year: number | null; + } | null; + format: Format | null; + genres: Genre[] | null; + status: Status | null; + idMal: number | null; + malData?: { + id: number; + title: string; + alternative_titles: { + en: string | null; + ja: string | null; + } | null; + media_type: FormatMalAnime | FormatMalManga; + status: StatusMalAnime | StatusMalManga; + genres: { id: number; name: keyof typeof GenreMalAnime | keyof typeof GenreMalManga }[]; + start_season?: { + year: number; + season: SeasonMalAnime; + }; + start_date: string | null; + }; +} + +async function getMediaData(mediaId: number, mal: boolean): Promise { + const data = await fetch('https://graphql.anilist.co', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + query: `query($id:Int){Media(id:$id){id,idMal,title{romaji,english,native},season,seasonYear,startDate{year},format,genres,status}}`, + variables: { id: mediaId } + }) + }).catch(error => { + console.log('AW: ', error); + return null; + }); + + if (!data) return null; + + const media: MediaData = (await data.json()).data.Media; + + if (mal && media.idMal && media.format) { + const path = media.format === 'MANGA' || media.format === 'ONE_SHOT' || media.format === 'NOVEL' ? 'manga' : 'anime'; + + const malData = await fetch(`https://aw-watcher-proxy.vercel.app/?path=${path}&id=${media.idMal}`, { + method: 'GET' + }).catch(error => { + console.error('AW: ', error); + return null; + }); + + if (!malData) return media; + + const malMedia: MediaData['malData'] | null = await malData + .json() + .then(data => { + if (data.error || data.error === '') { + console.error('AW: ', data.error); + return null; + } + + return data; + }) + .catch(e => { + console.error('AW: ', e); + return null; + }); + + if (!malMedia) return media; + + const compoundData: MediaData = { ...media, malData: malMedia, idMal: malMedia.id }; + + return compoundData; + } + + return media; +} +//#endregion + +//#region Provider Manager +interface UserPreferences { + directWatchPageLink?: boolean; + streamingSiteId?: string; + readingSiteId?: string; + langOrder?: string; + withGenres?: boolean; +} + +abstract class BaseProvider { + public abstract readonly type: 'anime' | 'manga'; + public abstract readonly usesMal: boolean; + public abstract readonly id: string; + public abstract readonly displayName: string; + public abstract readonly baseColor: string; + public abstract handle(data: MediaData, preferences: UserPreferences, preferredTitle: string): Promise; + public abstract getSearchUrl(data: MediaData, preferences: UserPreferences, preferredTitle: string): string; + public abstract readonly parsingTarget: string | null; + + public readonly normalAdjustDark: number = 0.1; + public readonly hoverAdjustDark: number = 0.4; + + public readonly normalAdjustLight: number = -0.1; + public readonly hoverAdjustLight: number = -0.4; +} + +class ProviderManager { + private providers: BaseProvider[] = []; + + public register(...providers: BaseProvider[]): this { + this.providers.push(...providers); + + return this; + } + + // public unregister(id: string): boolean { + // const index = this.providers.findIndex(provider => provider.id === id); + // if (index === -1) return false; + + // this.providers.splice(index, 1); + // return true; + // } + + public getAllIds(): string[] { + return this.providers.map(provider => provider.id); + } + + public getById(id: string): BaseProvider | null { + return this.providers.find(provider => provider.id === id) ?? null; + } + + public getUserProvider(path: string, preferences: UserPreferences): BaseProvider { + const requestedProviderId = path === 'anime' ? preferences.streamingSiteId! : preferences.readingSiteId!; + let provider = this.getById(requestedProviderId); + + if (!provider) { + console.log(`AW: Provider ${requestedProviderId} not found`); + + const defaultProvider = this.providers.find(p => p.type === path)!; + + chrome.storage.sync.set(path === 'anime' ? { streamingSiteId: defaultProvider.id } : { readingSiteId: defaultProvider.id }); + console.log(`AW: Changed default ${path} provider to ${defaultProvider.id}`); + + provider = defaultProvider; + } + + return provider; + } + + public async handle(data: MediaData, path: string, preferences: UserPreferences, preferredTitle: string | null): Promise<{ provider: BaseProvider | null; url: string | null }> { + const provider = this.getUserProvider(path, preferences); + + if (!preferredTitle) { + console.log(`AW: No preferred title!`); + return { provider: provider, url: null }; + } + + const url = await provider.handle(data, preferences, preferredTitle).catch(e => { + console.log(`AW: ERROR`, e); + return null; + }); + + return { provider, url }; + } +} +//#endregion + +//#region Utils +async function getRawHtml(url: string): Promise { + return await fetch(url).then(response => response.text()); +} + +function parseRawHtml(rawHtml: string, provider: BaseProvider): Promise { + return new Promise((resolve, reject) => { + chrome.tabs.query({ active: true, currentWindow: true }, tabs => { + if (tabs.length > 0 && tabs[0].id) { + chrome.tabs.sendMessage(tabs[0].id, { action: 'parseHtml', html: rawHtml, provider }, (response: string | null) => { + resolve(response); + }); + } else { + reject(null); + } + }); + }); +} + +function getTheme(): Promise<'light' | 'dark' | null> { + return new Promise((resolve, reject) => { + chrome.tabs.query({ active: true, currentWindow: true }, tabs => { + if (tabs.length > 0 && tabs[0].id) { + chrome.tabs.sendMessage(tabs[0].id, { action: 'getTheme' }, (response: string | null) => { + if (response === null || response === 'contrast') { + resolve('light'); + } else if (response === 'dark') { + resolve('dark'); + } + }); + } else { + reject(null); + } + }); + }); +} +//#endregion + +//#region Providers +class HianimeProvider extends BaseProvider { + public readonly usesMal = false; + public readonly type = 'anime'; + public readonly id = 'hianime'; + public readonly displayName = 'HiAnime'; + + public readonly baseColor = '#bd84a2'; + + public readonly parsingTarget = `#main-content > section > div.tab-content > div > div.film_list-wrap > div:nth-child(1) > div.film-poster > a`; + + public async handle(data: MediaData, preferences: UserPreferences, preferredTitle: string | null): Promise { + const direct = Boolean(preferences.directWatchPageLink); + + const searchUrl = this.getSearchUrl(data, preferences, preferredTitle); + + if (!direct) return searchUrl; + + const rawHtml = await getRawHtml(searchUrl).catch(e => { + console.log(`AW: ERROR`, e); + + return null; + }); + + if (!rawHtml) { + console.log(`AW: Could not fetch, falling back to search url`); + return searchUrl; + } + + const directUrl = await parseRawHtml(rawHtml, this); + + if (!directUrl) { + console.log(`AW: Could not find direct url, falling back to search url`); + return searchUrl; + } + + console.log(`AW: Found direct url: ${directUrl}`); + + return `https://hianime.to${directUrl}`; + } + + public getSearchUrl(data: MediaData, preferences: UserPreferences, preferredTitle: string | null): string { + if (!preferredTitle) throw new Error('No preferred title provided'); + + const endpoint = new URL('https://hianime.to/search'); + + endpoint.searchParams.set('keyword', preferredTitle); + + const typeMap: Record = { + TV: 2, + TV_SHORT: 2, // non-existent, TV fallback + MOVIE: 1, + SPECIAL: 5, + OVA: 3, + ONA: 4, + MUSIC: 6, + MANGA: 2, // non-existent, TV fallback + NOVEL: 2, // non-existent, TV fallback + ONE_SHOT: null // non-existent, no fallback + }; + + const type = data.format ? typeMap[data.format] : null; + + if (type) endpoint.searchParams.set('type', type.toString()); + + const statusMap: Record = { + FINISHED: 1, + RELEASING: 2, + NOT_YET_RELEASED: 3, + CANCELLED: null, // non-existent, no fallback + HIATUS: null // non-existent, no fallback + }; + + const status = data.status ? statusMap[data.status] : null; + + if (status) endpoint.searchParams.set('status', status.toString()); + + const seasonMap: Record = { + WINTER: 4, + SPRING: 1, + SUMMER: 2, + FALL: 3 + }; + + const season = data.season ? seasonMap[data.season] : null; + + if (season) endpoint.searchParams.set('season', season.toString()); + + if (data.seasonYear) endpoint.searchParams.set('sy', data.seasonYear.toString()); + + if (Boolean(preferences.withGenres)) { + console.log('AW: Appending genres...'); + + const genreMap: Record = { + Action: 1, + Adventure: 2, + Comedy: 4, + Drama: 8, + Ecchi: 9, + Fantasy: 10, + Horror: 14, + 'Mahou Shoujo': null, + Mecha: 18, + Music: 19, + Mystery: null, + Psychological: 40, + Romance: 22, + 'Sci-Fi': 24, + 'Slice of Life': 36, + Sports: 30, + Supernatural: 37, + Thriller: 41 + }; + + // const Hianime_genre_ids = [ + // 1, // Action + // 2, // Adventure + // 3, // Cars + // 4, // Comedy + // 5, // Dementia + // 6, // Demons + // 8, // Drama + // 9, // Ecchi + // 10, // Fantasy + // 11, // Game + // 35, // Harem + // 13, // Historical + // 14, // Horror + // 44, // Isekai + // 43, // Josei + // 15, // Kids + // 16, // Magic + // 17, // Martial Arts + // 18, // Mecha + // 38, // Military + // 19, // Music + // 7, // Mistery + // 20, // Parody + // 39, // Police + // 40, // Psychological + // 22, // Romance + // 21, // Samurai + // 23, // School + // 24, // Sci-Fi + // 42, // Seinen + // 26, // Shoujo + // 25, // Shouko Ai + // 27, // Shounen + // 28, // Shounen Ai + // 36, // Slice of Life + // 29, // Space + // 30, // Sports + // 31, // Super Power + // 37, // Supernatural + // 41, // Thriller + // 32 // Vampire + // ]; + + if (data.genres) { + const genreIds: string[] = []; + + for (const g of data.genres) { + const id = genreMap[g]; + + if (id) genreIds.push(id.toString()); + } + + const genresString = genreIds.join(','); + + endpoint.searchParams.set('genres', genresString); + } + } + + return endpoint.toString(); + } +} + +class MiruroProvider extends BaseProvider { + public readonly usesMal = false; + public readonly type = 'anime'; + public readonly id = 'miruro'; + public readonly displayName = 'Miruro'; + + public readonly baseColor = '#535388'; + + public readonly parsingTarget = null; + + public async handle(data: MediaData, preferences: UserPreferences, preferredTitle: string | null): Promise { + const direct = Boolean(preferences.directWatchPageLink); + + const searchUrl = this.getSearchUrl(data, preferences, preferredTitle); + + if (!direct) return searchUrl; + + return `https://www.miruro.tv/watch?id=${data.id}`; + } + + public getSearchUrl(data: MediaData, preferences: UserPreferences, preferredTitle: string | null): string { + if (!preferredTitle) throw new Error('No preferred title provided'); + + const endpoint = new URL('https://www.miruro.tv/search'); + + endpoint.searchParams.set('query', preferredTitle); + + const type = data.format === 'MANGA' || data.format === 'NOVEL' ? 'TV' : data.format === 'ONE_SHOT' ? null : data.format; + + if (type) endpoint.searchParams.set('format', type.toString()); + + const status = data.status === 'HIATUS' ? null : data.status; + + if (status) endpoint.searchParams.set('status', status); + + if (data.season) endpoint.searchParams.set('season', data.season); + + if (data.seasonYear) endpoint.searchParams.set('year', data.seasonYear.toString()); + + if (Boolean(preferences.withGenres)) { + console.log('AW: Appending genres...'); + + const genreMap: Record = { + Action: true, + Adventure: true, + Comedy: true, + Drama: true, + Ecchi: false, // Doesn't exist on miruro + Fantasy: true, + Horror: true, + 'Mahou Shoujo': true, + Mecha: true, + Music: true, + Mystery: true, + Psychological: true, + Romance: true, + 'Sci-Fi': true, + 'Slice of Life': true, + Sports: true, + Supernatural: true, + Thriller: true + }; + + if (data.genres) { + const genres: string[] = []; + + for (const g of data.genres) { + const enabled = genreMap[g]; + + if (enabled) genres.push(g); + } + + const genresString = genres.join(','); + + endpoint.searchParams.set('genres', genresString); + } + } + + return endpoint.toString(); + } +} + +// Possibly based on mal data +class AnitakuProvider extends BaseProvider { + public readonly usesMal = true; + public readonly type = 'anime'; + public readonly id = 'anitaku'; + public readonly displayName = 'Anitaku'; + + public readonly baseColor = '#ffc119'; + + public readonly parsingTarget = `#wrapper_bg > section > section.content_left > div > div.last_episodes > ul > li > div > a`; + + public async handle(data: MediaData, preferences: UserPreferences, preferredTitle: string | null): Promise { + const direct = Boolean(preferences.directWatchPageLink); + + const searchUrl = this.getSearchUrl(data, preferences, preferredTitle); + + if (!direct) return searchUrl; + + const rawHtml = await getRawHtml(searchUrl).catch(e => { + console.log(`AW: ERROR`, e); + + return null; + }); + + if (!rawHtml) { + console.log(`AW: Could not fetch, falling back to search url`); + return searchUrl; + } + + const directUrl = await parseRawHtml(rawHtml, this); + + if (!directUrl) { + console.log(`AW: Could not find direct url, falling back to search url`); + return searchUrl; + } + + console.log(`AW: Found direct url: ${directUrl}`); + + return `https://anitaku.pe${directUrl}`; + } + + public getSearchUrl(data: MediaData, preferences: UserPreferences, preferredTitle: string | null): string { + const order = preferences.langOrder!.split(''); + + const titles = [ + { lang: 'n', title: data.idMal ? data.malData?.alternative_titles?.ja : null }, + { lang: 'r', title: data.idMal ? data.malData?.title : null }, + { lang: 'e', title: data.idMal ? data.malData?.alternative_titles?.en : null } + ].map(t => (t.title === '' ? { lang: t.lang, title: null } : t)); + + titles.sort((a, b) => order.indexOf(a.lang) - order.indexOf(b.lang)); + + const title = titles.find(t => t.title !== null && t.title !== undefined)?.title || preferredTitle; + + if (!title) throw new Error(`Could not compute title!`); + + console.log(`AW: ${this.id} title: ${title}`); + + const endpoint = new URL('https://anitaku.pe/filter.html'); + + endpoint.searchParams.set('keyword', title); + + // Type seems to be fucked on anitaku, idfk why + // const typeMap: Record = { + // tv: 1, + // movie: 3, + // music: 32, + // ona: 30, + // ova: 26, + // special: 2, + // unknown: 1 // TV fallback + // }; + + // const type = typeMap[data.malData.media_type as FormatMalAnime]; + // endpoint.searchParams.set('type[]', type.toString()); + + const statusMap: Record = { + currently_airing: 'Ongoing', + finished_airing: 'Completed', + not_yet_aired: 'Upcoming' + }; + + const statusMapAnilist: Record = { + FINISHED: 'Completed', + RELEASING: 'Ongoing', + NOT_YET_RELEASED: 'Upcoming', + CANCELLED: null, + HIATUS: null + }; + + const status = data.malData ? statusMap[data.malData.status as StatusMalAnime] : data.status ? statusMapAnilist[data.status] : null; + if (status) endpoint.searchParams.set('status[]', status); + + if (data.malData && data.malData.start_season) { + // Season also seems to fuck it up + // endpoint.searchParams.set('season[]', data.malData.start_season.season); + endpoint.searchParams.set('year[]', data.malData.start_season.year.toString()); + } else if (data.seasonYear) { + endpoint.searchParams.set('year[]', data.seasonYear.toString()); + } else if (data.startDate && data.startDate.year) { + endpoint.searchParams.set('year[]', data.startDate.year.toString()); + } + + if (Boolean(preferences.withGenres)) { + console.log('AW: Appending genres...'); + + type AnitakuGenres = + | 'action' + | 'adult-cast' + | 'adventure' + | 'anthropomorphic' + | 'avant-garde' + | 'shounen-ai' + | 'cars' + | 'cgdct' + | 'childcare' + | 'comedy' + | 'comic' + | 'crime' + | 'crossdressing' + | 'cultivation' + | 'delinquents' + | 'dementia' + | 'demons' + | 'detective' + | 'drama' + | 'dub' + | 'ecchi' + | 'erotica' + | 'family' + | 'fantasy' + | 'gag-humor' + | 'game' + | 'gender-bender' + | 'gore' + | 'gourmet' + | 'harem' + | 'high-stakes-game' + | 'historical' + | 'horror' + | 'isekai' + | 'iyashikei' + | 'josei' + | 'kids' + | 'love-polygon' + | 'magic' + | 'magical-sex-shift' + | 'mahou-shoujo' + | 'martial-arts' + | 'mecha' + | 'medical' + | 'military' + | 'music' + | 'mystery' + | 'mythology' + | 'organized-crime' + | 'parody' + | 'performing-arts' + | 'pets' + | 'police' + | 'psychological' + | 'racing' + | 'reincarnation' + | 'romance' + | 'romantic-subtext' + | 'samurai' + | 'school' + | 'sci-fi' + | 'seinen' + | 'shoujo' + | 'shoujo-ai' + | 'shounen' + | 'showbiz' + | 'slice-of-life' + | 'space' + | 'sports' + | 'strategy-game' + | 'strong-male-lead' + | 'super-power' + | 'supernatural' + | 'survival' + | 'suspense' + | 'system' + | 'team-sports' + | 'thriller' + | 'time-travel' + | 'vampire' + | 'video-game' + | 'visual-arts' + | 'work-life' + | 'workplace' + | 'yaoi' + | 'yuri'; + + const genreMap: Record = { + Action: 'action', + Adventure: 'adventure', + Racing: 'racing', + Comedy: 'comedy', + 'Avant Garde': 'avant-garde', + Mythology: 'mythology', + Mystery: 'mystery', + Drama: 'drama', + Ecchi: 'ecchi', + Fantasy: 'fantasy', + 'Strategy Game': 'strategy-game', + Hentai: null, + Historical: 'historical', + Horror: 'horror', + Kids: 'kids', + 'Martial Arts': 'martial-arts', + Mecha: 'mecha', + Music: 'music', + Parody: 'parody', + Samurai: 'samurai', + Romance: 'romance', + School: 'school', + 'Sci-Fi': 'sci-fi', + Shoujo: 'shoujo', + 'Girls Love': 'yuri', // not literal + Shounen: 'shounen', + 'Boys Love': 'yaoi', // not literal + Space: 'space', + Sports: 'sports', + 'Super Power': 'super-power', + Vampire: 'vampire', + Harem: 'harem', + 'Slice of Life': 'slice-of-life', + Supernatural: 'supernatural', + Military: 'military', + Detective: 'detective', + Psychological: 'psychological', + Suspense: 'suspense', + Seinen: 'seinen', + Josei: 'josei', + 'Award Winning': null, + Gourmet: 'gourmet', + Workplace: 'workplace', + Erotica: 'erotica', + 'Adult Cast': 'adult-cast', + Anthropomorphic: 'anthropomorphic', + CGDCT: 'cgdct', + Childcare: 'childcare', + 'Combat Sports': null, + Delinquents: 'delinquents', + Educational: null, + 'Gag Humor': 'gag-humor', + Gore: 'gore', + 'High Stakes Game': 'high-stakes-game', + 'Idols (Female)': null, + 'Idols (Male)': null, + Isekai: 'isekai', + Iyashikei: 'iyashikei', + 'Love Polygon': 'love-polygon', + 'Magical Sex Shift': 'magical-sex-shift', + 'Mahou Shoujo': 'mahou-shoujo', + Medical: 'medical', + 'Organized Crime': 'organized-crime', + 'Otaku Culture': null, + 'Performing Arts': 'performing-arts', + Pets: 'pets', + Reincarnation: 'reincarnation', + 'Reverse Harem': null, + 'Romantic Subtext': 'romantic-subtext', + Showbiz: 'showbiz', + Survival: 'survival', + 'Team Sports': 'team-sports', + 'Time Travel': 'time-travel', + 'Video Game': 'video-game', + 'Visual Arts': 'visual-arts', + Crossdressing: 'crossdressing' + }; + + const genreMapAnilist: Record = { + Action: 'action', + Adventure: 'adventure', + Comedy: 'comedy', + Mystery: 'mystery', + Drama: 'drama', + Ecchi: 'ecchi', + Fantasy: 'fantasy', + Horror: 'horror', + Mecha: 'mecha', + Music: 'music', + Romance: 'romance', + 'Sci-Fi': 'sci-fi', + Sports: 'sports', + 'Slice of Life': 'slice-of-life', + Supernatural: 'supernatural', + Psychological: 'psychological', + 'Mahou Shoujo': 'mahou-shoujo', + Thriller: null + }; + + if (data.malData) { + for (const g of data.malData.genres) { + const genreName = g.name as keyof typeof GenreMalAnime; + + const id = genreMap[genreName]; + + if (id) endpoint.searchParams.append('genre[]', id); + } + } else if (data.genres) { + for (const g of data.genres) { + const id = genreMapAnilist[g]; + + if (id) endpoint.searchParams.append('genre[]', id); + } + } + } + + return endpoint.toString(); + } +} + +// Possibly based on mal data +class MangafireProvider extends BaseProvider { + public readonly usesMal = true; + public readonly type = 'manga'; + public readonly id = 'mangafire'; + public readonly displayName = 'MangaFire'; + + public readonly baseColor = '#225174'; + + public readonly parsingTarget = `body > div.wrapper > main > div.container > section > div.original.card-lg > div > div > div > a`; + + public async handle(data: MediaData, preferences: UserPreferences, preferredTitle: string | null): Promise { + const direct = Boolean(preferences.directWatchPageLink); + + const searchUrl = this.getSearchUrl(data, preferences, preferredTitle); + + if (!direct) return searchUrl; + + const rawHtml = await getRawHtml(searchUrl).catch(e => { + console.log(`AW: ERROR`, e); + + return null; + }); + + if (!rawHtml) { + console.log(`AW: Could not fetch, falling back to search url`); + return searchUrl; + } + + const directUrl = await parseRawHtml(rawHtml, this); + + if (!directUrl) { + console.log(`AW: Could not find direct url, falling back to search url`); + return searchUrl; + } + + console.log(`AW: Found direct url: ${directUrl}`); + + return `https://mangafire.to/read${directUrl.slice(6)}`; + } + + public getSearchUrl(data: MediaData, preferences: UserPreferences, preferredTitle: string | null): string { + const order = preferences.langOrder!.split(''); + + const titles = [ + { lang: 'n', title: data.idMal ? data.malData?.alternative_titles?.ja : null }, + { lang: 'r', title: data.idMal ? data.malData?.title : null }, + { lang: 'e', title: data.idMal ? data.malData?.alternative_titles?.en : null } + ].map(t => (t.title === '' ? { lang: t.lang, title: null } : t)); + + titles.sort((a, b) => order.indexOf(a.lang) - order.indexOf(b.lang)); + + const title = titles.find(t => t.title !== null && t.title !== undefined)?.title || preferredTitle; + + if (!title) throw new Error(`Could not compute title!`); + + console.log(`AW: ${this.id} title: ${title}`); + + const endpoint = new URL('https://mangafire.to/filter'); + + endpoint.searchParams.set('keyword', title); + + const typeMap: Record = { + unknown: null, + manga: 'manga', + novel: 'novel', + one_shot: 'one_shot', + doujinshi: 'doujinshi', + manhwa: 'manhwa', + manhua: 'manhua', + oel: null + }; + + const typeMapAnilist: Record = { + TV: null, + TV_SHORT: null, + MOVIE: null, + SPECIAL: null, + OVA: null, + ONA: null, + MUSIC: null, + MANGA: 'manga', + NOVEL: 'novel', + ONE_SHOT: 'one_shot' + }; + + const type = data.malData ? typeMap[data.malData.media_type as FormatMalManga] : data.format ? typeMapAnilist[data.format] : null; + if (type) endpoint.searchParams.set('type[]', type); + + const statusMap: Record = { + finished: 'completed', + currently_publishing: 'releasing', + not_yet_published: 'info' + }; + + const statusMapAnilist: Record = { + FINISHED: 'completed', + RELEASING: 'releasing', + NOT_YET_RELEASED: 'info', + CANCELLED: null, + HIATUS: null + }; + + const status = data.malData ? statusMap[data.malData.status as StatusMalManga] : data.status ? statusMapAnilist[data.status] : null; + if (status) endpoint.searchParams.set('status[]', status); + + function pickDecade(year: number): string | null { + const decades = ['2000s', '1990s', '1980s', '1970s', '1960s', '1950s', '1940s', '1930s']; + + if (year >= 2004) return year.toString(); + + return decades.find(decade => decade.startsWith(`${Math.floor(year / 10) * 10}s`)) || null; + } + + if (data.malData && data.malData.start_date) { + const startYear = parseInt(data.malData.start_date.slice(0, 4)); + + if (!isNaN(startYear)) { + const year = pickDecade(startYear); + if (year) endpoint.searchParams.set('year[]', year); + } + } else if (data.seasonYear) { + const year = pickDecade(data.seasonYear); + if (year) endpoint.searchParams.set('year[]', year); + } else if (data.startDate && data.startDate.year) { + endpoint.searchParams.set('year[]', data.startDate.year.toString()); + } + + if (Boolean(preferences.withGenres)) { + console.log('AW: Appending genres...'); + + const genreMap: Record = { + Action: 1, + Adventure: 78, + 'Avant Garde': 3, + 'Boys Love': 4, + Comedy: 5, + Drama: 6, + Ecchi: 7, + Fantasy: 79, + 'Girls Love': 9, + Gourmet: 10, + Harem: 11, + Horror: 530, + Isekai: 13, + Iyashikei: 531, + Josei: 15, + Kids: 532, + 'Mahou Shoujo': 533, + 'Martial Arts': 534, + Mecha: 19, + Military: 535, + Music: 21, + Mystery: 22, + Parody: 23, + Psychological: 536, + 'Reverse Harem': 25, + Romance: 26, + School: 73, + 'Sci-Fi': 28, + Seinen: 537, + Shoujo: 30, + Shounen: 31, + 'Slice of Life': 538, + Space: 33, + Sports: 34, + 'Super Power': 75, + Supernatural: 76, + Suspense: 37, + Vampire: 39, + Racing: null, + Mythology: null, + 'Strategy Game': null, + Hentai: null, + Historical: null, + Samurai: null, + Detective: null, + Crossdressing: null, + 'Award Winning': null, + Workplace: null, + Erotica: null, + 'Adult Cast': null, + Anthropomorphic: null, + CGDCT: null, + Childcare: null, + 'Combat Sports': null, + Delinquents: null, + Educational: null, + 'Gag Humor': null, + Gore: null, + 'High Stakes Game': null, + 'Idols (Female)': null, + 'Idols (Male)': null, + 'Love Polygon': null, + 'Magical Sex Shift': null, + Medical: null, + Memoir: null, + 'Organized Crime': null, + 'Otaku Culture': null, + 'Performing Arts': null, + Pets: null, + Reincarnation: null, + 'Romantic Subtext': null, + Showbiz: null, + Survival: null, + 'Team Sports': null, + 'Time Travel': null, + 'Video Game': null, + Villainess: null, + 'Visual Arts': null + }; + + const genreMapAnilist: Record = { + Action: 1, + Adventure: 78, + Comedy: 5, + Drama: 6, + Ecchi: 7, + Fantasy: 79, + Horror: 530, + 'Mahou Shoujo': 533, + Mecha: 19, + Music: 21, + Mystery: 22, + Psychological: 536, + Romance: 26, + 'Sci-Fi': 28, + 'Slice of Life': 538, + Sports: 34, + Supernatural: 76, + Thriller: null + }; + + if (data.malData) { + for (const g of data.malData.genres) { + const genreName = g.name as keyof typeof GenreMalManga; + + const id = genreMap[genreName]; + + if (id) endpoint.searchParams.append('genre[]', id.toString()); + } + } else if (data.genres) { + for (const g of data.genres) { + const id = genreMapAnilist[g]; + + if (id) endpoint.searchParams.append('genre[]', id.toString()); + } + } + } + + return endpoint.toString(); + } +} + +class MangareaderProvider extends BaseProvider { + public readonly usesMal = false; + public readonly type = 'manga'; + public readonly id = 'mangareader'; + public readonly displayName = 'MangaReader'; + + public readonly baseColor = '#6a5488'; + + public readonly parsingTarget = null; + + public async handle(data: MediaData, preferences: UserPreferences, preferredTitle: string | null): Promise { + return 'https://mangareader.to'; + } + + public getSearchUrl(data: MediaData, preferences: UserPreferences, preferredTitle: string | null): string { + const order = preferences.langOrder!.split(''); + + const titles = [ + { lang: 'n', title: data.idMal ? data.malData?.alternative_titles?.ja : null }, + { lang: 'r', title: data.idMal ? data.malData?.title : null }, + { lang: 'e', title: data.idMal ? data.malData?.alternative_titles?.en : null } + ].map(t => (t.title === '' ? { lang: t.lang, title: null } : t)); + + titles.sort((a, b) => order.indexOf(a.lang) - order.indexOf(b.lang)); + + const title = titles.find(t => t.title !== null && t.title !== undefined)?.title || preferredTitle; + + if (!title) throw new Error(`Could not compute title!`); + + console.log(`AW: ${this.id} title: ${title}`); + + const endpoint = new URL('https://mangareader.to/filter'); + + endpoint.searchParams.set('keyword', title); + + const typeMap: Record = { + unknown: null, + manga: 1, + novel: 4, + one_shot: 2, + doujinshi: 3, + manhwa: 5, + manhua: 6, + oel: null + }; + + const typeMapAnilist: Record = { + TV: null, + TV_SHORT: null, + MOVIE: null, + SPECIAL: null, + OVA: null, + ONA: null, + MUSIC: null, + MANGA: 1, + NOVEL: 4, + ONE_SHOT: 2 + }; + + const type = data.malData ? typeMap[data.malData.media_type as FormatMalManga] : data.format ? typeMapAnilist[data.format] : null; + if (type) endpoint.searchParams.set('type', type.toString()); + + const statusMap: Record = { + finished: 1, + currently_publishing: 2, + not_yet_published: 5 + }; + + const statusMapAnilist: Record = { + FINISHED: 1, + RELEASING: 2, + NOT_YET_RELEASED: 5, + CANCELLED: 4, + HIATUS: 3 + }; + + const status = data.malData ? statusMap[data.malData.status as StatusMalManga] : data.status ? statusMapAnilist[data.status] : null; + if (status) endpoint.searchParams.set('status', status.toString()); + + // go from here dumbass + + function pickDecade(year: number): string | null { + const decades = ['2000s', '1990s', '1980s', '1970s', '1960s', '1950s', '1940s', '1930s']; + + if (year >= 2004) return year.toString(); + + return decades.find(decade => decade.startsWith(`${Math.floor(year / 10) * 10}s`)) || null; + } + + if (data.malData && data.malData.start_date) { + const startYear = parseInt(data.malData.start_date.slice(0, 4)); + + if (!isNaN(startYear)) { + const year = pickDecade(startYear); + if (year) endpoint.searchParams.set('year[]', year); + } + } else if (data.seasonYear) { + const year = pickDecade(data.seasonYear); + if (year) endpoint.searchParams.set('year[]', year); + } else if (data.startDate && data.startDate.year) { + endpoint.searchParams.set('year[]', data.startDate.year.toString()); + } + + if (Boolean(preferences.withGenres)) { + console.log('AW: Appending genres...'); + + const genreMap: Record = { + Action: 1, + Adventure: 78, + 'Avant Garde': 3, + 'Boys Love': 4, + Comedy: 5, + Drama: 6, + Ecchi: 7, + Fantasy: 79, + 'Girls Love': 9, + Gourmet: 10, + Harem: 11, + Horror: 530, + Isekai: 13, + Iyashikei: 531, + Josei: 15, + Kids: 532, + 'Mahou Shoujo': 533, + 'Martial Arts': 534, + Mecha: 19, + Military: 535, + Music: 21, + Mystery: 22, + Parody: 23, + Psychological: 536, + 'Reverse Harem': 25, + Romance: 26, + School: 73, + 'Sci-Fi': 28, + Seinen: 537, + Shoujo: 30, + Shounen: 31, + 'Slice of Life': 538, + Space: 33, + Sports: 34, + 'Super Power': 75, + Supernatural: 76, + Suspense: 37, + Vampire: 39, + Racing: null, + Mythology: null, + 'Strategy Game': null, + Hentai: null, + Historical: null, + Samurai: null, + Detective: null, + Crossdressing: null, + 'Award Winning': null, + Workplace: null, + Erotica: null, + 'Adult Cast': null, + Anthropomorphic: null, + CGDCT: null, + Childcare: null, + 'Combat Sports': null, + Delinquents: null, + Educational: null, + 'Gag Humor': null, + Gore: null, + 'High Stakes Game': null, + 'Idols (Female)': null, + 'Idols (Male)': null, + 'Love Polygon': null, + 'Magical Sex Shift': null, + Medical: null, + Memoir: null, + 'Organized Crime': null, + 'Otaku Culture': null, + 'Performing Arts': null, + Pets: null, + Reincarnation: null, + 'Romantic Subtext': null, + Showbiz: null, + Survival: null, + 'Team Sports': null, + 'Time Travel': null, + 'Video Game': null, + Villainess: null, + 'Visual Arts': null + }; + + const genreMapAnilist: Record = { + Action: 1, + Adventure: 78, + Comedy: 5, + Drama: 6, + Ecchi: 7, + Fantasy: 79, + Horror: 530, + 'Mahou Shoujo': 533, + Mecha: 19, + Music: 21, + Mystery: 22, + Psychological: 536, + Romance: 26, + 'Sci-Fi': 28, + 'Slice of Life': 538, + Sports: 34, + Supernatural: 76, + Thriller: null + }; + + if (data.malData) { + for (const g of data.malData.genres) { + const genreName = g.name as keyof typeof GenreMalManga; + + const id = genreMap[genreName]; + + if (id) endpoint.searchParams.append('genre[]', id.toString()); + } + } else if (data.genres) { + for (const g of data.genres) { + const id = genreMapAnilist[g]; + + if (id) endpoint.searchParams.append('genre[]', id.toString()); + } + } + } + + return endpoint.toString(); + } +} + +class MangadexProvider extends BaseProvider { + public readonly usesMal = false; + public readonly type = 'manga'; + public readonly id = 'mangadex'; + public readonly displayName = 'MangaDex'; + + public readonly baseColor = '#9b3e26'; + + public readonly parsingTarget = null; + + public async handle(data: MediaData, preferences: UserPreferences, preferredTitle: string | null): Promise { + return 'https://mangadex.org'; + } + + public getSearchUrl(data: MediaData, preferences: UserPreferences, preferredTitle: string | null): string { + return 'https://mangadex.org'; + } +} + +//#endregion + +const manager = new ProviderManager().register( + new MiruroProvider(), // anime, default + new HianimeProvider(), // anime + new AnitakuProvider(), // anime + new MangafireProvider(), // manga, default + new MangadexProvider(), // manga + new MangareaderProvider() // manga +); + +// Runs on extension install, browser version update, extension version update +chrome.runtime.onInstalled.addListener(async () => { + const existing: UserPreferences = await chrome.storage.sync.get(['directWatchPageLink', 'streamingSiteId', 'readingSiteId', 'langOrder', 'withGenres']); + + // If not exists, create missing + if (!existing.directWatchPageLink) await chrome.storage.sync.set({ directWatchPageLink: true }); + if (!existing.streamingSiteId) await chrome.storage.sync.set({ streamingSiteId: 'miruro' }); + if (!existing.readingSiteId) await chrome.storage.sync.set({ readingSiteId: 'mangafire' }); + if (!existing.langOrder) await chrome.storage.sync.set({ langOrder: 'ren' }); + if (!existing.withGenres) await chrome.storage.sync.set({ withGenres: true }); + + // const url = new URL(chrome.runtime.getURL('views/options.html')); + // url.searchParams.append('installed', ''); + + // chrome.tabs.create({ url: url.toString() }); + + console.log('AW: Installed!'); +}); + +chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => { + if (changeInfo.status === 'complete') { + if (!tab.url) return console.log(`AW: No URL was found for tab ${tabId}`); + + const url = tab.url.split('/'); + console.log(`AW: Processing ${url.join('/')}`); + + if (url[2] !== 'anilist.co') return console.log(`AW: Domain ${url[2]} is not anilist.co, skipping processing...`); + + await chrome.scripting + .executeScript({ + target: { tabId: tabId }, + files: ['scripts/content/agent.js'] + }) + .then(() => { + console.log(`AW: Injected agent script for ${tab.url}`); + }) + .catch(err => { + console.warn(`An error ocurred but it was caught.\n\n${err}`); + }); + + if ((url[3] === 'anime' || url[3] === 'manga') && url[4] && url[5]) { + console.log(`AW: Processing ${url[3]} page...`); + + const preferences: UserPreferences = await chrome.storage.sync.get(['directWatchPageLink', 'streamingSiteId', 'readingSiteId', 'langOrder', 'withGenres']); + + const data = await getMediaData(parseInt(url[4]), manager.getUserProvider(url[3], preferences).usesMal); + // const data = await getMediaData(parseInt(url[4]), false); + + if (!data) return console.log(`AW: No data was found for ${url[4]}`); + + // Getting title + const order = preferences.langOrder!.split(''); + + console.log(`AW: Lang Order: ${order.join('')}`); + + const titles = [ + { lang: 'n', title: data.title?.native }, + { lang: 'r', title: data.title?.romaji }, + { lang: 'e', title: data.title?.english } + ]; + + titles.sort((a, b) => order.indexOf(a.lang) - order.indexOf(b.lang)); + + const title = titles.find(t => t.title !== null && t.title !== undefined)?.title || null; + + const mediaUrl = await manager.handle(data, url[3], preferences, title).catch(e => { + console.log(`AW: ERROR`, e); + return null; + }); + + if (!mediaUrl || !mediaUrl.url) return console.log(`AW: No media url was found for ${url[4]}`); + if (!mediaUrl.provider) return; + + const theme = (await getTheme()) ?? 'light'; + + await chrome.scripting + .executeScript({ + target: { tabId: tabId }, + func: async (media: MediaData, preferences: UserPreferences, mediaUrl: string, provider: BaseProvider, title: string | null, type: string, theme: 'light' | 'dark') => { + const buttonId = `aniwatcher_button_hopefully_this_is_uniue`; + + console.log(`AW: Cleaning up ${document.URL}`); + + const existingAwButton = document.getElementById(buttonId); + + if (existingAwButton) { + console.log('AW: Removing old button...'); + existingAwButton.remove(); + } else console.log('AW: No old button to remove.'); + + console.log(`AW: Removing default anilist watch button...`); + + const nav = document.querySelector('#app > div.page-content > div > div.header-wrap > div.header > div > div.content > div'); + + if (nav) { + const watchBtn = nav.querySelector('a[href^="/anime/"][href*="/watch"]') as HTMLElement | null; + + if (watchBtn) { + watchBtn.style.display = 'none'; + } else console.log('AW: No watch button to remove.'); + } else console.log('AW: Could not find nav.'); + + if (!title) throw new Error('Title was not found!'); + + console.log(`AW: Found title!`, title); + + // Construct the button + console.log('AW: Constructing button...'); + + function adjustBrightness(hex: string, percent: number) { + hex = hex.replace(/^\s*#|\s*$/g, ''); + + let r = parseInt(hex.substring(0, 2), 16); + let g = parseInt(hex.substring(2, 4), 16); + let b = parseInt(hex.substring(4, 6), 16); + + r = Math.min(255, Math.max(0, r + Math.round(r * percent))); + g = Math.min(255, Math.max(0, g + Math.round(g * percent))); + b = Math.min(255, Math.max(0, b + Math.round(b * percent))); + + const r2 = (r < 16 ? '0' : '') + r.toString(16); + const g2 = (g < 16 ? '0' : '') + g.toString(16); + const b2 = (b < 16 ? '0' : '') + b.toString(16); + + return `#${r2}${g2}${b2}`; + } + + const colorNormal = theme === 'light' ? adjustBrightness(provider.baseColor, provider.normalAdjustLight) : adjustBrightness(provider.baseColor, provider.normalAdjustDark); + const colorHover = theme === 'light' ? adjustBrightness(provider.baseColor, provider.hoverAdjustLight) : adjustBrightness(provider.baseColor, provider.hoverAdjustDark); + + const awButton = document.createElement('a'); + awButton.id = buttonId; + awButton.setAttribute('data-v-5776f768', ''); + awButton.className = 'link'; + // awButton.innerText = ` ${provider.displayName} `; + awButton.innerText = ` ${type === 'anime' ? 'Watch' : 'Read'} `; + awButton.setAttribute('target', '_blank'); + awButton.style.color = colorNormal; + awButton.style.transitionDuration = '150ms'; + awButton.addEventListener('mouseenter', () => { + awButton.style.color = colorHover; + }); + awButton.addEventListener('mouseleave', () => { + awButton.style.color = colorNormal; + }); + + awButton.setAttribute('href', mediaUrl); + + // Append the button to the action panel + const actionPanel = document.querySelector(`#app > div.page-content > div > div.header-wrap > div.header > div > div.content > div`); + if (!actionPanel) throw new Error('Action Panel was not found!'); + + const overview = actionPanel.firstElementChild; + + if (overview) { + overview.insertAdjacentElement('afterend', awButton); + } else { + actionPanel.appendChild(awButton); + } + + console.log(`AW: Button was appended to the action panel! Pointing to: ${mediaUrl}`); + }, + args: [data, preferences, mediaUrl.url, mediaUrl.provider, title, url[3], theme] + }) + .then(() => { + console.log(`AW: Injected content script for ${tab.url}`); + }) + .catch(err => { + console.warn(`An error ocurred but it was caught.\n\n${err}`); + }); + } else { + console.log('AW: Not an anime/manga page'); + } + } +}); + +chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + if (message.action === 'getProviders') { + const providers = manager.getAllIds().map(id => manager.getById(id)!); + + sendResponse(providers); + } +}); diff --git a/src/scripts/cleanup.ts b/src/scripts/cleanup.ts deleted file mode 100644 index 183936f..0000000 --- a/src/scripts/cleanup.ts +++ /dev/null @@ -1,10 +0,0 @@ -(async () => { - console.log(`Cleaning up ${document.URL}`); - - const existingAwButton = document.getElementById('aw_button'); - - if (existingAwButton) { - console.log('Removing old button...'); - existingAwButton.remove(); - } else console.log('No old button to remove.'); -})(); diff --git a/src/scripts/content.ts b/src/scripts/content.ts deleted file mode 100644 index 441ea38..0000000 --- a/src/scripts/content.ts +++ /dev/null @@ -1,236 +0,0 @@ -(async () => { - console.log(`Processing ${document.URL}`); - - //#region Helper functions - function getElementByPath(path: string) { - return document.querySelector(path); - } - //#endregion - - //#region Constants - const awButtonId = 'aw_button'; - //#endregion - - const url = 'https://graphql.anilist.co'; - const query = ` -query ($id: Int) { - Media (id: $id, type: ANIME) { - id - title { - romaji - english - native - } - season, - seasonYear, - format, - genres, - status - } -} -`; - - type Season = 'WINTER' | 'SPRING' | 'SUMMER' | 'FALL'; - - type Format = 'TV' | 'TV_SHORT' | 'MOVIE' | 'SPECIAL' | 'OVA' | 'ONA' | 'MUSIC' | 'MANGA' | 'NOVEL' | 'ONE_SHOT'; - - type Genre = - | 'Action' - | 'Adventure' - | 'Comedy' - | 'Drama' - | 'Ecchi' - | 'Fantasy' - | 'Horror' - | 'Mahou Shoujo' - | 'Mecha' - | 'Music' - | 'Mystery' - | 'Psychological' - | 'Romance' - | 'Sci-Fi' - | 'Slice of Life' - | 'Sports' - | 'Supernatural' - | 'Thriller'; - - type Status = 'FINISHED' | 'RELEASING' | 'NOT_YET_RELEASED' | 'CANCELLED' | 'HIATUS'; - - type Anime = { - id: number; - title: { - romaji: string | null; - english: string | null; - native: string | null; - }; - season: Season | null; - seasonYear: number | null; - format: Format | null; - genres: Genre[] | null; - status: Status | null; - }; - - const variables = { id: document.URL.split('/')[4] }; - - const data = await fetch(url, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - query, - variables - }) - }).catch(error => { - console.error(error); - return null; - }); - - if (!data) return console.error(`API ERROR!`); - - const anime: Anime = (await data.json()).data.Media; - - // Get the title in order of preference - - const order = ['native', 'romaji', 'english']; - const titles = [ - { lang: 'native', title: anime.title.native }, - { lang: 'romaji', title: anime.title.romaji }, - { lang: 'english', title: anime.title.english } - ]; - - titles.sort((a, b) => order.indexOf(a.lang) - order.indexOf(b.lang)); - - const title = titles.find(t => t.title !== null)?.title || null; - - if (!title) throw new Error('Title was not found!'); - - console.log(`Found title!`, title); - - // Construct the button - - console.log('Constructing button...'); - - const awButton = document.createElement('a'); - - awButton.id = awButtonId; - - awButton.setAttribute('data-v-5776f768', ''); - - awButton.className = 'link'; - - awButton.innerText = ' AniWave '; - - // Redirect - const endpoint = new URL('https://aniwave.to/filter'); - - endpoint.searchParams.set('keyword', title); - endpoint.searchParams.set('sort', 'most_relevance'); - - const year = anime.seasonYear; - if (year) endpoint.searchParams.set('year', `${year}`); - - if (anime.status) { - const valid = ['FINISHED', 'RELEASING', 'NOT_YET_RELEASED'].includes(anime.status); - - if (valid) { - const status = anime.status === 'FINISHED' ? 'completed' : anime.status === 'NOT_YET_RELEASED' ? 'info' : 'releasing'; - - endpoint.searchParams.append('status[]', status); - } - } - - if (anime.format) { - const valid = ['movie', 'tv', 'ova', 'ona', 'special', 'music'].includes(anime.format.toLowerCase()); - - if (valid) endpoint.searchParams.set('type', anime.format.toLowerCase()); - } - - if (anime.season) { - endpoint.searchParams.set('season', anime.season.toLowerCase()); - } - - if (anime.genres) { - const validGenres: { awId: number; name: Genre }[] = [ - { awId: 1, name: 'Action' }, - { awId: 2, name: 'Adventure' }, - { awId: 4, name: 'Comedy' }, - { awId: 7, name: 'Drama' }, - { awId: 8, name: 'Ecchi' }, - { awId: 9, name: 'Fantasy' }, - { awId: 14, name: 'Horror' }, - { awId: 3457321, name: 'Mahou Shoujo' }, - { awId: 19, name: 'Mecha' }, - { awId: 21, name: 'Music' }, - { awId: 22, name: 'Mystery' }, - { awId: 25, name: 'Psychological' }, - { awId: 26, name: 'Romance' }, - { awId: 29, name: 'Sci-Fi' }, - { awId: 35, name: 'Slice of Life' }, - { awId: 37, name: 'Sports' }, - { awId: 39, name: 'Supernatural' }, - { awId: 40, name: 'Thriller' } - ]; - - const animeGenres = validGenres.filter(genre => anime.genres!.includes(genre.name)); - - animeGenres.forEach(genre => endpoint.searchParams.append('genre[]', `${genre.awId}`)); - } - - awButton.setAttribute('target', '_blank'); - - // Get experimental option - chrome.storage.sync.get('directWatchPageLink', (data: { directWatchPageLink?: boolean }) => { - const directLink = Boolean(data.directWatchPageLink); - - if (directLink) { - // Trying to guess the direct anime url - - chrome.runtime.sendMessage({ contentScriptQuery: 'fetchUrl', url: endpoint.toString() }, r => { - const html = r.text as string | void; - - if (!html) { - awButton.setAttribute('href', endpoint.toString()); - return console.log('Could not fetch, falling back to search url.'); - } - - const parser = new DOMParser(); - const doc = parser.parseFromString(html, 'text/html'); - - const results = doc.querySelector('#list-items div'); - - if (!results) { - awButton.setAttribute('href', endpoint.toString()); - return console.log('No results were found, falling back to search url.'); - } - - const firstResult = doc.querySelector('#list-items div div div a'); - - if (!firstResult) { - awButton.setAttribute('href', endpoint.toString()); - return console.log('Unable to parse, falling back to search url.'); - } - - const direct = firstResult.getAttribute('href'); - - if (!direct) { - awButton.setAttribute('href', endpoint.toString()); - return console.log('Unable to find direct link, falling back to search url.'); - } - - console.log(`Found direct link ${direct}`); - awButton.setAttribute('href', `https://aniwave.to${direct}`); - }); - return; - } - - awButton.setAttribute('href', endpoint.toString()); - }); - - // Append the button to the action panel - const actionPanel = getElementByPath(`#app > div.page-content > div > div.header-wrap > div.header > div > div.content > div`); - if (!actionPanel) throw new Error('Action Panel was not found!'); - - actionPanel.appendChild(awButton); - console.log('Button was appended to the action panel!'); -})().catch((e: Error) => { - console.error(e); -}); diff --git a/src/scripts/content/agent.ts b/src/scripts/content/agent.ts new file mode 100644 index 0000000..e77a10c --- /dev/null +++ b/src/scripts/content/agent.ts @@ -0,0 +1,42 @@ +chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + if (message.action === 'parseHtml' && message.html && message.provider) { + sendResponse( + ((rawHtml: string, provider: BaseProvider) => { + try { + const doc = new DOMParser().parseFromString(rawHtml, 'text/html'); + + if (provider.parsingTarget === null) return null; + + const directUrl = doc.querySelector(provider.parsingTarget)?.getAttribute('href'); + + if (!directUrl) { + console.log(`AW: Could not find direct url for first result, falling back to search url`); + + return null; + } + + console.log(`AW: Found direct link ${directUrl}`); + + return directUrl; + } catch (error) { + console.log(`AW: ERROR`, error); + return null; + } + })(message.html, message.provider) + ); + } + + if (message.action === 'getTheme') { + const theme = localStorage.getItem('site-theme'); + + if (theme === 'system') { + if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { + sendResponse('dark'); + } else sendResponse(null); + } else { + sendResponse(theme); + } + } + + return true; +}); diff --git a/src/scripts/options.ts b/src/scripts/options.ts index 0a7b591..4dcf6f6 100644 --- a/src/scripts/options.ts +++ b/src/scripts/options.ts @@ -1,10 +1,127 @@ +function getProviders(): Promise { + return new Promise(resolve => { + chrome.runtime.sendMessage({ action: 'getProviders' }, (response: BaseProvider[]) => { + resolve(response); + }); + }); +} + document.addEventListener('DOMContentLoaded', async function (): Promise { - const checkbox: HTMLInputElement = document.getElementById('directWatchPageLink') as HTMLInputElement; + const providers = await getProviders(); + + console.log(`AW: Available providers: ${providers.map(p => p.id).join(', ')}`); + + const animeProviders = providers.filter(p => p.type === 'anime'); + const mangaProviders = providers.filter(p => p.type === 'manga'); - const data: { directWatchPageLink?: boolean } = await chrome.storage.sync.get('directWatchPageLink'); - checkbox.checked = Boolean(data.directWatchPageLink); + const defaultAnimeProvider = providers.find(p => p.type === 'anime')!; + const defaultMangaProvider = providers.find(p => p.type === 'manga')!; + + const preferences = (await chrome.storage.sync.get(['directWatchPageLink', 'streamingSiteId', 'readingSiteId', 'langOrder', 'withGenres'])) as Required; + + if (!animeProviders.map(p => p.id).includes(preferences.streamingSiteId)) { + chrome.storage.sync.set({ streamingSiteId: defaultAnimeProvider.id }); + preferences.streamingSiteId = defaultAnimeProvider.id; + } + + if (!mangaProviders.map(p => p.id).includes(preferences.readingSiteId)) { + chrome.storage.sync.set({ readingSiteId: defaultMangaProvider.id }); + preferences.readingSiteId = defaultMangaProvider.id; + } + + const checkbox: HTMLInputElement = document.getElementById('directWatchPageLink') as HTMLInputElement; + checkbox.checked = Boolean(preferences.directWatchPageLink); checkbox.addEventListener('change', function (): void { chrome.storage.sync.set({ directWatchPageLink: checkbox.checked }); }); + + const genreCheckbox: HTMLInputElement = document.getElementById('withGenres') as HTMLInputElement; + genreCheckbox.checked = Boolean(preferences.withGenres); + + genreCheckbox.addEventListener('change', function (): void { + chrome.storage.sync.set({ withGenres: genreCheckbox.checked }); + }); + + const streamingSelect: HTMLSelectElement = document.getElementById('streamingSiteId') as HTMLSelectElement; + + for (const provider of animeProviders) { + const option = document.createElement('option'); + option.value = provider.id; + option.textContent = provider.displayName; + streamingSelect.appendChild(option); + } + + streamingSelect.value = preferences.streamingSiteId; + + streamingSelect.addEventListener('change', event => { + const selectedValue = (event.target as HTMLSelectElement).value; + + chrome.storage.sync.set({ streamingSiteId: selectedValue }); + }); + + const readingSelect: HTMLSelectElement = document.getElementById('readingSiteId') as HTMLSelectElement; + + for (const provider of mangaProviders) { + const option = document.createElement('option'); + option.value = provider.id; + option.textContent = provider.displayName; + readingSelect.appendChild(option); + } + + readingSelect.value = preferences.readingSiteId; + + readingSelect.addEventListener('change', event => { + const selectedValue = (event.target as HTMLSelectElement).value; + + chrome.storage.sync.set({ readingSiteId: selectedValue }); + }); + + const langSelect: HTMLSelectElement = document.getElementById('langOrder') as HTMLSelectElement; + langSelect.value = preferences.langOrder; + + langSelect.addEventListener('change', event => { + const selectedValue = (event.target as HTMLSelectElement).value; + + chrome.storage.sync.set({ langOrder: selectedValue }); + }); + + const button: HTMLButtonElement = document.getElementById('resetOptions') as HTMLButtonElement; + + button.addEventListener('click', function (): void { + chrome.storage.sync.set({ + directWatchPageLink: true, + streamingSiteId: defaultAnimeProvider.id, + readingSiteId: defaultMangaProvider.id, + langOrder: 'ren', + withGenres: true + }); + + window.location.reload(); + }); + + // Maybe some other time + // const installed = new URL(window.location.href).searchParams.has('installed'); + + // if (installed) { + // const item = document.createElement('div'); + // item.classList.add('option-item'); + // item.classList.add('highlight'); + + // const title = document.createElement('h1'); + // title.textContent = 'AniList Watcher V8 was added!'; + // item.appendChild(title); + + // const text = document.createElement('p'); + // text.innerHTML = `I've opened the settings page so you can adjust things to your liking. The defaults might not fit everyone's needs, so feel free to customize them!`; + // item.appendChild(text); + + // const container = document.getElementById('container')!; + // container.prepend(item); + + // const newUrl = new URL(window.location.href); + // newUrl.searchParams.delete('installed'); + + // window.history.replaceState({}, document.title, newUrl); + // } }); diff --git a/src/scripts/popup.ts b/src/scripts/popup.ts new file mode 100644 index 0000000..5641924 --- /dev/null +++ b/src/scripts/popup.ts @@ -0,0 +1,11 @@ +chrome.runtime.openOptionsPage(); + +document.addEventListener('DOMContentLoaded', () => { + const assets = ['kiana_shake.gif', 'daijobu.png', 'grass_doge.png', 'laffey_shake.gif', 'icon256.png', 'nod.gif']; + + const img = document.createElement('img'); + img.src = `../assets/${assets[Math.floor(Math.random() * assets.length)]}`; + img.width = 64; + + document.body.insertBefore(img, document.body.firstChild); +}); diff --git a/src/scripts/service-worker.ts b/src/scripts/service-worker.ts deleted file mode 100644 index 4772a30..0000000 --- a/src/scripts/service-worker.ts +++ /dev/null @@ -1,80 +0,0 @@ -chrome.runtime.onInstalled.addListener(async () => { - await chrome.storage.sync.set({ directWatchPageLink: true }); - - console.log('Installed!'); -}); - -chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => { - if (changeInfo.status === 'complete') { - if (!tab.url) return console.log(`No URL was found for tab ${tabId}`); - - const url = tab.url.split('/'); - - console.log(`Processing ${url.join('/')}`); - - const domain = url[2]; - if (domain !== 'anilist.co') return; - - await chrome.scripting - .executeScript({ - target: { tabId: tabId }, - files: ['scripts/cleanup.js'] - }) - .then(() => { - console.log(`Injected cleanup script for ${tab.url}`); - }) - .catch(err => { - console.warn(`An error ocurred but it was caught.\n\n${err}`); - }); - - await chrome.scripting - .removeCSS({ - target: { tabId: tabId }, - files: ['assets/aw_button.css'] - }) - .then(() => { - console.log(`Button CSS removed successfully for ${tab.url}`); - }) - .catch(err => { - console.warn(`An error ocurred but it was caught.\n\n${err}`); - }); - - const page = url[3]; - - if (page === 'anime' && url[4] && url[5]) { - await chrome.scripting - .insertCSS({ - target: { tabId: tabId }, - files: ['assets/aw_button.css'] - }) - .then(() => { - console.log(`Button CSS injected successfully for ${tab.url}`); - }) - .catch(err => { - console.warn(`An error ocurred but it was caught.\n\n${err}`); - }); - - await chrome.scripting - .executeScript({ - target: { tabId: tabId }, - files: ['scripts/content.js'] - }) - .then(() => { - console.log(`Injected the content script for ${tab.url}`); - }) - .catch(err => { - console.warn(`An error ocurred but it was caught.\n\n${err}`); - }); - } - } -}); - -chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) { - if (request.contentScriptQuery == 'fetchUrl') { - fetch(request.url) - .then(response => response.text()) - .then(text => sendResponse({ text: text })) - .catch(error => console.log(error)); - return true; - } -}); diff --git a/src/styles/button.css b/src/styles/button.css new file mode 100644 index 0000000..525448e --- /dev/null +++ b/src/styles/button.css @@ -0,0 +1,19 @@ +button { + display: inline-block; + outline: 0; + cursor: pointer; + border: none; + padding: 0 56px; + height: 45px; + line-height: 45px; + border-radius: 7px; + background-color: rgb(110, 121, 214); + color: white; + font-weight: 400; + font-size: 16px; + transition: background 0.2s ease; +} + +button:hover { + background: rgba(83, 93, 179, 0.9); +} diff --git a/src/styles/checkbox.css b/src/styles/checkbox.css index 5f7e544..208d7ce 100644 --- a/src/styles/checkbox.css +++ b/src/styles/checkbox.css @@ -4,10 +4,10 @@ border-radius: 72px; border-style: none; flex-shrink: 0; - height: 20px; + height: 30px; margin: 0; position: relative; - width: 30px; + width: 45px; } .cb::before { @@ -28,11 +28,11 @@ background-color: #fff; border-radius: 50%; content: ''; - height: 14px; - left: 3px; + height: 21px; + left: 4.5px; position: absolute; - top: 3px; - width: 14px; + top: 4.5px; + width: 21px; } .cb:hover { @@ -46,7 +46,7 @@ .cb:checked::after { background-color: #fff; - left: 13px; + left: 19.5px; } .cb:checked:hover { diff --git a/src/styles/select.css b/src/styles/select.css new file mode 100644 index 0000000..61e30ca --- /dev/null +++ b/src/styles/select.css @@ -0,0 +1,29 @@ +/* Style the select dropdown */ +select { + appearance: none; + -webkit-appearance: none; + -moz-appearance: none; + background-color: #6e79d6; + border: none; + padding: 10px; + color: white; + font-size: 16px; + border-radius: 5px; + width: 200px; + outline: none; + cursor: pointer; + background-image: url('data:image/svg+xml;utf8,'); + background-repeat: no-repeat; + background-position: right 10px center; + background-size: 10px; + transition: background 0.2s ease; +} + +select:hover { + background-color: #535db3; +} + +select option { + background-color: #fff; + color: #333; +} diff --git a/src/views/options.html b/src/views/options.html index 340118c..934afc0 100644 --- a/src/views/options.html +++ b/src/views/options.html @@ -6,33 +6,123 @@ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; margin: 0; padding: 0; - background-color: #363636; + background-color: #252525; color: #fff; } + .container { - width: 80%; + width: fit-content; + min-width: 60%; + max-width: min-content; margin: 0 auto; padding: 20px; } + .option-item { - background-color: #717171; + background-color: rgba(115, 141, 169, 0.4); border-radius: 5px; padding: 20px; margin-bottom: 10px; } - .option-item label { - font-weight: bold; - font-size: larger; + + .highlight { + background-color: #505372; + } + + a { + text-decoration: none; + color: #20b2aa; + } + + p.header { + font-size: 24px; + } + + .reset { + font-size: 24px; + } + + p { + font-size: 18px; + } + + .horiz { + display: flex; + flex-direction: column; + justify-content: center; + justify-items: center; + justify-self: center; + text-align: center; + align-items: center; + } + + .grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + grid-template-rows: repeat(2, 1fr); + grid-column-gap: 10px; } + + -
+
+
+
+ +
+

AniList Watcher

+

Options for this extension are listed below.
They're automatically saved but existing tabs need to be refreshed for changes to apply.

+
+ +
+
+

Try to find the direct watch page link (Experimental)

+

Will try to set the button to episode/chapter 1 of the show you're looking for.

+ +
+ +
+

Use genres when searching

+

+ Some providers (like HiAnime and Anitaku) report mismatched genres for some titles, this option toggles wether to use genres reported by AniList when searching.
(Try toggling this off + if the extension didn't get a direct link or no search results are found on the provider's search page) +

+ +
+ +
+

Preferred providers

+

Streaming:

+ +

Reading:

+ +
+ +
+

AniList title language order

+

This will determine in which language order the titles from anilist are used when searching.
(Might help if you get no results)

+ +
+
+ +
+

Reset options to default

+ +
+
- -

- +

Version 8.0.0

+

If you have questions or concerns direct them to my Discord DM's: sans._.

diff --git a/src/views/popup.html b/src/views/popup.html new file mode 100644 index 0000000..ec8b3d1 --- /dev/null +++ b/src/views/popup.html @@ -0,0 +1,22 @@ + + + + + + + + + + + +