├── .CaptionConf ├── .eslintrc.json ├── .gitattributes ├── .github └── ISSUE_TEMPLATE │ └── bug_report.md ├── .gitignore ├── .prettierrc.json ├── README.md ├── builder_assets ├── 256x256.png ├── LargeIcon.png ├── icon.icns ├── icon.ico ├── icon.png └── installerMacro.nsh ├── docs └── images │ └── YTS-Streaming.jpg ├── electron ├── assets │ ├── bootstrap │ │ ├── bootstrap.bundle.min.js │ │ └── bootstrap.min.css │ ├── icons │ │ └── 256x256.png │ ├── react_cdn │ │ ├── react-dom.production.min.js │ │ └── react.production.min.js │ └── video_player │ │ ├── plyr3.7.3.min.css │ │ └── plyr3.7.3.polyfilled.min.js ├── components │ ├── DownloaderWindow.ts │ ├── MainWindow.ts │ ├── VideoPlayerWindow.ts │ └── preload.cts ├── configs.ts ├── electron.interface.ts ├── electron.ts ├── srt2vtt.d.ts ├── tsconfig.json └── views │ ├── download_jsx │ └── download.jsx │ └── html │ ├── download.html │ └── video.html ├── index.html ├── package-lock.json ├── package.json ├── public └── assets │ ├── bootstrap │ ├── bootstrap.bundle.min.js │ └── bootstrap.min.css │ └── images │ ├── logo-imdb-svg.svg │ ├── logo_final.png │ ├── play-button.svg │ └── preloader.gif ├── src ├── App.scss ├── App.tsx ├── components │ ├── ErrorHandling │ │ └── ErrorHandling.tsx │ ├── backdrop │ │ ├── BackDrop.module.scss │ │ └── BackDrop.tsx │ ├── footer │ │ └── footer.tsx │ ├── header │ │ ├── CustomCaption.tsx │ │ ├── Heading.tsx │ │ ├── SettingIcon.tsx │ │ ├── SettingsIcon.scss │ │ ├── SettingsModal.module.scss │ │ └── SettingsModal.tsx │ ├── movieScreenshot │ │ └── MovieScreenshots.tsx │ ├── movieSynopsisAndTrailer │ │ ├── MovieSynopsisTrailer.scss │ │ └── MovieSynopsisTrailer.tsx │ ├── moviecard │ │ ├── MovieCard.scss │ │ └── MovieCard.tsx │ ├── movieintro │ │ ├── MovieIntro.scss │ │ └── MovieIntro.tsx │ ├── movielist │ │ └── MovieList.tsx │ ├── search │ │ ├── Filter.tsx │ │ └── SearchAndFilterBox.tsx │ └── spinner │ │ └── Spinner.tsx ├── main.tsx ├── pages │ ├── EntryPage.tsx │ └── MovieDetails.tsx ├── store │ └── AppContextProvider.tsx └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.CaptionConf: -------------------------------------------------------------------------------- 1 | {"fontSize":{"small":13,"medium":15,"large":23}} -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "node": true 6 | }, 7 | "extends": ["plugin:react/recommended", "standard-with-typescript"], 8 | "overrides": [], 9 | "parserOptions": { 10 | "ecmaVersion": "latest", 11 | "sourceType": "module", 12 | "project": ["./tsconfig.node.json"] 13 | }, 14 | "plugins": ["react"], 15 | "rules": { 16 | "comma-dangle": ["always"] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Version [e.g. 22] 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build 39 | build/Release 40 | react_dist 41 | electron_dist 42 | 43 | # Dependency directories 44 | node_modules/ 45 | jspm_packages/ 46 | 47 | # TypeScript v1 declaration files 48 | typings/ 49 | 50 | # TypeScript cache 51 | *.tsbuildinfo 52 | 53 | # Optional npm cache directory 54 | .npm 55 | 56 | # Optional eslint cache 57 | .eslintcache 58 | 59 | # Microbundle cache 60 | .rpt2_cache/ 61 | .rts2_cache_cjs/ 62 | .rts2_cache_es/ 63 | .rts2_cache_umd/ 64 | 65 | # Optional REPL history 66 | .node_repl_history 67 | 68 | # Output of 'npm pack' 69 | *.tgz 70 | 71 | # Yarn Integrity file 72 | .yarn-integrity 73 | 74 | # dotenv environment variables file 75 | #.env 76 | .env.test 77 | 78 | # parcel-bundler cache (https://parceljs.org/) 79 | .cache 80 | 81 | # Next.js build output 82 | .next 83 | 84 | # Nuxt.js build / generate output 85 | .nuxt 86 | dist 87 | 88 | # Gatsby files 89 | .cache/ 90 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 91 | # https://nextjs.org/blog/next-9-1#public-directory-support 92 | # public 93 | 94 | # vuepress build output 95 | .vuepress/dist 96 | 97 | # Serverless directories 98 | .serverless/ 99 | 100 | # FuseBox cache 101 | .fusebox/ 102 | 103 | # DynamoDB Local files 104 | .dynamodb/ 105 | 106 | # TernJS port file 107 | .tern-port 108 | 109 | .idea 110 | .idea/** 111 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | # YTS-Streaming v3.0 - Migrating to vite. 10 | 11 | YTS-streaming application is an electron app for windows platform. Through YTS streaming application you can stream any movie available YTS(yify) movie website. 12 | YTS-streaming uses the content from the official YTS API v2. This app is simple to use with a minimalists design. It is more or less like the original YTS website with the ability to play the movie within the app and download the movies that are not supported by YTS player. 13 | 14 | ### Download App > [Link to Releases](https://github.com/mbpn1/YTS-Streaming/releases) Download latest release 15 | 16 | ### Features 17 | 18 | - YTS Website in windows app 19 | - Torrent streaming within the app 20 | - Built in player 21 | - Support for captions 22 | - Support for downloading if not playable 23 | - Support for playing external torrent link. 24 | - Support for Up/Down speed limit. 25 | 26 | ### Issues or Limitations 27 | 28 | - YST logins, review and commenting feature not implemented 29 | - An automatic update is not implemented. (Need to do manual update app if a new version is released) 30 | 31 | **Clear Cache after watching the movie to free up the disk space. The cache are saved at /webTorrent** 32 | 33 | ## Major dependency 34 | 35 | - [Link to WebTorrent.io](http://webtorrent.io) 36 | - [Link to plyr player](https://plyr.io/) 37 | - [Link to Electron](https://www.electronjs.org/) 38 | - [Link to Electron-Builder](https://github.com/electron-userland/electron-builder) 39 | - Check package.json for more dependency 40 | 41 | ## To-Do: 42 | 43 | - Support to see downloaded movies 44 | - Allowing user to copying downloaded movies directly form the app. 45 | 46 | # Preview 47 | 48 | **Home** 49 | ![YTS-Streaming Home](https://user-images.githubusercontent.com/21078512/123229175-c7420200-d4f5-11eb-90da-39dd3a09bad0.png) 50 | 51 | **Movie Player** 52 | ![YTS-Streaming Player](https://user-images.githubusercontent.com/21078512/111864151-77e4b680-8987-11eb-9a9b-26ec228162a8.png) 53 | 54 | **Settings** 55 | _Movies you have watched were downloaded in `/webtorrent/` folder of your os so, it's good to clear cache to free up space. You can also copy the fully downloaded movie from the temp folder. To clear cache, there is a clear cache button within the app settings._ 56 | -------------------------------------------------------------------------------- /builder_assets/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iambpn/YTS-Streaming/e2d2110734ee61b5d01025b3aaff70e5e96c5b83/builder_assets/256x256.png -------------------------------------------------------------------------------- /builder_assets/LargeIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iambpn/YTS-Streaming/e2d2110734ee61b5d01025b3aaff70e5e96c5b83/builder_assets/LargeIcon.png -------------------------------------------------------------------------------- /builder_assets/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iambpn/YTS-Streaming/e2d2110734ee61b5d01025b3aaff70e5e96c5b83/builder_assets/icon.icns -------------------------------------------------------------------------------- /builder_assets/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iambpn/YTS-Streaming/e2d2110734ee61b5d01025b3aaff70e5e96c5b83/builder_assets/icon.ico -------------------------------------------------------------------------------- /builder_assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iambpn/YTS-Streaming/e2d2110734ee61b5d01025b3aaff70e5e96c5b83/builder_assets/icon.png -------------------------------------------------------------------------------- /builder_assets/installerMacro.nsh: -------------------------------------------------------------------------------- 1 | ;!macro customInstall 2 | ; this will be run after completing install 3 | ; CreateShortCut "$INSTDIR\appname.lnk" "$INSTDIR\apname.exe" "" "" "" SW_SHOWNORMAL "CONTROL|SHIFT|`" "" 4 | ;!macroend 5 | 6 | ;to remove install dir after uninstall 7 | !macro customUnInstall 8 | RMDir /r $INSTDIR 9 | !macroend 10 | 11 | ;availabel macro function to use in electron-builder 12 | ;customHeader, preInit, customInit, customUnInit, customInstall, customUnInstall, customRemoveFiles, customInstallMode -------------------------------------------------------------------------------- /docs/images/YTS-Streaming.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iambpn/YTS-Streaming/e2d2110734ee61b5d01025b3aaff70e5e96c5b83/docs/images/YTS-Streaming.jpg -------------------------------------------------------------------------------- /electron/assets/icons/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iambpn/YTS-Streaming/e2d2110734ee61b5d01025b3aaff70e5e96c5b83/electron/assets/icons/256x256.png -------------------------------------------------------------------------------- /electron/assets/react_cdn/react.production.min.js: -------------------------------------------------------------------------------- 1 | /** @license React v17.0.2 2 | * react.production.min.js 3 | * 4 | * Copyright (c) Facebook, Inc. and its affiliates. 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | */ 9 | (function () { 10 | 'use strict'; (function (c, x) { typeof exports === 'object' && typeof module !== 'undefined' ? x(exports) : typeof define === 'function' && define.amd ? define(['exports'], x) : (c = c || self, x(c.React = {})) })(this, function (c) { 11 | function x (a) { if (a === null || typeof a !== 'object') return null; a = Y && a[Y] || a['@@iterator']; return typeof a === 'function' ? a : null } function y (a) { 12 | for (var b = 'https://reactjs.org/docs/error-decoder.html?invariant=' + a, e = 1; e < arguments.length; e++)b += '&args[]=' + encodeURIComponent(arguments[e]); return 'Minified React error #' + 13 | a + '; visit ' + b + ' for the full message or use the non-minified dev environment for full errors and additional helpful warnings.' 14 | } function v (a, b, e) { this.props = a; this.context = b; this.refs = Z; this.updater = e || aa } function ba () {} function K (a, b, e) { this.props = a; this.context = b; this.refs = Z; this.updater = e || aa } function ca (a, b, e) { 15 | let l; const f = {}; let c = null; let da = null; if (b != null) for (l in void 0 !== b.ref && (da = b.ref), void 0 !== b.key && (c = '' + b.key), b)ea.call(b, l) && !fa.hasOwnProperty(l) && (f[l] = b[l]); let k = arguments.length - 2; if (k === 16 | 1)f.children = e; else if (k > 1) { for (var h = Array(k), d = 0; d < k; d++)h[d] = arguments[d + 2]; f.children = h } if (a && a.defaultProps) for (l in k = a.defaultProps, k) void 0 === f[l] && (f[l] = k[l]); return { $$typeof: w, type: a, key: c, ref: da, props: f, _owner: L.current } 17 | } function va (a, b) { return { $$typeof: w, type: a.type, key: b, ref: a.ref, props: a.props, _owner: a._owner } } function M (a) { return typeof a === 'object' && a !== null && a.$$typeof === w } function wa (a) { const b = { '=': '=0', ':': '=2' }; return '$' + a.replace(/[=:]/g, function (a) { return b[a] }) } function N (a, b) { 18 | return typeof a === 19 | 'object' && a !== null && a.key != null 20 | ? wa('' + a.key) 21 | : b.toString(36) 22 | } function C (a, b, e, l, f) { 23 | let c = typeof a; if (c === 'undefined' || c === 'boolean')a = null; let d = !1; if (a === null)d = !0; else switch (c) { case 'string':case 'number':d = !0; break; case 'object':switch (a.$$typeof) { case w:case ha:d = !0 } } if (d) { 24 | return d = a, f = f(d), a = l === '' ? '.' + N(d, 0) : l, Array.isArray(f) 25 | ? (e = '', a != null && (e = a.replace(ia, '$&/') + '/'), C(f, b, e, '', function (a) { return a })) 26 | : f != null && (M(f) && (f = va(f, e + (!f.key || d && d.key === f.key ? '' : ('' + f.key).replace(ia, '$&/') + '/') + 27 | a)), b.push(f)), 1 28 | } d = 0; l = l === '' ? '.' : l + ':'; if (Array.isArray(a)) for (var k = 0; k < a.length; k++) { c = a[k]; var h = l + N(c, k); d += C(c, b, e, h, f) } else if (h = x(a), typeof h === 'function') for (a = h.call(a), k = 0; !(c = a.next()).done;)c = c.value, h = l + N(c, k++), d += C(c, b, e, h, f); else if (c === 'object') throw b = '' + a, Error(y(31, b === '[object Object]' ? 'object with keys {' + Object.keys(a).join(', ') + '}' : b)); return d 29 | } function D (a, b, e) { if (a == null) return a; const l = []; let c = 0; C(a, l, '', '', function (a) { return b.call(e, a, c++) }); return l } function xa (a) { 30 | if (a._status === 31 | -1) { let b = a._result; b = b(); a._status = 0; a._result = b; b.then(function (b) { a._status === 0 && (b = b.default, a._status = 1, a._result = b) }, function (b) { a._status === 0 && (a._status = 2, a._result = b) }) } if (a._status === 1) return a._result; throw a._result 32 | } function n () { const a = ja.current; if (a === null) throw Error(y(321)); return a } function O (a, b) { let e = a.length; a.push(b); a:for (;;) { const c = e - 1 >>> 1; const f = a[c]; if (void 0 !== f && E(f, b) > 0)a[c] = b, a[e] = f, e = c; else break a } } function p (a) { a = a[0]; return void 0 === a ? null : a } function F (a) { 33 | const b = 34 | a[0]; if (void 0 !== b) { const e = a.pop(); if (e !== b) { a[0] = e; a:for (let c = 0, f = a.length; c < f;) { const d = 2 * (c + 1) - 1; const g = a[d]; const k = d + 1; const h = a[k]; if (void 0 !== g && E(g, e) < 0) void 0 !== h && E(h, g) < 0 ? (a[c] = h, a[k] = e, c = k) : (a[c] = g, a[d] = e, c = d); else if (void 0 !== h && E(h, e) < 0)a[c] = h, a[k] = e, c = k; else break a } } return b } return null 35 | } function E (a, b) { const e = a.sortIndex - b.sortIndex; return e !== 0 ? e : a.id - b.id } function P (a) { for (let b = p(r); b !== null;) { if (b.callback === null)F(r); else if (b.startTime <= a)F(r), b.sortIndex = b.expirationTime, O(q, b); else break; b = p(r) } } 36 | function Q (a) { z = !1; P(a); if (!u) if (p(q) !== null)u = !0, A(R); else { const b = p(r); b !== null && G(Q, b.startTime - a) } } function R (a, b) { u = !1; z && (z = !1, S()); H = !0; const e = g; try { P(b); for (m = p(q); m !== null && (!(m.expirationTime > b) || a && !T());) { const c = m.callback; if (typeof c === 'function') { m.callback = null; g = m.priorityLevel; const f = c(m.expirationTime <= b); b = t(); typeof f === 'function' ? m.callback = f : m === p(q) && F(q); P(b) } else F(q); m = p(q) } if (m !== null) var d = !0; else { const n = p(r); n !== null && G(Q, n.startTime - b); d = !1 } return d } finally { m = null, g = e, H = !1 } } 37 | var w = 60103; var ha = 60106; c.Fragment = 60107; c.StrictMode = 60108; c.Profiler = 60114; let ka = 60109; let la = 60110; let ma = 60112; c.Suspense = 60113; let na = 60115; let oa = 60116; if (typeof Symbol === 'function' && Symbol.for) { var d = Symbol.for; w = d('react.element'); ha = d('react.portal'); c.Fragment = d('react.fragment'); c.StrictMode = d('react.strict_mode'); c.Profiler = d('react.profiler'); ka = d('react.provider'); la = d('react.context'); ma = d('react.forward_ref'); c.Suspense = d('react.suspense'); na = d('react.memo'); oa = d('react.lazy') } var Y = typeof Symbol === 38 | 'function' && Symbol.iterator; const ya = Object.prototype.hasOwnProperty; const U = Object.assign || function (a, b) { if (a == null) throw new TypeError('Object.assign target cannot be null or undefined'); for (var e = Object(a), c = 1; c < arguments.length; c++) { let d = arguments[c]; if (d != null) { let g = void 0; d = Object(d); for (g in d)ya.call(d, g) && (e[g] = d[g]) } } return e }; var aa = { isMounted: function (a) { return !1 }, enqueueForceUpdate: function (a, b, c) {}, enqueueReplaceState: function (a, b, c, d) {}, enqueueSetState: function (a, b, c, d) {} }; var Z = {}; v.prototype.isReactComponent = 39 | {}; v.prototype.setState = function (a, b) { if (typeof a !== 'object' && typeof a !== 'function' && a != null) throw Error(y(85)); this.updater.enqueueSetState(this, a, b, 'setState') }; v.prototype.forceUpdate = function (a) { this.updater.enqueueForceUpdate(this, a, 'forceUpdate') }; ba.prototype = v.prototype; d = K.prototype = new ba(); d.constructor = K; U(d, v.prototype); d.isPureReactComponent = !0; var L = { current: null }; var ea = Object.prototype.hasOwnProperty; var fa = { key: !0, ref: !0, __self: !0, __source: !0 }; var ia = /\/+/g; var ja = { current: null }; let V; if (typeof performance === 'object' && 40 | typeof performance.now === 'function') { const za = performance; var t = function () { return za.now() } } else { const pa = Date; const Aa = pa.now(); t = function () { return pa.now() - Aa } } if (typeof window === 'undefined' || typeof MessageChannel !== 'function') { 41 | let B = null; let qa = null; var ra = function () { if (B !== null) try { const a = t(); B(!0, a); B = null } catch (b) { throw setTimeout(ra, 0), b } }; var A = function (a) { B !== null ? setTimeout(A, 0, a) : (B = a, setTimeout(ra, 0)) }; var G = function (a, b) { qa = setTimeout(a, b) }; var S = function () { clearTimeout(qa) }; var T = function () { return !1 } 42 | d = V = function () {} 43 | } else { 44 | const Ba = window.setTimeout; const Ca = window.clearTimeout; typeof console !== 'undefined' && (d = window.cancelAnimationFrame, typeof window.requestAnimationFrame !== 'function' && console.error("This browser doesn't support requestAnimationFrame. Make sure that you load a polyfill in older browsers. https://reactjs.org/link/react-polyfills"), typeof d !== 'function' && console.error("This browser doesn't support cancelAnimationFrame. Make sure that you load a polyfill in older browsers. https://reactjs.org/link/react-polyfills")) 45 | let I = !1; let J = null; let W = -1; let sa = 5; let ta = 0; T = function () { return t() >= ta }; d = function () {}; V = function (a) { a < 0 || a > 125 ? console.error('forceFrameRate takes a positive int between 0 and 125, forcing frame rates higher than 125 fps is not supported') : sa = a > 0 ? Math.floor(1E3 / a) : 5 }; const ua = new MessageChannel(); const X = ua.port2; ua.port1.onmessage = function () { if (J !== null) { const a = t(); ta = a + sa; try { J(!0, a) ? X.postMessage(null) : (I = !1, J = null) } catch (b) { throw X.postMessage(null), b } } else I = !1 }; A = function (a) { J = a; I || (I = !0, X.postMessage(null)) }; G = 46 | function (a, b) { W = Ba(function () { a(t()) }, b) }; S = function () { Ca(W); W = -1 } 47 | } var q = []; var r = []; let Da = 1; var m = null; var g = 3; var H = !1; var u = !1; var z = !1; let Ea = 0; d = { 48 | ReactCurrentDispatcher: ja, 49 | ReactCurrentOwner: L, 50 | IsSomeRendererActing: { current: !1 }, 51 | ReactCurrentBatchConfig: { transition: 0 }, 52 | assign: U, 53 | Scheduler: { 54 | __proto__: null, 55 | unstable_ImmediatePriority: 1, 56 | unstable_UserBlockingPriority: 2, 57 | unstable_NormalPriority: 3, 58 | unstable_IdlePriority: 5, 59 | unstable_LowPriority: 4, 60 | unstable_runWithPriority: function (a, b) { 61 | switch (a) { 62 | case 1:case 2:case 3:case 4:case 5:break; default:a = 63 | 3 64 | } const c = g; g = a; try { return b() } finally { g = c } 65 | }, 66 | unstable_next: function (a) { switch (g) { case 1:case 2:case 3:var b = 3; break; default:b = g } const c = g; g = b; try { return a() } finally { g = c } }, 67 | unstable_scheduleCallback: function (a, b, c) { 68 | const d = t(); typeof c === 'object' && c !== null ? (c = c.delay, c = typeof c === 'number' && c > 0 ? d + c : d) : c = d; switch (a) { case 1:var e = -1; break; case 2:e = 250; break; case 5:e = 1073741823; break; case 4:e = 1E4; break; default:e = 5E3 }e = c + e; a = { id: Da++, callback: b, priorityLevel: a, startTime: c, expirationTime: e, sortIndex: -1 }; c > d 69 | ? (a.sortIndex = 70 | c, O(r, a), p(q) === null && a === p(r) && (z ? S() : z = !0, G(Q, c - d))) 71 | : (a.sortIndex = e, O(q, a), u || H || (u = !0, A(R))); return a 72 | }, 73 | unstable_cancelCallback: function (a) { a.callback = null }, 74 | unstable_wrapCallback: function (a) { const b = g; return function () { const c = g; g = b; try { return a.apply(this, arguments) } finally { g = c } } }, 75 | unstable_getCurrentPriorityLevel: function () { return g }, 76 | get unstable_shouldYield () { return T }, 77 | unstable_requestPaint: d, 78 | unstable_continueExecution: function () { u || H || (u = !0, A(R)) }, 79 | unstable_pauseExecution: function () {}, 80 | unstable_getFirstCallbackNode: function () { return p(q) }, 81 | get unstable_now () { return t }, 82 | get unstable_forceFrameRate () { return V }, 83 | unstable_Profiling: null 84 | }, 85 | SchedulerTracing: { __proto__: null, __interactionsRef: null, __subscriberRef: null, unstable_clear: function (a) { return a() }, unstable_getCurrent: function () { return null }, unstable_getThreadID: function () { return ++Ea }, unstable_trace: function (a, b, c) { return c() }, unstable_wrap: function (a) { return a }, unstable_subscribe: function (a) {}, unstable_unsubscribe: function (a) {} } 86 | }; c.Children = { 87 | map: D, 88 | forEach: function (a, b, c) { 89 | D(a, function () { 90 | b.apply(this, 91 | arguments) 92 | }, c) 93 | }, 94 | count: function (a) { let b = 0; D(a, function () { b++ }); return b }, 95 | toArray: function (a) { return D(a, function (a) { return a }) || [] }, 96 | only: function (a) { if (!M(a)) throw Error(y(143)); return a } 97 | }; c.Component = v; c.PureComponent = K; c.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = d; c.cloneElement = function (a, b, c) { 98 | if (a === null || void 0 === a) throw Error(y(267, a)); const d = U({}, a.props); let e = a.key; let g = a.ref; let n = a._owner; if (b != null) { 99 | void 0 !== b.ref && (g = b.ref, n = L.current); void 0 !== b.key && (e = '' + b.key); if (a.type && a.type.defaultProps) { 100 | var k = 101 | a.type.defaultProps 102 | } for (h in b)ea.call(b, h) && !fa.hasOwnProperty(h) && (d[h] = void 0 === b[h] && void 0 !== k ? k[h] : b[h]) 103 | } var h = arguments.length - 2; if (h === 1)d.children = c; else if (h > 1) { k = Array(h); for (let m = 0; m < h; m++)k[m] = arguments[m + 2]; d.children = k } return { $$typeof: w, type: a.type, key: e, ref: g, props: d, _owner: n } 104 | }; c.createContext = function (a, b) { 105 | void 0 === b && (b = null); a = { $$typeof: la, _calculateChangedBits: b, _currentValue: a, _currentValue2: a, _threadCount: 0, Provider: null, Consumer: null }; a.Provider = { $$typeof: ka, _context: a }; return a.Consumer = 106 | a 107 | }; c.createElement = ca; c.createFactory = function (a) { const b = ca.bind(null, a); b.type = a; return b }; c.createRef = function () { return { current: null } }; c.forwardRef = function (a) { return { $$typeof: ma, render: a } }; c.isValidElement = M; c.lazy = function (a) { return { $$typeof: oa, _payload: { _status: -1, _result: a }, _init: xa } }; c.memo = function (a, b) { return { $$typeof: na, type: a, compare: void 0 === b ? null : b } }; c.useCallback = function (a, b) { return n().useCallback(a, b) }; c.useContext = function (a, b) { return n().useContext(a, b) }; c.useDebugValue = function (a, 108 | b) {}; c.useEffect = function (a, b) { return n().useEffect(a, b) }; c.useImperativeHandle = function (a, b, c) { return n().useImperativeHandle(a, b, c) }; c.useLayoutEffect = function (a, b) { return n().useLayoutEffect(a, b) }; c.useMemo = function (a, b) { return n().useMemo(a, b) }; c.useReducer = function (a, b, c) { return n().useReducer(a, b, c) }; c.useRef = function (a) { return n().useRef(a) }; c.useState = function (a) { return n().useState(a) }; c.version = '17.0.2' 109 | }) 110 | })() 111 | -------------------------------------------------------------------------------- /electron/assets/video_player/plyr3.7.3.min.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8";@keyframes plyr-progress{to{background-position:25px 0;background-position:var(--plyr-progress-loading-size,25px) 0}}@keyframes plyr-popup{0%{opacity:.5;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}@keyframes plyr-fade-in{0%{opacity:0}to{opacity:1}}.plyr{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;align-items:center;direction:ltr;display:flex;flex-direction:column;font-family:inherit;font-family:var(--plyr-font-family,inherit);font-variant-numeric:tabular-nums;font-weight:400;font-weight:var(--plyr-font-weight-regular,400);line-height:1.7;line-height:var(--plyr-line-height,1.7);max-width:100%;min-width:200px;position:relative;text-shadow:none;transition:box-shadow .3s ease;z-index:0}.plyr audio,.plyr iframe,.plyr video{display:block;height:100%;width:100%}.plyr button{font:inherit;line-height:inherit;width:auto}.plyr:focus{outline:0}.plyr--full-ui{box-sizing:border-box}.plyr--full-ui *,.plyr--full-ui :after,.plyr--full-ui :before{box-sizing:inherit}.plyr--full-ui a,.plyr--full-ui button,.plyr--full-ui input,.plyr--full-ui label{touch-action:manipulation}.plyr__badge{background:#4a5464;background:var(--plyr-badge-background,#4a5464);border-radius:2px;border-radius:var(--plyr-badge-border-radius,2px);color:#fff;color:var(--plyr-badge-text-color,#fff);font-size:9px;font-size:var(--plyr-font-size-badge,9px);line-height:1;padding:3px 4px}.plyr--full-ui ::-webkit-media-text-track-container{display:none}.plyr__captions{animation:plyr-fade-in .3s ease;bottom:0;display:none;font-size:13px;font-size:var(--plyr-font-size-small,13px);left:0;padding:10px;padding:var(--plyr-control-spacing,10px);position:absolute;text-align:center;transition:transform .4s ease-in-out;width:100%}.plyr__captions span:empty{display:none}@media (min-width:480px){.plyr__captions{font-size:15px;font-size:var(--plyr-font-size-base,15px);padding:20px;padding:calc(var(--plyr-control-spacing,10px)*2)}}@media (min-width:768px){.plyr__captions{font-size:18px;font-size:var(--plyr-font-size-large,18px)}}.plyr--captions-active .plyr__captions{display:block}.plyr:not(.plyr--hide-controls) .plyr__controls:not(:empty)~.plyr__captions{transform:translateY(-40px);transform:translateY(calc(var(--plyr-control-spacing,10px)*-4))}.plyr__caption{background:rgba(0,0,0,.8);background:var(--plyr-captions-background,rgba(0,0,0,.8));border-radius:2px;-webkit-box-decoration-break:clone;box-decoration-break:clone;color:#fff;color:var(--plyr-captions-text-color,#fff);line-height:185%;padding:.2em .5em;white-space:pre-wrap}.plyr__caption div{display:inline}.plyr__control{background:0 0;border:0;border-radius:3px;border-radius:var(--plyr-control-radius,3px);color:inherit;cursor:pointer;flex-shrink:0;overflow:visible;padding:7px;padding:calc(var(--plyr-control-spacing,10px)*.7);position:relative;transition:all .3s ease}.plyr__control svg{fill:currentColor;display:block;height:18px;height:var(--plyr-control-icon-size,18px);pointer-events:none;width:18px;width:var(--plyr-control-icon-size,18px)}.plyr__control:focus{outline:0}.plyr__control.plyr__tab-focus{outline:3px dotted #00b2ff;outline:var(--plyr-tab-focus-color,var(--plyr-color-main,var(--plyr-color-main,#00b2ff))) dotted 3px;outline-offset:2px}a.plyr__control{text-decoration:none}.plyr__control.plyr__control--pressed .icon--not-pressed,.plyr__control.plyr__control--pressed .label--not-pressed,.plyr__control:not(.plyr__control--pressed) .icon--pressed,.plyr__control:not(.plyr__control--pressed) .label--pressed,a.plyr__control:after,a.plyr__control:before{display:none}.plyr--full-ui ::-webkit-media-controls{display:none}.plyr__controls{align-items:center;display:flex;justify-content:flex-end;text-align:center}.plyr__controls .plyr__progress__container{flex:1;min-width:0}.plyr__controls .plyr__controls__item{margin-left:2.5px;margin-left:calc(var(--plyr-control-spacing,10px)/ 4)}.plyr__controls .plyr__controls__item:first-child{margin-left:0;margin-right:auto}.plyr__controls .plyr__controls__item.plyr__progress__container{padding-left:2.5px;padding-left:calc(var(--plyr-control-spacing,10px)/ 4)}.plyr__controls .plyr__controls__item.plyr__time{padding:0 5px;padding:0 calc(var(--plyr-control-spacing,10px)/ 2)}.plyr__controls .plyr__controls__item.plyr__progress__container:first-child,.plyr__controls .plyr__controls__item.plyr__time+.plyr__time,.plyr__controls .plyr__controls__item.plyr__time:first-child{padding-left:0}.plyr [data-plyr=airplay],.plyr [data-plyr=captions],.plyr [data-plyr=fullscreen],.plyr [data-plyr=pip],.plyr__controls:empty{display:none}.plyr--airplay-supported [data-plyr=airplay],.plyr--captions-enabled [data-plyr=captions],.plyr--fullscreen-enabled [data-plyr=fullscreen],.plyr--pip-supported [data-plyr=pip]{display:inline-block}.plyr__menu{display:flex;position:relative}.plyr__menu .plyr__control svg{transition:transform .3s ease}.plyr__menu .plyr__control[aria-expanded=true] svg{transform:rotate(90deg)}.plyr__menu .plyr__control[aria-expanded=true] .plyr__tooltip{display:none}.plyr__menu__container{animation:plyr-popup .2s ease;background:hsla(0,0%,100%,.9);background:var(--plyr-menu-background,hsla(0,0%,100%,.9));border-radius:4px;border-radius:var(--plyr-menu-radius,4px);bottom:100%;box-shadow:0 1px 2px rgba(0,0,0,.15);box-shadow:var(--plyr-menu-shadow,0 1px 2px rgba(0,0,0,.15));color:#4a5464;color:var(--plyr-menu-color,#4a5464);font-size:15px;font-size:var(--plyr-font-size-base,15px);margin-bottom:10px;position:absolute;right:-3px;text-align:left;white-space:nowrap;z-index:3}.plyr__menu__container>div{overflow:hidden;transition:height .35s cubic-bezier(.4,0,.2,1),width .35s cubic-bezier(.4,0,.2,1)}.plyr__menu__container:after{border:4px solid transparent;border-top-color:hsla(0,0%,100%,.9);border:var(--plyr-menu-arrow-size,4px) solid transparent;border-top-color:var(--plyr-menu-background,hsla(0,0%,100%,.9));content:"";height:0;position:absolute;right:14px;right:calc(var(--plyr-control-icon-size,18px)/ 2 + var(--plyr-control-spacing,10px)*.7 - var(--plyr-menu-arrow-size,4px)/ 2);top:100%;width:0}.plyr__menu__container [role=menu]{padding:7px;padding:calc(var(--plyr-control-spacing,10px)*.7)}.plyr__menu__container [role=menuitem],.plyr__menu__container [role=menuitemradio]{margin-top:2px}.plyr__menu__container [role=menuitem]:first-child,.plyr__menu__container [role=menuitemradio]:first-child{margin-top:0}.plyr__menu__container .plyr__control{align-items:center;color:#4a5464;color:var(--plyr-menu-color,#4a5464);display:flex;font-size:13px;font-size:var(--plyr-font-size-menu,var(--plyr-font-size-small,13px));padding:4.66667px 10.5px;padding:calc(var(--plyr-control-spacing,10px)*.7/1.5) calc(var(--plyr-control-spacing,10px)*.7*1.5);-webkit-user-select:none;user-select:none;width:100%}.plyr__menu__container .plyr__control>span{align-items:inherit;display:flex;width:100%}.plyr__menu__container .plyr__control:after{border:4px solid transparent;border:var(--plyr-menu-item-arrow-size,4px) solid transparent;content:"";position:absolute;top:50%;transform:translateY(-50%)}.plyr__menu__container .plyr__control--forward{padding-right:28px;padding-right:calc(var(--plyr-control-spacing,10px)*.7*4)}.plyr__menu__container .plyr__control--forward:after{border-left-color:#728197;border-left-color:var(--plyr-menu-arrow-color,#728197);right:6.5px;right:calc(var(--plyr-control-spacing,10px)*.7*1.5 - var(--plyr-menu-item-arrow-size,4px))}.plyr__menu__container .plyr__control--forward.plyr__tab-focus:after,.plyr__menu__container .plyr__control--forward:hover:after{border-left-color:currentColor}.plyr__menu__container .plyr__control--back{font-weight:400;font-weight:var(--plyr-font-weight-regular,400);margin:7px;margin:calc(var(--plyr-control-spacing,10px)*.7);margin-bottom:3.5px;margin-bottom:calc(var(--plyr-control-spacing,10px)*.7/2);padding-left:28px;padding-left:calc(var(--plyr-control-spacing,10px)*.7*4);position:relative;width:calc(100% - 14px);width:calc(100% - var(--plyr-control-spacing,10px)*.7*2)}.plyr__menu__container .plyr__control--back:after{border-right-color:#728197;border-right-color:var(--plyr-menu-arrow-color,#728197);left:6.5px;left:calc(var(--plyr-control-spacing,10px)*.7*1.5 - var(--plyr-menu-item-arrow-size,4px))}.plyr__menu__container .plyr__control--back:before{background:#dcdfe5;background:var(--plyr-menu-back-border-color,#dcdfe5);box-shadow:0 1px 0 #fff;box-shadow:0 1px 0 var(--plyr-menu-back-border-shadow-color,#fff);content:"";height:1px;left:0;margin-top:3.5px;margin-top:calc(var(--plyr-control-spacing,10px)*.7/2);overflow:hidden;position:absolute;right:0;top:100%}.plyr__menu__container .plyr__control--back.plyr__tab-focus:after,.plyr__menu__container .plyr__control--back:hover:after{border-right-color:currentColor}.plyr__menu__container .plyr__control[role=menuitemradio]{padding-left:7px;padding-left:calc(var(--plyr-control-spacing,10px)*.7)}.plyr__menu__container .plyr__control[role=menuitemradio]:after,.plyr__menu__container .plyr__control[role=menuitemradio]:before{border-radius:100%}.plyr__menu__container .plyr__control[role=menuitemradio]:before{background:rgba(0,0,0,.1);content:"";display:block;flex-shrink:0;height:16px;margin-right:10px;margin-right:var(--plyr-control-spacing,10px);transition:all .3s ease;width:16px}.plyr__menu__container .plyr__control[role=menuitemradio]:after{background:#fff;border:0;height:6px;left:12px;opacity:0;top:50%;transform:translateY(-50%) scale(0);transition:transform .3s ease,opacity .3s ease;width:6px}.plyr__menu__container .plyr__control[role=menuitemradio][aria-checked=true]:before{background:#00b2ff;background:var(--plyr-control-toggle-checked-background,var(--plyr-color-main,var(--plyr-color-main,#00b2ff)))}.plyr__menu__container .plyr__control[role=menuitemradio][aria-checked=true]:after{opacity:1;transform:translateY(-50%) scale(1)}.plyr__menu__container .plyr__control[role=menuitemradio].plyr__tab-focus:before,.plyr__menu__container .plyr__control[role=menuitemradio]:hover:before{background:rgba(35,40,47,.1)}.plyr__menu__container .plyr__menu__value{align-items:center;display:flex;margin-left:auto;margin-right:-5px;margin-right:calc(var(--plyr-control-spacing,10px)*.7*-1 - -2px);overflow:hidden;padding-left:24.5px;padding-left:calc(var(--plyr-control-spacing,10px)*.7*3.5);pointer-events:none}.plyr--full-ui input[type=range]{-webkit-appearance:none;appearance:none;background:0 0;border:0;border-radius:26px;border-radius:calc(var(--plyr-range-thumb-height,13px)*2);color:#00b2ff;color:var(--plyr-range-fill-background,var(--plyr-color-main,var(--plyr-color-main,#00b2ff)));display:block;height:19px;height:calc(var(--plyr-range-thumb-active-shadow-width,3px)*2 + var(--plyr-range-thumb-height,13px));margin:0;min-width:0;padding:0;transition:box-shadow .3s ease;width:100%}.plyr--full-ui input[type=range]::-webkit-slider-runnable-track{background:0 0;background-image:linear-gradient(90deg,currentColor 0,transparent 0);background-image:linear-gradient(to right,currentColor var(--value,0),transparent var(--value,0));border:0;border-radius:2.5px;border-radius:calc(var(--plyr-range-track-height,5px)/ 2);height:5px;height:var(--plyr-range-track-height,5px);-webkit-transition:box-shadow .3s ease;transition:box-shadow .3s ease;-webkit-user-select:none;user-select:none}.plyr--full-ui input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;background:#fff;background:var(--plyr-range-thumb-background,#fff);border:0;border-radius:100%;box-shadow:0 1px 1px rgba(35,40,47,.15),0 0 0 1px rgba(35,40,47,.2);box-shadow:var(--plyr-range-thumb-shadow,0 1px 1px rgba(35,40,47,.15),0 0 0 1px rgba(35,40,47,.2));height:13px;height:var(--plyr-range-thumb-height,13px);margin-top:-4px;margin-top:calc((var(--plyr-range-thumb-height,13px) - var(--plyr-range-track-height,5px))/ 2*-1);position:relative;-webkit-transition:all .2s ease;transition:all .2s ease;width:13px;width:var(--plyr-range-thumb-height,13px)}.plyr--full-ui input[type=range]::-moz-range-track{background:0 0;border:0;border-radius:2.5px;border-radius:calc(var(--plyr-range-track-height,5px)/ 2);height:5px;height:var(--plyr-range-track-height,5px);-moz-transition:box-shadow .3s ease;transition:box-shadow .3s ease;user-select:none}.plyr--full-ui input[type=range]::-moz-range-thumb{background:#fff;background:var(--plyr-range-thumb-background,#fff);border:0;border-radius:100%;box-shadow:0 1px 1px rgba(35,40,47,.15),0 0 0 1px rgba(35,40,47,.2);box-shadow:var(--plyr-range-thumb-shadow,0 1px 1px rgba(35,40,47,.15),0 0 0 1px rgba(35,40,47,.2));height:13px;height:var(--plyr-range-thumb-height,13px);position:relative;-moz-transition:all .2s ease;transition:all .2s ease;width:13px;width:var(--plyr-range-thumb-height,13px)}.plyr--full-ui input[type=range]::-moz-range-progress{background:currentColor;border-radius:2.5px;border-radius:calc(var(--plyr-range-track-height,5px)/ 2);height:5px;height:var(--plyr-range-track-height,5px)}.plyr--full-ui input[type=range]::-ms-track{color:transparent}.plyr--full-ui input[type=range]::-ms-fill-upper,.plyr--full-ui input[type=range]::-ms-track{background:0 0;border:0;border-radius:2.5px;border-radius:calc(var(--plyr-range-track-height,5px)/ 2);height:5px;height:var(--plyr-range-track-height,5px);-ms-transition:box-shadow .3s ease;transition:box-shadow .3s ease;user-select:none}.plyr--full-ui input[type=range]::-ms-fill-lower{background:0 0;background:currentColor;border:0;border-radius:2.5px;border-radius:calc(var(--plyr-range-track-height,5px)/ 2);height:5px;height:var(--plyr-range-track-height,5px);-ms-transition:box-shadow .3s ease;transition:box-shadow .3s ease;user-select:none}.plyr--full-ui input[type=range]::-ms-thumb{background:#fff;background:var(--plyr-range-thumb-background,#fff);border:0;border-radius:100%;box-shadow:0 1px 1px rgba(35,40,47,.15),0 0 0 1px rgba(35,40,47,.2);box-shadow:var(--plyr-range-thumb-shadow,0 1px 1px rgba(35,40,47,.15),0 0 0 1px rgba(35,40,47,.2));height:13px;height:var(--plyr-range-thumb-height,13px);margin-top:0;position:relative;-ms-transition:all .2s ease;transition:all .2s ease;width:13px;width:var(--plyr-range-thumb-height,13px)}.plyr--full-ui input[type=range]::-ms-tooltip{display:none}.plyr--full-ui input[type=range]::-moz-focus-outer{border:0}.plyr--full-ui input[type=range]:focus{outline:0}.plyr--full-ui input[type=range].plyr__tab-focus::-webkit-slider-runnable-track{outline:3px dotted #00b2ff;outline:var(--plyr-tab-focus-color,var(--plyr-color-main,var(--plyr-color-main,#00b2ff))) dotted 3px;outline-offset:2px}.plyr--full-ui input[type=range].plyr__tab-focus::-moz-range-track{outline:3px dotted #00b2ff;outline:var(--plyr-tab-focus-color,var(--plyr-color-main,var(--plyr-color-main,#00b2ff))) dotted 3px;outline-offset:2px}.plyr--full-ui input[type=range].plyr__tab-focus::-ms-track{outline:3px dotted #00b2ff;outline:var(--plyr-tab-focus-color,var(--plyr-color-main,var(--plyr-color-main,#00b2ff))) dotted 3px;outline-offset:2px}.plyr__poster{background-color:#000;background-color:var(--plyr-video-background,var(--plyr-video-background,#000));background-position:50% 50%;background-repeat:no-repeat;background-size:contain;height:100%;left:0;opacity:0;position:absolute;top:0;transition:opacity .2s ease;width:100%;z-index:1}.plyr--stopped.plyr__poster-enabled .plyr__poster{opacity:1}.plyr--youtube.plyr--paused.plyr__poster-enabled:not(.plyr--stopped) .plyr__poster{display:none}.plyr__time{font-size:13px;font-size:var(--plyr-font-size-time,var(--plyr-font-size-small,13px))}.plyr__time+.plyr__time:before{content:"⁄";margin-right:10px;margin-right:var(--plyr-control-spacing,10px)}@media (max-width:767px){.plyr__time+.plyr__time{display:none}}.plyr__tooltip{background:hsla(0,0%,100%,.9);background:var(--plyr-tooltip-background,hsla(0,0%,100%,.9));border-radius:5px;border-radius:var(--plyr-tooltip-radius,5px);bottom:100%;box-shadow:0 1px 2px rgba(0,0,0,.15);box-shadow:var(--plyr-tooltip-shadow,0 1px 2px rgba(0,0,0,.15));color:#4a5464;color:var(--plyr-tooltip-color,#4a5464);font-size:13px;font-size:var(--plyr-font-size-small,13px);font-weight:400;font-weight:var(--plyr-font-weight-regular,400);left:50%;line-height:1.3;margin-bottom:10px;margin-bottom:calc(var(--plyr-control-spacing,10px)/ 2*2);opacity:0;padding:5px 7.5px;padding:calc(var(--plyr-control-spacing,10px)/ 2) calc(var(--plyr-control-spacing,10px)/ 2*1.5);pointer-events:none;position:absolute;transform:translate(-50%,10px) scale(.8);transform-origin:50% 100%;transition:transform .2s ease .1s,opacity .2s ease .1s;white-space:nowrap;z-index:2}.plyr__tooltip:before{border-left:4px solid transparent;border-left:var(--plyr-tooltip-arrow-size,4px) solid transparent;border-right:4px solid transparent;border-right:var(--plyr-tooltip-arrow-size,4px) solid transparent;border-top:4px solid hsla(0,0%,100%,.9);border-top:var(--plyr-tooltip-arrow-size,4px) solid var(--plyr-tooltip-background,hsla(0,0%,100%,.9));bottom:-4px;bottom:calc(var(--plyr-tooltip-arrow-size,4px)*-1);content:"";height:0;left:50%;position:absolute;transform:translateX(-50%);width:0;z-index:2}.plyr .plyr__control.plyr__tab-focus .plyr__tooltip,.plyr .plyr__control:hover .plyr__tooltip,.plyr__tooltip--visible{opacity:1;transform:translate(-50%) scale(1)}.plyr .plyr__control:hover .plyr__tooltip{z-index:3}.plyr__controls>.plyr__control:first-child .plyr__tooltip,.plyr__controls>.plyr__control:first-child+.plyr__control .plyr__tooltip{left:0;transform:translateY(10px) scale(.8);transform-origin:0 100%}.plyr__controls>.plyr__control:first-child .plyr__tooltip:before,.plyr__controls>.plyr__control:first-child+.plyr__control .plyr__tooltip:before{left:16px;left:calc(var(--plyr-control-icon-size,18px)/ 2 + var(--plyr-control-spacing,10px)*.7)}.plyr__controls>.plyr__control:last-child .plyr__tooltip{left:auto;right:0;transform:translateY(10px) scale(.8);transform-origin:100% 100%}.plyr__controls>.plyr__control:last-child .plyr__tooltip:before{left:auto;right:16px;right:calc(var(--plyr-control-icon-size,18px)/ 2 + var(--plyr-control-spacing,10px)*.7);transform:translateX(50%)}.plyr__controls>.plyr__control:first-child .plyr__tooltip--visible,.plyr__controls>.plyr__control:first-child+.plyr__control .plyr__tooltip--visible,.plyr__controls>.plyr__control:first-child+.plyr__control.plyr__tab-focus .plyr__tooltip,.plyr__controls>.plyr__control:first-child+.plyr__control:hover .plyr__tooltip,.plyr__controls>.plyr__control:first-child.plyr__tab-focus .plyr__tooltip,.plyr__controls>.plyr__control:first-child:hover .plyr__tooltip,.plyr__controls>.plyr__control:last-child .plyr__tooltip--visible,.plyr__controls>.plyr__control:last-child.plyr__tab-focus .plyr__tooltip,.plyr__controls>.plyr__control:last-child:hover .plyr__tooltip{transform:translate(0) scale(1)}.plyr__progress{left:6.5px;left:calc(var(--plyr-range-thumb-height,13px)*.5);margin-right:13px;margin-right:var(--plyr-range-thumb-height,13px);position:relative}.plyr__progress input[type=range],.plyr__progress__buffer{margin-left:-6.5px;margin-left:calc(var(--plyr-range-thumb-height,13px)*-.5);margin-right:-6.5px;margin-right:calc(var(--plyr-range-thumb-height,13px)*-.5);width:calc(100% + 13px);width:calc(100% + var(--plyr-range-thumb-height,13px))}.plyr__progress input[type=range]{position:relative;z-index:2}.plyr__progress .plyr__tooltip{left:0;max-width:120px;overflow-wrap:break-word}.plyr__progress__buffer{-webkit-appearance:none;background:0 0;border:0;border-radius:100px;height:5px;height:var(--plyr-range-track-height,5px);left:0;margin-top:-2.5px;margin-top:calc((var(--plyr-range-track-height,5px)/ 2)*-1);padding:0;position:absolute;top:50%}.plyr__progress__buffer::-webkit-progress-bar{background:0 0}.plyr__progress__buffer::-webkit-progress-value{background:currentColor;border-radius:100px;min-width:5px;min-width:var(--plyr-range-track-height,5px);-webkit-transition:width .2s ease;transition:width .2s ease}.plyr__progress__buffer::-moz-progress-bar{background:currentColor;border-radius:100px;min-width:5px;min-width:var(--plyr-range-track-height,5px);-moz-transition:width .2s ease;transition:width .2s ease}.plyr__progress__buffer::-ms-fill{border-radius:100px;-ms-transition:width .2s ease;transition:width .2s ease}.plyr--loading .plyr__progress__buffer{animation:plyr-progress 1s linear infinite;background-image:linear-gradient(-45deg,rgba(35,40,47,.6) 25%,transparent 0,transparent 50%,rgba(35,40,47,.6) 0,rgba(35,40,47,.6) 75%,transparent 0,transparent);background-image:linear-gradient(-45deg,var(--plyr-progress-loading-background,rgba(35,40,47,.6)) 25%,transparent 25%,transparent 50%,var(--plyr-progress-loading-background,rgba(35,40,47,.6)) 50%,var(--plyr-progress-loading-background,rgba(35,40,47,.6)) 75%,transparent 75%,transparent);background-repeat:repeat-x;background-size:25px 25px;background-size:var(--plyr-progress-loading-size,25px) var(--plyr-progress-loading-size,25px);color:transparent}.plyr--video.plyr--loading .plyr__progress__buffer{background-color:hsla(0,0%,100%,.25);background-color:var(--plyr-video-progress-buffered-background,hsla(0,0%,100%,.25))}.plyr--audio.plyr--loading .plyr__progress__buffer{background-color:rgba(193,200,209,.6);background-color:var(--plyr-audio-progress-buffered-background,rgba(193,200,209,.6))}.plyr__progress__marker{background-color:#fff;background-color:var(--plyr-progress-marker-background,#fff);border-radius:1px;height:5px;height:var(--plyr-range-track-height,5px);position:absolute;top:50%;transform:translate(-50%,-50%);width:3px;width:var(--plyr-progress-marker-width,3px);z-index:3}.plyr__volume{align-items:center;display:flex;max-width:110px;min-width:80px;position:relative;width:20%}.plyr__volume input[type=range]{margin-left:5px;margin-left:calc(var(--plyr-control-spacing,10px)/ 2);margin-right:5px;margin-right:calc(var(--plyr-control-spacing,10px)/ 2);position:relative;z-index:2}.plyr--is-ios .plyr__volume{min-width:0;width:auto}.plyr--audio{display:block}.plyr--audio .plyr__controls{background:#fff;background:var(--plyr-audio-controls-background,#fff);border-radius:inherit;color:#4a5464;color:var(--plyr-audio-control-color,#4a5464);padding:10px;padding:var(--plyr-control-spacing,10px)}.plyr--audio .plyr__control.plyr__tab-focus,.plyr--audio .plyr__control:hover,.plyr--audio .plyr__control[aria-expanded=true]{background:#00b2ff;background:var(--plyr-audio-control-background-hover,var(--plyr-color-main,var(--plyr-color-main,#00b2ff)));color:#fff;color:var(--plyr-audio-control-color-hover,#fff)}.plyr--full-ui.plyr--audio input[type=range]::-webkit-slider-runnable-track{background-color:rgba(193,200,209,.6);background-color:var(--plyr-audio-range-track-background,var(--plyr-audio-progress-buffered-background,rgba(193,200,209,.6)))}.plyr--full-ui.plyr--audio input[type=range]::-moz-range-track{background-color:rgba(193,200,209,.6);background-color:var(--plyr-audio-range-track-background,var(--plyr-audio-progress-buffered-background,rgba(193,200,209,.6)))}.plyr--full-ui.plyr--audio input[type=range]::-ms-track{background-color:rgba(193,200,209,.6);background-color:var(--plyr-audio-range-track-background,var(--plyr-audio-progress-buffered-background,rgba(193,200,209,.6)))}.plyr--full-ui.plyr--audio input[type=range]:active::-webkit-slider-thumb{box-shadow:0 1px 1px rgba(35,40,47,.15),0 0 0 1px rgba(35,40,47,.2),0 0 0 3px rgba(35,40,47,.1);box-shadow:var(--plyr-range-thumb-shadow,0 1px 1px rgba(35,40,47,.15),0 0 0 1px rgba(35,40,47,.2)),0 0 0 var(--plyr-range-thumb-active-shadow-width,3px) var(--plyr-audio-range-thumb-active-shadow-color,rgba(35,40,47,.1))}.plyr--full-ui.plyr--audio input[type=range]:active::-moz-range-thumb{box-shadow:0 1px 1px rgba(35,40,47,.15),0 0 0 1px rgba(35,40,47,.2),0 0 0 3px rgba(35,40,47,.1);box-shadow:var(--plyr-range-thumb-shadow,0 1px 1px rgba(35,40,47,.15),0 0 0 1px rgba(35,40,47,.2)),0 0 0 var(--plyr-range-thumb-active-shadow-width,3px) var(--plyr-audio-range-thumb-active-shadow-color,rgba(35,40,47,.1))}.plyr--full-ui.plyr--audio input[type=range]:active::-ms-thumb{box-shadow:0 1px 1px rgba(35,40,47,.15),0 0 0 1px rgba(35,40,47,.2),0 0 0 3px rgba(35,40,47,.1);box-shadow:var(--plyr-range-thumb-shadow,0 1px 1px rgba(35,40,47,.15),0 0 0 1px rgba(35,40,47,.2)),0 0 0 var(--plyr-range-thumb-active-shadow-width,3px) var(--plyr-audio-range-thumb-active-shadow-color,rgba(35,40,47,.1))}.plyr--audio .plyr__progress__buffer{color:rgba(193,200,209,.6);color:var(--plyr-audio-progress-buffered-background,rgba(193,200,209,.6))}.plyr--video{background:#000;background:var(--plyr-video-background,var(--plyr-video-background,#000));overflow:hidden}.plyr--video.plyr--menu-open{overflow:visible}.plyr__video-wrapper{background:#000;background:var(--plyr-video-background,var(--plyr-video-background,#000));height:100%;margin:auto;overflow:hidden;position:relative;width:100%}.plyr__video-embed,.plyr__video-wrapper--fixed-ratio{aspect-ratio:16/9}@supports not (aspect-ratio:16/9){.plyr__video-embed,.plyr__video-wrapper--fixed-ratio{height:0;padding-bottom:56.25%;position:relative}}.plyr__video-embed iframe,.plyr__video-wrapper--fixed-ratio video{border:0;height:100%;left:0;position:absolute;top:0;width:100%}.plyr--full-ui .plyr__video-embed>.plyr__video-embed__container{padding-bottom:240%;position:relative;transform:translateY(-38.28125%)}.plyr--video .plyr__controls{background:linear-gradient(transparent,rgba(0,0,0,.75));background:var(--plyr-video-controls-background,linear-gradient(transparent,rgba(0,0,0,.75)));border-bottom-left-radius:inherit;border-bottom-right-radius:inherit;bottom:0;color:#fff;color:var(--plyr-video-control-color,#fff);left:0;padding:5px;padding:calc(var(--plyr-control-spacing,10px)/ 2);padding-top:20px;padding-top:calc(var(--plyr-control-spacing,10px)*2);position:absolute;right:0;transition:opacity .4s ease-in-out,transform .4s ease-in-out;z-index:3}@media (min-width:480px){.plyr--video .plyr__controls{padding:10px;padding:var(--plyr-control-spacing,10px);padding-top:35px;padding-top:calc(var(--plyr-control-spacing,10px)*3.5)}}.plyr--video.plyr--hide-controls .plyr__controls{opacity:0;pointer-events:none;transform:translateY(100%)}.plyr--video .plyr__control.plyr__tab-focus,.plyr--video .plyr__control:hover,.plyr--video .plyr__control[aria-expanded=true]{background:#00b2ff;background:var(--plyr-video-control-background-hover,var(--plyr-color-main,var(--plyr-color-main,#00b2ff)));color:#fff;color:var(--plyr-video-control-color-hover,#fff)}.plyr__control--overlaid{background:#00b2ff;background:var(--plyr-video-control-background-hover,var(--plyr-color-main,var(--plyr-color-main,#00b2ff)));border:0;border-radius:100%;color:#fff;color:var(--plyr-video-control-color,#fff);display:none;left:50%;opacity:.9;padding:15px;padding:calc(var(--plyr-control-spacing,10px)*1.5);position:absolute;top:50%;transform:translate(-50%,-50%);transition:.3s;z-index:2}.plyr__control--overlaid svg{left:2px;position:relative}.plyr__control--overlaid:focus,.plyr__control--overlaid:hover{opacity:1}.plyr--playing .plyr__control--overlaid{opacity:0;visibility:hidden}.plyr--full-ui.plyr--video .plyr__control--overlaid{display:block}.plyr--full-ui.plyr--video input[type=range]::-webkit-slider-runnable-track{background-color:hsla(0,0%,100%,.25);background-color:var(--plyr-video-range-track-background,var(--plyr-video-progress-buffered-background,hsla(0,0%,100%,.25)))}.plyr--full-ui.plyr--video input[type=range]::-moz-range-track{background-color:hsla(0,0%,100%,.25);background-color:var(--plyr-video-range-track-background,var(--plyr-video-progress-buffered-background,hsla(0,0%,100%,.25)))}.plyr--full-ui.plyr--video input[type=range]::-ms-track{background-color:hsla(0,0%,100%,.25);background-color:var(--plyr-video-range-track-background,var(--plyr-video-progress-buffered-background,hsla(0,0%,100%,.25)))}.plyr--full-ui.plyr--video input[type=range]:active::-webkit-slider-thumb{box-shadow:0 1px 1px rgba(35,40,47,.15),0 0 0 1px rgba(35,40,47,.2),0 0 0 3px hsla(0,0%,100%,.5);box-shadow:var(--plyr-range-thumb-shadow,0 1px 1px rgba(35,40,47,.15),0 0 0 1px rgba(35,40,47,.2)),0 0 0 var(--plyr-range-thumb-active-shadow-width,3px) var(--plyr-audio-range-thumb-active-shadow-color,hsla(0,0%,100%,.5))}.plyr--full-ui.plyr--video input[type=range]:active::-moz-range-thumb{box-shadow:0 1px 1px rgba(35,40,47,.15),0 0 0 1px rgba(35,40,47,.2),0 0 0 3px hsla(0,0%,100%,.5);box-shadow:var(--plyr-range-thumb-shadow,0 1px 1px rgba(35,40,47,.15),0 0 0 1px rgba(35,40,47,.2)),0 0 0 var(--plyr-range-thumb-active-shadow-width,3px) var(--plyr-audio-range-thumb-active-shadow-color,hsla(0,0%,100%,.5))}.plyr--full-ui.plyr--video input[type=range]:active::-ms-thumb{box-shadow:0 1px 1px rgba(35,40,47,.15),0 0 0 1px rgba(35,40,47,.2),0 0 0 3px hsla(0,0%,100%,.5);box-shadow:var(--plyr-range-thumb-shadow,0 1px 1px rgba(35,40,47,.15),0 0 0 1px rgba(35,40,47,.2)),0 0 0 var(--plyr-range-thumb-active-shadow-width,3px) var(--plyr-audio-range-thumb-active-shadow-color,hsla(0,0%,100%,.5))}.plyr--video .plyr__progress__buffer{color:hsla(0,0%,100%,.25);color:var(--plyr-video-progress-buffered-background,hsla(0,0%,100%,.25))}.plyr:-webkit-full-screen{background:#000;border-radius:0!important;height:100%;margin:0;width:100%}.plyr:fullscreen{background:#000;border-radius:0!important;height:100%;margin:0;width:100%}.plyr:-webkit-full-screen video{height:100%}.plyr:fullscreen video{height:100%}.plyr:-webkit-full-screen .plyr__control .icon--exit-fullscreen{display:block}.plyr:fullscreen .plyr__control .icon--exit-fullscreen{display:block}.plyr:-webkit-full-screen .plyr__control .icon--exit-fullscreen+svg{display:none}.plyr:fullscreen .plyr__control .icon--exit-fullscreen+svg{display:none}.plyr:-webkit-full-screen.plyr--hide-controls{cursor:none}.plyr:fullscreen.plyr--hide-controls{cursor:none}@media (min-width:1024px){.plyr:-webkit-full-screen .plyr__captions{font-size:21px;font-size:var(--plyr-font-size-xlarge,21px)}.plyr:fullscreen .plyr__captions{font-size:21px;font-size:var(--plyr-font-size-xlarge,21px)}}.plyr--fullscreen-fallback{background:#000;border-radius:0!important;bottom:0;display:block;height:100%;left:0;margin:0;position:fixed;right:0;top:0;width:100%;z-index:10000000}.plyr--fullscreen-fallback video{height:100%}.plyr--fullscreen-fallback .plyr__control .icon--exit-fullscreen{display:block}.plyr--fullscreen-fallback .plyr__control .icon--exit-fullscreen+svg{display:none}.plyr--fullscreen-fallback.plyr--hide-controls{cursor:none}@media (min-width:1024px){.plyr--fullscreen-fallback .plyr__captions{font-size:21px;font-size:var(--plyr-font-size-xlarge,21px)}}.plyr__ads{border-radius:inherit;bottom:0;cursor:pointer;left:0;overflow:hidden;position:absolute;right:0;top:0;z-index:-1}.plyr__ads>div,.plyr__ads>div iframe{height:100%;position:absolute;width:100%}.plyr__ads:after{background:#23282f;border-radius:2px;bottom:10px;bottom:var(--plyr-control-spacing,10px);color:#fff;content:attr(data-badge-text);font-size:11px;padding:2px 6px;pointer-events:none;position:absolute;right:10px;right:var(--plyr-control-spacing,10px);z-index:3}.plyr__ads:empty:after{display:none}.plyr__cues{background:currentColor;display:block;height:5px;height:var(--plyr-range-track-height,5px);left:0;opacity:.8;position:absolute;top:50%;transform:translateY(-50%);width:3px;z-index:3}.plyr__preview-thumb{background-color:hsla(0,0%,100%,.9);background-color:var(--plyr-tooltip-background,hsla(0,0%,100%,.9));border-radius:5px;border-radius:var(--plyr-tooltip-radius,5px);bottom:100%;box-shadow:0 1px 2px rgba(0,0,0,.15);box-shadow:var(--plyr-tooltip-shadow,0 1px 2px rgba(0,0,0,.15));margin-bottom:10px;margin-bottom:calc(var(--plyr-control-spacing,10px)/ 2*2);opacity:0;padding:3px;pointer-events:none;position:absolute;transform:translateY(10px) scale(.8);transform-origin:50% 100%;transition:transform .2s ease .1s,opacity .2s ease .1s;z-index:2}.plyr__preview-thumb--is-shown{opacity:1;transform:translate(0) scale(1)}.plyr__preview-thumb:before{border-left:4px solid transparent;border-left:var(--plyr-tooltip-arrow-size,4px) solid transparent;border-right:4px solid transparent;border-right:var(--plyr-tooltip-arrow-size,4px) solid transparent;border-top:4px solid hsla(0,0%,100%,.9);border-top:var(--plyr-tooltip-arrow-size,4px) solid var(--plyr-tooltip-background,hsla(0,0%,100%,.9));bottom:-4px;bottom:calc(var(--plyr-tooltip-arrow-size,4px)*-1);content:"";height:0;left:calc(50% + var(--preview-arrow-offset));position:absolute;transform:translateX(-50%);width:0;z-index:2}.plyr__preview-thumb__image-container{background:#c1c8d1;border-radius:4px;border-radius:calc(var(--plyr-tooltip-radius,5px) - 1px);overflow:hidden;position:relative;z-index:0}.plyr__preview-thumb__image-container img,.plyr__preview-thumb__image-container:after{height:100%;left:0;position:absolute;top:0;width:100%}.plyr__preview-thumb__image-container:after{border-radius:inherit;box-shadow:inset 0 0 0 1px rgba(0,0,0,.15);content:"";pointer-events:none}.plyr__preview-thumb__image-container img{max-height:none;max-width:none}.plyr__preview-thumb__time-container{background:linear-gradient(transparent,rgba(0,0,0,.75));background:var(--plyr-video-controls-background,linear-gradient(transparent,rgba(0,0,0,.75)));border-bottom-left-radius:4px;border-bottom-left-radius:calc(var(--plyr-tooltip-radius,5px) - 1px);border-bottom-right-radius:4px;border-bottom-right-radius:calc(var(--plyr-tooltip-radius,5px) - 1px);bottom:0;left:0;line-height:1.1;padding:20px 6px 6px;position:absolute;right:0;z-index:3}.plyr__preview-thumb__time-container span{color:#fff;font-size:13px;font-size:var(--plyr-font-size-time,var(--plyr-font-size-small,13px))}.plyr__preview-scrubbing{bottom:0;filter:blur(1px);height:100%;left:0;margin:auto;opacity:0;overflow:hidden;pointer-events:none;position:absolute;right:0;top:0;transition:opacity .3s ease;width:100%;z-index:1}.plyr__preview-scrubbing--is-shown{opacity:1}.plyr__preview-scrubbing img{height:100%;left:0;max-height:none;max-width:none;-o-object-fit:contain;object-fit:contain;position:absolute;top:0;width:100%}.plyr--no-transition{transition:none!important}.plyr__sr-only{clip:rect(1px,1px,1px,1px);border:0!important;height:1px!important;overflow:hidden;padding:0!important;position:absolute!important;width:1px!important}.plyr [hidden]{display:none!important} -------------------------------------------------------------------------------- /electron/components/DownloaderWindow.ts: -------------------------------------------------------------------------------- 1 | import { BrowserWindow } from 'electron'; 2 | import isDev from 'electron-is-dev'; 3 | import path from 'path'; 4 | import { get__dirname } from '../configs.js'; 5 | 6 | export default class DownloaderWindow extends BrowserWindow { 7 | constructor(url: string) { 8 | super({ 9 | width: 550, 10 | height: 220, 11 | resizable: isDev, 12 | darkTheme: true, 13 | backgroundColor: '#060606', 14 | title: 'Downloader', 15 | webPreferences: { 16 | preload: path.join(get__dirname(import.meta.url), 'preload.js'), 17 | backgroundThrottling: false, 18 | }, 19 | icon: 20 | process.platform === 'linux' 21 | ? path.join( 22 | get__dirname(import.meta.url), 23 | '../assets/icons/256x256.png' 24 | ) 25 | : undefined, 26 | }); 27 | this.setMenuBarVisibility(false); 28 | this.loadURL(url); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /electron/components/MainWindow.ts: -------------------------------------------------------------------------------- 1 | import { BrowserWindow } from 'electron'; 2 | import windowStateKeeper from 'electron-window-state'; 3 | import path from 'path'; 4 | import { get__dirname } from '../configs.js'; 5 | 6 | class MainWindow extends BrowserWindow { 7 | constructor(url: string, state: windowStateKeeper.State) { 8 | super({ 9 | x: state.x, 10 | y: state.y, 11 | width: state.width, 12 | height: state.height, 13 | minWidth: 1000, 14 | minHeight: 600, 15 | darkTheme: true, 16 | backgroundColor: '#060606', 17 | title: 'YTS-Streaming', 18 | webPreferences: { 19 | preload: path.join(get__dirname(import.meta.url), 'preload.cjs'), 20 | backgroundThrottling: false, 21 | }, 22 | icon: 23 | process.platform === 'linux' 24 | ? path.join(get__dirname(import.meta.url), '../assets/icons/256x256.png') 25 | : undefined, 26 | }); 27 | this.loadURL(url); 28 | } 29 | } 30 | 31 | export default MainWindow; 32 | -------------------------------------------------------------------------------- /electron/components/VideoPlayerWindow.ts: -------------------------------------------------------------------------------- 1 | import { BrowserWindow, Menu, shell } from 'electron'; 2 | import path from 'path'; 3 | import { get__dirname } from '../configs.js'; 4 | 5 | class VideoPlayerWindow extends BrowserWindow { 6 | constructor(url: string) { 7 | super({ 8 | width: 1000, 9 | height: 600, 10 | minWidth: 1000, 11 | minHeight: 600, 12 | darkTheme: true, 13 | backgroundColor: '#060606', 14 | title: 'YTS-Player', 15 | autoHideMenuBar: true, 16 | webPreferences: {}, 17 | icon: 18 | process.platform === 'linux' 19 | ? path.join( 20 | get__dirname(import.meta.url), 21 | '../assets/icons/256x256.png' 22 | ) 23 | : undefined, 24 | }); 25 | this.setMenu( 26 | Menu.buildFromTemplate([ 27 | { 28 | label: 'About', 29 | submenu: [ 30 | { 31 | label: 'View Shortcuts', 32 | click: () => { 33 | shell.openExternal( 34 | 'https://github.com/sampotts/plyr#shortcuts' 35 | ); 36 | }, 37 | }, 38 | { 39 | label: 'Plyr Player', 40 | click: () => { 41 | shell.openExternal('https://github.com/sampotts/plyr'); 42 | }, 43 | }, 44 | ], 45 | }, 46 | ]) 47 | ); 48 | this.loadURL(url); 49 | } 50 | } 51 | 52 | export default VideoPlayerWindow; 53 | -------------------------------------------------------------------------------- /electron/components/preload.cts: -------------------------------------------------------------------------------- 1 | import { ipcRenderer, contextBridge } from 'electron'; 2 | 3 | contextBridge.exposeInMainWorld('api', { 4 | send: (channel: string, data: any) => { 5 | const allow_channels = [ 6 | 'ExternalLink:Open', 7 | 'Cache:ClearCache', 8 | 'Cache:ShowSpaceRequest', 9 | 'download:stop', 10 | 'download:pause', 11 | 'download:resume', 12 | 'style:caption', 13 | ]; 14 | if (allow_channels.includes(channel)) { 15 | ipcRenderer.send(channel, data); 16 | } 17 | }, 18 | invoke: async (channel: string, data: any) => { 19 | const allow_channels = ['video:play']; 20 | if (allow_channels.includes(channel)) { 21 | return await ipcRenderer.invoke(channel, data); 22 | } 23 | }, 24 | receive: (channel: string, func: (...data: any) => void) => { 25 | const allow_channels = [ 26 | 'Cache:ShowSpaceResponse', 27 | 'download:info', 28 | 'get:style:caption', 29 | ]; 30 | if (allow_channels.includes(channel)) { 31 | ipcRenderer.on(channel, (e, ...args) => func(...args)); 32 | } 33 | }, 34 | }); 35 | -------------------------------------------------------------------------------- /electron/configs.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { fileURLToPath } from 'url'; 3 | 4 | export const ROOT_PATH = process.cwd(); // Current working directory: from where it is called 5 | 6 | export const get__dirname = (fileUrl: string) => 7 | path.dirname(fileURLToPath(fileUrl)); 8 | 9 | export const ASSETS_PATHs = { 10 | PLYR_JS: path.join( 11 | get__dirname(import.meta.url), 12 | 'assets/video_player/plyr3.7.3.polyfilled.min.js' 13 | ), 14 | PLYR_CSS: path.join( 15 | get__dirname(import.meta.url), 16 | 'assets/video_player/plyr3.7.3.min.css' 17 | ), 18 | BOOTSTRAP: path.join( 19 | get__dirname(import.meta.url), 20 | 'assets/bootstrap/bootstrap.min.css' 21 | ), 22 | VIDEO_HTML_PATH: path.join( 23 | get__dirname(import.meta.url), 24 | 'views/html/video.html' 25 | ), 26 | REACT_BUILD: path.join(get__dirname(import.meta.url), 'assets'), 27 | }; 28 | 29 | export const WINDOW_PATHs = { 30 | DOWNLOAD_WINDOW_HTML: path.join( 31 | get__dirname(import.meta.url), 32 | 'views/html/download.html' 33 | ), 34 | MAIN_WINDOW_HTML: path.join(get__dirname(import.meta.url), 'index.html'), 35 | }; 36 | 37 | export const DEV_SERVER = { 38 | host: 'localhost', 39 | port: '3000', 40 | }; 41 | 42 | export const PROD_SERVER = { 43 | host: 'localhost', 44 | port: '18080', 45 | }; 46 | 47 | export const STREAM_SERVER = { 48 | host: 'localhost', 49 | port: '19000', 50 | }; 51 | 52 | /* Const Variable */ 53 | export const MB = 1e6; 54 | 55 | /* Setup caption config */ 56 | export const captionConf = path.join(ROOT_PATH, '.CaptionConf'); 57 | export const defaultCaptionFont = { 58 | fontSize: { small: 13, medium: 15, large: 21 }, 59 | }; 60 | -------------------------------------------------------------------------------- /electron/electron.interface.ts: -------------------------------------------------------------------------------- 1 | export interface captionData { 2 | type: string; 3 | data?: any; 4 | } 5 | export interface videoPlayData { 6 | hash: string; 7 | title?: string; 8 | maxCon: string | null; 9 | bandwidthLimit: string | null; 10 | } 11 | -------------------------------------------------------------------------------- /electron/electron.ts: -------------------------------------------------------------------------------- 1 | import { app, dialog, globalShortcut, ipcMain, shell } from 'electron'; 2 | import isDev from 'electron-is-dev'; 3 | import windowStateKeeper from 'electron-window-state'; 4 | import { BrowserWindow } from 'electron/main'; 5 | import express from 'express'; 6 | import fs from 'fs'; 7 | import http from 'http'; 8 | import os from 'os'; 9 | import path from 'path'; 10 | import srt2vtt from 'srt2vtt'; 11 | import WebTorrent, { Torrent } from 'webtorrent'; 12 | import DownloaderWindow from './components/DownloaderWindow.js'; 13 | import MainWindow from './components/MainWindow.js'; 14 | import VideoPlayerWindow from './components/VideoPlayerWindow.js'; 15 | import { 16 | ASSETS_PATHs, 17 | DEV_SERVER, 18 | MB, 19 | PROD_SERVER, 20 | STREAM_SERVER, 21 | WINDOW_PATHs, 22 | captionConf, 23 | defaultCaptionFont, 24 | } from './configs.js'; 25 | import { captionData, videoPlayData } from './electron.interface.js'; 26 | 27 | /* variable Initialization */ 28 | let mainWindow: Electron.BrowserWindow; 29 | let videoPlayerWindow: Electron.BrowserWindow | undefined = undefined; 30 | let downloaderWindow: BrowserWindow | undefined = undefined; 31 | 32 | let webtorrent_client: WebTorrent.Instance | undefined = undefined; 33 | let stream_server: http.Server | undefined = undefined; 34 | let static_server: http.Server | undefined = undefined; 35 | 36 | /* App Events */ 37 | // Starting Point 38 | app.on('ready', () => { 39 | if (!isDev) { 40 | static_server = serverReactContent(); 41 | } 42 | createWindow(); 43 | 44 | app.on('activate', () => { 45 | if (BrowserWindow.getAllWindows().length === 0) createWindow(); 46 | }); 47 | }); 48 | 49 | app.on('browser-window-focus', function () { 50 | globalShortcut.register('CommandOrControl+R', () => { 51 | console.log('CommandOrControl+R is pressed: Shortcut Disabled'); 52 | }); 53 | globalShortcut.register('F5', () => { 54 | console.log('F5 is pressed: Shortcut Disabled'); 55 | }); 56 | globalShortcut.register('CommandOrControl+Shift+I', () => { 57 | console.log('Inspect Element: Shortcut Disabled'); 58 | }); 59 | }); 60 | 61 | app.on('browser-window-blur', function () { 62 | globalShortcut.unregister('CommandOrControl+R'); 63 | globalShortcut.unregister('F5'); 64 | globalShortcut.unregister('CommandOrControl+Shift+I'); 65 | }); 66 | 67 | app.on('window-all-closed', () => { 68 | if (process.platform !== 'darwin') { 69 | app.quit(); 70 | } 71 | }); 72 | 73 | app.on('will-quit', () => { 74 | closeStreamServer(); 75 | }); 76 | 77 | /* IPC CALLS */ 78 | ipcMain.on('ExternalLink:Open', (event, link: string) => { 79 | shell.openExternal(link); 80 | }); 81 | 82 | ipcMain.on('Cache:ClearCache', (event, data: null) => { 83 | const dir = path.join(os.tmpdir(), 'webtorrent'); 84 | if (fs.existsSync(dir)) { 85 | fs.rm(dir, { recursive: true }, () => {}); 86 | } 87 | }); 88 | 89 | ipcMain.on('Cache:ShowSpaceRequest', (event, data: null) => { 90 | const dir = path.join(os.tmpdir(), 'webtorrent'); 91 | if (fs.existsSync(dir)) { 92 | fs.readdir(dir, (err, files) => { 93 | event.sender.send( 94 | 'Cache:ShowSpaceResponse', 95 | `${files.length} folder are in cache. About ${ 96 | files.length * 500 97 | } MB data` 98 | ); 99 | }); 100 | } else { 101 | event.sender.send('Cache:ShowSpaceResponse', '0 folder are in cache.'); 102 | } 103 | }); 104 | 105 | ipcMain.on('style:caption', (event, args: captionData) => { 106 | if (args.type === 'get') { 107 | try { 108 | const data = JSON.parse(fs.readFileSync(captionConf).toString()); 109 | event.reply('get:style:caption', data); 110 | } catch { 111 | fs.writeFileSync(captionConf, JSON.stringify(defaultCaptionFont)); 112 | } 113 | } else if (args.type === 'save') { 114 | fs.writeFileSync(captionConf, JSON.stringify(args.data)); 115 | } 116 | }); 117 | 118 | ipcMain.handle('video:play', async (event, data: videoPlayData) => { 119 | if (stream_server || webtorrent_client) { 120 | dialog.showErrorBox( 121 | 'Movie player or Downloader is already running', 122 | 'An instance of Movie Player | Downloader is already running. Please close the existing or downloader window and try again.' 123 | ); 124 | return; 125 | } 126 | 127 | webtorrent_client = getWebTorrentClient(data.maxCon, data.bandwidthLimit); 128 | 129 | const torrent = await new Promise((resolve, reject) => { 130 | webtorrent_client!.add(data.hash, {}, (torrent) => { 131 | resolve(torrent); 132 | }); 133 | }); 134 | 135 | const files = torrent.files.sort(); 136 | 137 | const videoFile = files.find(function (file) { 138 | return file.name.endsWith('.mp4'); 139 | }); 140 | 141 | if (!videoFile) { 142 | const torrent_path = torrent.path; 143 | closeWebTorrentClient(() => { 144 | console.error( 145 | 'Webtorrent client destroyed. Video File not found so, downloading movie instead' 146 | ); 147 | downloadMovieInstead( 148 | data.hash, 149 | data.maxCon, 150 | data.bandwidthLimit, 151 | torrent_path 152 | ); 153 | }); 154 | return; 155 | } 156 | 157 | stream_server = createStreamServer(videoFile, torrent, data); 158 | 159 | torrent.on('error', function (err) { 160 | console.error('Torrent error: ', err.toString()); 161 | 162 | resourceCleanUp(() => { 163 | console.error('Client destroyed due torrent error.'); 164 | }); 165 | 166 | dialog.showErrorBox('Torrent Error', err.toString()); 167 | }); 168 | 169 | torrent.on('noPeers', function (announceType) { 170 | console.warn('No Peer available to stream.', { announceType }); 171 | }); 172 | }); 173 | 174 | /* Helper Functions */ 175 | function createStreamServer( 176 | videoFile: WebTorrent.TorrentFile, 177 | torrent: WebTorrent.Torrent, 178 | data: videoPlayData 179 | ) { 180 | const express_app = express(); 181 | 182 | serveVideoPlayerAssets(express_app); 183 | 184 | express_app.get('/video', function (req, res) { 185 | const fileSize = videoFile.length; 186 | const range = req.headers.range; 187 | if (range) { 188 | const parts = range.replace(/bytes=/, '').split('-'); 189 | const start = parseInt(parts[0], 10); 190 | let end = fileSize - 1; 191 | 192 | // if end range is specified then 193 | if (parts[1] && parseInt(parts[1], 10) < end) { 194 | end = parseInt(parts[1], 10); 195 | } else if (start + MB < end) { 196 | // if end range is not specified then default to 1MB 197 | end = start + MB; 198 | } 199 | 200 | const contentLength = end - start + 1; 201 | const stream = videoFile.createReadStream({ start, end }); 202 | const head = { 203 | 'Content-Range': `bytes ${start}-${end}/${fileSize}`, 204 | 'Accept-Ranges': 'bytes', 205 | 'Content-Length': contentLength, 206 | 'Content-Type': 'video/mp4', 207 | }; 208 | res.writeHead(206, head); 209 | 210 | stream.once('error', (err) => { 211 | console.error('Stream error: ', err.toString()); 212 | }); 213 | // pipe readable stream through writable stream (res) 214 | stream.pipe(res); 215 | } else { 216 | const head = { 217 | 'Content-Length': fileSize, 218 | 'Content-Type': 'video/mp4', 219 | }; 220 | res.writeHead(200, head); 221 | const stream = videoFile.createReadStream(); 222 | 223 | stream.once('error', (err) => { 224 | console.error('Stream error: ', err.toString()); 225 | }); 226 | stream.pipe(res); 227 | } 228 | }); 229 | 230 | // subtitle api 231 | express_app.get('/subtitleApi/add', (req, res) => { 232 | try { 233 | if (req.query.path) { 234 | const path_query = req.query.path as string; 235 | const subtitle_path: string[] = path_query.split('.'); 236 | if (subtitle_path[subtitle_path.length - 1] === 'srt') { 237 | const srtData = fs.readFileSync(path_query); 238 | const newPath = path.join(torrent.path, '/', 'customCaption.vtt'); 239 | srt2vtt(srtData, function (err: any, vttData: any) { 240 | if (err) throw new Error(err); 241 | fs.writeFileSync(newPath, vttData); 242 | fs.createReadStream(newPath).pipe(res); 243 | }); 244 | } else if (subtitle_path[subtitle_path.length - 1] === 'vtt') { 245 | fs.createReadStream(path_query).pipe(res); 246 | } else { 247 | throw new Error( 248 | 'Subtitle MIME type not supported. Should be .srt or .vtt' 249 | ); 250 | } 251 | } else { 252 | throw new Error('Subtitle path not found'); 253 | } 254 | } catch (err: any) { 255 | dialog.showErrorBox('Error while adding subtitle', err.toString()); 256 | res.sendStatus(400); 257 | } 258 | }); 259 | 260 | // downloadInfo api 261 | express_app.get('/downloadInfo', (req, res) => { 262 | res.json({ 263 | total_downloaded: torrent.downloaded, 264 | total_size: torrent.length, 265 | path: torrent.path, 266 | }); 267 | }); 268 | 269 | // speed api 270 | express_app.get('/speed', (req, res) => { 271 | res.json({ up: torrent.uploadSpeed, down: torrent.downloadSpeed }); 272 | }); 273 | 274 | // get Title api 275 | express_app.get('/title', (req, res) => { 276 | res.json({ 277 | title: 278 | data.title === undefined ? 'YTS-Player' : 'YTS-Player - ' + data.title, 279 | }); 280 | }); 281 | 282 | const server = express_app.listen( 283 | +STREAM_SERVER.port, 284 | STREAM_SERVER.host, 285 | () => { 286 | console.log('Stream server ready'); 287 | createVideoPlayerWindow(); 288 | } 289 | ); 290 | 291 | return server; 292 | } 293 | 294 | function serveVideoPlayerAssets(app: express.Express) { 295 | app.get('/plyr-js', function (req, res) { 296 | res.sendFile(ASSETS_PATHs.PLYR_JS); 297 | }); 298 | 299 | app.get('/plyr-css', function (req, res) { 300 | res.sendFile(ASSETS_PATHs.PLYR_CSS); 301 | }); 302 | 303 | app.get('/bootstrapv5', function (req, res) { 304 | res.sendFile(ASSETS_PATHs.BOOTSTRAP); 305 | }); 306 | 307 | app.get('/streaming', function (req, res) { 308 | res.sendFile(ASSETS_PATHs.VIDEO_HTML_PATH); 309 | }); 310 | 311 | app.get('/custom-caption', function (req, res) { 312 | try { 313 | const data = JSON.parse(fs.readFileSync(captionConf).toString()); 314 | res.json(data); 315 | } catch { 316 | res.json(defaultCaptionFont); 317 | } 318 | }); 319 | } 320 | 321 | function closeStreamServer() { 322 | if (stream_server) { 323 | stream_server.close((err) => { 324 | console.log('Closing Stream server'); 325 | stream_server = undefined; 326 | 327 | if (err) { 328 | dialog.showErrorBox( 329 | 'Stream Server error', 330 | 'Error while closing streaming server' 331 | ); 332 | console.error('Close Stream Server error: ', err.toString()); 333 | } 334 | }); 335 | } 336 | } 337 | 338 | function closeWebTorrentClient(cb?: Function) { 339 | if (webtorrent_client) { 340 | webtorrent_client.destroy((err) => { 341 | console.log('Closing WebTorrent Client'); 342 | webtorrent_client = undefined; 343 | 344 | if (err) { 345 | dialog.showErrorBox( 346 | 'Webtorrent client error', 347 | 'Error while closing webtorrent client' 348 | ); 349 | console.error('Closing Webtorrent Error: ', err.toString()); 350 | return; 351 | } 352 | 353 | if (cb) { 354 | cb(); 355 | return; 356 | } 357 | }); 358 | } 359 | } 360 | 361 | function resourceCleanUp(cb?: Function) { 362 | closeStreamServer(); 363 | closeWebTorrentClient(cb); 364 | } 365 | 366 | function downloadMovieInstead( 367 | hash: string, 368 | maxCon: string | null, 369 | bandwidthLimit: string | null, 370 | previousPath: string 371 | ) { 372 | // delete previous path 373 | if (fs.existsSync(previousPath)) { 374 | fs.rm(previousPath, { recursive: true }, () => {}); 375 | } 376 | 377 | const downloadOption = dialog.showMessageBoxSync(mainWindow, { 378 | type: 'info', 379 | title: 'Media content not supported by YTS Player', 380 | message: 'Do you want to download it instead?', 381 | detail: 382 | 'No stream-able video source found in the torrent to stream. \nYou can download it instead and play with another video player', 383 | buttons: ['Download', 'Cancel'], 384 | defaultId: 0, 385 | cancelId: 1, 386 | noLink: true, 387 | }); 388 | 389 | let downloadPath: string[] | undefined; 390 | if (downloadOption === 0) { 391 | downloadPath = dialog.showOpenDialogSync({ 392 | title: 'Choose Download Location', 393 | properties: ['dontAddToRecent', 'openDirectory'], 394 | }); 395 | } 396 | 397 | if (downloadPath === undefined) { 398 | return; 399 | } 400 | 401 | // only download no need to stream from here 402 | webtorrent_client = getWebTorrentClient(maxCon, bandwidthLimit); 403 | webtorrent_client.add(hash, { path: downloadPath[0] }, (torrent) => { 404 | createDownloaderWindow(); 405 | 406 | // add IPC listener for torrent 407 | ipcMain.on('download:stop', () => { 408 | if (downloaderWindow) { 409 | downloaderWindow.close(); 410 | } 411 | }); 412 | 413 | ipcMain.on('download:pause', () => { 414 | console.log('torrent Paused'); 415 | if (webtorrent_client) { 416 | //@ts-expect-error 417 | webtorrent_client.throttleDownload(0); 418 | //@ts-expect-error 419 | webtorrent_client.throttleUpload(0); 420 | } 421 | }); 422 | 423 | ipcMain.on('download:resume', () => { 424 | console.log('torrent resumed'); 425 | if (webtorrent_client) { 426 | //@ts-expect-error 427 | webtorrent_client.throttleDownload(-1); 428 | //@ts-expect-error 429 | webtorrent_client.throttleUpload(-1); 430 | } 431 | }); 432 | 433 | // every time torrent downloads 434 | torrent.on('download', (bytes) => { 435 | downloaderWindow!.webContents.send('download:info', { 436 | progress: torrent.progress, 437 | downloadSpeed: torrent.downloadSpeed, 438 | uploadSpeed: torrent.uploadSpeed, 439 | title: torrent.name, 440 | downloadSize: torrent.length, 441 | totalDownloaded: torrent.downloaded, 442 | }); 443 | downloaderWindow!.setProgressBar(torrent.progress); 444 | }); 445 | 446 | // on torrent complete 447 | torrent.on('done', () => { 448 | downloaderWindow!.close(); 449 | const completeRes = dialog.showMessageBoxSync({ 450 | type: 'info', 451 | title: 'Download Completed', 452 | message: torrent.name + ' downloaded', 453 | buttons: ['Close', 'Open Folder'], 454 | cancelId: 0, 455 | defaultId: 1, 456 | noLink: true, 457 | }); 458 | if (completeRes === 1 && downloadPath !== undefined) { 459 | shell.openPath(downloadPath[0]); 460 | } 461 | }); 462 | 463 | // if torrent error occurs 464 | torrent.on('error', function (err) { 465 | console.error('Torrent error: ', err.toString()); 466 | dialog.showErrorBox('Torrent Error', err.toString()); 467 | }); 468 | 469 | // if no peers in torrent 470 | torrent.on('noPeers', function (announceType) { 471 | console.warn('No peers available to stream.', { announceType }); 472 | }); 473 | }); 474 | } 475 | 476 | function getWebTorrentClient( 477 | maxCon: string | null, 478 | bandwidthLimit: string | null 479 | ) { 480 | let limit = 481 | bandwidthLimit && Number(bandwidthLimit) > 0 ? Number(bandwidthLimit) : -1; 482 | 483 | const client = new WebTorrent({ 484 | maxConns: maxCon ? Number(maxCon) : 55, 485 | //@ts-expect-error 486 | downloadLimit: limit * MB, 487 | uploadLimit: limit * MB, 488 | }); 489 | 490 | client.on('error', (err) => { 491 | console.error('Webtorrent client error:', err.toString()); 492 | resourceCleanUp(); 493 | dialog.showErrorBox('Torrent Client Error', err.toString()); 494 | }); 495 | 496 | return client; 497 | } 498 | 499 | function createWindow() { 500 | const url = isDev 501 | ? `http://${DEV_SERVER.host}:${DEV_SERVER.port}` 502 | : `http://${PROD_SERVER.host}:${PROD_SERVER.port}`; 503 | const state = windowStateKeeper({ 504 | defaultWidth: 1200, 505 | defaultHeight: 1000, 506 | }); 507 | mainWindow = new MainWindow(url, state); 508 | 509 | if (isDev) { 510 | mainWindow.setAutoHideMenuBar(true); 511 | } else { 512 | mainWindow.setMenuBarVisibility(false); 513 | } 514 | 515 | mainWindow.on('closed', () => { 516 | if (videoPlayerWindow) { 517 | videoPlayerWindow.close(); 518 | } 519 | 520 | if (downloaderWindow) { 521 | downloaderWindow.close(); 522 | } 523 | }); 524 | 525 | state.manage(mainWindow); 526 | } 527 | 528 | function createVideoPlayerWindow() { 529 | const url = `http://${STREAM_SERVER.host}:${STREAM_SERVER.port}/streaming`; 530 | videoPlayerWindow = new VideoPlayerWindow(url); 531 | 532 | if (isDev) { 533 | videoPlayerWindow.webContents.toggleDevTools(); 534 | } 535 | 536 | videoPlayerWindow.on('closed', () => { 537 | videoPlayerWindow = undefined; 538 | resourceCleanUp(); 539 | }); 540 | } 541 | 542 | function createDownloaderWindow() { 543 | const url = `file://${WINDOW_PATHs.DOWNLOAD_WINDOW_HTML}`; 544 | downloaderWindow = new DownloaderWindow(url); 545 | 546 | if (isDev) { 547 | downloaderWindow.webContents.toggleDevTools(); 548 | } 549 | 550 | downloaderWindow.on('closed', () => { 551 | downloaderWindow = undefined; 552 | closeWebTorrentClient(() => { 553 | console.log('Downloader Window closed'); 554 | ipcMain.removeAllListeners('download:stop'); 555 | ipcMain.removeAllListeners('download:resume'); 556 | ipcMain.removeAllListeners('download:pause'); 557 | }); 558 | }); 559 | } 560 | 561 | function serverReactContent(): http.Server { 562 | let app: express.Express = express(); 563 | 564 | /* Serve React Assets */ 565 | app.use( 566 | '/assets', 567 | express.static(ASSETS_PATHs.REACT_BUILD, { 568 | index: false, 569 | }) 570 | ); 571 | 572 | app.get('/', (req, res) => { 573 | res.sendFile(WINDOW_PATHs.MAIN_WINDOW_HTML); 574 | }); 575 | 576 | return app.listen(+PROD_SERVER.port, PROD_SERVER.host, () => { 577 | console.log('Static Content is ready.'); 578 | }); 579 | } 580 | -------------------------------------------------------------------------------- /electron/srt2vtt.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'srt2vtt' { 2 | let srt2vtt: any; 3 | export default srt2vtt; 4 | } 5 | -------------------------------------------------------------------------------- /electron/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "ESNext" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */, 8 | "module": "NodeNext" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, 9 | // "lib": [], /* Specify library files to be included in the compilation. */ 10 | // "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */ 13 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./", /* Concatenate and emit output to single file. */ 17 | "outDir": "../build" /* Redirect output structure to the directory. */, 18 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "composite": true, /* Enable project compilation */ 20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 21 | // "removeComments": true, /* Do not emit comments to output. */ 22 | // "noEmit": true, /* Do not emit outputs. */ 23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 26 | 27 | /* Strict Type-Checking Options */ 28 | "strict": true /* Enable all strict type-checking options. */, 29 | "noImplicitAny": false /* Raise error on expressions and declarations with an implied 'any' type. */, 30 | // "strictNullChecks": true, /* Enable strict null checks. */ 31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 36 | 37 | /* Additional Checks */ 38 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 42 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 43 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an 'override' modifier. */ 44 | // "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */ 45 | 46 | /* Module Resolution Options */ 47 | "moduleResolution": "NodeNext" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, 48 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 49 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 50 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 51 | // "typeRoots": [], /* List of folders to include type definitions from. */ 52 | // "types": [], /* Type declaration files to be included in compilation. */ 53 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 54 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 55 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 56 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 57 | 58 | /* Source Map Options */ 59 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 60 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 61 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 62 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 63 | 64 | /* Experimental Options */ 65 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 66 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 67 | 68 | /* Advanced Options */ 69 | "skipLibCheck": true /* Skip type checking of declaration files. */, 70 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /electron/views/download_jsx/download.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * This need to be converted by babel before using this. 3 | * A babel compile minified of this is already added to download.html 4 | * */ 5 | function Download() { 6 | const [apiData, updateApiData] = React.useState({ 7 | progress: 0, 8 | downloadSpeed: 0, 9 | uploadSpeed: 0, 10 | title: '', 11 | downloadSize: 0, 12 | totalDownloaded: 0, 13 | }); 14 | 15 | const [isPaused, updatePause] = React.useState(false); 16 | 17 | React.useEffect(() => { 18 | window.api.receive('download:info', (data) => { 19 | // update speed 20 | data.uploadSpeed = (data.uploadSpeed / (1000 * 1000)).toString(); // to MB 21 | data.uploadSpeed = data.uploadSpeed.slice( 22 | 0, 23 | data.uploadSpeed.indexOf('.') + 3 24 | ); // Slice 25 | 26 | data.downloadSpeed = (data.downloadSpeed / (1000 * 1000)).toString(); 27 | data.downloadSpeed = data.downloadSpeed.slice( 28 | 0, 29 | data.downloadSpeed.indexOf('.') + 3 30 | ); 31 | 32 | // update progress bar 33 | data.progress = (data.progress * 100).toString(); // to 100% 34 | data.progress = data.progress.slice(0, data.progress.indexOf('.') + 2); // slice 35 | 36 | data.totalDownloaded = (data.totalDownloaded / 1000 ** 3).toString(); 37 | data.totalDownloaded = data.totalDownloaded.slice( 38 | 0, 39 | data.totalDownloaded.indexOf('.') + 4 40 | ); 41 | 42 | data.downloadSize = (data.downloadSize / 1000 ** 3).toString(); 43 | data.downloadSize = data.downloadSize.slice( 44 | 0, 45 | data.downloadSize.indexOf('.') + 4 46 | ); 47 | 48 | updateApiData(data); 49 | }); 50 | }, []); 51 | 52 | let pauseOnClick = () => { 53 | window.api.send('download:pause', null); 54 | updatePause(true); 55 | }; 56 | 57 | let resumeOnClick = () => { 58 | window.api.send('download:resume', null); 59 | updatePause(false); 60 | }; 61 | 62 | const cancelOnClick = () => { 63 | window.api.send('download:stop', null); 64 | }; 65 | 66 | return ( 67 |
71 |
72 |
73 |
74 |
75 | Downloading: 76 | {apiData.title} 77 |
78 |
79 |
80 | 81 | {apiData.uploadSpeed} ↑M 82 | 83 | / 84 | 85 | {apiData.downloadSpeed} ↓M 86 | 87 |
88 |
89 |
90 |
95 | {apiData.progress}% 96 |
97 |
98 |
99 |
100 | {apiData.totalDownloaded} G 101 | / 102 | {apiData.downloadSize} G 103 |
104 |
105 | {isPaused ? ( 106 | 113 | ) : ( 114 | 121 | )} 122 | 129 |
130 |
131 |
132 |
133 | ); 134 | } 135 | 136 | ReactDOM.render(, document.getElementById('root')); 137 | -------------------------------------------------------------------------------- /electron/views/html/download.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | Downloader 11 | 12 | 13 | 14 |
15 |
16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /electron/views/html/video.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 12 | 13 | YTS-Player 14 | 20 | 21 | 22 | 23 |
24 | 27 | 28 |
29 |
30 | Downloading 31 |
32 |
33 | 0 G 34 | / 35 | 0 G 36 |
37 |
38 | 39 |
40 |
41 | 0 ↑M 42 | / 43 | 0 ↓M 44 |
45 |
47 | 49 | 51 | 53 | 54 | Add Subtitle 55 |
56 |
57 | 58 |
59 |
60 |
61 | 62 |
63 |
64 | 220 | 221 | 222 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | YTS-Streaming 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yts-streaming", 3 | "version": "4.0.0", 4 | "private": true, 5 | "type": "module", 6 | "main": "build/electron.js", 7 | "scripts": { 8 | "start": "vite --port 3000", 9 | "build": "vite build && tsc --project electron", 10 | "postbuild": "shx cp -r ./electron/assets ./electron/views ./build", 11 | "clean": "shx rm -r dist build", 12 | "preview": "vite preview", 13 | "watch-electron": "tsc --watch --project electron", 14 | "preelectron": "mkdir ./build && npm run postbuild || npm run postbuild", 15 | "electron": "electron .", 16 | "preelectron-dev": "npm run preelectron && tsc --project electron", 17 | "electron-dev": "concurrently \"npm run watch-electron\" \"npm start\" \"npm run electron\"", 18 | "electron-pack": "electron-builder build -lw", 19 | "preelectron-pack": "npm run build", 20 | "format": "prettier --write \"./**/*.{ts,tsx,json}\"", 21 | "lint": "eslint --fix --ext .ts,.tsx,.js,.jsx ." 22 | }, 23 | "dependencies": { 24 | "electron-is-dev": "^3.0.1", 25 | "electron-window-state": "^5.0.3", 26 | "express": "^4.19.2", 27 | "srt2vtt": "^1.3.1", 28 | "webtorrent": "^2.1.36" 29 | }, 30 | "devDependencies": { 31 | "@types/express": "^4.17.14", 32 | "@types/node": "^20.11.23", 33 | "@types/react": "^18.0.24", 34 | "@types/react-dom": "^18.0.8", 35 | "@types/webtorrent": "^0.109.8", 36 | "@typescript-eslint/eslint-plugin": "^5.43.0", 37 | "@vitejs/plugin-react": "^2.2.0", 38 | "concurrently": "^8.2.2", 39 | "electron": "^29.1.0", 40 | "electron-builder": "^24.13.3", 41 | "eslint": "^8.27.0", 42 | "eslint-config-standard-with-typescript": "^23.0.0", 43 | "eslint-plugin-import": "^2.26.0", 44 | "eslint-plugin-n": "^15.5.1", 45 | "eslint-plugin-promise": "^6.1.1", 46 | "eslint-plugin-react": "^7.31.10", 47 | "prettier": "^2.7.1", 48 | "react": "^18.2.0", 49 | "react-dom": "^18.2.0", 50 | "react-router-dom": "^6.4.3", 51 | "sass": "^1.56.1", 52 | "shx": "^0.3.4", 53 | "typescript": "~4.8.0", 54 | "vite": "^3.2.3", 55 | "vite-plugin-eslint": "^1.8.1" 56 | }, 57 | "build": { 58 | "extraMetadata": { 59 | "main": "dist/electron.js" 60 | }, 61 | "files": [ 62 | { 63 | "from": "build", 64 | "to": "dist" 65 | }, 66 | "package.json" 67 | ], 68 | "directories": { 69 | "buildResources": "builder_assets" 70 | }, 71 | "asar": true, 72 | "appId": "com.mbpn1.yts-streaming", 73 | "copyright": "Copyright 2024 Maharjan-Bipin", 74 | "compression": "normal", 75 | "win": { 76 | "target": "nsis" 77 | }, 78 | "linux": { 79 | "target": "AppImage", 80 | "category": "Video" 81 | }, 82 | "nsis": { 83 | "oneClick": true, 84 | "perMachine": false, 85 | "allowToChangeInstallationDirectory": false, 86 | "deleteAppDataOnUninstall": true, 87 | "allowElevation": true, 88 | "include": "builder_assets/installerMacro.nsh", 89 | "warningsAsErrors": false 90 | } 91 | }, 92 | "volta": { 93 | "node": "18.19.1" 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /public/assets/images/logo-imdb-svg.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /public/assets/images/logo_final.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iambpn/YTS-Streaming/e2d2110734ee61b5d01025b3aaff70e5e96c5b83/public/assets/images/logo_final.png -------------------------------------------------------------------------------- /public/assets/images/play-button.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 9 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /public/assets/images/preloader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iambpn/YTS-Streaming/e2d2110734ee61b5d01025b3aaff70e5e96c5b83/public/assets/images/preloader.gif -------------------------------------------------------------------------------- /src/App.scss: -------------------------------------------------------------------------------- 1 | .scrollbar { 2 | &::-webkit-scrollbar { 3 | width: 0.6rem; 4 | background: #252525; 5 | border: .5px solid #424141; 6 | } 7 | } 8 | 9 | .scrollbar::-webkit-scrollbar-thumb { 10 | background-color: #555555; 11 | border-radius: 10px; 12 | } 13 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Outlet, Route, Routes } from 'react-router-dom'; 3 | import './App.scss'; 4 | import EntryPage from './pages/EntryPage'; 5 | import MovieDetails from './pages/MovieDetails'; 6 | import AppContextProvider from './store/AppContextProvider'; 7 | 8 | function App() { 9 | return ( 10 | 11 | 12 | 13 | } /> 14 | } /> 15 | 16 | 17 | 18 | ); 19 | } 20 | 21 | export default App; 22 | -------------------------------------------------------------------------------- /src/components/ErrorHandling/ErrorHandling.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function ErrorHandling(props: { error: any }) { 4 | return ( 5 |
6 |
7 | {typeof props.error === 'string' 8 | ? props.error 9 | : props.error.status_message} 10 |
11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/components/backdrop/BackDrop.module.scss: -------------------------------------------------------------------------------- 1 | .backdrop { 2 | position: fixed; 3 | z-index: 1; 4 | background-color: rgba(0, 0, 0, 0.75); 5 | width: 100%; 6 | height: 100vh; 7 | top: 0; 8 | left: 0; 9 | } -------------------------------------------------------------------------------- /src/components/backdrop/BackDrop.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './BackDrop.module.scss'; 3 | 4 | interface BackDropProps { 5 | onClick: Function; 6 | } 7 | 8 | function BackDrop(props: BackDropProps) { 9 | return ( 10 |
{ 13 | props.onClick(); 14 | }} 15 | /> 16 | ); 17 | } 18 | 19 | export default BackDrop; 20 | -------------------------------------------------------------------------------- /src/components/footer/footer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function Footer() { 4 | return ( 5 | 6 | 10 | Content from - YTS API V2 11 | 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/components/header/CustomCaption.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | interface Props { 4 | label: string; 5 | initialValue: number; 6 | handleFontSizeValue: Function; 7 | } 8 | 9 | export default function CustomCaption(props: Props): JSX.Element { 10 | const [fontSize, setFontSize] = useState(props.initialValue); 11 | 12 | const handleInputChange = (e: React.ChangeEvent) => { 13 | setFontSize(Number(e.currentTarget.value)); 14 | props.handleFontSizeValue(props.label, Number(e.currentTarget.value)); 15 | }; 16 | 17 | return ( 18 |
23 | 24 |

28 | AaBbCcDd 29 |

30 | 38 |
39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /src/components/header/Heading.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import SettingIcon from './SettingIcon'; 3 | import { useLocation, Link, useNavigate } from 'react-router-dom'; 4 | import { AppContext, defaultQueries } from '../../store/AppContextProvider'; 5 | 6 | export default function Heading(props: any) { 7 | const showBackBtn = useLocation().pathname !== '/'; 8 | const navigate = useNavigate(); 9 | const context = useContext(AppContext); 10 | 11 | const handleBack = () => { 12 | navigate(-1); 13 | }; 14 | 15 | const handleGoHome = () => { 16 | context.updateQueries.search(defaultQueries.search); 17 | context.updateQueries.quality(defaultQueries.quality); 18 | context.updateQueries.genre(defaultQueries.genre); 19 | context.updateQueries.rating(defaultQueries.rating); 20 | context.updateQueries.sortBy(defaultQueries.sortBy); 21 | }; 22 | 23 | return ( 24 |
25 |
26 |
27 | {showBackBtn && ( 28 |
29 | 30 | ← Back 31 | 32 |
33 | )} 34 | 43 |
47 | {/* Setting Icon */} 48 | 49 |
50 |
51 |
52 |
53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /src/components/header/SettingIcon.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import SettingModal from './SettingsModal'; 3 | import './SettingsIcon.scss'; 4 | 5 | export default function SettingIcon() { 6 | const [settingClick, setSettingClick] = useState(false); 7 | 8 | const handleSettingClick = (e: React.SyntheticEvent) => { 9 | e.preventDefault(); 10 | setSettingClick(true); 11 | }; 12 | 13 | return ( 14 | 15 | 16 | 24 | 28 | 29 | 30 | 31 | {settingClick && } 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /src/components/header/SettingsIcon.scss: -------------------------------------------------------------------------------- 1 | .biGearFill{ 2 | &:hover { 3 | color: var(--bs-success); 4 | -webkit-animation: spin 4s linear infinite; 5 | -moz-animation: spin 4s linear infinite; 6 | animation: spin 4s linear infinite; 7 | } 8 | } 9 | @-moz-keyframes spin { 100% { -moz-transform: rotate(360deg); } } 10 | @-webkit-keyframes spin { 100% { -webkit-transform: rotate(360deg); } } 11 | @keyframes spin { 100% { -webkit-transform: rotate(360deg); transform:rotate(360deg); } } -------------------------------------------------------------------------------- /src/components/header/SettingsModal.module.scss: -------------------------------------------------------------------------------- 1 | .modal { 2 | width: 33rem; 3 | z-index: 10; 4 | position: fixed; 5 | left: calc(50% - 15rem); 6 | } 7 | 8 | .playSvg { 9 | height: 35px; 10 | width: 35px; 11 | border-radius: 100%; 12 | cursor: pointer; 13 | transition: color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out; 14 | } 15 | 16 | .captionStyle { 17 | &:hover { 18 | color: var(--bs-success); 19 | } 20 | 21 | p { 22 | font-size: 1.2em; 23 | margin-bottom: 0; 24 | } 25 | } -------------------------------------------------------------------------------- /src/components/header/SettingsModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import BackDrop from '../backdrop/BackDrop'; 3 | import package_json from '../../../package.json'; 4 | import styles from './SettingsModal.module.scss'; 5 | import CustomCaption from './CustomCaption'; 6 | 7 | interface SettingsModalProps { 8 | openSettings: React.Dispatch>; 9 | } 10 | 11 | export interface CaptionStyleType { 12 | fontSize: { 13 | [key: string]: number; 14 | small: number; 15 | medium: number; 16 | large: number; 17 | }; 18 | } 19 | 20 | function getMaxConSettings(): string | null { 21 | return localStorage.getItem('MaxCon'); 22 | } 23 | 24 | function setMaxConSettings(maxCon: number) { 25 | localStorage.setItem('MaxCon', String(maxCon)); 26 | } 27 | 28 | function getBandWidthLimit(): string | null { 29 | return localStorage.getItem('Bandwidth'); 30 | } 31 | 32 | function setBandWidthLimit(speed: number) { 33 | localStorage.setItem('Bandwidth', String(speed)); 34 | } 35 | 36 | function SettingModal(props: SettingsModalProps) { 37 | const [maxCon, updateMaxCon] = useState(55); 38 | const [bandwidthLimit, updateBandwidthLimit] = useState(-1); 39 | const [cachedSpaceTitle, updateCachedSpaceTitle] = useState(''); 40 | const [torrentLink, updateTorrentLink] = useState(''); 41 | const [playing, setPlaying] = useState(false); 42 | const [isSubtitleExpand, updateIsSubtitleExpand] = useState(false); 43 | const [captionStyle, updateCaptionStyle] = useState({ 44 | fontSize: { 45 | small: 13, 46 | medium: 15, 47 | large: 21, 48 | }, 49 | }); 50 | 51 | useEffect(() => { 52 | const maxCon = getMaxConSettings(); 53 | const bandwidthLimit = getBandWidthLimit(); 54 | if (maxCon !== null) { 55 | updateMaxCon(Number(maxCon)); 56 | } 57 | 58 | if (bandwidthLimit !== null) { 59 | updateBandwidthLimit(Number(bandwidthLimit)); 60 | } 61 | 62 | // @ts-expect-error ** fetch caption style data** 63 | window.api.send('style:caption', { type: 'get' }); 64 | // @ts-expect-error 65 | window.api.receive('get:style:caption', (args: CaptionStyleType) => { 66 | updateCaptionStyle(args); 67 | }); 68 | }, []); 69 | 70 | const handleMaxConChange = (e: React.FormEvent) => { 71 | if (Number(e.currentTarget.value) <= 0) { 72 | updateMaxCon(55); 73 | } else { 74 | updateMaxCon(Number(e.currentTarget.value)); 75 | } 76 | }; 77 | 78 | const handleBandwidthLimitChange = (e: React.FormEvent) => { 79 | if (Number(e.currentTarget.value) <= 0) { 80 | updateBandwidthLimit(-1); 81 | } else { 82 | updateBandwidthLimit(Number(e.currentTarget.value)); 83 | } 84 | }; 85 | 86 | const handleCloseModal = () => { 87 | props.openSettings(false); 88 | setMaxConSettings(maxCon); 89 | setBandWidthLimit(bandwidthLimit); 90 | 91 | // @ts-expect-error 92 | window.api.send('style:caption', { 93 | type: 'save', 94 | data: captionStyle, 95 | }); 96 | }; 97 | 98 | const handleOpenLink = (link: string) => { 99 | // @ts-expect-error 100 | window.api.send('ExternalLink:Open', link); 101 | handleCloseModal(); 102 | }; 103 | 104 | const handleClearCache = () => { 105 | // @ts-expect-error 106 | window.api.send('Cache:ClearCache', null); 107 | handleCloseModal(); 108 | }; 109 | 110 | const handleShowCachedSpace = () => { 111 | // @ts-expect-error 112 | window.api.send('Cache:ShowSpaceRequest', null); 113 | // @ts-expect-error 114 | window.api.receive('Cache:ShowSpaceResponse', (data: string) => { 115 | updateCachedSpaceTitle(data); 116 | }); 117 | }; 118 | 119 | const handlePlayExternalSrc = async () => { 120 | if (torrentLink.trim() !== '') { 121 | setPlaying(true); 122 | // @ts-expect-error 123 | await window.api.invoke('video:play', { 124 | hash: torrentLink, 125 | maxCon, 126 | bandwidthLimit, 127 | }); 128 | setPlaying(false); 129 | handleCloseModal(); 130 | } 131 | }; 132 | 133 | const handleSubtitleExpand = () => { 134 | updateIsSubtitleExpand(!isSubtitleExpand); 135 | }; 136 | 137 | const handleFontSizeValue = (position: string, value: number) => { 138 | const temp = { ...captionStyle }; 139 | temp.fontSize[position.toLowerCase()] = value; 140 | updateCaptionStyle(temp); 141 | }; 142 | 143 | return ( 144 | 145 | 146 |
147 |
151 |
155 |
156 |
157 |
158 | YST Settings V.{package_json.version} 159 |
160 |
161 |
168 |
169 |
170 | 177 |
178 | 185 |
186 |
187 |
188 |
189 |
190 | 197 |
198 | 206 |
207 |
208 |
209 |
210 |
211 | 215 |
216 |
217 | { 223 | updateTorrentLink(e.currentTarget.value); 224 | }} 225 | onKeyUp={(e) => { 226 | if (e.code.toLowerCase() === 'enter') { 227 | handlePlayExternalSrc(); 228 | } 229 | }} 230 | placeholder={'Torrent magnet link / Torrent hash'} 231 | /> 232 |
233 |
234 | 239 | 240 | 241 |
242 |
243 |
244 |
245 |
246 |
252 | 253 |

258 |

259 | {isSubtitleExpand && ( 260 |
261 |

262 | Customize caption based of screen size: (Re-open player 263 | window to see the changes) 264 |

265 | {/* Small and Medium are not required as of now */} 266 | {/* */} 268 | {/* */} 270 | 275 |
276 | )} 277 |
278 |
279 |
280 | 290 | 299 | 308 |
309 |
310 | 311 | Special thanks to: 312 | 313 | 322 |
323 |
324 |
325 | 332 |
333 |
334 |
335 |
336 |
337 | ); 338 | } 339 | 340 | export default SettingModal; 341 | export { getMaxConSettings, getBandWidthLimit }; 342 | -------------------------------------------------------------------------------- /src/components/movieScreenshot/MovieScreenshots.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface MovieScreenshotsProps { 4 | title_long: string; 5 | medium_screenshot_image1: string; 6 | medium_screenshot_image2: string; 7 | medium_screenshot_image3: string; 8 | } 9 | 10 | export default function MovieScreenshots(props: MovieScreenshotsProps) { 11 | return ( 12 |
13 |
14 | {props.title_long} 19 |
20 |
21 | {props.title_long} 26 |
27 |
28 | {props.title_long} 33 |
34 |
35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /src/components/movieSynopsisAndTrailer/MovieSynopsisTrailer.scss: -------------------------------------------------------------------------------- 1 | .trailer { 2 | &:before { 3 | content: ''; 4 | display: block !important; 5 | position: absolute; 6 | top: 0; 7 | left: 0; 8 | right: 0; 9 | bottom: 0; 10 | pointer-events: none; 11 | background: url(https://yts.mx/assets/images/website/play-trailer.svg) no-repeat center center; 12 | } 13 | } -------------------------------------------------------------------------------- /src/components/movieSynopsisAndTrailer/MovieSynopsisTrailer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './MovieSynopsisTrailer.scss'; 3 | 4 | interface MovieSynopsisTrailerProps { 5 | title: string; 6 | screenshot: string; 7 | description_full: string; 8 | yt_trailer_code: string; 9 | download_count: string; 10 | } 11 | 12 | export default function MovieSynopsisTrailer(props: MovieSynopsisTrailerProps) { 13 | const handleOpenTrailer = () => { 14 | // @ts-expect-error 15 | window.api.send( 16 | 'ExternalLink:Open', 17 | 'https://www.youtube.com/watch?v=' + props.yt_trailer_code 18 | ); 19 | }; 20 | 21 | return ( 22 |
23 |
24 |

25 | Synopsis 26 |

27 |

31 | {props.description_full} 32 |

33 |
34 |
35 |

36 | Watch Trailer : 37 |

38 |
43 | {props.title} 49 |
50 |
51 |
52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /src/components/moviecard/MovieCard.scss: -------------------------------------------------------------------------------- 1 | .movie_title { 2 | white-space: nowrap; 3 | overflow: hidden; 4 | text-overflow: ellipsis; 5 | } 6 | 7 | .movie_date{ 8 | font-size: .9em; 9 | color: #ADAFAE !important; 10 | } 11 | .movie_link{ 12 | cursor: pointer; 13 | } 14 | 15 | .card:hover .overlay, .overlay:hover { 16 | display: block !important; 17 | position: absolute; 18 | top: 0; 19 | left: 0; 20 | right: 0; 21 | bottom: 0; 22 | background-color: rgba(0, 0, 0, 0.85); 23 | } 24 | 25 | .card:hover{ 26 | border-color: #6ac045 !important; 27 | } -------------------------------------------------------------------------------- /src/components/moviecard/MovieCard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './MovieCard.scss'; 3 | import { Link } from 'react-router-dom'; 4 | 5 | export interface MovieCardProps { 6 | id: string; 7 | title: string; 8 | year: string; 9 | rating: string; 10 | genres: string[]; 11 | mediumImageCover: string; 12 | } 13 | 14 | export default function MovieCard(props: MovieCardProps) { 15 | return ( 16 | 17 |
18 |
19 | 24 |
25 | {props.title} 32 |
36 |
37 |

41 |

{props.rating} / 10
42 |
43 |
44 | {props.genres && ( 45 |
46 |
47 | {props.genres.length > 0 ? props.genres[0] : ''} 48 |
49 |
50 | {props.genres.length > 1 ? props.genres[1] : ''} 51 |
52 |
53 | )} 54 |
55 |
56 | 59 |
60 |
61 |
62 |
63 | 64 | {props.title} 65 | 66 | {props.year} 67 |
68 | 69 |
70 |
71 |
72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /src/components/movieintro/MovieIntro.scss: -------------------------------------------------------------------------------- 1 | #movie_title_info { 2 | h1 { 3 | font-size: 2.5rem; 4 | font-weight: bolder; 5 | } 6 | 7 | h2 { 8 | font-size: 1.25rem; 9 | line-height: 24px; 10 | font-weight: bolder; 11 | } 12 | } 13 | 14 | .rating_info { 15 | font-size: 1.25em; 16 | font-weight: bolder; 17 | } 18 | 19 | .suggestion{ 20 | &:hover{ 21 | border-color: #6ac045 !important; 22 | } 23 | } -------------------------------------------------------------------------------- /src/components/movieintro/MovieIntro.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import './MovieIntro.scss'; 3 | import { useNavigate } from 'react-router-dom'; 4 | import { getBandWidthLimit, getMaxConSettings } from '../header/SettingsModal'; 5 | 6 | interface MovieIntroProps { 7 | movie: any; 8 | suggestions: any[]; 9 | } 10 | 11 | export default function MovieIntro(props: MovieIntroProps) { 12 | const navigate = useNavigate(); 13 | const [playing, setPlaying] = useState(false); 14 | 15 | const handleClickOnLink = async (hash: string) => { 16 | setPlaying(true); 17 | const maxCon = getMaxConSettings(); 18 | const bandwidthLimit = getBandWidthLimit(); 19 | // @ts-expect-error 20 | await window.api.invoke('video:play', { 21 | hash, 22 | title: props.movie.title, 23 | maxCon, 24 | bandwidthLimit, 25 | }); 26 | setPlaying(false); 27 | }; 28 | 29 | const downloadSubtitle = () => { 30 | // @ts-expect-error 31 | window.api.send( 32 | 'ExternalLink:Open', 33 | 'https://yifysubtitles.org/movie-imdb/' + props.movie.imdb_code 34 | ); 35 | }; 36 | 37 | const links = []; 38 | for (const torrent of props.movie.torrents) { 39 | const button = ( 40 | 52 | ); 53 | 54 | links.push(button); 55 | } 56 | 57 | const suggestions: JSX.Element[] = []; 58 | props.suggestions.forEach((suggestion) => { 59 | const tmp = ( 60 |
{ 64 | navigate('/movie/' + suggestion.id); 65 | }} 66 | key={suggestion.id} 67 | > 68 |
72 | {''} 79 |
80 |
81 | ); 82 | suggestions.push(tmp); 83 | }); 84 | 85 | return ( 86 | 87 |
88 |
89 |
93 | {''} 99 |
100 |
101 |
102 |
103 |
104 |

{props.movie.title}

105 |

{props.movie.year}

106 |

{props.movie.genres.toString()}

107 |
108 |
109 | 113 | Available in: {playing ? 'Opening Player...' : ''}{' '} 114 | 115 | {links} 116 |
117 |
118 | 126 |
127 |
128 |
129 | IMDB 130 |
131 |
132 | {props.movie.rating} 133 | 137 |
138 |
139 |
140 |
141 |
Similar Movies
142 |
{suggestions}
143 |
144 |
145 | 146 | ); 147 | } 148 | -------------------------------------------------------------------------------- /src/components/movielist/MovieList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MovieCard from '../moviecard/MovieCard'; 3 | import ErrorHandling from '../ErrorHandling/ErrorHandling'; 4 | 5 | interface MovieListProps { 6 | movieListOrError: any; 7 | handlePageChange: Function; 8 | currentPage: string; 9 | } 10 | 11 | export default function MovieList(props: MovieListProps) { 12 | // ErrorHandling 13 | if (props.movieListOrError.status !== 'ok') { 14 | return ; 15 | } 16 | 17 | const movieListCount = props.movieListOrError.data.movie_count; 18 | const limit = props.movieListOrError.data.limit; 19 | 20 | const total_page_number = Math.ceil(Number(movieListCount) / Number(limit)); 21 | const pageNumber: JSX.Element = ( 22 |
23 |
24 |
25 | 43 |
44 |
45 | ); 46 | 47 | const listOfMovieCard = props.movieListOrError.data.movies?.map( 48 | (movie: any, index: number) => { 49 | return ( 50 | 59 | ); 60 | } 61 | ); 62 | 63 | return ( 64 | 65 |
66 |
67 |
{`${movieListCount} YTS Movies Available`}
70 |
71 | {pageNumber} 72 |
73 |
{listOfMovieCard}
74 |
75 |
76 |
77 | ); 78 | } 79 | -------------------------------------------------------------------------------- /src/components/search/Filter.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface FilterProps { 4 | label: string; 5 | values: string[]; 6 | stateValue: string; 7 | updateStateValue: Function; 8 | updatePageNumber: Function; 9 | } 10 | 11 | function capitalizeFirstLetter(words: string) { 12 | return words 13 | .split(' ') 14 | .map((word, idx) => { 15 | return word.charAt(0).toUpperCase() + word.slice(1); 16 | }) 17 | .reduce((prev, current): string => { 18 | return prev + ' ' + current; 19 | }) 20 | .trim(); 21 | } 22 | 23 | export default function Filter(props: FilterProps) { 24 | const options = props.values.map((value, index) => { 25 | return ( 26 | 29 | ); 30 | }); 31 | 32 | const handleOnChangeSelect = (e: React.ChangeEvent) => { 33 | props.updateStateValue(e.target.value); 34 | props.updatePageNumber('1'); 35 | }; 36 | 37 | return ( 38 |
39 | 42 | 50 |
51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /src/components/search/SearchAndFilterBox.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useEffect, useRef } from 'react'; 2 | import Filter from './Filter'; 3 | import { AppContext } from '../../store/AppContextProvider'; 4 | 5 | const FilterTypes = { 6 | quality: ['All', '720p', '1080p', '2160p', '3D'], 7 | genre: [ 8 | 'All', 9 | 'Action', 10 | 'Adventure', 11 | 'Animation', 12 | 'Biography', 13 | 'Comedy', 14 | 'Crime', 15 | 'Documentary', 16 | 'Drama', 17 | 'Family', 18 | 'Fantasy', 19 | 'Film-Noir', 20 | 'Game-Show', 21 | 'History', 22 | 'Horror', 23 | 'Music', 24 | 'Musical', 25 | 'Mystery', 26 | 'News', 27 | 'Reality-TV', 28 | 'Romance', 29 | 'Sci-Fi', 30 | 'Sport', 31 | 'Talk-Show', 32 | 'Thriller', 33 | 'War', 34 | 'Western', 35 | ], 36 | rating: ['All', '9+', '8+', '7+', '6+', '5+', '4+', '3+', '2+', '1+'], 37 | 'sort by': [ 38 | 'date_added', 39 | 'download_count', 40 | 'like_count', 41 | 'title', 42 | 'year', 43 | 'rating', 44 | 'peers', 45 | 'seeds', 46 | ], 47 | }; 48 | const Base_URL = 'https://yts.mx/api/v2/list_movies.json'; 49 | 50 | interface SearchAndFilterBoxProps { 51 | fetchData: Function; 52 | } 53 | 54 | export default function SearchAndFilterBox(props: SearchAndFilterBoxProps) { 55 | // extracting data from context (AppContext) 56 | const context = useContext(AppContext); 57 | const [searchInput, quality, genre, rating, sortBy] = [ 58 | context.currentQueries.search, 59 | context.currentQueries.quality, 60 | context.currentQueries.genre, 61 | context.currentQueries.rating, 62 | context.currentQueries.sortBy, 63 | ]; 64 | 65 | const [ 66 | updateSearchInput, 67 | updateQuality, 68 | updateGenre, 69 | updateRating, 70 | updateSortBy, 71 | updatePageNumber, 72 | ] = [ 73 | context.updateQueries.search, 74 | context.updateQueries.quality, 75 | context.updateQueries.genre, 76 | context.updateQueries.rating, 77 | context.updateQueries.sortBy, 78 | context.updateCurrentPage, 79 | ]; 80 | 81 | const handleSearchInput = (e: React.FormEvent) => { 82 | updateSearchInput(e.currentTarget.value); 83 | }; 84 | 85 | const handleEnterKey = (keyCode: string) => { 86 | if (keyCode.toLowerCase() === 'enter') { 87 | handleSearch(); 88 | } 89 | }; 90 | 91 | const resetStates = () => { 92 | updateQuality('All'); 93 | updateGenre('All'); 94 | updateRating('All'); 95 | updateSortBy('date_added'); 96 | updatePageNumber('1'); 97 | }; 98 | 99 | const handleSearch = () => { 100 | const url = new URL(Base_URL); 101 | url.searchParams.set('query_term', searchInput); 102 | resetStates(); 103 | props.fetchData(url); 104 | }; 105 | 106 | const initial = useRef(true); 107 | useEffect(() => { 108 | if (initial.current) { 109 | initial.current = false; 110 | } else { 111 | // run only on update 112 | const url = new URL(Base_URL); 113 | if (searchInput.trim() !== '') { 114 | url.searchParams.set('query_term', searchInput); 115 | } 116 | if (quality.toLowerCase() !== 'all') { 117 | url.searchParams.set('quality', quality); 118 | } 119 | if (genre.toLowerCase() !== 'all') { 120 | url.searchParams.set('genre', genre); 121 | } 122 | if (rating.toLowerCase() !== 'all') { 123 | url.searchParams.set('minimum_rating', rating.replace('+', '')); 124 | } 125 | if (sortBy.toLowerCase() !== 'date_added') { 126 | url.searchParams.set('sort_by', sortBy); 127 | } 128 | props.fetchData(url); 129 | } 130 | }, [quality, genre, rating, sortBy]); 131 | 132 | const searchBox = ( 133 |
134 |
135 |
136 |
137 | { 144 | handleEnterKey(e.code); 145 | }} 146 | /> 147 |
148 |
149 | { 154 | e.currentTarget.blur(); 155 | handleSearch(); 156 | }} 157 | /> 158 |
159 |
160 |
161 |
162 | ); 163 | 164 | return ( 165 | 166 |
167 | {searchBox} 168 |
169 |
170 | 177 | 184 | 191 | 198 |
199 |
200 |
201 |
202 | 203 | ); 204 | } 205 | -------------------------------------------------------------------------------- /src/components/spinner/Spinner.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function Spinner() { 4 | return ( 5 |
6 | 13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import { BrowserRouter, createHashRouter, RouterProvider } from 'react-router-dom'; 4 | import App from './App'; 5 | 6 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 7 | 8 | 9 | 10 | 11 | 12 | ); 13 | -------------------------------------------------------------------------------- /src/pages/EntryPage.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useEffect, useState } from 'react'; 2 | import SearchAndFilterBox from '../components/search/SearchAndFilterBox'; 3 | import Spinner from '../components/spinner/Spinner'; 4 | import MovieList from '../components/movielist/MovieList'; 5 | import Heading from '../components/header/Heading'; 6 | import { AppContext } from '../store/AppContextProvider'; 7 | 8 | export default function EntryPage() { 9 | const [searchResponse, updateSearchResponse] = useState(''); 10 | const [loading, updateLoading] = useState(false); 11 | const [previousURL, updatePreviousUrl] = useState( 12 | new URL('https://yts.mx/api/v2/list_movies.json') 13 | ); 14 | const context = useContext(AppContext); 15 | 16 | const fetchData = (url: URL) => { 17 | updatePreviousUrl(url); 18 | updateLoading(true); 19 | fetch(url.href) 20 | .then(async (res) => await res.json()) 21 | .then((data) => { 22 | updateSearchResponse(data); 23 | updateLoading(false); 24 | }) 25 | .catch((err) => { 26 | updateSearchResponse(err.toString()); 27 | updateLoading(false); 28 | }); 29 | }; 30 | 31 | const handlePageChange = (page: string) => { 32 | context.updateCurrentPage(page); 33 | previousURL.searchParams.set('page', page); 34 | fetchData(previousURL); 35 | }; 36 | 37 | useEffect(() => { 38 | if (context.isQueryStateModified) { 39 | if (context.currentQueries.search.trim() !== '') { 40 | previousURL.searchParams.set( 41 | 'query_term', 42 | context.currentQueries.search 43 | ); 44 | } 45 | if (context.currentQueries.quality.toLowerCase() !== 'all') { 46 | previousURL.searchParams.set('quality', context.currentQueries.quality); 47 | } 48 | if (context.currentQueries.genre.toLowerCase() !== 'all') { 49 | previousURL.searchParams.set('genre', context.currentQueries.genre); 50 | } 51 | if (context.currentQueries.rating.toLowerCase() !== 'all') { 52 | previousURL.searchParams.set( 53 | 'minimum_rating', 54 | context.currentQueries.rating.replace('+', '') 55 | ); 56 | } 57 | if (context.currentQueries.sortBy.toLowerCase() !== 'date_added') { 58 | previousURL.searchParams.set('sort_by', context.currentQueries.sortBy); 59 | } 60 | } 61 | fetchData(previousURL); 62 | }, []); 63 | return ( 64 | 65 | 66 | 67 | {loading ? ( 68 | 69 | ) : ( 70 | 75 | )} 76 | {/* footer */} 77 | 78 | ); 79 | } 80 | -------------------------------------------------------------------------------- /src/pages/MovieDetails.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import Heading from '../components/header/Heading'; 3 | import Spinner from '../components/spinner/Spinner'; 4 | import MovieIntro from '../components/movieintro/MovieIntro'; 5 | import MovieScreenshots from '../components/movieScreenshot/MovieScreenshots'; 6 | import MovieSynopsisTrailer from '../components/movieSynopsisAndTrailer/MovieSynopsisTrailer'; 7 | import Footer from '../components/footer/footer'; 8 | import { useParams } from 'react-router-dom'; 9 | 10 | const BaseURL = 'https://yts.mx/api/v2/movie_details.json'; 11 | 12 | export default function MovieDetails() { 13 | const [loading, updateLoading] = useState(true); 14 | const [response, updateResponse] = useState(''); 15 | const [suggestionResponse, updateSuggestionResponse] = useState(''); 16 | 17 | const { id: movie_id } = useParams(); 18 | 19 | const fetchData = async (movieURL: URL, suggestionURL: URL) => { 20 | updateLoading(true); 21 | try { 22 | const res = await Promise.all([ 23 | fetch(movieURL.href), 24 | fetch(suggestionURL.href), 25 | ]); 26 | const data = await Promise.all([res[0].json(), res[1].json()]); 27 | updateResponse(data[0]); 28 | updateSuggestionResponse(data[1]); 29 | } catch (err: any) { 30 | updateResponse(err.toString()); 31 | } 32 | updateLoading(false); 33 | }; 34 | 35 | useEffect(() => { 36 | const movieUrl = new URL(BaseURL); 37 | // @ts-expect-error{ 38 | movieUrl.searchParams.set('movie_id', movie_id); 39 | movieUrl.searchParams.set('with_images', 'true'); 40 | movieUrl.searchParams.set('with_cast', 'true'); 41 | 42 | const suggestionUrl = new URL( 43 | 'https://yts.mx/api/v2/movie_suggestions.json?movie_id=' + movie_id 44 | ); 45 | 46 | fetchData(movieUrl, suggestionUrl); 47 | }, [movie_id]); 48 | 49 | return ( 50 | 51 | 52 | {loading ? ( 53 | 54 | ) : ( 55 |
66 |
67 | 71 | 83 | 90 |
91 |
92 |
93 | )} 94 |
95 | ); 96 | } 97 | -------------------------------------------------------------------------------- /src/store/AppContextProvider.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useState } from 'react'; 2 | 3 | interface Queries { 4 | search: string; 5 | quality: string; 6 | genre: string; 7 | rating: string; 8 | sortBy: string; 9 | } 10 | 11 | interface UpdateQueries { 12 | search: Function; 13 | quality: Function; 14 | genre: Function; 15 | rating: Function; 16 | sortBy: Function; 17 | } 18 | 19 | interface IContext { 20 | currentQueries: Queries; 21 | updateQueries: UpdateQueries; 22 | isQueryStateModified: Function; 23 | currentPage: string; 24 | updateCurrentPage: Function; 25 | } 26 | const defaultQueries = { 27 | search: '', 28 | quality: 'All', 29 | genre: 'All', 30 | rating: 'All', 31 | sortBy: 'date_added', 32 | }; 33 | 34 | const AppContext = createContext({ 35 | currentQueries: defaultQueries, 36 | updateQueries: { 37 | search: Function, 38 | quality: Function, 39 | genre: Function, 40 | rating: Function, 41 | sortBy: Function, 42 | }, 43 | isQueryStateModified: Function, 44 | currentPage: '1', 45 | updateCurrentPage: Function, 46 | }); 47 | 48 | export default function AppContextProvider(props: { 49 | children: React.ReactNode; 50 | }) { 51 | const [search, updateSearch] = useState(''); 52 | const [quality, updateQuality] = useState('All'); 53 | const [genre, updateGenre] = useState('All'); 54 | const [rating, updateRating] = useState('All'); 55 | const [sortBy, updateSortBy] = useState('date_added'); 56 | const [currentPage, updateCurrentPage] = useState('1'); 57 | 58 | const isQueryStateModified = (): boolean => { 59 | return ( 60 | search.trim() === '' && 61 | quality === 'All' && 62 | genre === 'All' && 63 | rating === 'All' && 64 | sortBy === 'date_added' 65 | ); 66 | }; 67 | 68 | const context: IContext = { 69 | currentQueries: { 70 | search, 71 | quality, 72 | genre, 73 | rating, 74 | sortBy, 75 | }, 76 | updateQueries: { 77 | search: updateSearch, 78 | quality: updateQuality, 79 | genre: updateGenre, 80 | rating: updateRating, 81 | sortBy: updateSortBy, 82 | }, 83 | isQueryStateModified, 84 | currentPage, 85 | updateCurrentPage, 86 | }; 87 | 88 | return ( 89 | {props.children} 90 | ); 91 | } 92 | 93 | export { AppContext, AppContextProvider, defaultQueries }; 94 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true, 7 | "strictNullChecks": true, 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | build: { 8 | outDir: 'build', 9 | }, 10 | }); 11 | --------------------------------------------------------------------------------