├── .eslintignore ├── .eslintrc ├── .gitignore ├── .huskyrc ├── .lintstagedrc ├── README.md ├── angular.json ├── build ├── constants.ts ├── ng-webpack-configs │ ├── analyze.js │ ├── common.js │ ├── development.js │ └── production.js ├── utils │ └── get-project-config │ │ └── index.ts ├── webpack.analyzer.config.ts ├── webpack.common.config.ts ├── webpack.dev.config.ts └── webpack.prod.config.ts ├── core ├── archive │ ├── .gitignore │ ├── README.md │ ├── index.ts │ ├── package-lock.json │ ├── package.json │ ├── tsconfig.json │ ├── webpack.config.ts │ └── worker.ts ├── favicon-indicator │ ├── .gitignore │ ├── README.md │ ├── jest.config.js │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── favicon │ │ │ ├── constants.ts │ │ │ ├── favicon.module.ts │ │ │ ├── favicon.service.ts │ │ │ ├── icons │ │ │ │ ├── @types.ts │ │ │ │ ├── canvas.utils.ts │ │ │ │ ├── error.icon.ts │ │ │ │ ├── icon-provider.ts │ │ │ │ ├── progress.icon.ts │ │ │ │ └── ready.icon.ts │ │ │ └── utils.ts │ │ └── index.ts │ └── tsconfig.json ├── gm-axios-adapter │ ├── .gitignore │ ├── @types.d.ts │ ├── index.ts │ ├── package-lock.json │ ├── package.json │ └── tsconfig.json ├── gm-download │ ├── .gitignore │ ├── package-lock.json │ ├── package.json │ ├── src │ │ └── index.ts │ └── tsconfig.json ├── gm-http-backend │ ├── .gitignore │ ├── @types.d.ts │ ├── README.md │ ├── __test__ │ │ └── gm-http-backend.spec.ts │ ├── index.ts │ ├── jest.config.js │ ├── package-lock.json │ ├── package.json │ ├── setup-jest.ts │ ├── tsconfig.json │ ├── tsconfig.spec.json │ └── utils.ts ├── gm-storage │ ├── .gitignore │ ├── README.md │ ├── index.ts │ ├── package-lock.json │ ├── package.json │ └── tsconfig.json ├── gm-types │ ├── index.d.ts │ └── package.json ├── save-file │ ├── .gitignore │ ├── index.ts │ ├── package-lock.json │ ├── package.json │ └── tsconfig.json └── wait-selector │ ├── index.ts │ ├── package-lock.json │ ├── package.json │ └── tsconfig.json ├── dist ├── animespirit-ost-downloader.user.js ├── get-qrcode.user.js ├── hover-zoom.user.js ├── soundcloud-downloader.user.js ├── youtube-blocker.user.js └── youtube-tweaks.user.js ├── lerna.json ├── package-lock.json ├── package.json ├── projects ├── animespirit-ost-downloader │ ├── README.md │ ├── headers.json │ ├── index.ts │ ├── package-lock.json │ ├── package.json │ ├── polyfills.ts │ ├── src │ │ └── components │ │ │ ├── app │ │ │ ├── app.component.ts │ │ │ ├── app.module.ts │ │ │ ├── app.style.scss │ │ │ └── app.template.html │ │ │ └── downloader │ │ │ ├── constants.ts │ │ │ └── downloader.service.ts │ ├── tsconfig.json │ └── webpack.config.ts ├── get-qrcode │ ├── README.md │ ├── headers.json │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── adapters │ │ │ ├── blob-url.adapter.ts │ │ │ └── iframe.adapter.ts │ │ ├── index.ts │ │ └── utils │ │ │ ├── build-qr-code-page │ │ │ ├── index.ts │ │ │ ├── styles.scss │ │ │ └── template.html │ │ │ ├── get-qr-code-url.ts │ │ │ ├── modal │ │ │ ├── index.ts │ │ │ ├── modal.styles.scss │ │ │ └── modal.template.html │ │ │ └── qr-code-as-promised.ts │ ├── tsconfig.json │ ├── types │ │ └── index.d.ts │ └── webpack.config.ts ├── hover-zoom │ ├── README.md │ ├── headers.json │ ├── index.ts │ ├── package-lock.json │ ├── package.json │ ├── tsconfig.json │ └── webpack.config.ts ├── soundcloud-downloader │ ├── README.md │ ├── angular.json │ ├── browserslist │ ├── headers.json │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── constants.ts │ │ ├── environments │ │ │ ├── environment.prod.ts │ │ │ └── environment.ts │ │ ├── main.ts │ │ ├── modules │ │ │ ├── app │ │ │ │ ├── app.component.ts │ │ │ │ ├── app.module.ts │ │ │ │ ├── app.styles.scss │ │ │ │ └── app.template.html │ │ │ ├── core │ │ │ │ ├── api │ │ │ │ │ ├── @types │ │ │ │ │ │ ├── Entry.ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── api.module.ts │ │ │ │ │ ├── api.service.spec.ts │ │ │ │ │ └── api.service.ts │ │ │ │ ├── core.module.ts │ │ │ │ ├── downloader │ │ │ │ │ ├── downloader.module.ts │ │ │ │ │ ├── downloader.service.spec.ts │ │ │ │ │ └── downloader.service.ts │ │ │ │ ├── http-interceptors │ │ │ │ │ ├── app-version.interceptor.ts │ │ │ │ │ ├── client-id.interceptor.ts │ │ │ │ │ └── http-interceptors.module.ts │ │ │ │ ├── http-sniffer │ │ │ │ │ ├── http-sniffer.module.ts │ │ │ │ │ ├── http-sniffer.service.spec.ts │ │ │ │ │ └── http-sniffer.service.ts │ │ │ │ └── utils │ │ │ │ │ ├── add-id3 │ │ │ │ │ └── index.ts │ │ │ │ │ ├── build-playlist-manifest │ │ │ │ │ └── index.ts │ │ │ │ │ ├── chunk │ │ │ │ │ └── index.ts │ │ │ │ │ ├── combine-buffers │ │ │ │ │ └── index.ts │ │ │ │ │ ├── combine-css-selectors │ │ │ │ │ └── index.ts │ │ │ │ │ ├── dom-observer │ │ │ │ │ └── index.ts │ │ │ │ │ ├── extract-ids │ │ │ │ │ └── index.ts │ │ │ │ │ ├── extract-media-urls │ │ │ │ │ └── index.ts │ │ │ │ │ └── extract-urls-from-manifest │ │ │ │ │ └── index.ts │ │ │ ├── dom-injector │ │ │ │ ├── dom-injector.module.ts │ │ │ │ ├── dom-injector.service.spec.ts │ │ │ │ └── dom-injector.service.ts │ │ │ ├── download-button │ │ │ │ ├── download-button.component.html │ │ │ │ ├── download-button.component.scss │ │ │ │ ├── download-button.component.spec.ts │ │ │ │ ├── download-button.component.ts │ │ │ │ └── download-button.module.ts │ │ │ ├── settings-popup │ │ │ │ ├── settings-popup.component.html │ │ │ │ ├── settings-popup.component.scss │ │ │ │ ├── settings-popup.component.spec.ts │ │ │ │ ├── settings-popup.component.ts │ │ │ │ └── settings-popup.module.ts │ │ │ └── store │ │ │ │ ├── downloads │ │ │ │ ├── downloads.actions.ts │ │ │ │ ├── downloads.state.ts │ │ │ │ └── types.ts │ │ │ │ ├── settings │ │ │ │ ├── settings.actions.ts │ │ │ │ └── settings.state.ts │ │ │ │ └── store.module.ts │ │ ├── polyfills.ts │ │ └── styles │ │ │ └── main.scss │ ├── tsconfig.json │ ├── webpack.analyze.config.js │ ├── webpack.dev.config.js │ └── webpack.prod.config.js ├── youtube-blocker │ ├── README.md │ ├── browserslist │ ├── headers.json │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── components │ │ │ ├── add-filter-form │ │ │ │ ├── add-filter-form.component.ts │ │ │ │ ├── add-filter-form.module.ts │ │ │ │ ├── add-filter-form.styles.scss │ │ │ │ └── add-filter-form.template.html │ │ │ ├── app │ │ │ │ ├── app.component.ts │ │ │ │ ├── app.constants.ts │ │ │ │ ├── app.module.ts │ │ │ │ ├── app.styles.scss │ │ │ │ └── app.template.html │ │ │ ├── backup-and-restore │ │ │ │ ├── backup-and-restore.component.ts │ │ │ │ ├── backup-and-restore.module.ts │ │ │ │ ├── backup-and-restore.styles.scss │ │ │ │ ├── backup-and-restore.template.html │ │ │ │ └── file-reader.promise.ts │ │ │ ├── block-list │ │ │ │ ├── block-list.component.ts │ │ │ │ ├── block-list.modue.ts │ │ │ │ ├── block-list.styles.scss │ │ │ │ ├── block-list.template.html │ │ │ │ └── filter-by-term.pipe.ts │ │ │ ├── block-video │ │ │ │ ├── block-video.component.ts │ │ │ │ ├── block-video.module.ts │ │ │ │ ├── block-video.styles.scss │ │ │ │ └── block-video.template.html │ │ │ ├── blocker │ │ │ │ ├── block-button-injector.service.ts │ │ │ │ ├── blocker.constants.ts │ │ │ │ ├── blocker.module.ts │ │ │ │ ├── blocker.service.ts │ │ │ │ └── blocker.utils.ts │ │ │ ├── preferences-popup │ │ │ │ ├── preferences-popup.component.ts │ │ │ │ ├── preferences-popup.module.ts │ │ │ │ ├── preferences-popup.styles.scss │ │ │ │ └── preferences-popup.template.html │ │ │ └── yt-control │ │ │ │ ├── @types.ts │ │ │ │ ├── yt-control.module.ts │ │ │ │ ├── yt-player-control.service.spec.ts │ │ │ │ └── yt-player-control.service.ts │ │ ├── environments │ │ │ ├── environment.prod.ts │ │ │ └── environment.ts │ │ ├── index.ts │ │ ├── polyfills.ts │ │ ├── store │ │ │ ├── block-list │ │ │ │ ├── block-list.actions.ts │ │ │ │ └── block-list.state.ts │ │ │ ├── preferences │ │ │ │ ├── preferences.actions.ts │ │ │ │ └── preferences.state.ts │ │ │ ├── storage.engine.ts │ │ │ └── store.module.ts │ │ ├── styles │ │ │ ├── _scrollbar.scss │ │ │ └── main.scss │ │ └── utils │ │ │ ├── combine-css-rules.ts │ │ │ ├── mutation-observer.observable.ts │ │ │ └── storage-migrate.ts │ ├── tsconfig.json │ ├── webpack.analyze.config.js │ ├── webpack.dev.config.js │ └── webpack.prod.config.js └── youtube-tweaks │ ├── README.md │ ├── browserslist │ ├── headers.json │ ├── package-lock.json │ ├── package.json │ ├── src │ ├── app │ │ ├── app.component.ts │ │ ├── app.module.ts │ │ ├── app.styles.scss │ │ ├── app.template.html │ │ └── modules │ │ │ ├── player │ │ │ ├── player-patcher.service.spec.ts │ │ │ ├── player-patcher.service.ts │ │ │ ├── player.module.ts │ │ │ ├── player.service.spec.ts │ │ │ └── player.service.ts │ │ │ ├── preferences-popup │ │ │ ├── preferences-popup.component.html │ │ │ ├── preferences-popup.component.scss │ │ │ ├── preferences-popup.component.spec.ts │ │ │ ├── preferences-popup.component.ts │ │ │ ├── preferences-popup.constants.ts │ │ │ ├── preferences-popup.module.ts │ │ │ └── shortcuts-panel │ │ │ │ ├── shortcut-edit │ │ │ │ ├── shortcut-edit.component.html │ │ │ │ ├── shortcut-edit.component.scss │ │ │ │ ├── shortcut-edit.component.spec.ts │ │ │ │ └── shortcut-edit.component.ts │ │ │ │ ├── shortcut-keys │ │ │ │ ├── shortcut-keys.component.html │ │ │ │ ├── shortcut-keys.component.scss │ │ │ │ ├── shortcut-keys.component.spec.ts │ │ │ │ └── shortcut-keys.component.ts │ │ │ │ ├── shortcuts-panel.component.html │ │ │ │ ├── shortcuts-panel.component.scss │ │ │ │ ├── shortcuts-panel.component.spec.ts │ │ │ │ └── shortcuts-panel.component.ts │ │ │ ├── shortcut │ │ │ ├── event-to-shortcut.ts │ │ │ ├── providers │ │ │ │ ├── quality-shortcut.service.spec.ts │ │ │ │ ├── quality-shortcut.service.ts │ │ │ │ ├── speed-shortcut.service.spec.ts │ │ │ │ └── speed-shortcut.service.ts │ │ │ ├── shortcut.component.ts │ │ │ ├── shortcut.module.ts │ │ │ ├── shortcut.service.spec.ts │ │ │ └── shortcut.service.ts │ │ │ └── store │ │ │ ├── preferences │ │ │ ├── preferences.actions.ts │ │ │ └── preferences.state.ts │ │ │ ├── shortcuts │ │ │ ├── shortcuts.actions.ts │ │ │ └── shortcuts.state.ts │ │ │ ├── storage.engine.ts │ │ │ └── store.module.ts │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── main.ts │ ├── polyfills.ts │ └── styles.scss │ ├── tsconfig.app.json │ ├── webpack.analyze.config.js │ ├── webpack.dev.config.js │ └── webpack.prod.config.js └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | **/dist/**/* 2 | node_modules 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "plugins": [ 4 | "jest", 5 | "@typescript-eslint" 6 | ], 7 | "env": { 8 | "browser": true, 9 | "node": true, 10 | "es6": true, 11 | "jest/globals": true 12 | }, 13 | "extends": [ 14 | "eslint:recommended", 15 | "plugin:@typescript-eslint/recommended" 16 | ], 17 | "rules": { 18 | "@typescript-eslint/explicit-function-return-type": 0, 19 | "@typescript-eslint/no-unused-vars": "error", 20 | "@typescript-eslint/no-explicit-any": 0, 21 | "@typescript-eslint/camelcase": 0 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | dist 4 | !dist/**/*.user.js 5 | -------------------------------------------------------------------------------- /.huskyrc: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | "pre-commit": "lint-staged" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "*.[jt]s": ["eslint --fix", "git add"] 3 | } 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Kawai user scripts 2 | 3 | Awesome user scripts for your browser 4 | 5 | ### List 6 | 7 | | Name | Description | Install url | 8 | |------|-------------|-------------| 9 | | [Hover zoom](./projects/hover-zoom#readme) | Show image preview on link hover | [Install](./dist/hover-zoom.user.js?raw=true) | 10 | | [AnimeSpirit ost downloader](./projects/animespirit-ost-downloader#readme) | Adds bulk download for www.animespirit.ru/ost and www.animespirit.su/ost | [Install](./dist/animespirit-ost-downloader.user.js?raw=true) | 11 | | [Youtube blocker](./projects/youtube-blocker#readme) | Adds the ability to block youtube videos from specific channels and users | [Install](./dist/youtube-blocker.user.js?raw=true) | 12 | | [Get Qr code](./projects/get-qrcode#readme) | Generates QR code for page | [Install](./dist/get-qrcode.user.js?raw=true) | 13 | | [SoundCloud downloader](./projects/soundcloud-downloader#readme) | Adds the ability to download any track or playlist from soundcloud.com | [Install](./dist/soundcloud-downloader.user.js?raw=true) | 14 | 15 | Note: If you don't have browser plugin to run user scripts install one listed bellow 16 | 17 | | Browser | Install url | 18 | |---------|-------------| 19 | | Firefox | [Violentmonkey](https://addons.mozilla.org/en-US/firefox/addon/violentmonkey/) or [Tampermonkey](https://addons.mozilla.org/ru/firefox/addon/tampermonkey/) | 20 | | Chrome | [Violentmonkey](https://chrome.google.com/webstore/detail/violentmonkey/jinjaccalgkegednnccohejagnlnfdag) or [Tampermonkey](https://chrome.google.com/webstore/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo) | 21 | | Edge | [Violentmonkey](https://microsoftedge.microsoft.com/addons/detail/violentmonkey/eeagobfjdenkkddmbclomhiblgggliao) or [Tampermonkey](https://microsoftedge.microsoft.com/addons/detail/tampermonkey/iikmkjmpaadaobahmlepeloendndfphd) | 22 | | Safari | [Tampermonkey](https://www.tampermonkey.net/?ext=dhdg&browser=safari) | 23 | 24 | ### Development 25 | 26 | ##### Setup 27 | ```shell script 28 | npm i 29 | npx lerna bootstrap 30 | npx lerna run build 31 | ``` 32 | 33 | ##### Build user script 34 | 35 | Run command in terminal `npm run build:` for example: 36 | 37 | ```shell script 38 | npm run build:hover-zoom 39 | ``` 40 | 41 | ##### Develop user script 42 | 43 | Run command in terminal `npm run dev:` for example: 44 | 45 | ```shell script 46 | npm run dev:hover-zoom 47 | ``` 48 | 49 | then open dist folder and copy contents of proxy script(`.dev.proxy.user.js`) 50 | into your your script manager or drag and drop proxy script into browser 51 | 52 | Also you can switch proxy to user file:// protocol with env variable for example: 53 | ```shell script 54 | # Linux/Mac 55 | export FILE_PROTOCOL=1 56 | 57 | # Windows 58 | set FILE_PROTOCOL=1 59 | ``` 60 | -------------------------------------------------------------------------------- /build/constants.ts: -------------------------------------------------------------------------------- 1 | export const DEV_PORT = 9000; 2 | -------------------------------------------------------------------------------- /build/ng-webpack-configs/analyze.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const provider = require('./production'); 3 | const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); 4 | 5 | module.exports = (project) => { 6 | const cfg = provider(project); 7 | const analyzerPlugin = new BundleAnalyzerPlugin({ 8 | analyzerMode: 'static', 9 | }); 10 | 11 | return { ...cfg, plugins: [...cfg.plugins, analyzerPlugin] }; 12 | }; 13 | -------------------------------------------------------------------------------- /build/ng-webpack-configs/common.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const { resolve, join } = require('path'); 3 | const { existsSync } = require('fs'); 4 | const WebpackUserscript = require('webpack-userscript'); 5 | 6 | const FILE_URL = join('file://', resolve('dist')); 7 | const HTTP_URL = 'http://localhost:4200/webpack-dev-server/'; 8 | 9 | module.exports = function provider(project, { dev = false } = {}) { 10 | const projectDir = resolve(`projects/${ project }`); 11 | const headersPath = join(projectDir, 'headers.json'); 12 | const pkgPath = join(projectDir, 'package.json'); 13 | const headers = existsSync(headersPath) ? require(headersPath) : {}; 14 | const pkg = require(pkgPath); 15 | const baseUrl = process.env.FILE_PROTOCOL ? FILE_URL : HTTP_URL; 16 | 17 | return { 18 | output: { 19 | filename: `${ project }${ dev ? '.dev' : '' }.user.js`, 20 | }, 21 | plugins: [ 22 | new WebpackUserscript({ 23 | metajs: false, 24 | proxyScript: { 25 | baseUrl, 26 | enable: dev, 27 | }, 28 | headers: { ...headers, license: pkg.license }, 29 | }), 30 | ], 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /build/ng-webpack-configs/development.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const provider = require('./common'); 3 | 4 | module.exports = (project) => ({ 5 | ...provider(project, { dev: true }), 6 | devServer: { 7 | hot: false, 8 | writeToDisk: true, 9 | injectHot: false, 10 | inline: false, 11 | compress: true, 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /build/ng-webpack-configs/production.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const provider = require('./common'); 3 | 4 | module.exports = (project) => provider(project, { dev: false }); 5 | -------------------------------------------------------------------------------- /build/utils/get-project-config/index.ts: -------------------------------------------------------------------------------- 1 | import { CliConfigOptions, Configuration } from 'webpack'; 2 | import { resolve } from 'path'; 3 | import { existsSync } from 'fs'; 4 | 5 | function getPkgHeaders(basePath: string) { 6 | const { 7 | version, 8 | name, 9 | description, 10 | author, 11 | homepage, 12 | bugs, 13 | license, 14 | } = require(resolve(basePath, 'package.json')); 15 | 16 | return { version, name, description, author, homepage, bugs, license }; 17 | } 18 | 19 | function getScriptHeaders(basePath: string) { 20 | const path = resolve(basePath, 'headers.json'); 21 | if(existsSync(path)) { 22 | return require(path); 23 | } 24 | return {}; 25 | } 26 | 27 | export default function getProjectConfig(env, args: CliConfigOptions) { 28 | const basePath = resolve(process.cwd(), 'projects', args['project']); 29 | 30 | const { default: provider } = require(resolve(basePath, 'webpack.config.ts')); 31 | const webpackConfig: Configuration = provider instanceof Function ? provider(env, args) : provider; 32 | const pkgHeaders = getPkgHeaders(basePath); 33 | const headers = getScriptHeaders(basePath); 34 | 35 | return { 36 | webpackConfig, 37 | headers: { ...pkgHeaders, ...headers }, 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /build/webpack.analyzer.config.ts: -------------------------------------------------------------------------------- 1 | import { ConfigurationFactory } from 'webpack'; 2 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; 3 | import merge from 'webpack-merge'; 4 | import PROD_CONFIG_PROVIDER from './webpack.prod.config'; 5 | 6 | const ANALYZER_CONFIG_PROVIDER: ConfigurationFactory = async (env, args) => { 7 | const prodConfig = await PROD_CONFIG_PROVIDER(env, args); 8 | 9 | return merge(prodConfig, { 10 | plugins: [ 11 | new BundleAnalyzerPlugin(), 12 | ], 13 | }); 14 | }; 15 | 16 | export default ANALYZER_CONFIG_PROVIDER; 17 | -------------------------------------------------------------------------------- /build/webpack.common.config.ts: -------------------------------------------------------------------------------- 1 | import { Configuration, ConfigurationFactory } from 'webpack'; 2 | import getProjectConfig from './utils/get-project-config'; 3 | import merge from 'webpack-merge'; 4 | import WebpackUserScript from 'webpack-userscript'; 5 | import { DEV_PORT } from "./constants"; 6 | import { resolve } from "path"; 7 | 8 | const COMMON_CONFIG: Configuration = { 9 | resolve: { 10 | extensions: ['.ts', '.tsx', '.js', '.jsx', '.html', '.css', '.scss'], 11 | }, 12 | 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.html$/, 17 | loader: 'html-loader' 18 | }, 19 | { 20 | test: /\.tsx?$/, 21 | loader: 'awesome-typescript-loader', 22 | }, 23 | ], 24 | }, 25 | 26 | plugins: [], 27 | }; 28 | 29 | const COMMON_CONFIG_PROVIDER: ConfigurationFactory = (env, args) => { 30 | const { webpackConfig, headers } = getProjectConfig(env, args); 31 | const filename = args.mode === 'development' ? `${ args['project'] }.dev.user.js` : `${ args['project'] }.user.js`; 32 | 33 | const OUTPUT: Configuration = { 34 | output: { 35 | path: resolve(__dirname, '../dist'), 36 | filename 37 | }, 38 | }; 39 | 40 | const userScriptPlugin: Configuration = { 41 | plugins: [ 42 | new WebpackUserScript({ 43 | metajs: false, 44 | proxyScript: { 45 | baseUrl: `http://localhost:${ DEV_PORT }`, 46 | enable: args.mode === 'development', 47 | }, 48 | headers, 49 | }) 50 | ] 51 | }; 52 | process.chdir(resolve(process.cwd(), 'projects', args['project'])); 53 | 54 | return merge(COMMON_CONFIG, OUTPUT, userScriptPlugin, webpackConfig); 55 | }; 56 | 57 | export default COMMON_CONFIG_PROVIDER; 58 | -------------------------------------------------------------------------------- /build/webpack.dev.config.ts: -------------------------------------------------------------------------------- 1 | import { Configuration, ConfigurationFactory } from 'webpack'; 2 | import COMMON_CONFIG_PROVIDER from './webpack.common.config'; 3 | import merge from 'webpack-merge'; 4 | import { resolve } from 'path'; 5 | import { DEV_PORT } from "./constants"; 6 | 7 | const DEV_CONFIG: Configuration = { 8 | mode: 'development', 9 | devtool: 'inline-source-map', 10 | devServer: { 11 | compress: true, 12 | port: DEV_PORT, 13 | contentBase: resolve(process.cwd(), 'dist'), 14 | writeToDisk: true, 15 | injectHot: false, 16 | inline: false, 17 | }, 18 | }; 19 | 20 | const DEV_CONFIG_PROVIDER: ConfigurationFactory = async (env, args) => { 21 | const commonConfig = await COMMON_CONFIG_PROVIDER(env, args); 22 | 23 | return merge(commonConfig, DEV_CONFIG); 24 | }; 25 | 26 | export default DEV_CONFIG_PROVIDER; 27 | -------------------------------------------------------------------------------- /build/webpack.prod.config.ts: -------------------------------------------------------------------------------- 1 | import { Configuration, ConfigurationFactory } from 'webpack'; 2 | import merge from 'webpack-merge'; 3 | import COMMON_CONFIG_PROVIDER from './webpack.common.config'; 4 | 5 | const PROD_CONFIG: Configuration = { 6 | mode: 'production', 7 | devtool: false, 8 | }; 9 | 10 | const PROD_CONFIG_PROVIDER: ConfigurationFactory = async (env, args) => { 11 | const commonConfig = await COMMON_CONFIG_PROVIDER(env, args); 12 | 13 | return merge(commonConfig, PROD_CONFIG); 14 | }; 15 | 16 | export default PROD_CONFIG_PROVIDER; 17 | -------------------------------------------------------------------------------- /core/archive/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /core/archive/README.md: -------------------------------------------------------------------------------- 1 | ### Archive 2 | 3 | Wrapper to run UZIP inside webworker 4 | 5 | ### Usage 6 | 7 | ```javascript 8 | import archive from "@kawai-scripts/archive"; 9 | 10 | const files = [ 11 | { name: 'test1.bin', file: new ArrayBuffer(100) }, 12 | { name: 'test2.bin', file: new ArrayBuffer(100) }, 13 | ]; 14 | 15 | archive( 16 | files 17 | ).then((zip) => { 18 | // Do something with zip 19 | }) 20 | ``` 21 | -------------------------------------------------------------------------------- /core/archive/index.ts: -------------------------------------------------------------------------------- 1 | import work from 'webworkify-webpack'; 2 | 3 | export interface FileEntry { 4 | file: ArrayBuffer; 5 | name: string; 6 | } 7 | 8 | interface WorkifyWorker extends Worker { 9 | objectURL: string; 10 | } 11 | 12 | interface Message { 13 | data: ArrayBuffer; 14 | } 15 | 16 | 17 | function archive(entries: FileEntry[]) { 18 | return new Promise((resolve) => { 19 | const worker: WorkifyWorker = work(require.resolve('./worker.ts')); 20 | const transfer = entries.map(({ file }) => file); 21 | 22 | worker.addEventListener('message', ({ data }: Message) => { 23 | resolve(new Blob([data], { type: 'application/zip' })); 24 | 25 | worker.terminate(); 26 | URL.revokeObjectURL(worker.objectURL); 27 | }); 28 | 29 | worker.postMessage({ entries }, transfer); 30 | }); 31 | } 32 | 33 | export default archive; 34 | -------------------------------------------------------------------------------- /core/archive/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@kawai-scripts/archive", 3 | "version": "1.0.0", 4 | "description": "Wrapper to run jszip inside webworker", 5 | "author": "kawaizombi", 6 | "license": "MIT", 7 | "scripts": { 8 | "build": "cross-env TS_NODE_PROJECT=\"tsconfig.json\" webpack" 9 | }, 10 | "main": "dist/archive.js", 11 | "types": "dist/index.d.ts", 12 | "devDependencies": { 13 | "cross-env": "^6.0.3", 14 | "ts-loader": "^6.2.1", 15 | "typescript": "^3.7.2", 16 | "webpack": "^4.41.2", 17 | "webpack-cli": "^3.3.10", 18 | "webworkify-webpack": "^2.1.5" 19 | }, 20 | "dependencies": { 21 | "uzip": "^0.20200204.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /core/archive/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es5", 5 | "sourceMap": true, 6 | "declaration": true, 7 | "declarationDir": "dist", 8 | "esModuleInterop": true 9 | }, 10 | "exclude": [ 11 | "node_modules", 12 | "webpack.config.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /core/archive/webpack.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | import { Configuration } from 'webpack'; 3 | 4 | const CONFIG: Configuration = { 5 | entry: resolve(__dirname, 'index.ts'), 6 | mode: 'production', 7 | output: { 8 | path: resolve(__dirname, 'dist'), 9 | filename: 'archive.js', 10 | libraryTarget: 'commonjs2', 11 | }, 12 | module: { 13 | rules: [ 14 | { 15 | test: /\.ts$/, 16 | loader: 'ts-loader', 17 | }, 18 | ], 19 | }, 20 | }; 21 | 22 | export default CONFIG; 23 | -------------------------------------------------------------------------------- /core/archive/worker.ts: -------------------------------------------------------------------------------- 1 | import { FileEntry } from './index'; 2 | import UZIP from 'uzip'; 3 | 4 | interface Message { 5 | data: { 6 | entries: FileEntry[]; 7 | }; 8 | } 9 | 10 | module.exports = function(self: Worker) { 11 | self.addEventListener('message', async ({ data: { entries } }: Message) => { 12 | const archive = entries.reduce((accumulator, { file, name }) => { 13 | accumulator[name] = new Uint8Array(file); 14 | 15 | return accumulator; 16 | }, {}); 17 | 18 | const zip = UZIP.encode(archive); 19 | 20 | self.postMessage(zip, [zip]); 21 | }); 22 | }; 23 | -------------------------------------------------------------------------------- /core/favicon-indicator/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /core/favicon-indicator/README.md: -------------------------------------------------------------------------------- 1 | ### Favicon indicator 2 | 3 | ### Usage 4 | 5 | ```typescript 6 | import { Injectable, NgModule } from '@angular/core'; 7 | import { 8 | FaviconModule, 9 | FaviconService, 10 | ProgressIcon, 11 | ErrorIcon, 12 | ReadyIcon, 13 | } from '@kawai-scripts/favicon-indicator'; 14 | 15 | @NgModule({ 16 | imports: [FaviconModule], 17 | }) 18 | class App {} 19 | 20 | @Injectable() 21 | class Service { 22 | constructor(private faviconService: FaviconService) {} 23 | 24 | someWork() { 25 | // ... 26 | this.faviconService.useIcon(new ProgressIcon().setPercentage(10)); 27 | 28 | if(this.error) { 29 | this.faviconService.useIcon(new ErrorIcon()); 30 | } else { 31 | this.faviconService.useIcon(new ReadyIcon()); 32 | } 33 | } 34 | } 35 | ``` 36 | -------------------------------------------------------------------------------- /core/favicon-indicator/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | }; 4 | -------------------------------------------------------------------------------- /core/favicon-indicator/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@kawai-scripts/favicon-indicator", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "build": "rimraf dist && tsc -b" 6 | }, 7 | "module": "dist/index.js", 8 | "main": "dist/index.js", 9 | "dependencies": { 10 | "rxjs": "^6.5.3", 11 | "@angular/common": "^8.2.14", 12 | "@angular/compiler": "^8.2.14", 13 | "@angular/core": "^8.2.14", 14 | "@angular/platform-browser": "^8.2.14", 15 | "@angular/platform-browser-dynamic": "^8.2.14" 16 | }, 17 | "devDependencies": { 18 | "typescript": "^3.7.4", 19 | "rimraf": "^3.0.0", 20 | "jest": "^24.9.0", 21 | "ts-jest": "^24.2.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /core/favicon-indicator/src/favicon/constants.ts: -------------------------------------------------------------------------------- 1 | export const FAVICON_SELECTOR = 'link[rel=icon], link[rel="shortcut icon"]'; 2 | -------------------------------------------------------------------------------- /core/favicon-indicator/src/favicon/favicon.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { FAVICON_SIZE, FaviconService } from './favicon.service'; 3 | 4 | @NgModule({ 5 | providers: [ 6 | FaviconService, 7 | { provide: FAVICON_SIZE, useValue: 256 }, 8 | ], 9 | }) 10 | export class FaviconModule { 11 | 12 | } 13 | -------------------------------------------------------------------------------- /core/favicon-indicator/src/favicon/favicon.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable, InjectionToken } from '@angular/core'; 2 | import { Provider } from './icons/@types'; 3 | import { createIcon, removeAllIcons } from './utils'; 4 | 5 | export const FAVICON_SIZE = new InjectionToken('FAVICON_SIZE'); 6 | 7 | @Injectable() 8 | export class FaviconService { 9 | constructor( 10 | @Inject(FAVICON_SIZE) private faviconSize, 11 | ) { 12 | } 13 | 14 | useIcon(provider: Provider) { 15 | const url = provider.getUrl({ size: this.faviconSize }); 16 | const icon = createIcon(url); 17 | 18 | removeAllIcons(); 19 | document.head.appendChild(icon); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /core/favicon-indicator/src/favicon/icons/@types.ts: -------------------------------------------------------------------------------- 1 | export interface Params { 2 | size: number; 3 | } 4 | 5 | export interface Provider { 6 | getUrl(params: Params): string; 7 | } 8 | -------------------------------------------------------------------------------- /core/favicon-indicator/src/favicon/icons/canvas.utils.ts: -------------------------------------------------------------------------------- 1 | export const drawCircle = (ctx: CanvasRenderingContext2D, { size, color }) => { 2 | const half = size / 2; 3 | 4 | ctx.beginPath(); 5 | ctx.moveTo(half, half); 6 | ctx.arc(half, half, half, 0, Math.PI * 2, false); 7 | ctx.fillStyle = color; 8 | ctx.fill(); 9 | }; 10 | -------------------------------------------------------------------------------- /core/favicon-indicator/src/favicon/icons/error.icon.ts: -------------------------------------------------------------------------------- 1 | import { IconProvider } from "./icon-provider"; 2 | import { Params, Provider } from "./@types"; 3 | import { getCanvas } from "../utils"; 4 | import { drawCircle } from "./canvas.utils"; 5 | 6 | const DEFAULT_OPTIONS = { 7 | color: '#bbb', 8 | backgroundColor: '#a1000d', 9 | }; 10 | 11 | export class ErrorIcon extends IconProvider implements Provider { 12 | options = DEFAULT_OPTIONS; 13 | 14 | constructor(optionsPartial: Partial = {}) { 15 | super(optionsPartial); 16 | } 17 | 18 | private drawCross(ctx, { size }) { 19 | ctx.font = `${ size }px Arial`; 20 | ctx.textAlign = 'center'; 21 | ctx.textBaseline = 'middle'; 22 | ctx.fillStyle = this.options.color; 23 | ctx.fillText('✘', size / 2, size / 2); 24 | } 25 | 26 | getUrl({ size }: Params) { 27 | const canvas = getCanvas(size); 28 | const ctx = canvas.getContext('2d'); 29 | ctx.clearRect(0, 0, size, size); 30 | 31 | drawCircle(ctx, { size, color: this.options.backgroundColor }); 32 | this.drawCross(ctx, { size }); 33 | 34 | return canvas.toDataURL(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /core/favicon-indicator/src/favicon/icons/icon-provider.ts: -------------------------------------------------------------------------------- 1 | export class IconProvider { 2 | options = {}; 3 | 4 | constructor(optionsPartial = {}) { 5 | this.setOptions(optionsPartial); 6 | } 7 | 8 | setOptions(optionsPartial: object) { 9 | this.options = Object.assign({}, this.options, optionsPartial); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /core/favicon-indicator/src/favicon/icons/progress.icon.ts: -------------------------------------------------------------------------------- 1 | import { Provider } from './@types'; 2 | import { getCanvas } from '../utils'; 3 | import { IconProvider } from './icon-provider'; 4 | import { drawCircle } from './canvas.utils'; 5 | 6 | const DEFAULT_OPTIONS = { 7 | color: '#00a100', 8 | backgroundColor: '#bbb', 9 | }; 10 | 11 | export class ProgressIcon extends IconProvider implements Provider { 12 | percentage = 0; 13 | options = DEFAULT_OPTIONS; 14 | 15 | constructor(optionsPartial: Partial = {}) { 16 | super(optionsPartial); 17 | } 18 | 19 | getUrl({ size }): string { 20 | const canvas = getCanvas(size); 21 | const ctx = canvas.getContext('2d'); 22 | ctx.clearRect(0, 0, size, size); 23 | 24 | drawCircle(ctx, { size, color: this.options.backgroundColor }); 25 | 26 | if(this.percentage > 0) { 27 | this.redrawPie(ctx, { size }); 28 | } 29 | 30 | return canvas.toDataURL(); 31 | } 32 | 33 | private redrawPie(ctx, { size }) { 34 | const half = size / 2; 35 | const startAngle = (-0.5) * Math.PI; 36 | const endAngle = (-0.5 + 2 * this.percentage / 100) * Math.PI; 37 | 38 | ctx.beginPath(); 39 | ctx.moveTo(half, half); 40 | ctx.arc(half, half, half, startAngle, endAngle, false); 41 | ctx.lineTo(half, half); 42 | ctx.fillStyle = this.options.color; 43 | ctx.fill(); 44 | } 45 | 46 | setPercentage(percentage) { 47 | this.percentage = percentage; 48 | 49 | return this; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /core/favicon-indicator/src/favicon/icons/ready.icon.ts: -------------------------------------------------------------------------------- 1 | import { IconProvider } from './icon-provider'; 2 | import { Params, Provider } from './@types'; 3 | import { getCanvas } from '../utils'; 4 | import { drawCircle } from './canvas.utils'; 5 | 6 | const DEFAULT_OPTIONS = { 7 | color: '#bbb', 8 | backgroundColor: '#00a100', 9 | }; 10 | 11 | export class ReadyIcon extends IconProvider implements Provider { 12 | options = DEFAULT_OPTIONS; 13 | 14 | constructor(optionsPartial: Partial = {}) { 15 | super(optionsPartial); 16 | } 17 | 18 | private drawCheck(ctx, { size }) { 19 | ctx.font = `${ size }px Arial`; 20 | ctx.textAlign = 'center'; 21 | ctx.textBaseline = 'middle'; 22 | ctx.fillStyle = this.options.color; 23 | ctx.fillText('✔', size / 2, size / 2); 24 | } 25 | 26 | getUrl({ size }: Params) { 27 | const canvas = getCanvas(size); 28 | const ctx = canvas.getContext('2d'); 29 | ctx.clearRect(0, 0, size, size); 30 | 31 | drawCircle(ctx, { size, color: this.options.backgroundColor }); 32 | this.drawCheck(ctx, { size }); 33 | 34 | return canvas.toDataURL(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /core/favicon-indicator/src/favicon/utils.ts: -------------------------------------------------------------------------------- 1 | import { FAVICON_SELECTOR } from './constants'; 2 | 3 | export const createIcon = (url) => { 4 | const icon = document.createElement('link'); 5 | icon.type = 'image/x-icon'; 6 | icon.rel = 'icon'; 7 | icon.href = url; 8 | 9 | return icon; 10 | }; 11 | 12 | export const removeAllIcons = () => { 13 | const icons = document.querySelectorAll(FAVICON_SELECTOR); 14 | icons.forEach((icon) => icon.remove()); 15 | }; 16 | 17 | export const getCanvas = (size = 256) => { 18 | const canvas = document.createElement('canvas'); 19 | 20 | canvas.width = size; 21 | canvas.height = size; 22 | 23 | return canvas; 24 | }; 25 | -------------------------------------------------------------------------------- /core/favicon-indicator/src/index.ts: -------------------------------------------------------------------------------- 1 | export { FaviconService } from './favicon/favicon.service'; 2 | export { FaviconModule } from './favicon/favicon.module'; 3 | export { ProgressIcon } from './favicon/icons/progress.icon.js'; 4 | export { ReadyIcon } from './favicon/icons/ready.icon' 5 | export { ErrorIcon } from './favicon/icons/error.icon' 6 | -------------------------------------------------------------------------------- /core/favicon-indicator/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es5", 5 | "sourceMap": true, 6 | "declaration": true, 7 | "declarationDir": "dist", 8 | "moduleResolution": "node", 9 | "outDir": "dist", 10 | "removeComments": true, 11 | "experimentalDecorators": true, 12 | "emitDecoratorMetadata": true 13 | }, 14 | "exclude": [ 15 | "node_modules" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /core/gm-axios-adapter/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /core/gm-axios-adapter/@types.d.ts: -------------------------------------------------------------------------------- 1 | export interface GMXMLHttpRequestProgressResponse extends GMXMLHttpRequestResponse { 2 | lengthComputable: boolean; 3 | loaded: number; 4 | total: number; 5 | } 6 | 7 | export interface GMXMLHttpRequestResponse { 8 | readyState: number; 9 | responseHeaders: string; 10 | responseText: string; 11 | status: number; 12 | response: any; 13 | statusText: string; 14 | context: any; 15 | finalUrl: string; 16 | } 17 | 18 | export interface GMXMLHttpRequestOptions { 19 | url: string; 20 | method?: string; 21 | binary?: boolean; 22 | context?: any; 23 | data?: string; 24 | headers?: Record; 25 | onabort?: (response: GMXMLHttpRequestResponse) => any; 26 | onerror?: (response: GMXMLHttpRequestResponse) => any; 27 | onload?: (response: GMXMLHttpRequestResponse) => any; 28 | onprogress?: (response: GMXMLHttpRequestProgressResponse) => any; 29 | onreadystatechange?: (response: GMXMLHttpRequestResponse) => any; 30 | ontimeout?: (response: GMXMLHttpRequestResponse) => any; 31 | overrideMimeType?: string; 32 | username?: string; 33 | password?: string; 34 | responseType?: string; 35 | timeout?: number; 36 | } 37 | 38 | export interface GMXMLHttpRequestResult { 39 | abort(): void; 40 | } 41 | -------------------------------------------------------------------------------- /core/gm-axios-adapter/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/camelcase */ 2 | import { AxiosRequestConfig, AxiosResponse } from 'axios'; 3 | import { GMXMLHttpRequestOptions, GMXMLHttpRequestResponse, GMXMLHttpRequestResult } from './@types'; 4 | 5 | interface HeadersObject { 6 | [key: string]: string; 7 | } 8 | 9 | const headerStringToObject = (str: string): HeadersObject => { 10 | const rows = str.split('\n'); 11 | const pairs = rows.map((s) => s.split(':').map((s) => s.trim())); 12 | return pairs.reduce((accumulator, [key, value]) => { 13 | accumulator[key] = value; 14 | 15 | return accumulator; 16 | }, {}); 17 | }; 18 | 19 | function createHandler(resolve: Function, reject: Function, config: AxiosRequestConfig) { 20 | const { validateStatus } = config; 21 | 22 | return function(response: GMXMLHttpRequestResponse) { 23 | const payload: AxiosResponse = { 24 | config, 25 | data: response.response, 26 | headers: headerStringToObject(response.responseHeaders), 27 | status: response.status, 28 | statusText: response.statusText, 29 | }; 30 | 31 | if(!validateStatus || validateStatus(response.status)) { 32 | resolve(payload); 33 | } else { 34 | reject(payload); 35 | } 36 | }; 37 | } 38 | 39 | declare function GM_xmlhttpRequest(options: GMXMLHttpRequestOptions): GMXMLHttpRequestResult; 40 | 41 | function gmAdapter(config: AxiosRequestConfig): Promise { 42 | return new Promise((resolve, reject) => { 43 | const handler = createHandler(resolve, reject, config); 44 | const { auth: { username, password } = {} as any } = config; 45 | 46 | GM_xmlhttpRequest({ 47 | url: config.url, 48 | method: config.method, 49 | headers: config.headers, 50 | data: config.data, 51 | timeout: config.timeout, 52 | responseType: config.responseType, 53 | username, 54 | password, 55 | onload: handler, 56 | onerror: handler, 57 | ontimeout: handler, 58 | }); 59 | }); 60 | } 61 | 62 | export default gmAdapter; 63 | -------------------------------------------------------------------------------- /core/gm-axios-adapter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@kawai-scripts/gm-axios-adapter", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "build": "rimraf dist && tsc -b" 6 | }, 7 | "types": "dist/index.d.ts", 8 | "main": "dist/index.js", 9 | "devDependencies": { 10 | "axios": "^0.21.2", 11 | "typescript": "^3.7.5", 12 | "rimraf": "^3.0.1" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /core/gm-axios-adapter/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "esnext", 4 | "target": "es5", 5 | "moduleResolution": "node", 6 | "outDir": "dist", 7 | "sourceMap": true, 8 | "declaration": true, 9 | "declarationDir": "dist" 10 | }, 11 | "exclude": [ 12 | "node_modules" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /core/gm-download/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /core/gm-download/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@kawai-scripts/gm-download", 3 | "version": "1.0.0", 4 | "module": "dist/index.js", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "build": "rimraf dist && tsc -b" 8 | }, 9 | "dependencies": {}, 10 | "devDependencies": { 11 | "rimraf": "^3.0.0", 12 | "typescript": "^3.7.4" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /core/gm-download/src/index.ts: -------------------------------------------------------------------------------- 1 | declare function GM_download(url: string, name: string): void; 2 | 3 | export default async function gmDownload(url: string | Blob, name: string) { 4 | if(url instanceof Blob) { 5 | const reader = new FileReader(); 6 | url = await new Promise((resolve) => { 7 | reader.onload = (e) => resolve(e.target.result as string); 8 | reader.readAsDataURL(url as Blob); 9 | }); 10 | } 11 | 12 | GM_download(url as string, name); 13 | } 14 | -------------------------------------------------------------------------------- /core/gm-download/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es5", 5 | "sourceMap": true, 6 | "outDir": "dist", 7 | "declaration": true, 8 | "declarationDir": "dist" 9 | }, 10 | "exclude": [ 11 | "node_modules" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /core/gm-http-backend/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /core/gm-http-backend/@types.d.ts: -------------------------------------------------------------------------------- 1 | export interface GMXMLHttpRequestProgressResponse extends GMXMLHttpRequestResponse { 2 | lengthComputable: boolean; 3 | loaded: number; 4 | total: number; 5 | } 6 | 7 | export interface GMXMLHttpRequestResponse { 8 | readyState: number; 9 | responseHeaders: string; 10 | responseText: string; 11 | status: number; 12 | response: any; 13 | statusText: string; 14 | context: any; 15 | finalUrl: string; 16 | } 17 | 18 | export interface GMXMLHttpRequestOptions { 19 | url: string; 20 | method?: string; 21 | binary?: boolean; 22 | context?: any; 23 | data?: string; 24 | headers?: Record; 25 | onabort?: (response: GMXMLHttpRequestResponse) => any; 26 | onerror?: (response: GMXMLHttpRequestResponse) => any; 27 | onload?: (response: GMXMLHttpRequestResponse) => any; 28 | onprogress?: (response: GMXMLHttpRequestProgressResponse) => any; 29 | onreadystatechange?: (response: GMXMLHttpRequestResponse) => any; 30 | ontimeout?: (response: GMXMLHttpRequestResponse) => any; 31 | overrideMimeType?: string; 32 | username?: string; 33 | password?: string; 34 | responseType?: string; 35 | timeout?: number; 36 | } 37 | 38 | export interface GMXMLHttpRequestResult { 39 | abort(): void; 40 | } 41 | -------------------------------------------------------------------------------- /core/gm-http-backend/README.md: -------------------------------------------------------------------------------- 1 | ### GM Http backend 2 | 3 | Provides angular http backend based on GM_xmlhttpRequest 4 | 5 | ### Usage 6 | 7 | ```typescript 8 | import { Injectable, NgModule } from "@angular/core"; 9 | import { HttpBackend,HttpClient, HttpClientModule } from "@angular/common/http"; 10 | import GMBackend from '@kawai-scripts/gm-http-backend'; 11 | 12 | @NgModule({ 13 | imports: [HttpClientModule], 14 | providers: [{provide: HttpBackend, useClass: GMBackend}] 15 | }) 16 | class App { 17 | 18 | } 19 | 20 | @Injectable() 21 | class Service { 22 | constructor(private http: HttpClient) {} 23 | 24 | doRequest() { 25 | this.http.get('example.com').subscribe((res) => {/* do awesome things */}); 26 | } 27 | } 28 | ``` 29 | 30 | ### Achtung!!! 31 | 32 | If `this.http.get('example.com').subscribe(callback)` don't trigger callback, but 33 | `this.http.get('example.com', { observe: "events" }).subscribe(callback)` works try add this code to your webpack config 34 | ```typescript 35 | module.exports = { 36 | resolve: { 37 | alias: { 38 | '@angular': path.resolve(__dirname, './node_modules/@angular'), 39 | } 40 | } 41 | } 42 | ``` 43 | -------------------------------------------------------------------------------- /core/gm-http-backend/__test__/gm-http-backend.spec.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@angular/core"; 2 | import { HttpBackend, HttpClient, HttpClientModule } from "@angular/common/http"; 3 | import { TestBed } from "@angular/core/testing"; 4 | import GMBackend from ".."; 5 | 6 | @Injectable() 7 | class TestService { 8 | constructor(private http: HttpClient) { 9 | } 10 | 11 | testRequest() { 12 | return this.http.get('spec://test.com', { observe: 'response' }); 13 | } 14 | } 15 | 16 | describe('GMBackend', () => { 17 | let service: TestService; 18 | 19 | beforeAll(() => { 20 | // eslint-disable-next-line @typescript-eslint/camelcase 21 | (global as any).GM_xmlhttpRequest = jest.fn(({ onload, url }) => { 22 | onload({ 23 | response: "test", 24 | responseHeaders: "accept: all\ncontent: text", 25 | status: 200, 26 | statusCode: "OK", 27 | finalUrl: url, 28 | }) 29 | }); 30 | }); 31 | 32 | beforeEach(() => { 33 | TestBed.configureTestingModule({ 34 | imports: [HttpClientModule], 35 | providers: [ 36 | { provide: HttpBackend, useClass: GMBackend }, 37 | TestService, 38 | ], 39 | }); 40 | 41 | service = TestBed.get(TestService); 42 | }); 43 | 44 | it('should resolve', async () => { 45 | const res = await service.testRequest().toPromise(); 46 | 47 | expect(res.body).toBe("test"); 48 | 49 | }); 50 | 51 | it('should parse headers', async () => { 52 | const res = await service.testRequest().toPromise(); 53 | 54 | expect(res.headers.get('content')).toBe("text"); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /core/gm-http-backend/index.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs'; 2 | import { 3 | HttpEvent, 4 | HttpHandler, 5 | HttpHeaders, 6 | HttpRequest, 7 | HttpResponse, 8 | HttpEventType, 9 | HttpErrorResponse, 10 | } from '@angular/common/http'; 11 | import { GMXMLHttpRequestOptions, GMXMLHttpRequestResult } from './@types'; 12 | import { headerStringToObject } from './utils'; 13 | import { Injectable, NgZone } from '@angular/core'; 14 | 15 | // eslint-disable-next-line @typescript-eslint/camelcase 16 | declare function GM_xmlhttpRequest(options: GMXMLHttpRequestOptions): GMXMLHttpRequestResult; 17 | 18 | @Injectable() 19 | class GMBackend implements HttpHandler { 20 | constructor(private readonly zone: NgZone) { 21 | } 22 | 23 | handle(req: HttpRequest): Observable> { 24 | return new Observable>((observer) => { 25 | const request = GM_xmlhttpRequest({ 26 | url: req.urlWithParams, 27 | method: req.method, 28 | headers: req.headers, 29 | data: req.body, 30 | responseType: req.responseType, 31 | onprogress: (event) => { 32 | this.zone.run(() => { 33 | observer.next({ 34 | type: HttpEventType.DownloadProgress, 35 | loaded: event.loaded, 36 | total: event.total, 37 | }); 38 | }); 39 | }, 40 | onload: (res) => { 41 | this.zone.run(() => { 42 | observer.next(new HttpResponse({ 43 | headers: new HttpHeaders(headerStringToObject(res.responseHeaders)), 44 | body: res.response, 45 | status: res.status, 46 | statusText: res.statusText, 47 | url: res.finalUrl, 48 | })); 49 | observer.complete(); 50 | }); 51 | }, 52 | onerror: (err) => { 53 | this.zone.run(() => { 54 | observer.error(new HttpErrorResponse({ 55 | error: err.response, 56 | headers: new HttpHeaders(headerStringToObject(err.responseHeaders)), 57 | status: err.status, 58 | statusText: err.statusText, 59 | url: err.finalUrl, 60 | })); 61 | }); 62 | }, 63 | }); 64 | 65 | observer.next({ type: HttpEventType.Sent }); 66 | 67 | return () => request.abort(); 68 | }); 69 | } 70 | } 71 | 72 | export default GMBackend; 73 | -------------------------------------------------------------------------------- /core/gm-http-backend/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'jest-preset-angular', 3 | setupFilesAfterEnv: ['/setup-jest.ts'] 4 | }; 5 | -------------------------------------------------------------------------------- /core/gm-http-backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@kawai-scripts/gm-http-backend", 3 | "version": "1.1.0", 4 | "scripts": { 5 | "build": "rimraf dist && tsc -b", 6 | "test": "jest" 7 | }, 8 | "typings": "dist", 9 | "main": "dist/index.js", 10 | "dependencies": { 11 | "rxjs": "^6.5.3", 12 | "@angular/common": "^8.2.14", 13 | "@angular/compiler": "^8.2.14", 14 | "@angular/core": "^8.2.14", 15 | "@angular/platform-browser": "^8.2.14", 16 | "@angular/platform-browser-dynamic": "^8.2.14" 17 | }, 18 | "devDependencies": { 19 | "jest-preset-angular": "^8.0.0", 20 | "@types/jest": "^24.0.24", 21 | "@types/node": "^12.12.20", 22 | "rimraf": "^3.0.0", 23 | "typescript": "^3.7.3", 24 | "jest": "^24.9.0", 25 | "ts-jest": "^24.2.0", 26 | "zone.js": "^0.10.2" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /core/gm-http-backend/setup-jest.ts: -------------------------------------------------------------------------------- 1 | import 'jest-preset-angular'; 2 | -------------------------------------------------------------------------------- /core/gm-http-backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es5", 5 | "sourceMap": true, 6 | "declaration": true, 7 | "declarationDir": "dist", 8 | "moduleResolution": "node", 9 | "outDir": "dist", 10 | "removeComments": true, 11 | "experimentalDecorators": true, 12 | "emitDecoratorMetadata": true 13 | }, 14 | "exclude": [ 15 | "node_modules", 16 | "**/*.spec.ts", 17 | "setup-jest.ts" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /core/gm-http-backend/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es5", 5 | "sourceMap": true, 6 | "declaration": true, 7 | "declarationDir": "dist", 8 | "outDir": "dist", 9 | "experimentalDecorators": true, 10 | "emitDecoratorMetadata": true 11 | }, 12 | "exclude": [ 13 | "node_modules" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /core/gm-http-backend/utils.ts: -------------------------------------------------------------------------------- 1 | export const headerStringToObject = (str: string) => { 2 | const rows = str.split('\n'); 3 | const pairs = rows.map((s) => s.split(':').map((s) => s.trim())); 4 | return pairs.reduce((accumulator, [key, value]) => { 5 | accumulator[key] = value; 6 | 7 | return accumulator; 8 | }, {}); 9 | }; 10 | -------------------------------------------------------------------------------- /core/gm-storage/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /core/gm-storage/README.md: -------------------------------------------------------------------------------- 1 | ### GM storage 2 | 3 | Wrapper around `GM_getValue`, `GM_setValue`, `GM_deleteValue`, `GM_listValues` 4 | 5 | ### Usage 6 | 7 | ```javascript 8 | import GMStorage from "@kawai-scripts/gm-storage"; 9 | 10 | const storage = new GMStorage(); 11 | 12 | 13 | storage.setValue('flag', true); 14 | 15 | storage.getValue('flag'); 16 | 17 | storage.listValues(); // => ['flag'] 18 | 19 | storage.deleteValue('flag'); 20 | ``` 21 | -------------------------------------------------------------------------------- /core/gm-storage/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/camelcase */ 2 | type StorageTypes = boolean | number | string; 3 | 4 | type GM_getValue = (key: string, defaultValue?: any) => StorageTypes | Promise; 5 | type GM_setValue = (key: string, value: StorageTypes) => void | Promise; 6 | type GM_deleteValue = (key: string) => void | Promise; 7 | type GM_listValues = () => string[] | Promise; 8 | 9 | interface GM { 10 | getValue: GM_getValue; 11 | setValue: GM_setValue; 12 | deleteValue: GM_deleteValue; 13 | listValues: GM_listValues; 14 | } 15 | 16 | declare global { 17 | interface Window { 18 | GM: GM; 19 | GM_getValue: GM_getValue; 20 | GM_setValue: GM_setValue; 21 | GM_deleteValue: GM_deleteValue; 22 | GM_listValues: GM_listValues; 23 | } 24 | } 25 | 26 | class GMStorage { 27 | getValue(key: string, defaultValue?: any) { 28 | return Promise.resolve((window.GM_getValue || window.GM.getValue)(key, defaultValue)); 29 | } 30 | 31 | setValue(key: string, value: StorageTypes) { 32 | return Promise.resolve((window.GM_setValue || window.GM.setValue)(key, value)); 33 | } 34 | 35 | deleteValue(key: string) { 36 | return Promise.resolve((window.GM_deleteValue || window.GM.deleteValue)(key)); 37 | } 38 | 39 | listValues() { 40 | return Promise.resolve((window.GM_listValues || window.GM.listValues)()) 41 | } 42 | } 43 | 44 | export default GMStorage; 45 | -------------------------------------------------------------------------------- /core/gm-storage/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@kawai-scripts/gm-storage", 3 | "version": "1.0.0", 4 | "description": "Storage for user scripts based on GM_setValue GM_getValue", 5 | "dependencies": {}, 6 | "module": "dist/index.js", 7 | "main": "dist/index.js", 8 | "types": "dist/index.d.ts", 9 | "scripts": { 10 | "build": "rimraf dist && tsc -b" 11 | }, 12 | "devDependencies": { 13 | "rimraf": "^3.0.0", 14 | "typescript": "^3.7.5" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /core/gm-storage/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es5", 5 | "outDir": "dist", 6 | "sourceMap": true, 7 | "moduleResolution": "node", 8 | "declaration": true, 9 | "declarationDir": "dist" 10 | }, 11 | "exclude": [ 12 | "node_modules" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /core/gm-types/index.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/camelcase */ 2 | 3 | type ValueChangeCallback = (name: string, oldValue: any, newValue: any, remote: boolean) => void; 4 | 5 | interface GMTab { 6 | onclose?: () => void; 7 | closed: boolean; 8 | close: () => void; 9 | } 10 | 11 | interface NotificationConfig { 12 | text: string; 13 | title?: string; 14 | image?: string; 15 | 16 | onclick?(): void; 17 | 18 | ondone?(): void; 19 | } 20 | 21 | interface GMXMLHttpRequestProgressResponse extends GMXMLHttpRequestResponse { 22 | lengthComputable: boolean; 23 | loaded: number; 24 | total: number; 25 | } 26 | 27 | interface GMXMLHttpRequestResponse { 28 | readyState: number; 29 | responseHeaders: string; 30 | responseText: string; 31 | status: number; 32 | response: any; 33 | statusText: string; 34 | context: any; 35 | finalUrl: string; 36 | } 37 | 38 | interface GMXMLHttpRequestOptions { 39 | url: string; 40 | method?: string; 41 | binary?: boolean; 42 | context?: any; 43 | data?: string; 44 | headers?: Record; 45 | onabort?: (response: GMXMLHttpRequestResponse) => any; 46 | onerror?: (response: GMXMLHttpRequestResponse) => any; 47 | onload?: (response: GMXMLHttpRequestResponse) => any; 48 | onprogress?: (response: GMXMLHttpRequestProgressResponse) => any; 49 | onreadystatechange?: (response: GMXMLHttpRequestResponse) => any; 50 | ontimeout?: (response: GMXMLHttpRequestResponse) => any; 51 | overrideMimeType?: string; 52 | username?: string; 53 | password?: string; 54 | responseType?: string; 55 | timeout?: number; 56 | } 57 | 58 | 59 | interface GMXMLHttpRequestResult { 60 | abort(): void; 61 | } 62 | 63 | interface GMDownloadConfig extends Pick { 64 | name?: string; 65 | } 66 | 67 | type GM_getValue = (key: string, defaultValue?: any) => void; 68 | 69 | type GM_setValue = (key: string, value: any) => void; 70 | 71 | type GM_deleteValue = (key: string) => void; 72 | 73 | type GM_listValues = () => string[]; 74 | 75 | type GM_addValueChangeListener = (name: string, cb: ValueChangeCallback) => number; 76 | 77 | type GM_removeValueChangeListener = (id: ReturnType) => void; 78 | 79 | type GM_getResourceText = (name: string) => string; 80 | 81 | type GM_getResourceURL = (name: string) => string; 82 | 83 | type GM_addStyle = (css: string) => void; 84 | 85 | type GM_openInTab = (url: string, options: { active: boolean } | boolean) => GMTab; 86 | 87 | type GM_registerMenuCommand = (caption: string, onClick: () => void) => void; 88 | 89 | type GM_unregisterMenuCommand = (caption: string) => void; 90 | 91 | type GM_notification = 92 | ((config: NotificationConfig) => void) | 93 | ((text: string, title?: string, image?: string, onclick?: () => void, ondone?: () => void) => void); 94 | 95 | type GM_setClipboard = (data: string, type?: string) => void; 96 | 97 | type GM_xmlhttpRequest = (config: GMXMLHttpRequestOptions) => GMXMLHttpRequestResult; 98 | 99 | type GM_download = 100 | ((config: GMDownloadConfig) => void) | 101 | ((url: string, name?: string) => void); 102 | -------------------------------------------------------------------------------- /core/gm-types/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@kawai-scripts/gm-types", 3 | "version": "1.0.0", 4 | "typings": "./index.d.ts", 5 | "dependencies": { 6 | 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /core/save-file/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /core/save-file/index.ts: -------------------------------------------------------------------------------- 1 | function saveFile(file: Blob, name: string) { 2 | const url = URL.createObjectURL(file); 3 | const link = document.createElement('a'); 4 | link.style.display = 'none'; 5 | link.href = url; 6 | link.download = name; 7 | document.body.appendChild(link); 8 | link.click(); 9 | document.body.removeChild(link); 10 | URL.revokeObjectURL(url); 11 | } 12 | 13 | export default saveFile; 14 | -------------------------------------------------------------------------------- /core/save-file/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@kawai-scripts/save-file", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "build": "rimraf dist && tsc -b" 6 | }, 7 | "main": "dist/index.js", 8 | "module": "dist/index.js", 9 | "types": "dist/index.d.ts", 10 | "devDependencies": { 11 | "rimraf": "^3.0.0", 12 | "typescript": "^3.7.2" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /core/save-file/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es5", 5 | "outDir": "dist", 6 | "sourceMap": true, 7 | "moduleResolution": "node", 8 | "declaration": true, 9 | "declarationDir": "dist" 10 | }, 11 | "exclude": [ 12 | "node_modules" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /core/wait-selector/index.ts: -------------------------------------------------------------------------------- 1 | export default async function waitSelector(selector: string) { 2 | while(document.querySelector(selector) === null) { 3 | await new Promise(resolve => requestAnimationFrame(resolve)); 4 | } 5 | 6 | return document.querySelector(selector); 7 | } 8 | -------------------------------------------------------------------------------- /core/wait-selector/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@kawai-scripts/wait-selector", 3 | "version": "1.0.0", 4 | "main": "dist/index.js", 5 | "module": "dist/index.js", 6 | "scripts": { 7 | "build": "rimraf dist && tsc -b" 8 | }, 9 | "devDependencies": { 10 | "typescript": "^3.7.5", 11 | "rimraf": "^3.0.1" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /core/wait-selector/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "esnext", 4 | "target": "es5", 5 | "sourceMap": true, 6 | "moduleResolution": "node", 7 | "outDir": "dist", 8 | "declaration": true, 9 | "declarationDir": "dist" 10 | }, 11 | "exclude": [ 12 | "node_modules" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "core/*", 4 | "projects/*" 5 | ], 6 | "npmClient": "npm", 7 | "version": "0.0.0" 8 | } 9 | -------------------------------------------------------------------------------- /projects/animespirit-ost-downloader/README.md: -------------------------------------------------------------------------------- 1 | ### AnimeSpirit ost downloader 2 | 3 | Adds bulk download for www.animespirit.ru/ost and http://www.animespirit.su/ost 4 | 5 | ### Install 6 | 7 | If you already have browser plugin to run user scripts just hit [Install](https://github.com/Kawaizombi/kawai-scripts/raw/master/dist/animespirit-ost-downloader.user.js) 8 | or install one of listed bellow 9 | 10 | | Browser | Install url | 11 | |---------|-------------| 12 | | Firefox | [Violentmonkey](https://addons.mozilla.org/en-US/firefox/addon/violentmonkey/) or [Tampermonkey](https://addons.mozilla.org/ru/firefox/addon/tampermonkey/) | 13 | | Chrome | [Violentmonkey](https://chrome.google.com/webstore/detail/violentmonkey/jinjaccalgkegednnccohejagnlnfdag) or [Tampermonkey](https://chrome.google.com/webstore/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo) | 14 | | Edge | [Violentmonkey](https://microsoftedge.microsoft.com/addons/detail/violentmonkey/eeagobfjdenkkddmbclomhiblgggliao) or [Tampermonkey](https://microsoftedge.microsoft.com/addons/detail/tampermonkey/iikmkjmpaadaobahmlepeloendndfphd) | 15 | | Safari | [Tampermonkey](https://www.tampermonkey.net/?ext=dhdg&browser=safari) | 16 | -------------------------------------------------------------------------------- /projects/animespirit-ost-downloader/headers.json: -------------------------------------------------------------------------------- 1 | { 2 | "grant": "GM_xmlhttpRequest", 3 | "connect": "mp3.animespirit.ru", 4 | "match": [ 5 | "*://www.animespirit.ru/ost/*", 6 | "*://animespirit.ru/ost/*", 7 | "*://www.animespirit.su/ost/*", 8 | "*://animespirit.su/ost/*" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /projects/animespirit-ost-downloader/index.ts: -------------------------------------------------------------------------------- 1 | import './polyfills'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | import { AppModule } from './src/components/app/app.module'; 4 | import { enableProdMode } from '@angular/core'; 5 | 6 | if(process.env.NODE_ENV === 'production') enableProdMode(); 7 | 8 | const mountPoint = document.createElement('app'); 9 | document.querySelector('.accordion').prepend(mountPoint); 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule); 12 | -------------------------------------------------------------------------------- /projects/animespirit-ost-downloader/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@kawai-scripts/animespirit-ost-downloader", 3 | "version": "1.2.3", 4 | "license": "MIT", 5 | "author": "kawaizombi", 6 | "description": "Adds bulk download for www.animespirit.ru/ost", 7 | "dependencies": { 8 | "@angular/core": "^8.2.14", 9 | "@angular/common": "^8.2.14", 10 | "@angular/platform-browser": "^8.2.14", 11 | "@angular/platform-browser-dynamic": "^8.2.14", 12 | "@angular/compiler": "^8.2.14", 13 | "@kawai-scripts/archive": "^1.0.0", 14 | "@kawai-scripts/gm-http-backend": "^1.1.0", 15 | "@kawai-scripts/save-file": "^1.0.0", 16 | "@kawai-scripts/favicon-indicator": "^1.0.0", 17 | "core-js": "^2.6.11", 18 | "rxjs": "^6.5.3", 19 | "zone.js": "^0.10.2", 20 | "typescript": "^3.7.2", 21 | "awesome-typescript-loader": "^5.2.1", 22 | "jszip": "^3.2.2" 23 | }, 24 | "devDependencies": { 25 | "@types/jszip": "^3.1.6", 26 | "@types/webpack": "^4.41.0", 27 | "webpack": "^4.41.4", 28 | "webpack-cli": "^3.3.10", 29 | "angular2-template-loader": "^0.6.2", 30 | "node-sass": "^4.13.0", 31 | "sass-loader": "^8.0.0", 32 | "css-loader": "^3.4.0", 33 | "style-loader": "^1.0.2", 34 | "to-string-loader": "^1.1.6" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /projects/animespirit-ost-downloader/polyfills.ts: -------------------------------------------------------------------------------- 1 | import 'core-js/es6/set'; 2 | import 'core-js/es7/reflect'; 3 | import 'zone.js/dist/zone'; 4 | -------------------------------------------------------------------------------- /projects/animespirit-ost-downloader/src/components/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { throwError } from 'rxjs'; 3 | import { catchError, finalize, tap } from 'rxjs/operators'; 4 | import archive from '@kawai-scripts/archive'; 5 | import saveFile from '@kawai-scripts/save-file'; 6 | import { FaviconService, ProgressIcon, ReadyIcon, ErrorIcon } from '@kawai-scripts/favicon-indicator'; 7 | import { Downloader } from '../downloader/downloader.service'; 8 | 9 | @Component({ 10 | selector: 'app', 11 | templateUrl: './app.template.html', 12 | styleUrls: ['./app.style.scss'], 13 | }) 14 | export class AppComponent implements OnInit { 15 | trackList: any[]; 16 | files: { [key: string]: ArrayBuffer } = {}; 17 | inProgress = false; 18 | status = ''; 19 | 20 | constructor( 21 | private readonly downloader: Downloader, 22 | private readonly favicon: FaviconService, 23 | ) { 24 | } 25 | 26 | ngOnInit() { 27 | this.trackList = this.downloader.getTrackList(); 28 | } 29 | 30 | createArchive() { 31 | this.status = 'Archiving'; 32 | 33 | return archive(this.trackList.map((item) => ({ ...item, file: this.files[item.name] }))); 34 | } 35 | 36 | download() { 37 | this.inProgress = true; 38 | this.status = 'Downloading'; 39 | this.favicon.useIcon(new ProgressIcon()); 40 | 41 | this.downloader.download() 42 | .pipe( 43 | tap(() => { 44 | const readyCount = Object.keys(this.files).length + 1; 45 | const percentage = 100 / this.trackList.length * readyCount; 46 | 47 | this.favicon.useIcon(new ProgressIcon().setPercentage(percentage)); 48 | }), 49 | finalize(async () => { 50 | const zip = await this.createArchive(); 51 | saveFile(zip, this.downloader.getAlbumName()); 52 | 53 | this.inProgress = false; 54 | this.favicon.useIcon(new ReadyIcon()) 55 | }), 56 | catchError((error) => { 57 | this.favicon.useIcon(new ErrorIcon()); 58 | 59 | return throwError(error); 60 | }), 61 | ) 62 | .subscribe(({ name, file }) => { 63 | this.files = { ...this.files, [name]: file }; 64 | this.trackList[name] = { ...this.trackList[name], file }; 65 | }); 66 | } 67 | } 68 | 69 | -------------------------------------------------------------------------------- /projects/animespirit-ost-downloader/src/components/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { HttpBackend, HttpClientModule } from '@angular/common/http'; 4 | import GMBackend from '@kawai-scripts/gm-http-backend'; 5 | import { FaviconModule } from '@kawai-scripts/favicon-indicator'; 6 | import { Downloader } from '../downloader/downloader.service'; 7 | import { AppComponent } from './app.component'; 8 | 9 | @NgModule({ 10 | declarations: [AppComponent], 11 | imports: [ 12 | BrowserModule, 13 | HttpClientModule, 14 | FaviconModule, 15 | ], 16 | bootstrap: [AppComponent], 17 | providers: [ 18 | { provide: Downloader, useClass: Downloader }, 19 | { provide: HttpBackend, useClass: GMBackend }, 20 | ], 21 | }) 22 | export class AppModule { 23 | } 24 | -------------------------------------------------------------------------------- /projects/animespirit-ost-downloader/src/components/app/app.style.scss: -------------------------------------------------------------------------------- 1 | @mixin spinner { 2 | display: inline-flex; 3 | width: 1em; 4 | height: 1em; 5 | border: 1px solid transparent; 6 | border-top: 1px solid #0089cc; 7 | border-radius: 50%; 8 | animation: 1s rotate infinite linear; 9 | @keyframes rotate { 10 | from { 11 | transform: rotate(0); 12 | } 13 | 14 | to { 15 | transform: rotate(360deg); 16 | } 17 | } 18 | } 19 | 20 | .track-name { 21 | &:after { 22 | content: ''; 23 | } 24 | 25 | &.in-progress:after { 26 | @include spinner; 27 | } 28 | 29 | &.ready:after { 30 | content: '✔'; 31 | color: green; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /projects/animespirit-ost-downloader/src/components/app/app.template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
8 | {{item.name}} 9 |
10 |
11 | -------------------------------------------------------------------------------- /projects/animespirit-ost-downloader/src/components/downloader/constants.ts: -------------------------------------------------------------------------------- 1 | export const TRACK_LIST_SELECTOR = '.tracklist tbody tr:not(:first-child)'; 2 | export const ALBUM_NAME_SELECTOR = '.content-block-title a'; 3 | -------------------------------------------------------------------------------- /projects/animespirit-ost-downloader/src/components/downloader/downloader.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { from } from 'rxjs'; 4 | import { delay, map, mergeAll, retry } from 'rxjs/operators'; 5 | import { ALBUM_NAME_SELECTOR, TRACK_LIST_SELECTOR } from './constants'; 6 | 7 | @Injectable() 8 | export class Downloader { 9 | constructor( 10 | private readonly http: HttpClient, 11 | ) { 12 | } 13 | 14 | getTrackList() { 15 | const rows = Array.from(document.querySelectorAll(TRACK_LIST_SELECTOR)); 16 | 17 | return rows.map((el) => { 18 | const title = el.querySelector(':nth-child(2)').textContent; 19 | const url = el.querySelector('a[href^=http]').getAttribute('href'); 20 | const [ext] = url.split('.').reverse(); 21 | 22 | return { name: `${ title }.${ ext }`, url }; 23 | }); 24 | } 25 | 26 | getAlbumName() { 27 | return document.querySelector(ALBUM_NAME_SELECTOR).textContent; 28 | } 29 | 30 | download() { 31 | return from(this.getTrackList()) 32 | .pipe( 33 | delay(500), 34 | map(({ url, name }) => 35 | this.http.get(url, { responseType: 'arraybuffer' }).pipe( 36 | retry(3), 37 | map((file) => ({ file, name, url })), 38 | ), 39 | ), 40 | mergeAll(4), 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /projects/animespirit-ost-downloader/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es5", 5 | "sourceMap": true, 6 | "esModuleInterop": true, 7 | "experimentalDecorators": true, 8 | "emitDecoratorMetadata": true 9 | }, 10 | "exclude": [ 11 | "node_modules" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /projects/animespirit-ost-downloader/webpack.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | import { TsConfigPathsPlugin } from 'awesome-typescript-loader'; 3 | import { Configuration } from 'webpack'; 4 | 5 | const CONFIG: Configuration = { 6 | entry: resolve(__dirname), 7 | 8 | resolve: { 9 | plugins: [ 10 | new TsConfigPathsPlugin({ configFileName: resolve(__dirname, 'tsconfig.json') }), 11 | ], 12 | alias: { 13 | '@angular': resolve(__dirname, './node_modules/@angular'), 14 | rxjs: resolve(__dirname, './node_modules/rxjs'), 15 | } 16 | }, 17 | module: { 18 | rules: [ 19 | { 20 | test: /\.tsx?$/, 21 | loader: 'angular2-template-loader', 22 | }, 23 | { 24 | test: /\.s[ac]ss$/i, 25 | use: [ 26 | 'to-string-loader', 27 | 'style-loader', 28 | 'css-loader', 29 | 'sass-loader', 30 | ], 31 | } 32 | ], 33 | } 34 | }; 35 | 36 | export default CONFIG; 37 | -------------------------------------------------------------------------------- /projects/get-qrcode/README.md: -------------------------------------------------------------------------------- 1 | ### Youtube blocker 2 | 3 | Generates QR code for page 4 | 5 | ### Preparation 6 | 7 | Install one of userscript manager(skip if you already have one) 8 | 9 | | Browser | Install url | 10 | |---------|-------------| 11 | | Firefox | [Violentmonkey](https://addons.mozilla.org/en-US/firefox/addon/violentmonkey/) or [Tampermonkey](https://addons.mozilla.org/ru/firefox/addon/tampermonkey/) | 12 | | Chrome | [Violentmonkey](https://chrome.google.com/webstore/detail/violentmonkey/jinjaccalgkegednnccohejagnlnfdag) or [Tampermonkey](https://chrome.google.com/webstore/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo) | 13 | | Edge | [Violentmonkey](https://microsoftedge.microsoft.com/addons/detail/violentmonkey/eeagobfjdenkkddmbclomhiblgggliao) or [Tampermonkey](https://microsoftedge.microsoft.com/addons/detail/tampermonkey/iikmkjmpaadaobahmlepeloendndfphd) | 14 | | Safari | [Tampermonkey](https://www.tampermonkey.net/?ext=dhdg&browser=safari) | 15 | 16 | ### Install 17 | 18 | Just click [Install](../../dist/get-qrcode.user.js?raw=true) 19 | 20 | -------------------------------------------------------------------------------- /projects/get-qrcode/headers.json: -------------------------------------------------------------------------------- 1 | { 2 | "grant": [ 3 | "GM_openInTab", 4 | "GM_registerMenuCommand" 5 | ], 6 | "supportURL": "https://github.com/Kawaizombi/kawai-scripts/issues", 7 | "noframes": true 8 | } 9 | -------------------------------------------------------------------------------- /projects/get-qrcode/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@kawai-scripts/get-qrcode", 3 | "description": "Generates QR code for page", 4 | "homepage": "https://github.com/Kawaizombi/kawai-scripts/tree/master/projects/get-qrcode", 5 | "author": "kawaizombi", 6 | "license": "MIT", 7 | "version": "1.1.2", 8 | "dependencies": { 9 | "qrcode": "^1.4.4", 10 | "sakura.css": "^1.0.0" 11 | }, 12 | "devDependencies": { 13 | "@types/qrcode": "^1.3.4", 14 | "node-sass": "^7.0.0", 15 | "sass-loader": "^8.0.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /projects/get-qrcode/src/adapters/blob-url.adapter.ts: -------------------------------------------------------------------------------- 1 | export default function blobUrlAdapter(page: string) { 2 | const blob = new Blob([page], { type: 'text/html' }); 3 | 4 | return URL.createObjectURL(blob); 5 | } 6 | -------------------------------------------------------------------------------- /projects/get-qrcode/src/adapters/iframe.adapter.ts: -------------------------------------------------------------------------------- 1 | export default function iframeAdapter(page: string) { 2 | const iframe = document.createElement('iframe'); 3 | 4 | iframe.srcdoc = page; 5 | iframe.referrerPolicy = 'no-referrer'; 6 | 7 | return iframe; 8 | } 9 | -------------------------------------------------------------------------------- /projects/get-qrcode/src/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/camelcase */ 2 | import modal from './utils/modal'; 3 | import blobUrlAdapter from './adapters/blob-url.adapter'; 4 | import iframeAdapter from './adapters/iframe.adapter'; 5 | import buildQRCodePage from './utils/build-qr-code-page'; 6 | import getQrCodeUrl from './utils/get-qr-code-url'; 7 | 8 | interface GMTab { 9 | onclose?: () => void; 10 | closed: boolean; 11 | close: () => void; 12 | } 13 | 14 | type GM_registerMenuCommand = (name: string, callback: () => void) => void; 15 | type GM_openInTab = (url: string, inBackground: boolean) => GMTab; 16 | 17 | declare global { 18 | interface Window { 19 | GM_registerMenuCommand: GM_registerMenuCommand; 20 | GM_openInTab: GM_openInTab; 21 | } 22 | } 23 | 24 | const COMMANDS = [ 25 | { 26 | caption: 'Get QR code(new tab)', 27 | async command() { 28 | const page = buildQRCodePage(await getQrCodeUrl()); 29 | const pageUrl = blobUrlAdapter(page); 30 | 31 | const tab = window.GM_openInTab(pageUrl, false); 32 | tab.onclose = () => URL.revokeObjectURL(pageUrl); 33 | }, 34 | enabled: () => navigator.userAgent.toLowerCase().indexOf('firefox') < 0, 35 | }, 36 | { 37 | caption: 'Get QR code(current tab)', 38 | async command() { 39 | const page = buildQRCodePage(await getQrCodeUrl()); 40 | const iframe = iframeAdapter(page); 41 | 42 | modal(iframe).show(); 43 | }, 44 | } 45 | ]; 46 | 47 | COMMANDS.forEach(({ caption, command, enabled }) => { 48 | if(!enabled || enabled()) { 49 | window.GM_registerMenuCommand(caption, command); 50 | } 51 | }); 52 | 53 | -------------------------------------------------------------------------------- /projects/get-qrcode/src/utils/build-qr-code-page/index.ts: -------------------------------------------------------------------------------- 1 | import template from './template.html'; 2 | import styles from './styles.scss'; 3 | 4 | export default function buildQRCodePage(qrCodeUrl) { 5 | const doc = new DOMParser().parseFromString(template, 'text/html'); 6 | doc.querySelector('.qr-code').setAttribute('src', qrCodeUrl); 7 | 8 | return `${ doc.body.innerHTML }`; 9 | } 10 | -------------------------------------------------------------------------------- /projects/get-qrcode/src/utils/build-qr-code-page/styles.scss: -------------------------------------------------------------------------------- 1 | @import "~sakura.css"; 2 | 3 | .fork-me { 4 | position: fixed; 5 | top: 0; 6 | right: 0; 7 | &:hover { 8 | border-bottom: none; 9 | } 10 | } 11 | 12 | .wrapper { 13 | display: flex; 14 | height: 100%; 15 | justify-content: center; 16 | align-items: center; 17 | 18 | .container { 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | padding: 20px; 23 | background: white; 24 | border-radius: 4px; 25 | box-shadow: 0 2px 1px -1px rgba(0, 0, 0, 0.2), 26 | 0 1px 1px 0 rgba(0, 0, 0, 0.14), 27 | 0 1px 3px 0 rgba(0, 0, 0, 0.12); 28 | .url-box { 29 | display: inline-flex; 30 | flex-direction: column; 31 | width: 100%; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /projects/get-qrcode/src/utils/build-qr-code-page/template.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Here is your QR code!

4 | Qr code image 5 |
6 |
7 | 8 | 12 | Fork me on GitHub 17 | 18 | -------------------------------------------------------------------------------- /projects/get-qrcode/src/utils/get-qr-code-url.ts: -------------------------------------------------------------------------------- 1 | import qrCodeAsPromised from './qr-code-as-promised'; 2 | 3 | export default function getQrCodeUrl() { 4 | const { href: currentUrl } = document.location; 5 | const { innerHeight, innerWidth } = window; 6 | const minDimension = Math.min(innerHeight, innerWidth); 7 | const qrCodeWidth = minDimension * 0.8; 8 | 9 | return qrCodeAsPromised(currentUrl, { width: qrCodeWidth }); 10 | } 11 | -------------------------------------------------------------------------------- /projects/get-qrcode/src/utils/modal/index.ts: -------------------------------------------------------------------------------- 1 | import styles from './modal.styles.scss'; 2 | import template from './modal.template.html'; 3 | 4 | export default function modal(element) { 5 | const doc = new DOMParser().parseFromString(template, 'text/html'); 6 | 7 | const modal = doc.body.firstChild as HTMLElement; 8 | const style = document.createElement('style'); 9 | const container: HTMLElement = modal.querySelector('.simple-modal-container'); 10 | container.append(element); 11 | style.innerHTML = styles; 12 | 13 | const show = () => { 14 | document.head.append(style); 15 | document.body.append(modal); 16 | container.focus(); 17 | }; 18 | 19 | const hide = () => { 20 | modal.remove(); 21 | style.remove(); 22 | }; 23 | 24 | modal.querySelector('.simple-modal-close').addEventListener('click', hide); 25 | 26 | return { show, hide } 27 | } 28 | -------------------------------------------------------------------------------- /projects/get-qrcode/src/utils/modal/modal.styles.scss: -------------------------------------------------------------------------------- 1 | .simple-modal-wrapper { 2 | position: fixed; 3 | top: 0; 4 | left: 0; 5 | width: 100%; 6 | height: 100%; 7 | display: flex; 8 | justify-content: center; 9 | align-items: center; 10 | background-color: rgba(black, 0.5); 11 | z-index: 9999999; 12 | 13 | .simple-modal-close { 14 | border: none; 15 | margin: 0; 16 | padding: 0; 17 | overflow: visible; 18 | background: rgba(white, 0.8); 19 | color: inherit; 20 | font: inherit; 21 | line-height: normal; 22 | position: absolute; 23 | border-radius: 50%; 24 | display: flex; 25 | justify-content: center; 26 | align-items: center; 27 | top: 15px; 28 | right: 15px; 29 | width: 40px; 30 | height: 40px; 31 | font-size: 20px; 32 | } 33 | 34 | .simple-modal-container { 35 | width: calc(100% - 20px); 36 | height: calc(100% - 150px); 37 | > * { 38 | width: 100%; 39 | height: 100%; 40 | border: none; 41 | border-radius: 5px; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /projects/get-qrcode/src/utils/modal/modal.template.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 | -------------------------------------------------------------------------------- /projects/get-qrcode/src/utils/qr-code-as-promised.ts: -------------------------------------------------------------------------------- 1 | import QRCode, { QRCodeToDataURLOptions } from 'qrcode'; 2 | 3 | export default function qrCodeAsPromised(text: string, options: QRCodeToDataURLOptions) { 4 | return new Promise((resolve, reject) => { 5 | QRCode.toDataURL(text, options, (err, url) => { 6 | err ? reject(err) : resolve(url); 7 | }); 8 | }); 9 | } 10 | -------------------------------------------------------------------------------- /projects/get-qrcode/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es5", 5 | "sourceMap": true, 6 | "esModuleInterop": true 7 | }, 8 | "exclude": [ 9 | "node_modules" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /projects/get-qrcode/types/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.scss' { 2 | const value: string; 3 | export default value; 4 | } 5 | 6 | declare module '*.html' { 7 | const value: string; 8 | export default value; 9 | } 10 | -------------------------------------------------------------------------------- /projects/get-qrcode/webpack.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | import { TsConfigPathsPlugin } from 'awesome-typescript-loader'; 3 | import { Configuration } from 'webpack'; 4 | 5 | const CONFIG: Configuration = { 6 | entry: resolve(__dirname, 'src'), 7 | module: { 8 | rules: [ 9 | { 10 | test: /\.s[ac]ss$/i, 11 | use: [ 12 | 'to-string-loader', 13 | 'css-loader', 14 | 'sass-loader', 15 | ], 16 | }, 17 | ] 18 | }, 19 | resolve: { 20 | plugins: [ 21 | new TsConfigPathsPlugin({ configFileName: resolve(__dirname, 'tsconfig.json') }), 22 | ], 23 | }, 24 | }; 25 | 26 | export default CONFIG; 27 | -------------------------------------------------------------------------------- /projects/hover-zoom/README.md: -------------------------------------------------------------------------------- 1 | ### Hover zoom 2 | 3 | Show image preview on hover(mouse over) link 4 | 5 | ### Install 6 | 7 | If you already have browser plugin to run user scripts just hit [Install](https://github.com/Kawaizombi/kawai-scripts/raw/master/dist/hover-zoom.user.js) 8 | or install one of listed bellow 9 | 10 | | Browser | Install url | 11 | |---------|-------------| 12 | | Firefox | [Violentmonkey](https://addons.mozilla.org/en-US/firefox/addon/violentmonkey/) or [Tampermonkey](https://addons.mozilla.org/ru/firefox/addon/tampermonkey/) | 13 | | Chrome | [Violentmonkey](https://chrome.google.com/webstore/detail/violentmonkey/jinjaccalgkegednnccohejagnlnfdag) or [Tampermonkey](https://chrome.google.com/webstore/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo) | 14 | | Edge | [Violentmonkey](https://microsoftedge.microsoft.com/addons/detail/violentmonkey/eeagobfjdenkkddmbclomhiblgggliao) or [Tampermonkey](https://microsoftedge.microsoft.com/addons/detail/tampermonkey/iikmkjmpaadaobahmlepeloendndfphd) | 15 | | Safari | [Tampermonkey](https://www.tampermonkey.net/?ext=dhdg&browser=safari) | 16 | -------------------------------------------------------------------------------- /projects/hover-zoom/headers.json: -------------------------------------------------------------------------------- 1 | { 2 | "grant": "none", 3 | "supportURL": "https://github.com/Kawaizombi/kawai-scripts/issues" 4 | } 5 | -------------------------------------------------------------------------------- /projects/hover-zoom/index.ts: -------------------------------------------------------------------------------- 1 | const IMAGE_FORMATS = ['jpeg', 'jpg', 'png', 'gif', 'webp', 'bmp']; 2 | 3 | const STYLES: Partial = { 4 | position: 'absolute', 5 | zIndex: '9999999', 6 | pointerEvents: 'none', 7 | maxWidth: '50%', 8 | maxHeight: '50%', 9 | border: '2px solid #ccc', 10 | borderRadius: '3px', 11 | }; 12 | 13 | function createPreview(url) { 14 | const preview = new Image(); 15 | Object.assign(preview.style, STYLES); 16 | preview.src = url; 17 | return preview; 18 | } 19 | 20 | function updatePos(el: HTMLElement, x: number, y: number) { 21 | el.style.left = `${ x }px`; 22 | el.style.top = `${ y }px`; 23 | } 24 | 25 | function addHandlers(img: HTMLImageElement, link: HTMLAnchorElement) { 26 | const moveHandler = ({ pageX, pageY }) => updatePos(img, pageX, pageY); 27 | const leaveHandler = () => { 28 | img.remove(); 29 | link.removeEventListener('mousemove', moveHandler); 30 | link.removeEventListener('mouseleave', leaveHandler); 31 | }; 32 | 33 | link.addEventListener('mousemove', moveHandler); 34 | link.addEventListener('mouseleave', leaveHandler); 35 | } 36 | 37 | document.querySelectorAll('a').forEach((link) => { 38 | link.addEventListener('mouseenter', async ({ pageX, pageY }) => { 39 | const url = link.getAttribute('href'); 40 | const [ext] = url.split('.').reverse(); 41 | 42 | if(IMAGE_FORMATS.includes(ext) && !link.querySelector('img')) { 43 | const preview = createPreview(url); 44 | 45 | document.body.appendChild(preview); 46 | updatePos(preview, pageX, pageY); 47 | 48 | addHandlers(preview, link); 49 | } 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /projects/hover-zoom/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@kawai-scripts/hover-zoom", 3 | "version": "1.0.3", 4 | "author": "kawaizombi", 5 | "description": "Show image preview on link hover", 6 | "homepage": "https://github.com/Kawaizombi/kawai-scripts/tree/master/projects/hover-zoom", 7 | "devDependencies": { 8 | "awesome-typescript-loader": "^5.2.1", 9 | "typescript": "^3.7.2" 10 | }, 11 | "dependencies": {}, 12 | "license": "MIT" 13 | } 14 | -------------------------------------------------------------------------------- /projects/hover-zoom/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es5", 5 | "sourceMap": true, 6 | "resolveJsonModule": true, 7 | "esModuleInterop": true 8 | }, 9 | "exclude": [ 10 | "node_modules" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /projects/hover-zoom/webpack.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | import { TsConfigPathsPlugin } from 'awesome-typescript-loader'; 3 | import { Configuration } from 'webpack'; 4 | 5 | const CONFIG: Configuration = { 6 | entry: resolve(__dirname), 7 | 8 | resolve: { 9 | plugins: [ 10 | new TsConfigPathsPlugin({ configFileName: resolve(__dirname, 'tsconfig.json') }), 11 | ], 12 | }, 13 | }; 14 | 15 | export default CONFIG; 16 | -------------------------------------------------------------------------------- /projects/soundcloud-downloader/README.md: -------------------------------------------------------------------------------- 1 | ### SoundCloud downloader 2 | 3 | Adds the ability to download any track or playlist from soundcloud.com 4 | 5 | ### Preparation 6 | 7 | Install one of userscript manager(skip if you already have one) 8 | 9 | | Browser | Install url | 10 | |---------|-------------| 11 | | Firefox | [Violentmonkey](https://addons.mozilla.org/en-US/firefox/addon/violentmonkey/) or [Tampermonkey](https://addons.mozilla.org/ru/firefox/addon/tampermonkey/) | 12 | | Chrome | [Violentmonkey](https://chrome.google.com/webstore/detail/violentmonkey/jinjaccalgkegednnccohejagnlnfdag) or [Tampermonkey](https://chrome.google.com/webstore/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo) | 13 | | Edge | [Violentmonkey](https://microsoftedge.microsoft.com/addons/detail/violentmonkey/eeagobfjdenkkddmbclomhiblgggliao) or [Tampermonkey](https://microsoftedge.microsoft.com/addons/detail/tampermonkey/iikmkjmpaadaobahmlepeloendndfphd) | 14 | | Safari | [Tampermonkey](https://www.tampermonkey.net/?ext=dhdg&browser=safari) | 15 | 16 | ### Install 17 | 18 | Just click [Install](../../dist/soundcloud-downloader.user.js?raw=true) 19 | 20 | -------------------------------------------------------------------------------- /projects/soundcloud-downloader/browserslist: -------------------------------------------------------------------------------- 1 | last 2 Chrome versions 2 | last 2 Safari versions 3 | last 2 Edge versions 4 | Firefox ESR 5 | -------------------------------------------------------------------------------- /projects/soundcloud-downloader/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@kawai-scripts/soundcloud-downloader", 3 | "author": "kawaizombi", 4 | "description": "Adds the ability to download any track or playlist from soundcloud.com", 5 | "homepage": "https://github.com/Kawaizombi/kawai-scripts/tree/master/projects/soundcloud-downloader", 6 | "license": "MIT", 7 | "version": "1.5.4", 8 | "dependencies": { 9 | "@fortawesome/angular-fontawesome": "^0.5.0", 10 | "@fortawesome/fontawesome-svg-core": "^1.2.26", 11 | "@fortawesome/free-solid-svg-icons": "^5.12.0", 12 | "@kawai-scripts/archive": "^1.0.0", 13 | "@kawai-scripts/save-file": "^1.0.0", 14 | "@ngxs/store": "^3.6.0", 15 | "@ngxs/storage-plugin": "^3.7.1", 16 | "browser-id3-writer": "^4.3.0", 17 | "m3u8-parser": "^4.4.0" 18 | }, 19 | "devDependencies": { 20 | "@ngxs/devtools-plugin": "^3.6.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /projects/soundcloud-downloader/src/constants.ts: -------------------------------------------------------------------------------- 1 | export const MOBILE_VERSION = document.location.host === 'm.soundcloud.com'; 2 | export const CONFIG_TOKEN = 'config'; 3 | -------------------------------------------------------------------------------- /projects/soundcloud-downloader/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | }; 4 | -------------------------------------------------------------------------------- /projects/soundcloud-downloader/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: false, 3 | }; 4 | -------------------------------------------------------------------------------- /projects/soundcloud-downloader/src/main.ts: -------------------------------------------------------------------------------- 1 | import './polyfills'; 2 | import './styles/main.scss'; 3 | import { enableProdMode } from '@angular/core'; 4 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 5 | import { environment } from './environments/environment'; 6 | import { AppModule } from './modules/app/app.module'; 7 | 8 | if(environment.production) enableProdMode(); 9 | 10 | (function mountApp() { 11 | const appElement = document.createElement('sc-downloader-root'); 12 | 13 | document.body.append(appElement); 14 | 15 | platformBrowserDynamic() 16 | .bootstrapModule(AppModule) 17 | .catch((error) => console.error('Error has occurred while booting soundcloud-downloader', error)); 18 | })(); 19 | -------------------------------------------------------------------------------- /projects/soundcloud-downloader/src/modules/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { DomInjectorService } from '../dom-injector/dom-injector.service'; 3 | 4 | @Component({ 5 | selector: 'sc-downloader-root', 6 | templateUrl: './app.template.html', 7 | styleUrls: ['./app.styles.scss'], 8 | }) 9 | export class AppComponent implements OnInit { 10 | constructor( 11 | private domInjector: DomInjectorService, 12 | ) { 13 | } 14 | 15 | ngOnInit() { 16 | this.domInjector.enable(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /projects/soundcloud-downloader/src/modules/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 4 | import { AppComponent } from './app.component'; 5 | import { CoreModule } from '../core/core.module'; 6 | import { StoreModule } from '../store/store.module'; 7 | import { DomInjectorModule } from '../dom-injector/dom-injector.module'; 8 | 9 | @NgModule({ 10 | imports: [ 11 | BrowserModule, 12 | BrowserAnimationsModule, 13 | CoreModule, 14 | StoreModule, 15 | DomInjectorModule, 16 | ], 17 | declarations: [AppComponent], 18 | bootstrap: [AppComponent], 19 | }) 20 | export class AppModule { 21 | 22 | } 23 | -------------------------------------------------------------------------------- /projects/soundcloud-downloader/src/modules/app/app.styles.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kawaizombi/kawai-scripts/a1497c4399e8e5ad9c34f63c165ef40d1d0127e4/projects/soundcloud-downloader/src/modules/app/app.styles.scss -------------------------------------------------------------------------------- /projects/soundcloud-downloader/src/modules/app/app.template.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /projects/soundcloud-downloader/src/modules/core/api/@types/Entry.ts: -------------------------------------------------------------------------------- 1 | export interface User { 2 | username: string; 3 | } 4 | export type Policies = 'ALLOW' | 'BLOCK' | 'SNIP'; 5 | 6 | export interface Track { 7 | description: string; 8 | title: string; 9 | artwork_url: string; 10 | genre: string; 11 | id: number; 12 | kind: 'track'; 13 | user: User; 14 | policy: Policies; 15 | } 16 | 17 | export interface Playlist { 18 | genre: string; 19 | description: string; 20 | tracks: Track[]; 21 | id: number; 22 | kind: 'playlist'; 23 | title: string; 24 | artwork_url: string; 25 | user: User; 26 | } 27 | 28 | export type Entry = Playlist | Track; 29 | -------------------------------------------------------------------------------- /projects/soundcloud-downloader/src/modules/core/api/api.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { ApiService } from './api.service'; 4 | import { HttpClientModule } from '@angular/common/http'; 5 | 6 | 7 | @NgModule({ 8 | imports: [ 9 | CommonModule, 10 | HttpClientModule, 11 | ], 12 | providers: [ApiService], 13 | }) 14 | export class ApiModule { 15 | } 16 | -------------------------------------------------------------------------------- /projects/soundcloud-downloader/src/modules/core/api/api.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { ApiService } from './api.service'; 4 | 5 | describe('ApiService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: ApiService = TestBed.get(ApiService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /projects/soundcloud-downloader/src/modules/core/api/api.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { map, mergeMap, toArray } from 'rxjs/operators'; 4 | import { TrackMetadata } from './@types'; 5 | import { forkJoin, from } from 'rxjs'; 6 | import combineBuffers from '../utils/combine-buffers'; 7 | import { Entry } from './@types/Entry'; 8 | import buildPlaylistManifest from '../utils/build-playlist-manifest'; 9 | 10 | const API_URL = 'https://api-v2.soundcloud.com'; 11 | const RESOLVE_URL = `${ API_URL }/resolve`; 12 | const TRACKS_URL = `${ API_URL }/tracks`; 13 | 14 | @Injectable({ 15 | providedIn: 'root', 16 | }) 17 | export class ApiService { 18 | constructor( 19 | private http: HttpClient, 20 | ) { 21 | } 22 | 23 | resolveByUrl(url: string) { 24 | return this.http.get(RESOLVE_URL, { params: { url } }); 25 | } 26 | 27 | getTracksMetadata(ids: number[]) { 28 | return this.http.get(TRACKS_URL, { params: { ids: ids.join(',') } }); 29 | } 30 | 31 | getPlaylistUrls(urls: string[]) { 32 | return forkJoin(urls.map((url) => { 33 | return this.http 34 | .get<{ url: string }>(url) 35 | .pipe(map(({ url }) => url)); 36 | })); 37 | } 38 | 39 | getPlaylists(urls: string[]) { 40 | return forkJoin(urls.map((url) => { 41 | return this.http 42 | .get(url, { responseType: 'text' }) 43 | .pipe(map(buildPlaylistManifest)); 44 | })); 45 | } 46 | 47 | downloadSegments(files: string[][]) { 48 | return from(files).pipe( 49 | mergeMap( 50 | (urls, index) => this.downloadFiles(urls).pipe( 51 | map((buffers) => ({ value: combineBuffers(buffers), index })), 52 | ), 53 | 3, 54 | ), 55 | toArray(), 56 | map(pairs => pairs.sort((l, r) => l.index - r.index).map(pair => pair.value)), 57 | ); 58 | } 59 | 60 | downloadFiles(urls: string[]) { 61 | return forkJoin(urls.map((url) => { 62 | return this.http.get(url, { responseType: 'arraybuffer' }); 63 | })); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /projects/soundcloud-downloader/src/modules/core/core.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { HttpSnifferModule } from './http-sniffer/http-sniffer.module'; 4 | import { HttpInterceptorsModule } from './http-interceptors/http-interceptors.module'; 5 | import { ApiModule } from './api/api.module'; 6 | import { DownloaderModule } from './downloader/downloader.module'; 7 | 8 | @NgModule({ 9 | imports: [ 10 | CommonModule, 11 | HttpSnifferModule, 12 | HttpInterceptorsModule, 13 | ApiModule, 14 | DownloaderModule, 15 | ], 16 | }) 17 | export class CoreModule { 18 | } 19 | -------------------------------------------------------------------------------- /projects/soundcloud-downloader/src/modules/core/downloader/downloader.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { DownloaderService } from './downloader.service'; 3 | import { MatSnackBarModule } from '@angular/material/snack-bar'; 4 | 5 | 6 | @NgModule({ 7 | providers: [DownloaderService], 8 | imports: [MatSnackBarModule], 9 | }) 10 | export class DownloaderModule { 11 | } 12 | -------------------------------------------------------------------------------- /projects/soundcloud-downloader/src/modules/core/downloader/downloader.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { DownloaderService } from './downloader.service'; 4 | 5 | describe('DownloaderService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: DownloaderService = TestBed.get(DownloaderService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /projects/soundcloud-downloader/src/modules/core/http-interceptors/app-version.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http'; 3 | import { CONFIG_TOKEN } from '../../../constants'; 4 | 5 | // It work even without this interceptor 6 | // but just in case, and also to mimic actual app 7 | @Injectable({ 8 | providedIn: 'root', 9 | }) 10 | export class AppVersionInterceptor implements HttpInterceptor { 11 | intercept(req: HttpRequest, next: HttpHandler) { 12 | const isApiRequest = new URL(req.url).host.indexOf('soundcloud.com') > -1; 13 | const appVersion = this.getAppVersion(); 14 | 15 | if(isApiRequest && appVersion) { 16 | req = req.clone({ 17 | params: req.params.set('app_version', appVersion), 18 | }); 19 | } 20 | 21 | return next.handle(req); 22 | } 23 | 24 | getAppVersion() { 25 | try { 26 | return window['unsafeWindow'].require(CONFIG_TOKEN).get('app_version'); 27 | } catch(e) { 28 | try { 29 | return window['unsafeWindow'].__sc_version; 30 | } catch(e) { 31 | return null; 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /projects/soundcloud-downloader/src/modules/core/http-interceptors/client-id.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpSnifferService } from '../http-sniffer/http-sniffer.service'; 3 | import { HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http'; 4 | import { CONFIG_TOKEN } from '../../../constants'; 5 | 6 | @Injectable({ 7 | providedIn: 'root', 8 | }) 9 | export class ClientIdInterceptor implements HttpInterceptor { 10 | constructor( 11 | private sniffer: HttpSnifferService, 12 | ) { 13 | } 14 | 15 | intercept(req: HttpRequest, next: HttpHandler) { 16 | const isApiRequest = new URL(req.url).host.indexOf('soundcloud.com') > -1; 17 | 18 | if(isApiRequest) { 19 | req = req.clone({ 20 | params: req.params.set('client_id', this.getClientId()), 21 | }); 22 | } 23 | 24 | return next.handle(req); 25 | } 26 | 27 | getClientId() { 28 | try { 29 | // in mobile version soundcloud use requirejs, so we can just read it straight from store 30 | return window['unsafeWindow'].require(CONFIG_TOKEN).get('client_id'); 31 | } catch(e) { 32 | return this.sniffer.clientId; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /projects/soundcloud-downloader/src/modules/core/http-interceptors/http-interceptors.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { HTTP_INTERCEPTORS } from '@angular/common/http'; 4 | import { ClientIdInterceptor } from './client-id.interceptor'; 5 | import { AppVersionInterceptor } from './app-version.interceptor'; 6 | 7 | @NgModule({ 8 | imports: [ 9 | CommonModule, 10 | ], 11 | providers: [ 12 | { provide: HTTP_INTERCEPTORS, useClass: ClientIdInterceptor, multi: true }, 13 | { provide: HTTP_INTERCEPTORS, useClass: AppVersionInterceptor, multi: true }, 14 | ], 15 | }) 16 | export class HttpInterceptorsModule { 17 | } 18 | -------------------------------------------------------------------------------- /projects/soundcloud-downloader/src/modules/core/http-sniffer/http-sniffer.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { HttpSnifferService } from './http-sniffer.service'; 4 | 5 | 6 | @NgModule({ 7 | imports: [CommonModule], 8 | providers: [HttpSnifferService], 9 | }) 10 | export class HttpSnifferModule { 11 | constructor( 12 | private sniffer: HttpSnifferService, 13 | ) { 14 | this.sniffer.enable(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /projects/soundcloud-downloader/src/modules/core/http-sniffer/http-sniffer.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { HttpSnifferService } from './http-sniffer.service'; 4 | 5 | describe('HttpSnifferService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: HttpSnifferService = TestBed.get(HttpSnifferService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /projects/soundcloud-downloader/src/modules/core/http-sniffer/http-sniffer.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable, Subscription } from 'rxjs'; 3 | import { filter, map } from 'rxjs/operators'; 4 | 5 | @Injectable({ 6 | providedIn: 'root', 7 | }) 8 | export class HttpSnifferService { 9 | private snifferSubscription = new Subscription(); 10 | 11 | clientId: string; 12 | 13 | enable() { 14 | this.snifferSubscription = this.attach() 15 | .pipe( 16 | map(([, url]) => url), 17 | map((url) => new URLSearchParams(url).get('client_id')), 18 | filter(Boolean), 19 | ) 20 | .subscribe((clientId: string) => { 21 | this.clientId = clientId; 22 | }); 23 | } 24 | 25 | disable() { 26 | this.snifferSubscription.unsubscribe(); 27 | } 28 | 29 | private attach() { 30 | return new Observable((subscriber) => { 31 | XMLHttpRequest.prototype.open = function(...args) { 32 | HttpSnifferService.originalOpen.apply(this, args); 33 | subscriber.next(args); 34 | }; 35 | 36 | return HttpSnifferService.restore; 37 | }); 38 | } 39 | 40 | private static originalOpen = XMLHttpRequest.prototype.open; 41 | 42 | private static restore() { 43 | XMLHttpRequest.prototype.open = HttpSnifferService.originalOpen; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /projects/soundcloud-downloader/src/modules/core/utils/add-id3/index.ts: -------------------------------------------------------------------------------- 1 | import ID3Writer from 'browser-id3-writer'; 2 | 3 | export default function addId3(buffer: ArrayBuffer, metadata: any): ArrayBuffer { 4 | const writer = new ID3Writer(buffer) 5 | .setFrame('TCON', metadata.genre ? metadata.genre.split(' & ') : []) 6 | .setFrame('TIT2', metadata.title) 7 | .setFrame('TPE1', [metadata.user.username]); 8 | 9 | if (metadata.trackNumber) { 10 | writer.setFrame('TRCK', metadata.trackNumber); 11 | } 12 | 13 | if (metadata.albumTitle) { 14 | writer.setFrame('TALB', metadata.albumTitle); 15 | } 16 | 17 | if (metadata.artwork) { 18 | writer.setFrame('APIC', { 19 | type: 18, 20 | data: metadata.artwork, 21 | description: 'Artwork', 22 | }); 23 | } 24 | 25 | return writer.addTag(); 26 | } 27 | -------------------------------------------------------------------------------- /projects/soundcloud-downloader/src/modules/core/utils/build-playlist-manifest/index.ts: -------------------------------------------------------------------------------- 1 | import { Parser } from 'm3u8-parser'; 2 | 3 | export default function buildPlaylistManifest(playlist) { 4 | const parser = new Parser(); 5 | parser.push(playlist); 6 | parser.end(); 7 | 8 | return parser.manifest; 9 | } 10 | -------------------------------------------------------------------------------- /projects/soundcloud-downloader/src/modules/core/utils/chunk/index.ts: -------------------------------------------------------------------------------- 1 | export function chunk(array: any[], size: number) { 2 | const chunks = array.map((elem, i) => i % size ? [] : [array.slice(i, i + size)]); 3 | 4 | return [].concat(...chunks); 5 | } 6 | -------------------------------------------------------------------------------- /projects/soundcloud-downloader/src/modules/core/utils/combine-buffers/index.ts: -------------------------------------------------------------------------------- 1 | export default function combineBuffers(buffers: ArrayBuffer[]): ArrayBuffer { 2 | let offset = 0; 3 | const size = buffers.reduce((sum, { byteLength }) => sum + byteLength, 0); 4 | 5 | return buffers.reduce((result: Uint8Array, buffer: ArrayBuffer) => { 6 | result.set(new Uint8Array(buffer), offset); 7 | offset += buffer.byteLength; 8 | 9 | return result 10 | }, new Uint8Array(size)); 11 | } 12 | -------------------------------------------------------------------------------- /projects/soundcloud-downloader/src/modules/core/utils/combine-css-selectors/index.ts: -------------------------------------------------------------------------------- 1 | const combineCssSelectors = (...selectors: string[]) => selectors.join(', '); 2 | 3 | export default combineCssSelectors; 4 | -------------------------------------------------------------------------------- /projects/soundcloud-downloader/src/modules/core/utils/dom-observer/index.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs'; 2 | import { filter } from 'rxjs/operators'; 3 | 4 | export const ADD_TYPE = 'ADD'; 5 | export const REMOVE_TYPE = 'REMOVE'; 6 | 7 | type EventType = typeof ADD_TYPE | typeof REMOVE_TYPE; 8 | 9 | interface Event { 10 | type: EventType; 11 | node: Node; 12 | } 13 | 14 | export const ofType = (t: EventType) => filter(({ type }) => t === type); 15 | 16 | export default function domObserver(el: string | Element, options: MutationObserverInit) { 17 | return new Observable((subscriber) => { 18 | const _el = typeof el === 'string' ? document.querySelector(el) : el; 19 | const observer = new MutationObserver((records) => { 20 | records.forEach((record) => { 21 | Array.from(record.addedNodes).forEach((node) => subscriber.next({ type: ADD_TYPE, node })); 22 | Array.from(record.removedNodes).forEach((node) => subscriber.next({ type: REMOVE_TYPE, node })); 23 | }); 24 | }); 25 | 26 | observer.observe(_el, options); 27 | 28 | return () => observer.disconnect(); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /projects/soundcloud-downloader/src/modules/core/utils/extract-ids/index.ts: -------------------------------------------------------------------------------- 1 | import { Entry } from '../../api/@types/Entry'; 2 | 3 | 4 | export default function extractIds(entry: Entry) { 5 | if (entry.kind === 'playlist') { 6 | return entry.tracks.filter(({ policy }) => policy !== 'BLOCK').map(({ id }) => id); 7 | } 8 | 9 | return [entry.id]; 10 | } 11 | -------------------------------------------------------------------------------- /projects/soundcloud-downloader/src/modules/core/utils/extract-media-urls/index.ts: -------------------------------------------------------------------------------- 1 | import { TrackMetadata } from '../../api/@types'; 2 | 3 | export default function extractMediaUrls(tracks: TrackMetadata[]) { 4 | return tracks.map(({ media: { transcodings } }) => { 5 | const { url } = transcodings.find(({ format: { protocol } }) => protocol === 'hls'); 6 | 7 | return url; 8 | }); 9 | } 10 | -------------------------------------------------------------------------------- /projects/soundcloud-downloader/src/modules/core/utils/extract-urls-from-manifest/index.ts: -------------------------------------------------------------------------------- 1 | export default function extractUrlsFromManifest(manifests: any[]): string[][] { 2 | return manifests.map((manifest) => manifest.segments.map(({ uri }) => uri)); 3 | } 4 | -------------------------------------------------------------------------------- /projects/soundcloud-downloader/src/modules/dom-injector/dom-injector.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { DomInjectorService } from './dom-injector.service'; 3 | import { DownloadButtonModule } from '../download-button/download-button.module'; 4 | 5 | @NgModule({ 6 | imports: [DownloadButtonModule], 7 | providers: [DomInjectorService], 8 | }) 9 | export class DomInjectorModule { 10 | } 11 | -------------------------------------------------------------------------------- /projects/soundcloud-downloader/src/modules/dom-injector/dom-injector.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { DomInjectorService } from './dom-injector.service'; 4 | 5 | describe('DomInjectorService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: DomInjectorService = TestBed.get(DomInjectorService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /projects/soundcloud-downloader/src/modules/download-button/download-button.component.html: -------------------------------------------------------------------------------- 1 | 10 | 11 | 14 | -------------------------------------------------------------------------------- /projects/soundcloud-downloader/src/modules/download-button/download-button.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: inline-block; 3 | 4 | &.mobile { 5 | margin-top: 8px; 6 | width: 100%; 7 | .sc-button { 8 | background: white; 9 | padding: 0 10px 0 10px; 10 | height: 22px; 11 | border-radius: 3px; 12 | color: #333; 13 | border: 1px solid #e5e5e5; 14 | width: 100%; 15 | &:hover, &:focus { 16 | border-color: #ccc; 17 | } 18 | &[disabled] { 19 | background-color: #f2f2f2; 20 | color: #ccc; 21 | } 22 | } 23 | } 24 | 25 | .sc-button { 26 | font-size: small; 27 | text-indent: initial !important; 28 | 29 | &:not(:last-child) { 30 | margin-right: 4px; 31 | } 32 | 33 | .soundActions__small & { 34 | height: 22px; 35 | padding: 0 8px 0 8px; 36 | } 37 | 38 | &.download-button fa-icon { 39 | margin-right: 4px; 40 | } 41 | } 42 | } 43 | 44 | .listenEngagement__footer { 45 | & :host { 46 | float: left; 47 | clear: none; 48 | vertical-align: middle; 49 | } 50 | } 51 | 52 | .wrapper { 53 | display: flex; 54 | align-items: center; 55 | } 56 | -------------------------------------------------------------------------------- /projects/soundcloud-downloader/src/modules/download-button/download-button.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { DownloadButtonComponent } from './download-button.component'; 4 | 5 | describe('DownloadButtonComponent', () => { 6 | let component: DownloadButtonComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ DownloadButtonComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(DownloadButtonComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /projects/soundcloud-downloader/src/modules/download-button/download-button.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectorRef, Component, ElementRef, Input, OnDestroy, OnInit } from '@angular/core'; 2 | import { Select } from '@ngxs/store'; 3 | import { faCog, faDownload } from '@fortawesome/free-solid-svg-icons'; 4 | import { Observable, Subscription } from 'rxjs'; 5 | import { first, map } from 'rxjs/operators'; 6 | import { DownloaderService } from '../core/downloader/downloader.service'; 7 | import { DownloadsModel, DownloadsState } from '../store/downloads/downloads.state'; 8 | import { MOBILE_VERSION } from '../../constants'; 9 | import { MatDialog } from '@angular/material/dialog'; 10 | import { SettingsPopupComponent } from '../settings-popup/settings-popup.component'; 11 | import { SettingsState } from '../store/settings/settings.state'; 12 | 13 | @Component({ 14 | selector: 'sc-downloader-download-button', 15 | templateUrl: './download-button.component.html', 16 | styleUrls: ['./download-button.component.scss'], 17 | }) 18 | export class DownloadButtonComponent implements OnInit, OnDestroy { 19 | @Input() rootUrl: string; 20 | @Select(DownloadsState) downloads$: Observable; 21 | @Select(SettingsState.addTrackNumberToFileName) addTrackNumberToFileName$: Observable; 22 | private rootSub = new Subscription(); 23 | 24 | faDownload = faDownload; 25 | faCog = faCog; 26 | inProgress = false; 27 | 28 | constructor( 29 | private downloader: DownloaderService, 30 | private cd: ChangeDetectorRef, 31 | private el: ElementRef, 32 | private dialog: MatDialog, 33 | ) { 34 | } 35 | 36 | ngOnInit() { 37 | if(MOBILE_VERSION) { 38 | this.el.nativeElement.classList.add('mobile'); 39 | } 40 | 41 | const item$ = this.downloads$.pipe(map(({ [this.rootUrl]: item }) => item)); 42 | 43 | this.rootSub.add(item$.subscribe((inProgress) => { 44 | this.inProgress = inProgress; 45 | 46 | setTimeout(() => this.cd.detectChanges()); 47 | })); 48 | } 49 | 50 | ngOnDestroy() { 51 | this.rootSub.unsubscribe(); 52 | } 53 | 54 | download(event: MouseEvent) { 55 | event.preventDefault(); 56 | event.stopPropagation(); 57 | this.addTrackNumberToFileName$.pipe(first()).subscribe(status => { 58 | this.downloader.download(this.rootUrl, status); 59 | }) 60 | 61 | } 62 | 63 | openSettings() { 64 | this.dialog.open(SettingsPopupComponent) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /projects/soundcloud-downloader/src/modules/download-button/download-button.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; 5 | import { MatDialogModule } from '@angular/material/dialog'; 6 | import { SettingsPopupModule } from '../settings-popup/settings-popup.module'; 7 | import { DownloadButtonComponent } from './download-button.component'; 8 | 9 | @NgModule({ 10 | imports: [CommonModule, FontAwesomeModule, FormsModule, MatDialogModule, SettingsPopupModule], 11 | declarations: [DownloadButtonComponent], 12 | entryComponents: [DownloadButtonComponent], 13 | }) 14 | export class DownloadButtonModule { 15 | } 16 | -------------------------------------------------------------------------------- /projects/soundcloud-downloader/src/modules/settings-popup/settings-popup.component.html: -------------------------------------------------------------------------------- 1 |

Soundcloud downloader settings

2 | 3 |
4 |
5 | 6 | Add track number to file name 7 | 8 | 9 |
10 |
11 | 12 |
13 | 14 |
15 | 16 | -------------------------------------------------------------------------------- /projects/soundcloud-downloader/src/modules/settings-popup/settings-popup.component.scss: -------------------------------------------------------------------------------- 1 | [mat-dialog-content] { 2 | height: 200px; 3 | } 4 | 5 | .control { 6 | display: flex; 7 | align-items: center; 8 | fa-icon { 9 | margin-left: 8px; 10 | font-size: 16px; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /projects/soundcloud-downloader/src/modules/settings-popup/settings-popup.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { SettingsPopupComponent } from './settings-popup.component'; 4 | 5 | describe('SettingsPopupComponent', () => { 6 | let component: SettingsPopupComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ SettingsPopupComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(SettingsPopupComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /projects/soundcloud-downloader/src/modules/settings-popup/settings-popup.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { Select, Store } from '@ngxs/store'; 3 | import { SettingsState } from '../store/settings/settings.state'; 4 | import { Observable } from 'rxjs'; 5 | import { ToggleTrackNumber } from '../store/settings/settings.actions'; 6 | import { MatCheckboxChange } from '@angular/material/checkbox'; 7 | import { faInfoCircle } from '@fortawesome/free-solid-svg-icons'; 8 | 9 | @Component({ 10 | selector: 'downloader-settings-popup', 11 | templateUrl: './settings-popup.component.html', 12 | styleUrls: ['./settings-popup.component.scss'] 13 | }) 14 | export class SettingsPopupComponent { 15 | faInfoCircle = faInfoCircle; 16 | @Select(SettingsState.addTrackNumberToFileName) addTrackNumberToFileName$: Observable; 17 | 18 | constructor(private store: Store) { } 19 | 20 | toggleTrackNumbers(e: MatCheckboxChange) { 21 | this.store.dispatch(new ToggleTrackNumber(e.checked)) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /projects/soundcloud-downloader/src/modules/settings-popup/settings-popup.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { MatDialogModule } from '@angular/material/dialog'; 4 | import { SettingsPopupComponent } from './settings-popup.component'; 5 | import { MatCheckboxModule } from '@angular/material/checkbox'; 6 | import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; 7 | import { MatTooltipModule } from '@angular/material/tooltip'; 8 | 9 | 10 | @NgModule({ 11 | declarations: [SettingsPopupComponent], 12 | entryComponents: [SettingsPopupComponent], 13 | imports: [CommonModule, MatDialogModule, MatCheckboxModule, FontAwesomeModule, MatTooltipModule], 14 | }) 15 | export class SettingsPopupModule { 16 | } 17 | -------------------------------------------------------------------------------- /projects/soundcloud-downloader/src/modules/store/downloads/downloads.actions.ts: -------------------------------------------------------------------------------- 1 | export class AddDownloadItem { 2 | static readonly type = '[Downloads] Add'; 3 | 4 | constructor( 5 | public key: string, 6 | ) { 7 | } 8 | } 9 | 10 | export class RemoveDownloadItem { 11 | static readonly type = '[Downloads] Remove'; 12 | 13 | constructor( 14 | public key: string, 15 | ) { 16 | } 17 | } 18 | 19 | export class UpdateDownloadItem { 20 | static readonly type = '[Downloads] Update'; 21 | 22 | constructor( 23 | public key: string, 24 | ) { 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /projects/soundcloud-downloader/src/modules/store/downloads/downloads.state.ts: -------------------------------------------------------------------------------- 1 | import { Action, State, StateContext } from '@ngxs/store'; 2 | import { AddDownloadItem, RemoveDownloadItem } from './downloads.actions'; 3 | 4 | export interface DownloadsModel { 5 | [key: string]: boolean; 6 | } 7 | 8 | type Context = StateContext 9 | 10 | @State({ 11 | name: 'downloads', 12 | defaults: {}, 13 | }) 14 | export class DownloadsState { 15 | @Action(AddDownloadItem) 16 | add(ctx: Context, { key }: AddDownloadItem) { 17 | ctx.patchState({ [key]: true }); 18 | } 19 | 20 | @Action(RemoveDownloadItem) 21 | update(ctx: Context, { key }: RemoveDownloadItem) { 22 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 23 | const { [key]: item, ...rest } = ctx.getState(); 24 | 25 | ctx.setState(rest); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /projects/soundcloud-downloader/src/modules/store/downloads/types.ts: -------------------------------------------------------------------------------- 1 | export interface Base { 2 | title: string; 3 | readySegments: number; 4 | totalSegments: number; 5 | } 6 | 7 | export interface DownloadItem extends Base { 8 | status: 'pending' | 'in-progress' | 'ready'; 9 | kind: 'track' | 'playlist'; 10 | tracks: Base[]; 11 | } 12 | -------------------------------------------------------------------------------- /projects/soundcloud-downloader/src/modules/store/settings/settings.actions.ts: -------------------------------------------------------------------------------- 1 | export class ToggleTrackNumber { 2 | static readonly type = '[Settings] Toggle track number'; 3 | 4 | constructor(public status: boolean) { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /projects/soundcloud-downloader/src/modules/store/settings/settings.state.ts: -------------------------------------------------------------------------------- 1 | import { Action, Selector, State, StateContext } from '@ngxs/store'; 2 | import { ToggleTrackNumber } from './settings.actions'; 3 | 4 | export interface SettingsModel { 5 | addTrackNumberToFileName: boolean; 6 | } 7 | 8 | type Context = StateContext 9 | 10 | @State({ 11 | name: 'settings', 12 | defaults: { 13 | addTrackNumberToFileName: false, 14 | }, 15 | }) 16 | export class SettingsState { 17 | @Action(ToggleTrackNumber) 18 | toggleTrackNumber(ctx: Context, { status }: ToggleTrackNumber) { 19 | ctx.patchState({ addTrackNumberToFileName: status }); 20 | } 21 | 22 | @Selector() 23 | static addTrackNumberToFileName(state: SettingsModel) { 24 | return state.addTrackNumberToFileName; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /projects/soundcloud-downloader/src/modules/store/store.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { NgxsModule } from '@ngxs/store'; 3 | import { NgxsReduxDevtoolsPluginModule } from '@ngxs/devtools-plugin'; 4 | import { environment } from '../../environments/environment'; 5 | import { DownloadsState } from './downloads/downloads.state'; 6 | import { SettingsState } from './settings/settings.state'; 7 | import { NgxsStoragePluginModule } from '@ngxs/storage-plugin'; 8 | 9 | @NgModule({ 10 | imports: [ 11 | NgxsModule.forRoot([DownloadsState, SettingsState], { 12 | developmentMode: !environment.production, 13 | }), 14 | NgxsStoragePluginModule.forRoot({ 15 | key: [SettingsState], 16 | }), 17 | NgxsReduxDevtoolsPluginModule.forRoot({ 18 | name: 'SoundCloud Downloader', 19 | disabled: environment.production, 20 | }), 21 | ], 22 | }) 23 | export class StoreModule { 24 | } 25 | -------------------------------------------------------------------------------- /projects/soundcloud-downloader/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | import 'zone.js/dist/zone'; 2 | -------------------------------------------------------------------------------- /projects/soundcloud-downloader/src/styles/main.scss: -------------------------------------------------------------------------------- 1 | @import "~@angular/material/prebuilt-themes/deeppurple-amber.css"; 2 | 3 | .cdk-overlay-container { 4 | z-index: 1002; 5 | } 6 | -------------------------------------------------------------------------------- /projects/soundcloud-downloader/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "esnext", 4 | "target": "es2015", 5 | "moduleResolution": "node", 6 | "lib": [ 7 | "es2015", 8 | "dom" 9 | ], 10 | "sourceMap": true, 11 | "esModuleInterop": true, 12 | "experimentalDecorators": true, 13 | "emitDecoratorMetadata": true 14 | }, 15 | "exclude": [ 16 | "node_modules" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /projects/soundcloud-downloader/webpack.analyze.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const provider = require('../../build/ng-webpack-configs/analyze'); 3 | 4 | module.exports = provider('soundcloud-downloader'); 5 | -------------------------------------------------------------------------------- /projects/soundcloud-downloader/webpack.dev.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const provider = require('../../build/ng-webpack-configs/development'); 3 | 4 | module.exports = provider('soundcloud-downloader'); 5 | -------------------------------------------------------------------------------- /projects/soundcloud-downloader/webpack.prod.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const provider = require('../../build/ng-webpack-configs/production'); 3 | 4 | module.exports = provider('soundcloud-downloader'); 5 | -------------------------------------------------------------------------------- /projects/youtube-blocker/README.md: -------------------------------------------------------------------------------- 1 | ### Youtube blocker 2 | 3 | Adds the ability to block youtube videos from specific channels and users 4 | 5 | ### Preparation 6 | 7 | Install one of userscript manager(skip if you already have one) 8 | 9 | | Browser | Install url | 10 | |---------|-------------| 11 | | Firefox | [Violentmonkey](https://addons.mozilla.org/en-US/firefox/addon/violentmonkey/) or [Tampermonkey](https://addons.mozilla.org/ru/firefox/addon/tampermonkey/) | 12 | | Chrome | [Violentmonkey](https://chrome.google.com/webstore/detail/violentmonkey/jinjaccalgkegednnccohejagnlnfdag) or [Tampermonkey](https://chrome.google.com/webstore/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo) | 13 | | Edge | [Violentmonkey](https://microsoftedge.microsoft.com/addons/detail/violentmonkey/eeagobfjdenkkddmbclomhiblgggliao) or [Tampermonkey](https://microsoftedge.microsoft.com/addons/detail/tampermonkey/iikmkjmpaadaobahmlepeloendndfphd) | 14 | | Safari | [Tampermonkey](https://www.tampermonkey.net/?ext=dhdg&browser=safari) | 15 | 16 | ### Install 17 | 18 | Just click [Install](../../dist/youtube-blocker.user.js?raw=true) 19 | 20 | -------------------------------------------------------------------------------- /projects/youtube-blocker/browserslist: -------------------------------------------------------------------------------- 1 | last 2 Chrome versions 2 | last 2 Safari versions 3 | last 2 Edge versions 4 | Firefox ESR 5 | -------------------------------------------------------------------------------- /projects/youtube-blocker/headers.json: -------------------------------------------------------------------------------- 1 | { 2 | "grant": [ 3 | "GM_getValue", 4 | "GM_setValue", 5 | "GM_deleteValue", 6 | "GM.deleteValue", 7 | "GM.getValue", 8 | "GM.setValue" 9 | ], 10 | "noframes": true, 11 | "match": [ 12 | "*://www.youtube.com/*", 13 | "*://youtube.com/*" 14 | ], 15 | "run-at": "document-idle", 16 | "supportURL": "https://github.com/Kawaizombi/kawai-scripts/issues", 17 | "icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEYAAAAxCAMAAABQ1y/PAAAAWlBMVEUAAAD/AAD/AAD/AAD/AAD/AAD9AAD+AAD+AAD+AAD+AAD+AAD+AAD+DAz+FRX+Hx/+Ly/+Rkb+Wlr+cXH+g4P+k5P+oqL+trb+y8v+2tr+6Oj+9PT++/v+/v4M4ypJAAAADHRSTlMAEiNAYHuSrcjd7vzXMBaiAAACkElEQVRIx62XDZuCIAyAyfzAHKKioKj//2/ehtaZB1x3tacyFV7GGNtg7CGX5JqmWZbnRVFwXpLcUCoAoKt7wDkvijzPsjS9Jhd2liQrSoCKuuDl8XOWCptQo+0DZZFfD5BLDv+Vij9ACfeP/SIo3XV5i4J9N33ytyjIuZGtkzcpKBliMnhbSsQUsQZCiMMlKAljtxjkeBNdrEsU0qp+0HroVYcP6ohxrqFX2KkbxnWXSasIp2BpkCKGaT3IrNvgzHhooWqozbou62qdLPR/6kKckuVer5HQTNhxsaOi29ZMM95aFcDcWB6Y0UiqGCI2jcTOgyVOhwP4JOQ2hiizBilRAVHLBloCj43fzox7p6RmZ9bFwH147E8cHcCUP20jQKJ553E+cTq70rRqL8anTL+g/qCXE0cvIXV8mBo0DtujWc8cdCQjfavl2VICGpzTBE194jSATj0rnzpeTDdtGPHMkaDRXINvzVnlmZOy5DK4Ns8c6V4MXm28GHRZTT13zrpxardWf8Ds2sCzPjFtQrYZHcZx5l0f6f76beNdqdGZGE6c2Ep5/EY4v1H7qHV950jyG9xV4mUvpl24q3PgxLy4Csxq7h+cfV6jdUrWr+9w2lS4Cw+cxe14VEa8HG/EFm9wZBexkINg6+JoGwgUgejnYqjVRGgw/AFhlm+7v4bBxq2LxZNp3f3gYjGaCwKxOJgZGpejli0zUAQjXYKZIZynpLZbJF32gNpF8lQ0a+4gFGvUKacfpCpiORy/nRq0MWboOxHP4dGKYgt6jesfqU2o/Pulvnm+BiT5WLX1gdov/1wlyi7lR+riN6t0SL/PDP/HlMfDR5IW/O+oG386wfw8UPHtQHU/T1X3A1XoPPUFM12wQgKaLQIAAAAASUVORK5CYII=", 18 | "description:de": "Fügt die Möglichkeit hinzu, Videos von bestimmten Kanälen und Benutzern zu blockieren", 19 | "description:fr": "Ajoute la possibilité de bloquer les vidéos de chaînes et d'utilisateurs spécifiques", 20 | "description:pt": "Adiciona a capacidade de bloquear vídeos de canais e usuários específicos", 21 | "description:es": "Agrega la capacidad de bloquear videos de canales y usuarios específicos", 22 | "description:uk": "Додає можливість блокувати відео від визначених каналів та користувачів", 23 | "description:ru": "Добавляет возможность блокировать видео от определенных каналов и пользователей" 24 | } 25 | -------------------------------------------------------------------------------- /projects/youtube-blocker/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@kawai-scripts/youtube-blocker", 3 | "version": "1.5.4", 4 | "author": "kawaizombi", 5 | "description": "Adds the ability to block videos from specific channels and users", 6 | "homepage": "https://github.com/Kawaizombi/kawai-scripts/tree/master/projects/youtube-blocker", 7 | "license": "MIT", 8 | "dependencies": { 9 | "@fortawesome/free-solid-svg-icons": "^5.12.0", 10 | "@fortawesome/fontawesome-svg-core": "^1.2.26", 11 | "@fortawesome/angular-fontawesome": "^0.5.0", 12 | "@ngxs/store": "^3.6.1", 13 | "@ngxs-labs/async-storage-plugin": "0.1.1", 14 | "@kawai-scripts/gm-storage": "^1.0.0", 15 | "@kawai-scripts/save-file": "^1.0.0", 16 | "@kawai-scripts/wait-selector": "^1.0.0", 17 | "autobind-decorator": "^2.4.0", 18 | "element-matches-polyfill": "^1.0.0" 19 | }, 20 | "devDependencies": { 21 | "@ngxs/devtools-plugin": "^3.6.1" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /projects/youtube-blocker/src/components/add-filter-form/add-filter-form.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { faPlusCircle } from '@fortawesome/free-solid-svg-icons'; 3 | import { Store } from '@ngxs/store'; 4 | import { AddFilterAction } from '../../store/block-list/block-list.actions'; 5 | 6 | @Component({ 7 | selector: 'add-filter-form', 8 | templateUrl: './add-filter-form.template.html', 9 | styleUrls: ['./add-filter-form.styles.scss'], 10 | }) 11 | export class AddFilterFormComponent { 12 | faPlusCircle = faPlusCircle; 13 | newFilter = ''; 14 | 15 | constructor( 16 | private store: Store 17 | ) { 18 | } 19 | 20 | addFilter() { 21 | this.store.dispatch(new AddFilterAction(this.newFilter)); 22 | this.newFilter = ''; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /projects/youtube-blocker/src/components/add-filter-form/add-filter-form.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { AddFilterFormComponent } from './add-filter-form.component'; 3 | import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; 4 | import { FormsModule } from '@angular/forms'; 5 | import { MatButtonModule, MatInputModule, MatFormFieldModule } from '@angular/material'; 6 | 7 | @NgModule({ 8 | declarations: [AddFilterFormComponent], 9 | exports: [AddFilterFormComponent], 10 | imports: [ 11 | MatFormFieldModule, 12 | FontAwesomeModule, 13 | FormsModule, 14 | MatButtonModule, 15 | MatInputModule, 16 | ], 17 | }) 18 | export class AddFilterFormModule { 19 | 20 | } 21 | -------------------------------------------------------------------------------- /projects/youtube-blocker/src/components/add-filter-form/add-filter-form.styles.scss: -------------------------------------------------------------------------------- 1 | button, .box { 2 | width: 100%; 3 | } 4 | 5 | -------------------------------------------------------------------------------- /projects/youtube-blocker/src/components/add-filter-form/add-filter-form.template.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | Add channel/user to block list 4 | 5 | 6 | 7 | 8 | Supports wildcards, for example: *background 9 | 10 | 11 | 12 | 16 |
17 | -------------------------------------------------------------------------------- /projects/youtube-blocker/src/components/app/app.constants.ts: -------------------------------------------------------------------------------- 1 | import combineCssSelectors from '../../utils/combine-css-rules'; 2 | 3 | export const OBSERVER_ELEMENT_SELECTOR = combineCssSelectors( 4 | '#content', 5 | 'ytd-page-manager', 6 | ); 7 | 8 | export const OBSERVER_CONFIG = { childList: true, subtree: true }; 9 | -------------------------------------------------------------------------------- /projects/youtube-blocker/src/components/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { MatButtonModule, MatTooltipModule, MatDialogModule, MatSnackBarModule } from '@angular/material'; 4 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 5 | import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; 6 | import { AppComponent } from './app.component'; 7 | import { PreferencesPopupModule } from '../preferences-popup/preferences-popup.module'; 8 | import { BlockerModule } from '../blocker/blocker.module'; 9 | import { StoreModule } from '../../store/store.module'; 10 | 11 | 12 | @NgModule({ 13 | declarations: [AppComponent], 14 | imports: [ 15 | MatDialogModule, 16 | BrowserAnimationsModule, 17 | BrowserModule, 18 | FontAwesomeModule, 19 | StoreModule, 20 | PreferencesPopupModule, 21 | MatButtonModule, 22 | BlockerModule, 23 | MatTooltipModule, 24 | MatSnackBarModule, 25 | ], 26 | bootstrap: [AppComponent], 27 | }) 28 | export class AppModule { 29 | 30 | } 31 | -------------------------------------------------------------------------------- /projects/youtube-blocker/src/components/app/app.styles.scss: -------------------------------------------------------------------------------- 1 | :host-context([dark]) { 2 | button { 3 | color: white; 4 | border-color: white; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /projects/youtube-blocker/src/components/app/app.template.html: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /projects/youtube-blocker/src/components/backup-and-restore/backup-and-restore.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ElementRef, ViewChild } from '@angular/core'; 2 | import { formatDate } from '@angular/common'; 3 | import { MatSnackBar } from '@angular/material'; 4 | import { Select, Store } from '@ngxs/store'; 5 | import { faUpload } from '@fortawesome/free-solid-svg-icons'; 6 | import { faDownload } from '@fortawesome/free-solid-svg-icons'; 7 | import saveFile from '@kawai-scripts/save-file'; 8 | import { from, Observable } from 'rxjs'; 9 | import { map } from 'rxjs/operators'; 10 | import readFile from './file-reader.promise'; 11 | import { AddFilterAction } from '../../store/block-list/block-list.actions'; 12 | import { BlockListState } from '../../store/block-list/block-list.state'; 13 | 14 | const TRY_ANOTHER_MSG = 'Try another?'; 15 | const RESTORE_ERROR_MSG = 'Error while restoring backup'; 16 | 17 | @Component({ 18 | selector: 'backup-and-restore', 19 | templateUrl: './backup-and-restore.template.html', 20 | styleUrls: ['./backup-and-restore.styles.scss'], 21 | }) 22 | export class BackupAndRestoreComponent { 23 | faDownload = faDownload; 24 | faUpload = faUpload; 25 | @Select(BlockListState.getFiltersCount) filterCount$: Observable; 26 | @ViewChild('fileInput', { static: true }) fileInput: ElementRef; 27 | 28 | constructor( 29 | private store: Store, 30 | private snackBar: MatSnackBar, 31 | ) { 32 | } 33 | 34 | private showErrorMsg() { 35 | this.snackBar 36 | .open(RESTORE_ERROR_MSG, TRY_ANOTHER_MSG, { duration: 3000 }) 37 | .onAction() 38 | .subscribe(() => this.fileInput.nativeElement.click()); 39 | } 40 | 41 | restoreBackup() { 42 | const file = this.fileInput.nativeElement.files[0]; 43 | 44 | if(file) { 45 | from(readFile(file)) 46 | .pipe( 47 | map((result) => JSON.parse(result)), 48 | ) 49 | .subscribe( 50 | ({ blockList: { filters } }) => this.store.dispatch(new AddFilterAction(filters)), 51 | () => this.showErrorMsg() 52 | ); 53 | } 54 | 55 | this.fileInput.nativeElement.value = ''; 56 | } 57 | 58 | createBackup() { 59 | this.store.selectSnapshot(({ blockList }) => { 60 | const timestamp = formatDate(new Date(), 'y-MM-d', 'en'); 61 | const backup = JSON.stringify({ blockList }); 62 | const name = `youtube-blocker.backup.${ timestamp }.json`; 63 | saveFile(new Blob([backup], { type: 'text/plain' }), name); 64 | }); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /projects/youtube-blocker/src/components/backup-and-restore/backup-and-restore.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BackupAndRestoreComponent } from './backup-and-restore.component'; 3 | import { MatButtonModule, MatSnackBarModule } from '@angular/material'; 4 | import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; 5 | import { FormsModule } from '@angular/forms'; 6 | import { CommonModule } from '@angular/common'; 7 | 8 | @NgModule({ 9 | declarations: [BackupAndRestoreComponent], 10 | exports: [BackupAndRestoreComponent], 11 | imports: [ 12 | MatButtonModule, 13 | FontAwesomeModule, 14 | FormsModule, 15 | MatSnackBarModule, 16 | CommonModule, 17 | ], 18 | }) 19 | export class BackupAndRestoreModule { 20 | 21 | } 22 | -------------------------------------------------------------------------------- /projects/youtube-blocker/src/components/backup-and-restore/backup-and-restore.styles.scss: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | display: flex; 3 | flex-direction: column; 4 | 5 | .line { 6 | button:not(:last-child) { 7 | margin-right: 10px; 8 | } 9 | 10 | &:not(:first-child) { 11 | margin-top: 10px; 12 | } 13 | } 14 | } 15 | 16 | -------------------------------------------------------------------------------- /projects/youtube-blocker/src/components/backup-and-restore/backup-and-restore.template.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 7 | 8 | 12 | 18 |
19 | 20 |
21 |

Filter count: {{ filterCount$ | async }}

22 |
23 |
24 | -------------------------------------------------------------------------------- /projects/youtube-blocker/src/components/backup-and-restore/file-reader.promise.ts: -------------------------------------------------------------------------------- 1 | export default function readFile(file: File): Promise { 2 | return new Promise((resolve, reject) => { 3 | const reader = new FileReader(); 4 | 5 | reader.onload = (ev) => resolve((ev.target as any).result as string); 6 | reader.onerror = reject; 7 | 8 | reader.readAsText(file); 9 | }); 10 | } 11 | -------------------------------------------------------------------------------- /projects/youtube-blocker/src/components/block-list/block-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs'; 2 | import { Component } from '@angular/core'; 3 | import { Select, Store } from '@ngxs/store'; 4 | import { BlockListState } from '../../store/block-list/block-list.state'; 5 | import { RemoveFilterAction } from '../../store/block-list/block-list.actions'; 6 | import { faTimesCircle } from '@fortawesome/free-solid-svg-icons'; 7 | 8 | @Component({ 9 | selector: 'block-list', 10 | templateUrl: './block-list.template.html', 11 | styleUrls: ['./block-list.styles.scss'], 12 | }) 13 | export class BlockListComponent { 14 | @Select(BlockListState.getFilters) filters$: Observable; 15 | searchTerm = ''; 16 | faTimesCircle = faTimesCircle; 17 | 18 | constructor( 19 | private store: Store, 20 | ) { 21 | } 22 | 23 | deleteFilter(filter: string) { 24 | this.store.dispatch(new RemoveFilterAction(filter)); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /projects/youtube-blocker/src/components/block-list/block-list.modue.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BlockListComponent } from './block-list.component'; 3 | import { MatFormFieldModule, MatListModule, MatButtonModule, MatInputModule } from '@angular/material'; 4 | import { ScrollingModule } from '@angular/cdk/scrolling'; 5 | import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; 6 | import { CommonModule } from '@angular/common'; 7 | import { FormsModule } from '@angular/forms'; 8 | import { FilterByTerm } from './filter-by-term.pipe'; 9 | 10 | 11 | @NgModule({ 12 | declarations: [BlockListComponent, FilterByTerm], 13 | imports: [ 14 | MatFormFieldModule, 15 | MatListModule, 16 | ScrollingModule, 17 | FontAwesomeModule, 18 | CommonModule, 19 | MatButtonModule, 20 | MatInputModule, 21 | FormsModule, 22 | ], 23 | exports: [ 24 | BlockListComponent 25 | ] 26 | }) 27 | export class BlockListModule { 28 | 29 | } 30 | -------------------------------------------------------------------------------- /projects/youtube-blocker/src/components/block-list/block-list.styles.scss: -------------------------------------------------------------------------------- 1 | @import "../../styles/scrollbar"; 2 | 3 | .search-box { 4 | width: 100%; 5 | } 6 | 7 | .filters-list { 8 | height: 250px; 9 | width: 540px; 10 | 11 | @include material-scroll; 12 | 13 | .filter-item { 14 | width: 100%; 15 | height: 40px; 16 | color: black; 17 | display: flex; 18 | justify-content: space-between; 19 | align-items: center; 20 | 21 | &.centered { 22 | justify-content: center; 23 | } 24 | 25 | .item-name { 26 | width: 450px; 27 | overflow: hidden; 28 | text-overflow: ellipsis; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /projects/youtube-blocker/src/components/block-list/block-list.template.html: -------------------------------------------------------------------------------- 1 | 2 | Search 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | {{ filter }} 11 | 17 |
18 |
19 | 20 | 21 |
22 | You don't have any filters 23 |
24 |
25 | 26 | 28 |
29 | Nothing was found ¯\_(ツ)_/¯ 30 |
31 |
32 |
33 |
34 | -------------------------------------------------------------------------------- /projects/youtube-blocker/src/components/block-list/filter-by-term.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | 3 | @Pipe({ name: 'filterByTerm' }) 4 | export class FilterByTerm implements PipeTransform { 5 | transform(items: string[], term?: string): string[] { 6 | return !term ? items : items.filter((item) => { 7 | return item.toLowerCase().includes(term.toLowerCase()); 8 | }); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /projects/youtube-blocker/src/components/block-video/block-video.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { faTimesCircle } from '@fortawesome/free-solid-svg-icons'; 3 | import { Store } from '@ngxs/store'; 4 | import { AddFilterAction, RemoveFilterAction } from '../../store/block-list/block-list.actions'; 5 | import { MatSnackBar } from '@angular/material'; 6 | 7 | @Component({ 8 | selector: 'block-video', 9 | templateUrl: './block-video.template.html', 10 | styleUrls: ['./block-video.styles.scss'], 11 | }) 12 | export class BlockVideoComponent { 13 | faTimesCircle = faTimesCircle; 14 | @Input() channelName: string; 15 | 16 | constructor( 17 | private store: Store, 18 | private snackBar: MatSnackBar, 19 | ) { 20 | } 21 | 22 | blockChannel(event: MouseEvent) { 23 | event.preventDefault(); 24 | event.stopPropagation(); 25 | 26 | this.store.dispatch(new AddFilterAction(this.channelName)); 27 | 28 | this.snackBar 29 | .open(`Blocked ${ this.channelName }`, 'Cancel?', { 30 | duration: 3000, 31 | horizontalPosition: 'end', 32 | }) 33 | .onAction() 34 | .subscribe(() => this.store.dispatch(new RemoveFilterAction(this.channelName))); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /projects/youtube-blocker/src/components/block-video/block-video.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BlockVideoComponent } from './block-video.component'; 3 | import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; 4 | import { MatButtonModule, MatSnackBarModule } from '@angular/material'; 5 | 6 | 7 | @NgModule({ 8 | imports: [ 9 | MatButtonModule, 10 | MatSnackBarModule, 11 | FontAwesomeModule, 12 | ], 13 | entryComponents: [BlockVideoComponent], 14 | declarations: [BlockVideoComponent], 15 | exports: [BlockVideoComponent], 16 | }) 17 | export class BlockVideoModule { 18 | 19 | } 20 | -------------------------------------------------------------------------------- /projects/youtube-blocker/src/components/block-video/block-video.styles.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | margin-right: 6px; 3 | } 4 | 5 | .block-button { 6 | width: 1em !important; 7 | height: 1em !important; 8 | line-height: 1em !important; 9 | display: inline-flex !important; 10 | } 11 | -------------------------------------------------------------------------------- /projects/youtube-blocker/src/components/block-video/block-video.template.html: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /projects/youtube-blocker/src/components/blocker/block-button-injector.service.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFactory, ComponentFactoryResolver, ComponentRef, Injectable, Injector } from '@angular/core'; 2 | import { CHANNEL_NAME_SELECTOR, WHITE_LISTED_CHANNELS } from './blocker.constants'; 3 | import { BlockVideoComponent } from '../block-video/block-video.component'; 4 | import { getVideoElements } from './blocker.utils'; 5 | 6 | 7 | @Injectable() 8 | export class BlockButtonInjectorService { 9 | private blockButtons: ComponentRef[] = []; 10 | private factory: ComponentFactory; 11 | 12 | constructor( 13 | private componentFactoryResolver: ComponentFactoryResolver, 14 | private injector: Injector, 15 | ) { 16 | this.factory = this.componentFactoryResolver.resolveComponentFactory(BlockVideoComponent); 17 | } 18 | 19 | attachButton(node: Element) { 20 | const componentRef = this.factory.create(this.injector); 21 | 22 | componentRef.instance.channelName = node.textContent.trim(); 23 | componentRef.changeDetectorRef.detectChanges(); 24 | node.prepend(componentRef.location.nativeElement); 25 | 26 | return componentRef; 27 | } 28 | 29 | attachButtons(context: HTMLElement = document.body) { 30 | const addedButtons = getVideoElements(context) 31 | .filter((node) => !node.querySelector('block-video')) 32 | .map((node) => node.querySelector(CHANNEL_NAME_SELECTOR)) 33 | .filter(Boolean) 34 | .filter(({ textContent }: HTMLElement) => !WHITE_LISTED_CHANNELS.includes(textContent)) 35 | .map((node) => this.attachButton(node)); 36 | 37 | this.blockButtons = [...this.blockButtons, ...addedButtons]; 38 | } 39 | 40 | private destroyButton(ref: ComponentRef) { 41 | ref.destroy(); 42 | ref.location.nativeElement.remove(); 43 | this.blockButtons.splice(this.blockButtons.indexOf(ref), 1); 44 | } 45 | 46 | removeButtons() { 47 | this.blockButtons.forEach((ref) => this.destroyButton(ref)); 48 | } 49 | 50 | removeButtonsFromNode(node: HTMLElement) { 51 | Array.from(node.querySelectorAll(this.factory.selector)) 52 | .map((node) => this.blockButtons.find((ref) => ref.location.nativeElement === node)) 53 | .forEach((ref) => this.destroyButton(ref)); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /projects/youtube-blocker/src/components/blocker/blocker.constants.ts: -------------------------------------------------------------------------------- 1 | import combineCssSelectors from '../../utils/combine-css-rules'; 2 | 3 | export const BANNED_VIDEO_CLASS = 'banned-video'; 4 | export const BANNED_VIDEO_SELECTOR = `.${ BANNED_VIDEO_CLASS }`; 5 | 6 | export const WHITE_LISTED_CHANNELS = ['YouTube']; 7 | 8 | export const VIDEO_ITEM_SELECTOR = combineCssSelectors( 9 | '.video-list-item', 10 | '.yt-shelf-grid-item', 11 | '.yt-lockup-video:not(.yt-lockup-grid)', 12 | '.yt-lockup-playlist:not(.yt-lockup-grid)', 13 | 'ytd-rich-item-renderer', 14 | 'ytd-compact-video-renderer', 15 | 'ytd-video-renderer', 16 | ); 17 | 18 | export const CHANNEL_NAME_SELECTOR = combineCssSelectors( 19 | '.stat.attribution', 20 | '.yt-uix-sessionlink[href^="/channel"]', 21 | '.yt-uix-sessionlink[href^="/user"]', 22 | '.yt-lockup-byline [href^="/channel"]', 23 | '.yt-lockup-byline [href^="/user"]', 24 | 'yt-formatted-string [href^="/channel"]', 25 | 'yt-formatted-string [href^="/user"]', 26 | 'ytd-channel-name yt-formatted-string.ytd-channel-name', 27 | ); 28 | -------------------------------------------------------------------------------- /projects/youtube-blocker/src/components/blocker/blocker.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BlockerService } from './blocker.service'; 3 | import { BlockVideoModule } from '../block-video/block-video.module'; 4 | import { BlockButtonInjectorService } from './block-button-injector.service'; 5 | 6 | @NgModule({ 7 | imports: [BlockVideoModule], 8 | providers: [BlockerService, BlockButtonInjectorService], 9 | }) 10 | export class BlockerModule { 11 | 12 | } 13 | -------------------------------------------------------------------------------- /projects/youtube-blocker/src/components/blocker/blocker.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { getVideoElements } from './blocker.utils'; 3 | import { 4 | BANNED_VIDEO_CLASS, 5 | VIDEO_ITEM_SELECTOR, 6 | BANNED_VIDEO_SELECTOR, 7 | CHANNEL_NAME_SELECTOR, 8 | } from './blocker.constants'; 9 | 10 | export const isInBlockList = function (item: string, blockList: string[]) { 11 | return blockList.find((rule) => { 12 | if(rule.includes('*')) { 13 | return RegExp(`^${ rule.replace(/\*/g, '.+') }$`, 'i').test(item); 14 | } 15 | 16 | return item.toLowerCase() === rule.toLowerCase(); 17 | }); 18 | }; 19 | 20 | @Injectable() 21 | export class BlockerService { 22 | applyBlock(blockList: string[], context: HTMLElement = document.body) { 23 | getVideoElements(context) 24 | .map((node) => node.querySelector(CHANNEL_NAME_SELECTOR)) 25 | .filter(Boolean) 26 | .filter(({ textContent }: HTMLElement) => isInBlockList(textContent, blockList)) 27 | .map((node) => node.closest(VIDEO_ITEM_SELECTOR)) 28 | .forEach((node) => node.classList.add(BANNED_VIDEO_CLASS)); 29 | } 30 | 31 | suspendBlock() { 32 | Array.from(document.querySelectorAll(BANNED_VIDEO_SELECTOR)) 33 | .forEach((node) => node.classList.remove(BANNED_VIDEO_CLASS)); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /projects/youtube-blocker/src/components/blocker/blocker.utils.ts: -------------------------------------------------------------------------------- 1 | import { VIDEO_ITEM_SELECTOR } from './blocker.constants'; 2 | 3 | export function getVideoElements(context: HTMLElement) { 4 | const isVideoElement = context.matches(VIDEO_ITEM_SELECTOR); 5 | 6 | return isVideoElement ? [context] : Array.from(context.querySelectorAll(VIDEO_ITEM_SELECTOR)); 7 | } 8 | -------------------------------------------------------------------------------- /projects/youtube-blocker/src/components/preferences-popup/preferences-popup.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { Select, Store } from '@ngxs/store'; 3 | import { Observable } from 'rxjs'; 4 | import { faTimes } from '@fortawesome/free-solid-svg-icons'; 5 | import { PreferencesState, PreferencesStateModel } from '../../store/preferences/preferences.state'; 6 | import { ToggleButtonInsert, ToggleStopBlocked, ToggleSuspend } from '../../store/preferences/preferences.actions'; 7 | 8 | @Component({ 9 | selector: 'preferences-popup', 10 | templateUrl: './preferences-popup.template.html', 11 | styleUrls: ['./preferences-popup.styles.scss'], 12 | }) 13 | export class PreferencesPopupComponent { 14 | faTimes = faTimes; 15 | 16 | @Select(PreferencesState) preferences$: Observable; 17 | 18 | constructor( 19 | private store: Store, 20 | ) { 21 | } 22 | 23 | toggleBlock() { 24 | this.store.dispatch(new ToggleSuspend()); 25 | } 26 | 27 | toggleButtons() { 28 | this.store.dispatch(new ToggleButtonInsert()); 29 | } 30 | 31 | toggleStopBlocked() { 32 | this.store.dispatch(new ToggleStopBlocked()); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /projects/youtube-blocker/src/components/preferences-popup/preferences-popup.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { PreferencesPopupComponent } from './preferences-popup.component'; 3 | import { 4 | MatTabsModule, 5 | MatSlideToggleModule, 6 | MatCardModule, 7 | MatButtonModule, 8 | MatDialogModule, 9 | } from '@angular/material'; 10 | import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; 11 | import { BlockListModule } from '../block-list/block-list.modue'; 12 | import { AddFilterFormModule } from '../add-filter-form/add-filter-form.module'; 13 | import { FormsModule } from '@angular/forms'; 14 | import { CommonModule } from '@angular/common'; 15 | import { BackupAndRestoreModule } from '../backup-and-restore/backup-and-restore.module'; 16 | 17 | @NgModule({ 18 | declarations: [PreferencesPopupComponent], 19 | entryComponents: [PreferencesPopupComponent], 20 | imports: [ 21 | MatTabsModule, 22 | FontAwesomeModule, 23 | MatSlideToggleModule, 24 | MatCardModule, 25 | MatButtonModule, 26 | BlockListModule, 27 | AddFilterFormModule, 28 | FormsModule, 29 | CommonModule, 30 | BackupAndRestoreModule, 31 | MatDialogModule, 32 | ], 33 | }) 34 | export class PreferencesPopupModule { 35 | 36 | } 37 | -------------------------------------------------------------------------------- /projects/youtube-blocker/src/components/preferences-popup/preferences-popup.styles.scss: -------------------------------------------------------------------------------- 1 | .header { 2 | width: 100%; 3 | display: flex; 4 | justify-content: flex-end; 5 | } 6 | 7 | .tab-content { 8 | padding: 10px; 9 | mat-card:not(:last-child) { 10 | margin-bottom: 10px; 11 | } 12 | } 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /projects/youtube-blocker/src/components/preferences-popup/preferences-popup.template.html: -------------------------------------------------------------------------------- 1 |
2 | 5 |
6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |
19 | 20 | 21 |
22 | 23 |
24 | 27 | Suspend blocking 28 | 29 |
30 | 31 |
32 | 35 | Insert block buttons 36 | 37 |
38 | 39 |
40 | 43 | Stop blacklisted video 44 | 45 |
46 |
47 | 48 | 49 | 50 | 51 |
52 |
53 |
54 | -------------------------------------------------------------------------------- /projects/youtube-blocker/src/components/yt-control/@types.ts: -------------------------------------------------------------------------------- 1 | interface VideoMeta { 2 | video_id: string | undefined; 3 | author: string; 4 | title: string; 5 | } 6 | 7 | export interface Player extends HTMLDivElement { 8 | stopVideo(): void; 9 | getVideoData(): VideoMeta; 10 | } 11 | -------------------------------------------------------------------------------- /projects/youtube-blocker/src/components/yt-control/yt-control.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { YtPlayerControlService } from './yt-player-control.service'; 4 | 5 | 6 | @NgModule({ 7 | declarations: [], 8 | imports: [ 9 | CommonModule 10 | ], 11 | providers: [YtPlayerControlService], 12 | }) 13 | export class YtControlModule { } 14 | -------------------------------------------------------------------------------- /projects/youtube-blocker/src/components/yt-control/yt-player-control.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { YtPlayerControlService } from './yt-player-control.service'; 4 | 5 | describe('YtPlayerControlService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: YtPlayerControlService = TestBed.get(YtPlayerControlService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /projects/youtube-blocker/src/components/yt-control/yt-player-control.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Player } from './@types'; 3 | 4 | export const PLAYER_ELEMENT_SELECTOR = '#movie_player'; 5 | 6 | @Injectable({ 7 | providedIn: 'root', 8 | }) 9 | export class YtPlayerControlService { 10 | get player(): Player { 11 | return document.querySelector(PLAYER_ELEMENT_SELECTOR); 12 | } 13 | 14 | stop() { 15 | this.player.stopVideo(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /projects/youtube-blocker/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | }; 4 | -------------------------------------------------------------------------------- /projects/youtube-blocker/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: false, 3 | }; 4 | -------------------------------------------------------------------------------- /projects/youtube-blocker/src/index.ts: -------------------------------------------------------------------------------- 1 | import './polyfills'; 2 | import './styles/main.scss'; 3 | import { enableProdMode } from '@angular/core'; 4 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 5 | import { AppModule } from './components/app/app.module'; 6 | import combineCssSelectors from './utils/combine-css-rules'; 7 | import { environment } from './environments/environment'; 8 | import { migrate } from './utils/storage-migrate'; 9 | import waitSelector from '@kawai-scripts/wait-selector'; 10 | 11 | if(environment.production) enableProdMode(); 12 | 13 | const APP_ELEMENT = document.createElement('youtube-blocker'); 14 | const MOUNT_POINT = combineCssSelectors( 15 | '#yt-masthead-user', 16 | '#yt-masthead-signin', 17 | '#end', 18 | ); 19 | 20 | async function mountApp() { 21 | await migrate(); 22 | await waitSelector(MOUNT_POINT); 23 | document.querySelector(MOUNT_POINT).prepend(APP_ELEMENT); 24 | platformBrowserDynamic().bootstrapModule(AppModule); 25 | } 26 | 27 | mountApp(); 28 | -------------------------------------------------------------------------------- /projects/youtube-blocker/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | import 'element-matches-polyfill'; 2 | import 'zone.js/dist/zone'; 3 | -------------------------------------------------------------------------------- /projects/youtube-blocker/src/store/block-list/block-list.actions.ts: -------------------------------------------------------------------------------- 1 | export enum ActionTypes { 2 | ADD_FILTER = '[Block list] Add filter', 3 | REMOVE_FILTER = '[Block list] Remove filter' 4 | } 5 | 6 | export class AddFilterAction { 7 | static readonly type = ActionTypes.ADD_FILTER; 8 | filter: string[]; 9 | 10 | constructor( 11 | filter: string | string[], 12 | ) { 13 | this.filter = Array.isArray(filter) ? filter : [filter]; 14 | } 15 | } 16 | 17 | export class RemoveFilterAction { 18 | static readonly type = ActionTypes.REMOVE_FILTER; 19 | 20 | constructor( 21 | public filter: string, 22 | ) { 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /projects/youtube-blocker/src/store/block-list/block-list.state.ts: -------------------------------------------------------------------------------- 1 | import { Action, Selector, State, StateContext, StateToken } from '@ngxs/store'; 2 | import { AddFilterAction, RemoveFilterAction } from './block-list.actions'; 3 | import { append, patch, removeItem } from '@ngxs/store/operators'; 4 | 5 | export interface BlockListStateModel { 6 | filters: string[]; 7 | } 8 | 9 | const BLOCK_LIST_STATE_TOKEN = new StateToken('blockList'); 10 | 11 | @State({ 12 | name: BLOCK_LIST_STATE_TOKEN, 13 | defaults: { filters: [] }, 14 | }) 15 | export class BlockListState { 16 | @Action(AddFilterAction) 17 | addFilter(ctx: StateContext, { filter }: AddFilterAction) { 18 | const { filters } = ctx.getState(); 19 | const payload = filter 20 | .filter((item) => filters.indexOf(item) < 0) 21 | .map((item) => item.trim()); 22 | 23 | ctx.setState(patch({ 24 | filters: append(payload), 25 | })); 26 | } 27 | 28 | @Action(RemoveFilterAction) 29 | removeFilter(ctx: StateContext, { filter }: RemoveFilterAction) { 30 | ctx.setState(patch({ 31 | filters: removeItem((item: string) => item === filter), 32 | })); 33 | } 34 | 35 | @Selector() 36 | static getFilters(state: BlockListStateModel) { 37 | return state.filters; 38 | } 39 | 40 | @Selector() 41 | static getFiltersCount(state: BlockListStateModel) { 42 | return state.filters.length; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /projects/youtube-blocker/src/store/preferences/preferences.actions.ts: -------------------------------------------------------------------------------- 1 | export enum ActionTypes { 2 | TOGGLE_SUSPEND = '[Preferences] Toggle suspend', 3 | BUTTON_INSERT = '[Preferences] Toggle button insert', 4 | STOP_BLOCKED = '[Preferences] Toggle stop blocked', 5 | } 6 | 7 | export class ToggleSuspend { 8 | static readonly type = ActionTypes.TOGGLE_SUSPEND; 9 | } 10 | 11 | export class ToggleButtonInsert { 12 | static readonly type = ActionTypes.BUTTON_INSERT; 13 | } 14 | 15 | export class ToggleStopBlocked { 16 | static readonly type = ActionTypes.STOP_BLOCKED; 17 | } 18 | -------------------------------------------------------------------------------- /projects/youtube-blocker/src/store/preferences/preferences.state.ts: -------------------------------------------------------------------------------- 1 | import { Action, State, StateContext, StateToken } from '@ngxs/store'; 2 | import { ToggleButtonInsert, ToggleStopBlocked, ToggleSuspend } from './preferences.actions'; 3 | 4 | export interface PreferencesStateModel { 5 | suspend: boolean; 6 | insertButtons: boolean; 7 | stopBlocked: boolean; 8 | } 9 | 10 | type Context = StateContext; 11 | 12 | const PREFERENCES_STATE_TOKEN = new StateToken('preferences'); 13 | 14 | @State({ 15 | name: PREFERENCES_STATE_TOKEN, 16 | defaults: { 17 | suspend: false, 18 | insertButtons: true, 19 | stopBlocked: false, 20 | } 21 | }) 22 | export class PreferencesState { 23 | @Action(ToggleButtonInsert) 24 | toggleButtonInsert(ctx: Context) { 25 | const { insertButtons } = ctx.getState(); 26 | 27 | ctx.patchState({ insertButtons: !insertButtons }); 28 | } 29 | 30 | @Action(ToggleSuspend) 31 | toggleSuspend(ctx: Context) { 32 | const { suspend } = ctx.getState(); 33 | 34 | ctx.patchState({ suspend: !suspend }); 35 | } 36 | 37 | @Action(ToggleStopBlocked) 38 | toggleStopBlocked(ctx: Context) { 39 | const { stopBlocked } = ctx.getState(); 40 | 41 | ctx.patchState({ stopBlocked: !stopBlocked }); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /projects/youtube-blocker/src/store/storage.engine.ts: -------------------------------------------------------------------------------- 1 | import { AsyncStorageEngine } from '@ngxs-labs/async-storage-plugin'; 2 | import GMStorage from '@kawai-scripts/gm-storage'; 3 | import { from } from 'rxjs'; 4 | import { map } from 'rxjs/operators'; 5 | 6 | export class GMStorageEngine implements AsyncStorageEngine { 7 | private storage = new GMStorage(); 8 | 9 | getItem(key) { 10 | return from(this.storage.getValue(key)); 11 | } 12 | 13 | setItem(key, val) { 14 | this.storage.setValue(key, val); 15 | } 16 | 17 | removeItem(key: string) { 18 | this.storage.deleteValue(key); 19 | } 20 | 21 | clear() { 22 | // noop 23 | } 24 | 25 | length() { 26 | return from(this.storage.listValues()).pipe(map((keys) => keys.length)); 27 | } 28 | 29 | key(val: number) { 30 | return from(this.storage.listValues()).pipe(map(({ [val]: key }) => key)); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /projects/youtube-blocker/src/store/store.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { NgxsModule } from '@ngxs/store'; 3 | import { NgxsAsyncStoragePluginModule } from '@ngxs-labs/async-storage-plugin'; 4 | import { NgxsReduxDevtoolsPluginModule } from '@ngxs/devtools-plugin'; 5 | import { environment } from '../environments/environment'; 6 | import { GMStorageEngine } from './storage.engine'; 7 | import { BlockListState } from './block-list/block-list.state'; 8 | import { PreferencesState } from './preferences/preferences.state'; 9 | 10 | if(!environment.production) { 11 | // eslint-disable-next-line @typescript-eslint/ban-ts-ignore 12 | // @ts-ignore 13 | // eslint-disable-next-line no-undef 14 | window.__REDUX_DEVTOOLS_EXTENSION__ = unsafeWindow.__REDUX_DEVTOOLS_EXTENSION__; 15 | } 16 | 17 | @NgModule({ 18 | imports: [ 19 | NgxsModule.forRoot([ 20 | BlockListState, 21 | PreferencesState, 22 | ], { 23 | developmentMode: !environment.production, 24 | }), 25 | NgxsAsyncStoragePluginModule.forRoot(GMStorageEngine), 26 | NgxsReduxDevtoolsPluginModule.forRoot({ 27 | name: 'Youtube blocker', 28 | disabled: environment.production, 29 | }), 30 | ], 31 | }) 32 | export class StoreModule { 33 | 34 | } 35 | -------------------------------------------------------------------------------- /projects/youtube-blocker/src/styles/_scrollbar.scss: -------------------------------------------------------------------------------- 1 | @mixin material-scroll { 2 | &::-webkit-scrollbar { 3 | width: 8px; 4 | } 5 | 6 | &::-webkit-scrollbar-track { 7 | background-color: rgba(black, 0.1); 8 | } 9 | 10 | &::-webkit-scrollbar-thumb { 11 | min-height: 40px; 12 | background-color: rgba(black, 0.4); 13 | &:hover { 14 | background-color: rgba(black, 0.5); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /projects/youtube-blocker/src/styles/main.scss: -------------------------------------------------------------------------------- 1 | @import '~@angular/material/prebuilt-themes/indigo-pink.css'; 2 | 3 | .cdk-overlay-container { 4 | z-index: 1999999999; 5 | } 6 | 7 | .dialog-popup { 8 | font-size: 15px; 9 | } 10 | 11 | .banned-video { 12 | display: none !important; 13 | } 14 | -------------------------------------------------------------------------------- /projects/youtube-blocker/src/utils/combine-css-rules.ts: -------------------------------------------------------------------------------- 1 | const combineCssSelectors = (...selectors: string[]) => selectors.join(', '); 2 | 3 | export default combineCssSelectors; 4 | -------------------------------------------------------------------------------- /projects/youtube-blocker/src/utils/mutation-observer.observable.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs'; 2 | 3 | const createMutationObserver = (el: Element, observerOptions: MutationObserverInit) => { 4 | return new Observable((subscriber) => { 5 | const observer = new MutationObserver((mutations) => { 6 | mutations.forEach((mutation) => subscriber.next(mutation)); 7 | }); 8 | 9 | observer.observe(el, observerOptions); 10 | 11 | return () => observer.disconnect(); 12 | }); 13 | }; 14 | 15 | export default createMutationObserver; 16 | -------------------------------------------------------------------------------- /projects/youtube-blocker/src/utils/storage-migrate.ts: -------------------------------------------------------------------------------- 1 | import GMStorage from '@kawai-scripts/gm-storage'; 2 | 3 | // WAT? Why? Backward compatibility 4 | // In old version state stored under 'youtube-blocker-state' key 5 | // but in new it must be stored under '@@STATE' key 6 | export async function migrate() { 7 | const OLD_KEY = 'youtube-blocker-state'; 8 | const NEW_KEY = '@@STATE'; 9 | const storage = new GMStorage(); 10 | const oldStorage = await storage.getValue(OLD_KEY); 11 | const newStorage = await storage.getValue(NEW_KEY); 12 | 13 | if(!newStorage && oldStorage) { 14 | await storage.setValue(NEW_KEY, oldStorage); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /projects/youtube-blocker/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "esnext", 4 | "target": "es2015", 5 | "sourceMap": true, 6 | "moduleResolution": "node", 7 | "emitDecoratorMetadata": true, 8 | "experimentalDecorators": true, 9 | "allowSyntheticDefaultImports": true, 10 | "esModuleInterop": true, 11 | "lib": [ 12 | "dom", 13 | "es5", 14 | "scripthost", 15 | "es2015.promise" 16 | ] 17 | }, 18 | "exclude": [ 19 | "node_modules", 20 | "src/test.ts", 21 | "src/**/*.spec.ts" 22 | ], 23 | "files": [ 24 | "src/index.ts", 25 | "src/polyfills.ts" 26 | ], 27 | "include": [ 28 | "src/**/*.ts" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /projects/youtube-blocker/webpack.analyze.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const provider = require('../../build/ng-webpack-configs/analyze'); 3 | 4 | module.exports = provider('youtube-blocker'); 5 | -------------------------------------------------------------------------------- /projects/youtube-blocker/webpack.dev.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const provider = require('../../build/ng-webpack-configs/development'); 3 | 4 | module.exports = provider('youtube-blocker'); 5 | -------------------------------------------------------------------------------- /projects/youtube-blocker/webpack.prod.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const provider = require('../../build/ng-webpack-configs/production'); 3 | 4 | module.exports = provider('youtube-blocker'); 5 | -------------------------------------------------------------------------------- /projects/youtube-tweaks/README.md: -------------------------------------------------------------------------------- 1 | ### Youtube tweaks 2 | 3 | Adds to youtube features such: 4 | - Set default video quality 5 | - Set default video speed 6 | - Prevent video auto start 7 | - Shortcuts to control video speed and quality 8 | 9 | ### Preparation 10 | 11 | Install one of userscript manager(skip if you already have one) 12 | 13 | | Browser | Install url | 14 | |---------|-------------| 15 | | Firefox | [Violentmonkey](https://addons.mozilla.org/en-US/firefox/addon/violentmonkey/) or [Tampermonkey](https://addons.mozilla.org/ru/firefox/addon/tampermonkey/) | 16 | | Chrome | [Violentmonkey](https://chrome.google.com/webstore/detail/violentmonkey/jinjaccalgkegednnccohejagnlnfdag) or [Tampermonkey](https://chrome.google.com/webstore/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo) | 17 | | Edge | [Violentmonkey](https://microsoftedge.microsoft.com/addons/detail/violentmonkey/eeagobfjdenkkddmbclomhiblgggliao) or [Tampermonkey](https://microsoftedge.microsoft.com/addons/detail/tampermonkey/iikmkjmpaadaobahmlepeloendndfphd) | 18 | | Safari | [Tampermonkey](https://www.tampermonkey.net/?ext=dhdg&browser=safari) | 19 | 20 | ### Install 21 | 22 | Just click [Install](../../dist/youtube-tweaks.user.js?raw=true) 23 | 24 | ### Warning 25 | 26 | Has conflicts with [Youtube blocker](../projects/youtube-blocker#readme) 27 | -------------------------------------------------------------------------------- /projects/youtube-tweaks/browserslist: -------------------------------------------------------------------------------- 1 | last 2 Chrome versions 2 | last 2 Safari versions 3 | last 2 Edge versions 4 | Firefox ESR 5 | not IE 9-11 6 | -------------------------------------------------------------------------------- /projects/youtube-tweaks/headers.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": "*://www.youtube.com/*", 3 | "run-at": "document-end", 4 | "grant": [ 5 | "GM_getValue", 6 | "GM_setValue", 7 | "GM_deleteValue" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /projects/youtube-tweaks/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@kawai-scripts/youtube-tweaks", 3 | "description": "Youtube tweaks", 4 | "version": "1.0.0", 5 | "license": "MIT", 6 | "dependencies": { 7 | "@fortawesome/free-solid-svg-icons": "^5.12.0", 8 | "@fortawesome/fontawesome-svg-core": "^1.2.26", 9 | "@fortawesome/angular-fontawesome": "^0.5.0", 10 | "@ngxs/devtools-plugin": "^3.6.1", 11 | "@ngxs/store": "^3.6.1", 12 | "@ngxs-labs/async-storage-plugin": "^0.1.1", 13 | "@kawai-scripts/gm-storage": "^1.0.0", 14 | "@kawai-scripts/wait-selector": "^1.0.0", 15 | "autobind-decorator": "^2.4.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /projects/youtube-tweaks/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { MatDialog } from '@angular/material/dialog'; 3 | import { PreferencesPopupComponent } from './modules/preferences-popup/preferences-popup.component'; 4 | import { ShortcutService } from './modules/shortcut/shortcut.service'; 5 | import { PlayerPatcherService } from './modules/player/player-patcher.service'; 6 | import { Select } from '@ngxs/store'; 7 | import { PreferencesModel, PreferencesState } from './modules/store/preferences/preferences.state'; 8 | import { Observable } from 'rxjs'; 9 | import { map } from 'rxjs/operators'; 10 | import waitSelector from '@kawai-scripts/wait-selector'; 11 | 12 | @Component({ 13 | selector: 'yt-tweaks-root', 14 | templateUrl: './app.template.html', 15 | styleUrls: ['./app.styles.scss'], 16 | }) 17 | export class AppComponent implements OnInit { 18 | @Select(PreferencesState) preferences$: Observable; 19 | 20 | constructor( 21 | private dialog: MatDialog, 22 | private shortcutService: ShortcutService, 23 | private playerPatcherService: PlayerPatcherService, 24 | ) { 25 | } 26 | 27 | ngOnInit() { 28 | this.playerPatcherService.attach(); 29 | 30 | this.preferences$ 31 | .pipe(map(({shortcutsEnabled}) => shortcutsEnabled)) 32 | .subscribe((enabled) => { 33 | enabled ? this.shortcutService.attach() : this.shortcutService.detach(); 34 | }); 35 | 36 | waitSelector('#movie_player').then(() => this.playerPatcherService.handle()); 37 | } 38 | 39 | openOptionsPopup(event: MouseEvent) { 40 | event.stopPropagation(); 41 | 42 | this.dialog.open(PreferencesPopupComponent, { 43 | minWidth: 640, 44 | panelClass: 'dialog-popup', 45 | }); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /projects/youtube-tweaks/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | import { AppComponent } from './app.component'; 4 | import { MatButtonModule } from '@angular/material/button'; 5 | import { MatDialogModule } from '@angular/material/dialog'; 6 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 7 | import { PreferencesPopupModule } from './modules/preferences-popup/preferences-popup.module'; 8 | import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; 9 | import { StoreModule } from './modules/store/store.module'; 10 | import { ShortcutModule } from './modules/shortcut/shortcut.module'; 11 | import { PlayerModule } from './modules/player/player.module'; 12 | 13 | @NgModule({ 14 | declarations: [ 15 | AppComponent 16 | ], 17 | imports: [ 18 | BrowserModule, 19 | MatButtonModule, 20 | MatDialogModule, 21 | BrowserAnimationsModule, 22 | PreferencesPopupModule, 23 | FontAwesomeModule, 24 | StoreModule, 25 | ShortcutModule, 26 | PlayerModule, 27 | ], 28 | providers: [], 29 | bootstrap: [AppComponent] 30 | }) 31 | export class AppModule { } 32 | -------------------------------------------------------------------------------- /projects/youtube-tweaks/src/app/app.styles.scss: -------------------------------------------------------------------------------- 1 | :host-context([dark]) { 2 | button { 3 | color: white; 4 | border-color: white; 5 | } 6 | } 7 | 8 | :host.booting { 9 | display: none; 10 | } 11 | -------------------------------------------------------------------------------- /projects/youtube-tweaks/src/app/app.template.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /projects/youtube-tweaks/src/app/modules/player/player-patcher.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { PlayerPatcherService } from './player-patcher.service'; 4 | 5 | describe('PlayerPatcherService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: PlayerPatcherService = TestBed.get(PlayerPatcherService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /projects/youtube-tweaks/src/app/modules/player/player-patcher.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { PlayerService } from './player.service'; 3 | import autobind from 'autobind-decorator'; 4 | import { Select } from '@ngxs/store'; 5 | import { PreferencesModel, PreferencesState } from '../store/preferences/preferences.state'; 6 | import { Observable } from 'rxjs'; 7 | import { first } from 'rxjs/operators'; 8 | 9 | // yt-navigate-finish - new youtube, spfdone - old youtube 10 | const NAVIGATE_EVENT = document.querySelector('ytd-app') ? 'yt-navigate-finish' : 'spfdone'; 11 | 12 | @Injectable({ 13 | providedIn: 'root', 14 | }) 15 | export class PlayerPatcherService { 16 | @Select(PreferencesState) preferences$: Observable; 17 | 18 | constructor( 19 | private playerService: PlayerService, 20 | ) { 21 | } 22 | 23 | attach() { 24 | window.addEventListener(NAVIGATE_EVENT, this.handle); 25 | } 26 | 27 | detach() { 28 | window.removeEventListener(NAVIGATE_EVENT, this.handle); 29 | } 30 | 31 | @autobind 32 | handle() { 33 | this.preferences$ 34 | .pipe(first()) 35 | .subscribe(({ defaultQuality, defaultSpeed, autoStart }) => { 36 | this.playerService.setQuality(defaultQuality); 37 | this.playerService.setSpeed(defaultSpeed); 38 | if (!autoStart) this.playerService.stop(); 39 | }); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /projects/youtube-tweaks/src/app/modules/player/player.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { PlayerService } from './player.service'; 4 | import { PlayerPatcherService } from './player-patcher.service'; 5 | 6 | 7 | @NgModule({ 8 | declarations: [], 9 | imports: [CommonModule], 10 | providers: [ 11 | PlayerService, 12 | PlayerPatcherService, 13 | ], 14 | }) 15 | export class PlayerModule { } 16 | -------------------------------------------------------------------------------- /projects/youtube-tweaks/src/app/modules/player/player.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { PlayerService } from './player.service'; 4 | 5 | describe('PlayerService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: PlayerService = TestBed.get(PlayerService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /projects/youtube-tweaks/src/app/modules/player/player.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | @Injectable({ 4 | providedIn: 'root', 5 | }) 6 | export class PlayerService { 7 | getPlayer(): any { 8 | return document.querySelector('#movie_player'); 9 | } 10 | 11 | getAvailableQualityLevels(): string[] { 12 | return this.getPlayer().getAvailableQualityLevels(); 13 | } 14 | 15 | getQuality() { 16 | return this.getPlayer().getPlaybackQuality(); 17 | } 18 | 19 | getSpeed(): number { 20 | return this.getPlayer().getPlaybackRate(); 21 | } 22 | 23 | setSpeed(speed: number) { 24 | const min = 0.25; 25 | const max = 2; 26 | const _speed = Math.min(Math.max(min, speed), max); 27 | 28 | this.getPlayer().setPlaybackRate(_speed); 29 | } 30 | 31 | setQuality(quality: string) { 32 | this.getPlayer().setPlaybackQualityRange(quality); 33 | } 34 | 35 | stop() { 36 | this.getPlayer().stopVideo(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /projects/youtube-tweaks/src/app/modules/preferences-popup/preferences-popup.component.html: -------------------------------------------------------------------------------- 1 | 56 | -------------------------------------------------------------------------------- /projects/youtube-tweaks/src/app/modules/preferences-popup/preferences-popup.component.scss: -------------------------------------------------------------------------------- 1 | .popup { 2 | .popup-header { 3 | display: flex; 4 | justify-content: flex-end; 5 | } 6 | .popup-content { 7 | .tab-content { 8 | padding: 10px; 9 | .column-block { 10 | display: flex; 11 | flex-direction: column; 12 | } 13 | 14 | .row { 15 | display: flex; 16 | padding: 10px 0; 17 | > * { 18 | margin: 0 10px; 19 | &:first-child { 20 | margin-left: 0; 21 | } 22 | &:last-child { 23 | margin-right: 0; 24 | } 25 | } 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /projects/youtube-tweaks/src/app/modules/preferences-popup/preferences-popup.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { PreferencesPopupComponent } from './preferences-popup.component'; 4 | 5 | describe('PreferencesPopupComponent', () => { 6 | let component: PreferencesPopupComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ PreferencesPopupComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(PreferencesPopupComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /projects/youtube-tweaks/src/app/modules/preferences-popup/preferences-popup.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnDestroy, OnInit } from '@angular/core'; 2 | import { MatDialogRef } from '@angular/material/dialog'; 3 | import { faTimes } from '@fortawesome/free-solid-svg-icons/faTimes'; 4 | import { QUALITY_CHOICES, SPEED_CHOICES } from './preferences-popup.constants'; 5 | import { Select, Store } from '@ngxs/store'; 6 | import { PreferencesModel, PreferencesState } from '../store/preferences/preferences.state'; 7 | import { Observable, Subscription } from 'rxjs'; 8 | import { 9 | SetDefaultQuality, 10 | SetDefaultSpeed, 11 | ToggleAutoStart, 12 | ToggleShortcuts, 13 | } from '../store/preferences/preferences.actions'; 14 | 15 | @Component({ 16 | selector: 'yt-tweaks-preferences-popup', 17 | templateUrl: './preferences-popup.component.html', 18 | styleUrls: ['./preferences-popup.component.scss'], 19 | }) 20 | export class PreferencesPopupComponent implements OnInit, OnDestroy { 21 | private rootSub = new Subscription(); 22 | 23 | 24 | faTimes = faTimes; 25 | 26 | qualityChoices = QUALITY_CHOICES; 27 | speedChoices = SPEED_CHOICES; 28 | currentSpeed: string; 29 | currentQuality: string; 30 | 31 | @Select(PreferencesState) preferences$: Observable; 32 | 33 | constructor( 34 | public dialogRef: MatDialogRef, 35 | private store: Store, 36 | ) { 37 | } 38 | 39 | ngOnInit() { 40 | this.rootSub.add( 41 | this.preferences$ 42 | .subscribe(({ defaultQuality, defaultSpeed }) => { 43 | this.currentQuality = this.qualityChoices.find(({ value }) => value === defaultQuality).label; 44 | this.currentSpeed = this.speedChoices.find(({ value }) => value === defaultSpeed).label; 45 | }), 46 | ); 47 | } 48 | 49 | ngOnDestroy() { 50 | this.rootSub.unsubscribe(); 51 | } 52 | 53 | close() { 54 | this.dialogRef.close(); 55 | } 56 | 57 | toggleShortcuts() { 58 | this.store.dispatch(new ToggleShortcuts()); 59 | } 60 | 61 | toggleAutoStart() { 62 | this.store.dispatch(new ToggleAutoStart()); 63 | } 64 | 65 | setSpeed(speed: number) { 66 | this.store.dispatch(new SetDefaultSpeed(speed)); 67 | } 68 | 69 | setQuality(quality: string) { 70 | this.store.dispatch(new SetDefaultQuality(quality)); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /projects/youtube-tweaks/src/app/modules/preferences-popup/preferences-popup.constants.ts: -------------------------------------------------------------------------------- 1 | export const QUALITY_CHOICES = [ 2 | { label: 'Auto', value: 'auto' }, 3 | { label: '144p', value: 'tiny' }, 4 | { label: '240p', value: 'small' }, 5 | { label: '360p', value: 'medium' }, 6 | { label: '480p', value: 'large' }, 7 | { label: '720p', value: 'hd720' }, 8 | { label: '1080p', value: 'hd1080' }, 9 | { label: '1440p', value: 'hd1440' }, 10 | { label: '2160p', value: 'hd2160' }, 11 | { label: 'Max', value: 'highres' }, 12 | ]; 13 | 14 | 15 | export const SPEED_CHOICES = [ 16 | { label: 'x0.25', value: 0.25 }, 17 | { label: 'x0.5', value: 0.5 }, 18 | { label: 'x0.75', value: 0.75 }, 19 | { label: 'Normal', value: 1 }, 20 | { label: 'x1.25', value: 1.25 }, 21 | { label: 'x1.5', value: 1.5 }, 22 | { label: 'x1.75', value: 1.75 }, 23 | { label: 'x2', value: 2 }, 24 | ]; 25 | -------------------------------------------------------------------------------- /projects/youtube-tweaks/src/app/modules/preferences-popup/preferences-popup.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { PreferencesPopupComponent } from './preferences-popup.component'; 4 | import { MatButtonModule } from '@angular/material/button'; 5 | import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; 6 | import { MatTabsModule } from '@angular/material/tabs'; 7 | import { MatSlideToggleModule } from '@angular/material/slide-toggle'; 8 | import { MatCardModule } from '@angular/material/card'; 9 | import { FormsModule } from '@angular/forms'; 10 | import { ShortcutsPanelComponent } from './shortcuts-panel/shortcuts-panel.component'; 11 | import { MatListModule } from '@angular/material/list'; 12 | import { ShortcutKeysComponent } from './shortcuts-panel/shortcut-keys/shortcut-keys.component'; 13 | import { ShortcutEditComponent } from './shortcuts-panel/shortcut-edit/shortcut-edit.component'; 14 | import { MatTooltipModule } from '@angular/material/tooltip'; 15 | import { MatMenuModule } from '@angular/material/menu'; 16 | 17 | @NgModule({ 18 | declarations: [PreferencesPopupComponent, ShortcutsPanelComponent, ShortcutKeysComponent, ShortcutEditComponent], 19 | entryComponents: [PreferencesPopupComponent], 20 | imports: [ 21 | CommonModule, 22 | MatButtonModule, 23 | FontAwesomeModule, 24 | MatTabsModule, 25 | MatSlideToggleModule, 26 | MatCardModule, 27 | FormsModule, 28 | MatListModule, 29 | MatTooltipModule, 30 | MatMenuModule, 31 | ], 32 | }) 33 | export class PreferencesPopupModule { } 34 | -------------------------------------------------------------------------------- /projects/youtube-tweaks/src/app/modules/preferences-popup/shortcuts-panel/shortcut-edit/shortcut-edit.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 10 | -------------------------------------------------------------------------------- /projects/youtube-tweaks/src/app/modules/preferences-popup/shortcuts-panel/shortcut-edit/shortcut-edit.component.scss: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /projects/youtube-tweaks/src/app/modules/preferences-popup/shortcuts-panel/shortcut-edit/shortcut-edit.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ShortcutEditComponent } from './shortcut-edit.component'; 4 | 5 | describe('ShortcutEditComponent', () => { 6 | let component: ShortcutEditComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ShortcutEditComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ShortcutEditComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /projects/youtube-tweaks/src/app/modules/preferences-popup/shortcuts-panel/shortcut-edit/shortcut-edit.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, HostListener, Input, Output } from '@angular/core'; 2 | import eventToShortcut from '../../../shortcut/event-to-shortcut'; 3 | import { faTimes } from '@fortawesome/free-solid-svg-icons/faTimes'; 4 | import { faCheck } from '@fortawesome/free-solid-svg-icons/faCheck'; 5 | 6 | @Component({ 7 | selector: 'yt-tweaks-shortcut-edit', 8 | templateUrl: './shortcut-edit.component.html', 9 | styleUrls: ['./shortcut-edit.component.scss'], 10 | }) 11 | export class ShortcutEditComponent{ 12 | faTimes = faTimes; 13 | faCheck = faCheck; 14 | @Input() shortcut: string; 15 | @Output() shortcutChange = new EventEmitter(); 16 | @Output() cancel = new EventEmitter(); 17 | 18 | 19 | @HostListener('document:keypress', ['$event']) 20 | onKeyPress($event: KeyboardEvent) { 21 | if($event.code === 'Enter') { 22 | this.shortcutChange.emit(this.shortcut); 23 | } else { 24 | this.shortcut = eventToShortcut($event); 25 | } 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /projects/youtube-tweaks/src/app/modules/preferences-popup/shortcuts-panel/shortcut-keys/shortcut-keys.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ key }} 4 | + 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /projects/youtube-tweaks/src/app/modules/preferences-popup/shortcuts-panel/shortcut-keys/shortcut-keys.component.scss: -------------------------------------------------------------------------------- 1 | .shortcut { 2 | .key { 3 | background: #dedede; 4 | padding: 4px; 5 | border-radius: 4px; 6 | box-shadow: 0 1px 0 rgba(29, 29, 29, 0.8) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /projects/youtube-tweaks/src/app/modules/preferences-popup/shortcuts-panel/shortcut-keys/shortcut-keys.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ShortcutKeysComponent } from './shortcut-keys.component'; 4 | 5 | describe('ShortcutKeysComponent', () => { 6 | let component: ShortcutKeysComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ShortcutKeysComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ShortcutKeysComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /projects/youtube-tweaks/src/app/modules/preferences-popup/shortcuts-panel/shortcut-keys/shortcut-keys.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'yt-tweaks-shortcut-keys', 5 | templateUrl: './shortcut-keys.component.html', 6 | styleUrls: ['./shortcut-keys.component.scss'] 7 | }) 8 | export class ShortcutKeysComponent implements OnChanges { 9 | @Input() shortcut: string; 10 | keys: string[] = []; 11 | 12 | ngOnChanges(changes: SimpleChanges) { 13 | if(changes.shortcut) { 14 | this.keys = this.shortcut.split(' + '); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /projects/youtube-tweaks/src/app/modules/preferences-popup/shortcuts-panel/shortcuts-panel.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | {{ mapping[shortcut.key] }} 5 |
6 | 7 | 8 | 13 | 14 | 15 | 21 |
22 |
23 |
24 |
25 |
26 | 27 |
28 | -------------------------------------------------------------------------------- /projects/youtube-tweaks/src/app/modules/preferences-popup/shortcuts-panel/shortcuts-panel.component.scss: -------------------------------------------------------------------------------- 1 | .shortcuts { 2 | .item { 3 | display: flex; 4 | justify-content: space-between; 5 | align-items: center; 6 | width: 100%; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /projects/youtube-tweaks/src/app/modules/preferences-popup/shortcuts-panel/shortcuts-panel.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ShortcutsPanelComponent } from './shortcuts-panel.component'; 4 | 5 | describe('ShortcutsPanelComponent', () => { 6 | let component: ShortcutsPanelComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ShortcutsPanelComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ShortcutsPanelComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /projects/youtube-tweaks/src/app/modules/preferences-popup/shortcuts-panel/shortcuts-panel.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { Select, Store } from '@ngxs/store'; 3 | import { ShortcutsModel, ShortcutsState } from '../../store/shortcuts/shortcuts.state'; 4 | import { Observable } from 'rxjs'; 5 | import { faPen } from '@fortawesome/free-solid-svg-icons/faPen'; 6 | import { ChangeShortcut, ResetShortcuts } from '../../store/shortcuts/shortcuts.actions'; 7 | 8 | type Mapping = { 9 | [key in keyof ShortcutsModel]: string; 10 | } 11 | 12 | @Component({ 13 | selector: 'yt-tweaks-shortcuts-panel', 14 | templateUrl: './shortcuts-panel.component.html', 15 | styleUrls: ['./shortcuts-panel.component.scss'] 16 | }) 17 | export class ShortcutsPanelComponent { 18 | faPen = faPen; 19 | 20 | editing = null; 21 | 22 | mapping: Mapping = { 23 | SPEED_UP: 'Speed up', 24 | SPEED_DOWN: 'Speed down', 25 | SPEED_RESET: 'Reset speed', 26 | QUALITY_UP: 'Quality up', 27 | QUALITY_DOWN: 'Quality down', 28 | }; 29 | 30 | @Select(ShortcutsState) shortcuts$: Observable; 31 | 32 | constructor( 33 | private store: Store, 34 | ) { 35 | } 36 | 37 | edit(key: any) { 38 | this.editing = key; 39 | } 40 | 41 | editDone(shortcut?: string) { 42 | if(shortcut) { 43 | this.store.dispatch(new ChangeShortcut(this.editing, shortcut)); 44 | } 45 | this.editing = null; 46 | } 47 | 48 | reset() { 49 | this.store.dispatch(new ResetShortcuts()); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /projects/youtube-tweaks/src/app/modules/shortcut/event-to-shortcut.ts: -------------------------------------------------------------------------------- 1 | export default function eventToShortcut({shiftKey, ctrlKey, altKey, metaKey, code}: KeyboardEvent) { 2 | const path = []; 3 | ctrlKey && path.push('Ctrl'); 4 | metaKey && path.push('Super'); 5 | altKey && path.push('Alt'); 6 | shiftKey && path.push('Shift'); 7 | path.push(code); 8 | 9 | return path.join(' + '); 10 | } 11 | -------------------------------------------------------------------------------- /projects/youtube-tweaks/src/app/modules/shortcut/providers/quality-shortcut.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { QualityShortcutService } from './quality-shortcut.service'; 4 | 5 | describe('QualityShortcutService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: QualityShortcutService = TestBed.get(QualityShortcutService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /projects/youtube-tweaks/src/app/modules/shortcut/providers/quality-shortcut.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ShortcutHandler } from '../shortcut.service'; 3 | import { Store } from '@ngxs/store'; 4 | import { PlayerService } from '../../player/player.service'; 5 | 6 | @Injectable({ 7 | providedIn: 'root', 8 | }) 9 | export class QualityShortcutService implements ShortcutHandler { 10 | constructor( 11 | private store: Store, 12 | private playerService: PlayerService, 13 | ) { 14 | } 15 | 16 | handle(shortcut) { 17 | const { QUALITY_UP, QUALITY_DOWN } = this.store.selectSnapshot(({ shortcuts }) => shortcuts); 18 | const qualityLevels = this.playerService.getAvailableQualityLevels(); 19 | const currentQuality = this.playerService.getQuality(); 20 | const index = qualityLevels.indexOf(currentQuality); 21 | const next = qualityLevels[index - 1]; 22 | const prev = qualityLevels[index + 1]; 23 | 24 | if(QUALITY_UP === shortcut && next) { 25 | this.playerService.setQuality(next); 26 | } else if(QUALITY_DOWN === shortcut && prev) { 27 | this.playerService.setQuality(prev); 28 | } 29 | 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /projects/youtube-tweaks/src/app/modules/shortcut/providers/speed-shortcut.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { SpeedShortcutService } from './speed-shortcut.service'; 4 | 5 | describe('SpeedShortcutService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: SpeedShortcutService = TestBed.get(SpeedShortcutService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /projects/youtube-tweaks/src/app/modules/shortcut/providers/speed-shortcut.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ShortcutHandler } from '../shortcut.service'; 3 | import { Store } from '@ngxs/store'; 4 | import { PlayerService } from '../../player/player.service'; 5 | 6 | @Injectable({ 7 | providedIn: 'root', 8 | }) 9 | export class SpeedShortcutService implements ShortcutHandler { 10 | constructor( 11 | private store: Store, 12 | private playerService: PlayerService, 13 | ) { 14 | } 15 | 16 | handle(shortcut) { 17 | const speed = this.playerService.getSpeed(); 18 | const { SPEED_UP, SPEED_DOWN, SPEED_RESET } = this.store.selectSnapshot(({ shortcuts }) => shortcuts); 19 | 20 | switch(shortcut) { 21 | case SPEED_UP: { 22 | this.playerService.setSpeed(speed + 0.25); 23 | break; 24 | } 25 | 26 | case SPEED_DOWN: { 27 | this.playerService.setSpeed(speed - 0.25); 28 | break; 29 | } 30 | 31 | case SPEED_RESET: { 32 | const defaultSpeed = this.store.selectSnapshot(({ preferences }) => preferences.defaultSpeed); 33 | this.playerService.setSpeed(defaultSpeed); 34 | break; 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /projects/youtube-tweaks/src/app/modules/shortcut/shortcut.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, HostListener, Input } from '@angular/core'; 2 | import { Subject } from 'rxjs'; 3 | import eventToShortcut from './event-to-shortcut'; 4 | 5 | @Component({ 6 | selector: 'yt-tweaks-shortcut', 7 | template: '', 8 | }) 9 | export class ShortcutComponent { 10 | @Input() events: Subject; 11 | 12 | @HostListener('document:keypress', ['$event']) 13 | onKeyPress($event: KeyboardEvent) { 14 | const { target } = $event; 15 | const fromInput = target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement; 16 | 17 | if(!fromInput) { 18 | this.events.next(eventToShortcut($event)); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /projects/youtube-tweaks/src/app/modules/shortcut/shortcut.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { SHORTCUT, ShortcutService } from './shortcut.service'; 4 | import { ShortcutComponent } from './shortcut.component'; 5 | import { SpeedShortcutService } from './providers/speed-shortcut.service'; 6 | import { QualityShortcutService } from './providers/quality-shortcut.service'; 7 | 8 | @NgModule({ 9 | declarations: [ShortcutComponent], 10 | imports: [CommonModule], 11 | exports: [ShortcutComponent], 12 | entryComponents: [ShortcutComponent], 13 | providers: [ 14 | ShortcutService, 15 | { provide: SHORTCUT, useClass: SpeedShortcutService, multi: true }, 16 | { provide: SHORTCUT, useClass: QualityShortcutService, multi: true }, 17 | ], 18 | }) 19 | export class ShortcutModule { 20 | } 21 | -------------------------------------------------------------------------------- /projects/youtube-tweaks/src/app/modules/shortcut/shortcut.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { ShortcutService } from './shortcut.service'; 4 | 5 | describe('ShortcutService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: ShortcutService = TestBed.get(ShortcutService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /projects/youtube-tweaks/src/app/modules/shortcut/shortcut.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ComponentFactory, 3 | ComponentFactoryResolver, ComponentRef, 4 | Inject, 5 | Injectable, 6 | InjectionToken, 7 | Injector, 8 | } from '@angular/core'; 9 | import { Subject, Subscription } from 'rxjs'; 10 | import { ShortcutComponent } from './shortcut.component'; 11 | 12 | export interface ShortcutHandler { 13 | handle(shortcut: string): void; 14 | } 15 | 16 | export const SHORTCUT = new InjectionToken('SHORTCUT'); 17 | 18 | @Injectable({ 19 | providedIn: 'root', 20 | }) 21 | export class ShortcutService { 22 | private readonly factory: ComponentFactory; 23 | private shortcutListener: ComponentRef; 24 | private rootSub = new Subscription(); 25 | public events = new Subject(); 26 | 27 | constructor( 28 | @Inject(SHORTCUT) private shortcuts: ShortcutHandler[], 29 | private componentFactoryResolver: ComponentFactoryResolver, 30 | private injector: Injector, 31 | ) { 32 | this.factory = this.componentFactoryResolver.resolveComponentFactory(ShortcutComponent); 33 | } 34 | 35 | private buildShortcutListener() { 36 | this.shortcutListener = this.factory.create(this.injector); 37 | this.shortcutListener.instance.events = this.events; 38 | document.body.append(this.shortcutListener.location.nativeElement); 39 | this.shortcutListener.changeDetectorRef.detectChanges(); 40 | } 41 | 42 | attach() { 43 | if(!this.shortcutListener) { 44 | const subs = this.shortcuts.map((provider) => { 45 | return this.events.subscribe((shortcut) => provider.handle(shortcut)); 46 | }); 47 | 48 | subs.forEach((sub) => this.rootSub.add(sub)); 49 | this.buildShortcutListener(); 50 | } 51 | } 52 | 53 | detach() { 54 | if(this.shortcutListener) { 55 | this.rootSub.unsubscribe(); 56 | this.shortcutListener.destroy(); 57 | this.shortcutListener.location.nativeElement.remove(); 58 | this.shortcutListener = null; 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /projects/youtube-tweaks/src/app/modules/store/preferences/preferences.actions.ts: -------------------------------------------------------------------------------- 1 | export class ToggleShortcuts { 2 | static type = '[Preferences] Toggle shortcuts'; 3 | } 4 | 5 | export class ToggleAutoStart { 6 | static type = '[Preferences] Toggle auto start'; 7 | } 8 | 9 | export class SetDefaultSpeed { 10 | static type = '[Preferences] Set default speed'; 11 | 12 | constructor( 13 | public speed: number, 14 | ) { 15 | } 16 | } 17 | 18 | export class SetDefaultQuality { 19 | static type = '[Preferences] Set default quality'; 20 | 21 | constructor( 22 | public quality: string, 23 | ) { 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /projects/youtube-tweaks/src/app/modules/store/preferences/preferences.state.ts: -------------------------------------------------------------------------------- 1 | import { Action, State, StateContext } from '@ngxs/store'; 2 | import { SetDefaultQuality, SetDefaultSpeed, ToggleAutoStart, ToggleShortcuts } from './preferences.actions'; 3 | 4 | export interface PreferencesModel { 5 | defaultSpeed: number; 6 | defaultQuality: string; 7 | autoStart: boolean; 8 | shortcutsEnabled: boolean; 9 | } 10 | 11 | type Context = StateContext; 12 | 13 | @State({ 14 | name: 'preferences', 15 | defaults: { 16 | defaultSpeed: 1, 17 | defaultQuality: 'auto', 18 | autoStart: true, 19 | shortcutsEnabled: true, 20 | }, 21 | }) 22 | export class PreferencesState { 23 | @Action(ToggleShortcuts) 24 | toggleShortcuts(ctx: Context) { 25 | const { shortcutsEnabled } = ctx.getState(); 26 | 27 | ctx.patchState({ shortcutsEnabled: !shortcutsEnabled }); 28 | } 29 | 30 | @Action(ToggleAutoStart) 31 | toggleAutoStart(ctx: Context) { 32 | const { autoStart } = ctx.getState(); 33 | 34 | ctx.patchState({ autoStart: !autoStart }); 35 | } 36 | 37 | @Action(SetDefaultSpeed) 38 | setDefaultSpeed(ctx: Context, { speed }: SetDefaultSpeed) { 39 | ctx.patchState({ defaultSpeed: speed }); 40 | } 41 | 42 | @Action(SetDefaultQuality) 43 | setDefaultQuality(ctx: Context, { quality }: SetDefaultQuality) { 44 | ctx.patchState({ defaultQuality: quality }); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /projects/youtube-tweaks/src/app/modules/store/shortcuts/shortcuts.actions.ts: -------------------------------------------------------------------------------- 1 | export class ChangeShortcut { 2 | static type = '[Shortcuts] Change'; 3 | 4 | constructor( 5 | public key: string, 6 | public shortcut: string, 7 | ) { 8 | } 9 | } 10 | 11 | export class ResetShortcuts { 12 | static type = '[Shortcuts] Reset'; 13 | } 14 | -------------------------------------------------------------------------------- /projects/youtube-tweaks/src/app/modules/store/shortcuts/shortcuts.state.ts: -------------------------------------------------------------------------------- 1 | import { Action, State, StateContext } from '@ngxs/store'; 2 | import { ChangeShortcut, ResetShortcuts } from './shortcuts.actions'; 3 | 4 | export interface ShortcutsModel { 5 | SPEED_UP: string; 6 | SPEED_DOWN: string; 7 | SPEED_RESET: string; 8 | QUALITY_UP: string; 9 | QUALITY_DOWN: string; 10 | } 11 | 12 | type Context = StateContext; 13 | 14 | const DEFAULTS: ShortcutsModel = { 15 | SPEED_UP: 'NumpadAdd', 16 | SPEED_DOWN: 'NumpadSubtract', 17 | SPEED_RESET: 'NumpadMultiply', 18 | QUALITY_UP: 'Shift + NumpadAdd', 19 | QUALITY_DOWN: 'Shift + NumpadSubtract', 20 | }; 21 | 22 | @State({ 23 | name: 'shortcuts', 24 | defaults: DEFAULTS, 25 | }) 26 | export class ShortcutsState { 27 | @Action(ChangeShortcut) 28 | changeShortcut(ctx: Context, { key, shortcut }: ChangeShortcut) { 29 | ctx.patchState({ [key]: shortcut }); 30 | } 31 | 32 | @Action(ResetShortcuts) 33 | reset(ctx: Context) { 34 | ctx.setState(DEFAULTS); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /projects/youtube-tweaks/src/app/modules/store/storage.engine.ts: -------------------------------------------------------------------------------- 1 | import { AsyncStorageEngine } from '@ngxs-labs/async-storage-plugin'; 2 | import GMStorage from '@kawai-scripts/gm-storage'; 3 | import { from } from 'rxjs'; 4 | import { map } from 'rxjs/operators'; 5 | 6 | export class GMStorageEngine implements AsyncStorageEngine { 7 | private storage = new GMStorage(); 8 | 9 | getItem(key) { 10 | return from(this.storage.getValue(key)); 11 | } 12 | 13 | setItem(key, val) { 14 | this.storage.setValue(key, val); 15 | } 16 | 17 | removeItem(key: string) { 18 | this.storage.deleteValue(key); 19 | } 20 | 21 | clear() { 22 | // noop 23 | } 24 | 25 | length() { 26 | return from(this.storage.listValues()).pipe(map((keys) => keys.length)); 27 | } 28 | 29 | key(val: number) { 30 | return from(this.storage.listValues()).pipe(map(({ [val]: key }) => key)); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /projects/youtube-tweaks/src/app/modules/store/store.module.ts: -------------------------------------------------------------------------------- 1 | import { environment } from '../../../environments/environment'; 2 | import { NgModule } from '@angular/core'; 3 | import { NgxsModule } from '@ngxs/store'; 4 | import { NgxsReduxDevtoolsPluginModule } from '@ngxs/devtools-plugin'; 5 | import { ShortcutsState } from './shortcuts/shortcuts.state'; 6 | import { PreferencesState } from './preferences/preferences.state'; 7 | import { GMStorageEngine } from './storage.engine'; 8 | import { NgxsAsyncStoragePluginModule } from '@ngxs-labs/async-storage-plugin'; 9 | 10 | if(!environment.production) { 11 | // eslint-disable-next-line @typescript-eslint/ban-ts-ignore 12 | // @ts-ignore 13 | // eslint-disable-next-line no-undef 14 | window.__REDUX_DEVTOOLS_EXTENSION__ = unsafeWindow.__REDUX_DEVTOOLS_EXTENSION__; 15 | } 16 | 17 | 18 | @NgModule({ 19 | imports: [ 20 | NgxsModule.forRoot([ 21 | PreferencesState, 22 | ShortcutsState, 23 | ], { 24 | developmentMode: !environment.production, 25 | }), 26 | NgxsReduxDevtoolsPluginModule.forRoot({ 27 | name: 'Youtube tweaks', 28 | disabled: environment.production, 29 | }), 30 | NgxsAsyncStoragePluginModule.forRoot(GMStorageEngine), 31 | ], 32 | }) 33 | export class StoreModule { 34 | } 35 | -------------------------------------------------------------------------------- /projects/youtube-tweaks/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /projects/youtube-tweaks/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: false 3 | }; 4 | -------------------------------------------------------------------------------- /projects/youtube-tweaks/src/main.ts: -------------------------------------------------------------------------------- 1 | import './polyfills'; 2 | import './styles.scss'; 3 | import { enableProdMode } from '@angular/core'; 4 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 5 | import { AppModule } from './app/app.module'; 6 | import { environment } from './environments/environment'; 7 | import combineCssSelectors from '../../youtube-blocker/src/utils/combine-css-rules'; 8 | import waitSelector from '@kawai-scripts/wait-selector'; 9 | 10 | if (environment.production) { 11 | enableProdMode(); 12 | } 13 | 14 | const ROOT_ELEMENT = document.createElement('yt-tweaks-root'); 15 | const MOUNT_POINT = combineCssSelectors( 16 | '#yt-masthead-user', 17 | '#yt-masthead-signin', 18 | '#end', 19 | ); 20 | 21 | 22 | function mountApp() { 23 | ROOT_ELEMENT.classList.add('booting'); 24 | document.body.append(ROOT_ELEMENT); 25 | 26 | platformBrowserDynamic() 27 | .bootstrapModule(AppModule) 28 | .catch(err => console.error(err)); 29 | 30 | waitSelector(MOUNT_POINT).then((mountPoint) => { 31 | mountPoint.prepend(ROOT_ELEMENT); 32 | ROOT_ELEMENT.classList.remove('booting'); 33 | }); 34 | } 35 | 36 | mountApp(); 37 | -------------------------------------------------------------------------------- /projects/youtube-tweaks/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | import 'zone.js/dist/zone'; 2 | -------------------------------------------------------------------------------- /projects/youtube-tweaks/src/styles.scss: -------------------------------------------------------------------------------- 1 | @import "~@angular/material/prebuilt-themes/indigo-pink.css"; 2 | 3 | .cdk-overlay-container { 4 | z-index: 1999999999; 5 | } 6 | 7 | .dialog-popup { 8 | font-size: 15px; 9 | } 10 | -------------------------------------------------------------------------------- /projects/youtube-tweaks/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/app", 5 | "types": [], 6 | "module": "esnext", 7 | "target": "es2015", 8 | "moduleResolution": "node", 9 | "lib": [ 10 | "es2015", 11 | "dom" 12 | ] 13 | }, 14 | "include": [ 15 | "src/**/*.ts" 16 | ], 17 | "exclude": [ 18 | "src/test.ts", 19 | "src/**/*.spec.ts" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /projects/youtube-tweaks/webpack.analyze.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const provider = require('../../build/ng-webpack-configs/analyze'); 3 | 4 | module.exports = provider('youtube-tweaks'); 5 | -------------------------------------------------------------------------------- /projects/youtube-tweaks/webpack.dev.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const provider = require('../../build/ng-webpack-configs/development'); 3 | 4 | module.exports = provider('youtube-tweaks'); 5 | -------------------------------------------------------------------------------- /projects/youtube-tweaks/webpack.prod.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const provider = require('../../build/ng-webpack-configs/production'); 3 | 4 | module.exports = provider('youtube-tweaks'); 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es5", 5 | "sourceMap": true, 6 | "esModuleInterop": true, 7 | "resolveJsonModule": true, 8 | "experimentalDecorators": true, 9 | "emitDecoratorMetadata": true 10 | }, 11 | "exclude": [ 12 | "node_modules" 13 | ] 14 | } 15 | --------------------------------------------------------------------------------