Merge branch 'refs/heads/development' into non-hi-only

# Conflicts:
#	bazarr/app/database.py
#	bazarr/subtitles/indexer/movies.py
#	bazarr/subtitles/indexer/series.py
non-hi-only
morpheus65535 6 months ago
commit 6be6e2c1ad

@ -36,7 +36,7 @@ jobs:
- name: Setup NodeJS
uses: actions/setup-node@v4
with:
node-version: "lts/*"
node-version-file: "${{ env.UI_DIRECTORY }}/.nvmrc"
- name: Install dependencies
run: npm install

@ -38,7 +38,7 @@ jobs:
- name: Setup NodeJS
uses: actions/setup-node@v4
with:
node-version: "lts/*"
node-version-file: "${{ env.UI_DIRECTORY }}/.nvmrc"
- name: Install Global Tools
run: npm install -g release-it auto-changelog

@ -40,7 +40,7 @@ jobs:
- name: Setup NodeJS
uses: actions/setup-node@v4
with:
node-version: "lts/*"
node-version-file: "${{ env.UI_DIRECTORY }}/.nvmrc"
- name: Install Global Tools
run: npm install -g release-it auto-changelog

@ -24,7 +24,7 @@ jobs:
- name: Setup NodeJS
uses: actions/setup-node@v4
with:
node-version: "lts/*"
node-version-file: "${{ env.UI_DIRECTORY }}/.nvmrc"
- name: Install UI Dependencies
run: npm install

@ -2,9 +2,12 @@
## Dependencies
- [Node.js](https://nodejs.org/)
- Either [Node.js](https://nodejs.org/) installed manually or using [Node Version Manager](https://github.com/nvm-sh/nvm)
- npm (included in Node.js)
> The recommended Node version to use and maintained is managed on the `.nvmrc` file. You can either install manually
> or use `nvm install` followed by `nvm use`.
## Getting Started
1. Clone or download this repository

@ -1,10 +1,11 @@
// eslint-disable-next-line no-restricted-imports
import { dependencies } from "../package.json";
const vendors = [
"react",
"react-router-dom",
"react-dom",
"react-query",
"@tanstack/react-query",
"axios",
"socket.io-client",
];

@ -15,10 +15,10 @@
"@mantine/hooks": "^7.10.1",
"@mantine/modals": "^7.10.1",
"@mantine/notifications": "^7.10.1",
"@tanstack/react-query": "^5.40.1",
"axios": "^1.6.8",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-query": "^3.39.3",
"react-router-dom": "^6.23.1",
"socket.io-client": "^4.7.5"
},
@ -28,7 +28,8 @@
"@fortawesome/free-brands-svg-icons": "^6.5.2",
"@fortawesome/free-regular-svg-icons": "^6.5.2",
"@fortawesome/free-solid-svg-icons": "^6.5.2",
"@fortawesome/react-fontawesome": "^0.2.0",
"@fortawesome/react-fontawesome": "^0.2.2",
"@tanstack/react-query-devtools": "^5.40.1",
"@testing-library/jest-dom": "^6.4.2",
"@testing-library/react": "^15.0.5",
"@testing-library/user-event": "^14.5.2",
@ -2792,9 +2793,9 @@
}
},
"node_modules/@fortawesome/react-fontawesome": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.0.tgz",
"integrity": "sha512-uHg75Rb/XORTtVt7OS9WoK8uM276Ufi7gCzshVWkUJbHhh3svsUUeqXerrM96Wm7fRiDzfKRwSoahhMIkGAYHw==",
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.2.tgz",
"integrity": "sha512-EnkrprPNqI6SXJl//m29hpaNzOp1bruISWaOiRtkMi/xSvHJlzc2j2JAYS7egxt/EbjSNV/k6Xy0AQI6vB2+1g==",
"dev": true,
"dependencies": {
"prop-types": "^15.8.1"
@ -3331,6 +3332,61 @@
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
"integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg=="
},
"node_modules/@tanstack/query-core": {
"version": "5.40.0",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.40.0.tgz",
"integrity": "sha512-eD8K8jsOIq0Z5u/QbvOmfvKKE/XC39jA7yv4hgpl/1SRiU+J8QCIwgM/mEHuunQsL87dcvnHqSVLmf9pD4CiaA==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/query-devtools": {
"version": "5.37.1",
"resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.37.1.tgz",
"integrity": "sha512-XcG4IIHIv0YQKrexTqo2zogQWR1Sz672tX2KsfE9kzB+9zhx44vRKH5si4WDILE1PIWQpStFs/NnrDQrBAUQpg==",
"dev": true,
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/react-query": {
"version": "5.40.1",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.40.1.tgz",
"integrity": "sha512-gOcmu+gpFd2taHrrgMM9RemLYYEDYfsCqszxCC0xtx+csDa4R8t7Hr7SfWXQP13S2sF+mOxySo/+FNXJFYBqcA==",
"license": "MIT",
"dependencies": {
"@tanstack/query-core": "5.40.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"react": "^18.0.0"
}
},
"node_modules/@tanstack/react-query-devtools": {
"version": "5.40.1",
"resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.40.1.tgz",
"integrity": "sha512-/AN2UsbuL+28/KSlBkVHq/4chHTEp4l2UWTKWixXbn4pprLQrZGmQTAKN4tYxZDuNwNZY5+Zp67pDfXj+F/UBA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@tanstack/query-devtools": "5.37.1"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"@tanstack/react-query": "^5.40.1",
"react": "^18 || ^19"
}
},
"node_modules/@testing-library/dom": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.0.0.tgz",
@ -4726,15 +4782,8 @@
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
"node_modules/big-integer": {
"version": "1.6.52",
"resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz",
"integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==",
"engines": {
"node": ">=0.6"
}
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true
},
"node_modules/binary-extensions": {
"version": "2.3.0",
@ -4752,38 +4801,25 @@
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/braces": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dev": true,
"license": "MIT",
"dependencies": {
"fill-range": "^7.0.1"
"fill-range": "^7.1.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/broadcast-channel": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/broadcast-channel/-/broadcast-channel-3.7.0.tgz",
"integrity": "sha512-cIAKJXAxGJceNZGTZSBzMxzyOn72cVgPnKx4dc6LRjQgbaJUQqhy5rzL3zbMxkMWsGKkv2hSFkPRMEXfoMZ2Mg==",
"dependencies": {
"@babel/runtime": "^7.7.2",
"detect-node": "^2.1.0",
"js-sha3": "0.8.0",
"microseconds": "0.2.0",
"nano-time": "1.0.0",
"oblivious-set": "1.0.0",
"rimraf": "3.0.2",
"unload": "2.2.0"
}
},
"node_modules/browserslist": {
"version": "4.23.0",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz",
@ -5029,7 +5065,8 @@
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true
},
"node_modules/confusing-browser-globals": {
"version": "1.0.11",
@ -5412,11 +5449,6 @@
"node": ">=6"
}
},
"node_modules/detect-node": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz",
"integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g=="
},
"node_modules/detect-node-es": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
@ -6378,10 +6410,11 @@
}
},
"node_modules/fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dev": true,
"license": "MIT",
"dependencies": {
"to-regex-range": "^5.0.1"
},
@ -6492,7 +6525,8 @@
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
"dev": true
},
"node_modules/fsevents": {
"version": "2.3.3",
@ -6622,6 +6656,7 @@
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"dev": true,
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
@ -6931,6 +6966,7 @@
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
"dev": true,
"dependencies": {
"once": "^1.3.0",
"wrappy": "1"
@ -6939,7 +6975,8 @@
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true
},
"node_modules/internal-slot": {
"version": "1.0.7",
@ -7180,6 +7217,7 @@
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.12.0"
}
@ -7601,11 +7639,6 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
"node_modules/js-sha3": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz",
"integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q=="
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@ -7965,15 +7998,6 @@
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"dev": true
},
"node_modules/match-sorter": {
"version": "6.3.4",
"resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-6.3.4.tgz",
"integrity": "sha512-jfZW7cWS5y/1xswZo8VBOdudUiSd9nifYRWphc9M5D/ee4w4AoXLgBEdRbgVaxbMuagBPeUC5y2Hi8DO6o9aDg==",
"dependencies": {
"@babel/runtime": "^7.23.8",
"remove-accents": "0.5.0"
}
},
"node_modules/merge-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
@ -8002,11 +8026,6 @@
"node": ">=8.6"
}
},
"node_modules/microseconds": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/microseconds/-/microseconds-0.2.0.tgz",
"integrity": "sha512-n7DHHMjR1avBbSpsTBj6fmMGh2AGrifVV4e+WYc3Q9lO+xnSZ3NyhcBND3vzzatt05LFhoKFRxrIyklmLlUtyA=="
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
@ -8048,6 +8067,7 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"dependencies": {
"brace-expansion": "^1.1.7"
},
@ -8099,14 +8119,6 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/nano-time": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/nano-time/-/nano-time-1.0.0.tgz",
"integrity": "sha512-flnngywOoQ0lLQOTRNexn2gGSNuM9bKj9RZAWSzhQ+UJYaAFG9bac4DW9VHjUAzrOaIcajHybCTHe/bkvozQqA==",
"dependencies": {
"big-integer": "^1.6.16"
}
},
"node_modules/nanoid": {
"version": "3.3.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
@ -8294,15 +8306,11 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/oblivious-set": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/oblivious-set/-/oblivious-set-1.0.0.tgz",
"integrity": "sha512-z+pI07qxo4c2CulUHCDf9lcqDlMSo72N/4rLUpRXf6fu+q8vjt8y0xS+Tlf8NTJDdTXHbdeO1n3MlbctwEoXZw=="
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dev": true,
"dependencies": {
"wrappy": "1"
}
@ -8424,6 +8432,7 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
@ -8857,31 +8866,6 @@
"react-dom": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/react-query": {
"version": "3.39.3",
"resolved": "https://registry.npmjs.org/react-query/-/react-query-3.39.3.tgz",
"integrity": "sha512-nLfLz7GiohKTJDuT4us4X3h/8unOh+00MLb2yJoGTPjxKs2bc1iDhkNx2bd5MKklXnOD3NrVZ+J2UXujA5In4g==",
"dependencies": {
"@babel/runtime": "^7.5.5",
"broadcast-channel": "^3.4.1",
"match-sorter": "^6.0.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
},
"react-native": {
"optional": true
}
}
},
"node_modules/react-refresh": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz",
@ -9213,11 +9197,6 @@
"jsesc": "bin/jsesc"
}
},
"node_modules/remove-accents": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.5.0.tgz",
"integrity": "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A=="
},
"node_modules/requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
@ -9264,6 +9243,7 @@
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
"dev": true,
"dependencies": {
"glob": "^7.1.3"
},
@ -9866,6 +9846,7 @@
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-number": "^7.0.0"
},
@ -10154,15 +10135,6 @@
"node": ">= 4.0.0"
}
},
"node_modules/unload": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/unload/-/unload-2.2.0.tgz",
"integrity": "sha512-B60uB5TNBLtN6/LsgAf3udH9saB5p7gqJwcFfbOEZ8BcBHnGwCf6G/TGiEqkRAxX7zAFIUtzdrXQSdL3Q/wqNA==",
"dependencies": {
"@babel/runtime": "^7.6.2",
"detect-node": "^2.0.4"
}
},
"node_modules/update-browserslist-db": {
"version": "1.0.13",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz",
@ -10974,7 +10946,8 @@
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"dev": true
},
"node_modules/ws": {
"version": "8.16.0",

@ -19,10 +19,10 @@
"@mantine/hooks": "^7.10.1",
"@mantine/modals": "^7.10.1",
"@mantine/notifications": "^7.10.1",
"@tanstack/react-query": "^5.40.1",
"axios": "^1.6.8",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-query": "^3.39.3",
"react-router-dom": "^6.23.1",
"socket.io-client": "^4.7.5"
},
@ -32,7 +32,8 @@
"@fortawesome/free-brands-svg-icons": "^6.5.2",
"@fortawesome/free-regular-svg-icons": "^6.5.2",
"@fortawesome/free-solid-svg-icons": "^6.5.2",
"@fortawesome/react-fontawesome": "^0.2.0",
"@fortawesome/react-fontawesome": "^0.2.2",
"@tanstack/react-query-devtools": "^5.40.1",
"@testing-library/jest-dom": "^6.4.2",
"@testing-library/react": "^15.0.5",
"@testing-library/user-event": "^14.5.2",

@ -64,10 +64,9 @@ const AppHeader: FunctionComponent = () => {
label="System"
tooltip={{ position: "left", openDelay: 2000 }}
loading={offline}
color={offline ? "yellow" : undefined}
c={offline ? "yellow" : undefined}
icon={faGear}
size="lg"
variant="light"
></Action>
</Menu.Target>
<Menu.Dropdown>

@ -130,8 +130,7 @@ const AppNavbar: FunctionComponent = () => {
<Group gap="xs">
<Action
label="Change Theme"
color={dark ? "yellow" : "indigo"}
variant="subtle"
c={dark ? "yellow" : "indigo"}
onClick={() => toggleColorScheme()}
icon={dark ? faSun : faMoon}
></Action>
@ -139,12 +138,7 @@ const AppNavbar: FunctionComponent = () => {
href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=XHHRWXT9YB7WE&source=url"
target="_blank"
>
<Action
label="Donate"
icon={faHeart}
variant="subtle"
color="red"
></Action>
<Action label="Donate" icon={faHeart} c="red"></Action>
</Anchor>
</Group>
</AppShell.Section>

@ -1,7 +1,6 @@
import { FunctionComponent, PropsWithChildren } from "react";
import {
ActionIcon,
AppShell,
Badge,
Button,
createTheme,
@ -13,7 +12,6 @@ import "@mantine/core/styles.layer.css";
import "@mantine/notifications/styles.layer.css";
import styleVars from "@/assets/_variables.module.scss";
import actionIconClasses from "@/assets/action_icon.module.scss";
import appShellClasses from "@/assets/app_shell.module.scss";
import badgeClasses from "@/assets/badge.module.scss";
import buttonClasses from "@/assets/button.module.scss";
import paginationClasses from "@/assets/pagination.module.scss";
@ -39,9 +37,6 @@ const themeProvider = createTheme({
ActionIcon: ActionIcon.extend({
classNames: actionIconClasses,
}),
AppShell: AppShell.extend({
classNames: appShellClasses,
}),
Badge: Badge.extend({
classNames: badgeClasses,
}),

@ -1,9 +1,10 @@
import { useEffect } from "react";
import {
QueryClient,
useMutation,
useQuery,
useQueryClient,
} from "react-query";
} from "@tanstack/react-query";
import { usePaginationQuery } from "@/apis/queries/hooks";
import { QueryKeys } from "@/apis/queries/keys";
import api from "@/apis/raw";
@ -26,28 +27,36 @@ const cacheEpisodes = (client: QueryClient, episodes: Item.Episode[]) => {
export function useEpisodesByIds(ids: number[]) {
const client = useQueryClient();
return useQuery(
[QueryKeys.Series, QueryKeys.Episodes, ids],
() => api.episodes.byEpisodeId(ids),
{
onSuccess: (data) => {
cacheEpisodes(client, data);
},
},
);
const query = useQuery({
queryKey: [QueryKeys.Series, QueryKeys.Episodes, ids],
queryFn: () => api.episodes.byEpisodeId(ids),
});
useEffect(() => {
if (query.isSuccess && query.data) {
cacheEpisodes(client, query.data);
}
}, [query.isSuccess, query.data, client]);
return query;
}
export function useEpisodesBySeriesId(id: number) {
const client = useQueryClient();
return useQuery(
[QueryKeys.Series, id, QueryKeys.Episodes, QueryKeys.All],
() => api.episodes.bySeriesId([id]),
{
onSuccess: (data) => {
cacheEpisodes(client, data);
},
},
);
const query = useQuery({
queryKey: [QueryKeys.Series, id, QueryKeys.Episodes, QueryKeys.All],
queryFn: () => api.episodes.bySeriesId([id]),
});
useEffect(() => {
if (query.isSuccess && query.data) {
cacheEpisodes(client, query.data);
}
}, [query.isSuccess, query.data, client]);
return query;
}
export function useEpisodeWantedPagination() {
@ -57,17 +66,18 @@ export function useEpisodeWantedPagination() {
}
export function useEpisodeBlacklist() {
return useQuery(
[QueryKeys.Series, QueryKeys.Episodes, QueryKeys.Blacklist],
() => api.episodes.blacklist(),
);
return useQuery({
queryKey: [QueryKeys.Series, QueryKeys.Episodes, QueryKeys.Blacklist],
queryFn: () => api.episodes.blacklist(),
});
}
export function useEpisodeAddBlacklist() {
const client = useQueryClient();
return useMutation(
[QueryKeys.Series, QueryKeys.Episodes, QueryKeys.Blacklist],
(param: {
return useMutation({
mutationKey: [QueryKeys.Series, QueryKeys.Episodes, QueryKeys.Blacklist],
mutationFn: (param: {
seriesId: number;
episodeId: number;
form: FormType.AddBlacklist;
@ -75,35 +85,32 @@ export function useEpisodeAddBlacklist() {
const { seriesId, episodeId, form } = param;
return api.episodes.addBlacklist(seriesId, episodeId, form);
},
{
onSuccess: (_, { seriesId, episodeId }) => {
client.invalidateQueries([
QueryKeys.Series,
QueryKeys.Episodes,
QueryKeys.Blacklist,
]);
client.invalidateQueries([QueryKeys.Series, seriesId]);
},
onSuccess: (_, { seriesId }) => {
client.invalidateQueries({
queryKey: [QueryKeys.Series, QueryKeys.Episodes, QueryKeys.Blacklist],
});
client.invalidateQueries({
queryKey: [QueryKeys.Series, seriesId],
});
},
);
});
}
export function useEpisodeDeleteBlacklist() {
const client = useQueryClient();
return useMutation(
[QueryKeys.Series, QueryKeys.Episodes, QueryKeys.Blacklist],
(param: { all?: boolean; form?: FormType.DeleteBlacklist }) =>
return useMutation({
mutationKey: [QueryKeys.Series, QueryKeys.Episodes, QueryKeys.Blacklist],
mutationFn: (param: { all?: boolean; form?: FormType.DeleteBlacklist }) =>
api.episodes.deleteBlacklist(param.all, param.form),
{
onSuccess: (_, param) => {
client.invalidateQueries([
QueryKeys.Series,
QueryKeys.Episodes,
QueryKeys.Blacklist,
]);
},
onSuccess: (_) => {
client.invalidateQueries({
queryKey: [QueryKeys.Series, QueryKeys.Episodes, QueryKeys.Blacklist],
});
},
);
});
}
export function useEpisodeHistoryPagination() {
@ -115,12 +122,20 @@ export function useEpisodeHistoryPagination() {
}
export function useEpisodeHistory(episodeId?: number) {
return useQuery(
[QueryKeys.Series, QueryKeys.Episodes, QueryKeys.History, episodeId],
() => {
return useQuery({
queryKey: [
QueryKeys.Series,
QueryKeys.Episodes,
QueryKeys.History,
episodeId,
],
queryFn: () => {
if (episodeId) {
return api.episodes.historyBy(episodeId);
}
return [];
},
);
});
}

@ -1,4 +1,4 @@
import { useQuery } from "react-query";
import { useQuery } from "@tanstack/react-query";
import { QueryKeys } from "@/apis/queries/keys";
import api from "@/apis/raw";
@ -8,14 +8,19 @@ export function useHistoryStats(
provider: System.Provider | null,
language: Language.Info | null,
) {
return useQuery(
[QueryKeys.System, QueryKeys.History, { time, action, provider, language }],
() =>
return useQuery({
queryKey: [
QueryKeys.System,
QueryKeys.History,
{ time, action, provider, language },
],
queryFn: () =>
api.history.stats(
time,
action ?? undefined,
provider?.name,
language?.code2,
),
);
});
}

@ -1,23 +1,19 @@
import { useQuery } from "react-query";
import { useQuery } from "@tanstack/react-query";
import { QueryKeys } from "@/apis/queries/keys";
import api from "@/apis/raw";
export function useLanguages(history?: boolean) {
return useQuery(
[QueryKeys.System, QueryKeys.Languages, history ?? false],
() => api.system.languages(history),
{
staleTime: Infinity,
},
);
return useQuery({
queryKey: [QueryKeys.System, QueryKeys.Languages, history ?? false],
queryFn: () => api.system.languages(history),
staleTime: Infinity,
});
}
export function useLanguageProfiles() {
return useQuery(
[QueryKeys.System, QueryKeys.LanguagesProfiles],
() => api.system.languagesProfileList(),
{
staleTime: Infinity,
},
);
return useQuery({
queryKey: [QueryKeys.System, QueryKeys.LanguagesProfiles],
queryFn: () => api.system.languagesProfileList(),
staleTime: Infinity,
});
}

@ -1,9 +1,10 @@
import { useEffect } from "react";
import {
QueryClient,
useMutation,
useQuery,
useQueryClient,
} from "react-query";
} from "@tanstack/react-query";
import { usePaginationQuery } from "@/apis/queries/hooks";
import { QueryKeys } from "@/apis/queries/keys";
import api from "@/apis/raw";
@ -16,31 +17,47 @@ const cacheMovies = (client: QueryClient, movies: Item.Movie[]) => {
export function useMoviesByIds(ids: number[]) {
const client = useQueryClient();
return useQuery([QueryKeys.Movies, ...ids], () => api.movies.movies(ids), {
onSuccess: (data) => {
cacheMovies(client, data);
},
const query = useQuery({
queryKey: [QueryKeys.Movies, ...ids],
queryFn: () => api.movies.movies(ids),
});
useEffect(() => {
if (query.isSuccess && query.data) {
cacheMovies(client, query.data);
}
}, [query.isSuccess, query.data, client]);
return query;
}
export function useMovieById(id: number) {
return useQuery([QueryKeys.Movies, id], async () => {
const response = await api.movies.movies([id]);
return response.length > 0 ? response[0] : undefined;
return useQuery({
queryKey: [QueryKeys.Movies, id],
queryFn: async () => {
const response = await api.movies.movies([id]);
return response.length > 0 ? response[0] : undefined;
},
});
}
export function useMovies() {
const client = useQueryClient();
return useQuery(
[QueryKeys.Movies, QueryKeys.All],
() => api.movies.movies(),
{
onSuccess: (data) => {
cacheMovies(client, data);
},
},
);
const query = useQuery({
queryKey: [QueryKeys.Movies, QueryKeys.All],
queryFn: () => api.movies.movies(),
});
useEffect(() => {
if (query.isSuccess && query.data) {
cacheMovies(client, query.data);
}
}, [query.isSuccess, query.data, client]);
return query;
}
export function useMoviesPagination() {
@ -51,32 +68,36 @@ export function useMoviesPagination() {
export function useMovieModification() {
const client = useQueryClient();
return useMutation(
[QueryKeys.Movies],
(form: FormType.ModifyItem) => api.movies.modify(form),
{
onSuccess: (_, form) => {
form.id.forEach((v) => {
client.invalidateQueries([QueryKeys.Movies, v]);
return useMutation({
mutationKey: [QueryKeys.Movies],
mutationFn: (form: FormType.ModifyItem) => api.movies.modify(form),
onSuccess: (_, form) => {
form.id.forEach((v) => {
client.invalidateQueries({
queryKey: [QueryKeys.Movies, v],
});
// TODO: query less
client.invalidateQueries([QueryKeys.Movies]);
},
});
// TODO: query less
client.invalidateQueries({
queryKey: [QueryKeys.Movies],
});
},
);
});
}
export function useMovieAction() {
const client = useQueryClient();
return useMutation(
[QueryKeys.Actions, QueryKeys.Movies],
(form: FormType.MoviesAction) => api.movies.action(form),
{
onSuccess: () => {
client.invalidateQueries([QueryKeys.Movies]);
},
return useMutation({
mutationKey: [QueryKeys.Actions, QueryKeys.Movies],
mutationFn: (form: FormType.MoviesAction) => api.movies.action(form),
onSuccess: () => {
client.invalidateQueries({
queryKey: [QueryKeys.Movies],
});
},
);
});
}
export function useMovieWantedPagination() {
@ -86,40 +107,48 @@ export function useMovieWantedPagination() {
}
export function useMovieBlacklist() {
return useQuery([QueryKeys.Movies, QueryKeys.Blacklist], () =>
api.movies.blacklist(),
);
return useQuery({
queryKey: [QueryKeys.Movies, QueryKeys.Blacklist],
queryFn: () => api.movies.blacklist(),
});
}
export function useMovieAddBlacklist() {
const client = useQueryClient();
return useMutation(
[QueryKeys.Movies, QueryKeys.Blacklist],
(param: { id: number; form: FormType.AddBlacklist }) => {
return useMutation({
mutationKey: [QueryKeys.Movies, QueryKeys.Blacklist],
mutationFn: (param: { id: number; form: FormType.AddBlacklist }) => {
const { id, form } = param;
return api.movies.addBlacklist(id, form);
},
{
onSuccess: (_, { id }) => {
client.invalidateQueries([QueryKeys.Movies, QueryKeys.Blacklist]);
client.invalidateQueries([QueryKeys.Movies, id]);
},
onSuccess: (_, { id }) => {
client.invalidateQueries({
queryKey: [QueryKeys.Movies, QueryKeys.Blacklist],
});
client.invalidateQueries({
queryKey: [QueryKeys.Movies, id],
});
},
);
});
}
export function useMovieDeleteBlacklist() {
const client = useQueryClient();
return useMutation(
[QueryKeys.Movies, QueryKeys.Blacklist],
(param: { all?: boolean; form?: FormType.DeleteBlacklist }) =>
return useMutation({
mutationKey: [QueryKeys.Movies, QueryKeys.Blacklist],
mutationFn: (param: { all?: boolean; form?: FormType.DeleteBlacklist }) =>
api.movies.deleteBlacklist(param.all, param.form),
{
onSuccess: (_, param) => {
client.invalidateQueries([QueryKeys.Movies, QueryKeys.Blacklist]);
},
onSuccess: (_, param) => {
client.invalidateQueries({
queryKey: [QueryKeys.Movies, QueryKeys.Blacklist],
});
},
);
});
}
export function useMovieHistoryPagination() {
@ -131,9 +160,15 @@ export function useMovieHistoryPagination() {
}
export function useMovieHistory(radarrId?: number) {
return useQuery([QueryKeys.Movies, QueryKeys.History, radarrId], () => {
if (radarrId) {
return api.movies.historyBy(radarrId);
}
return useQuery({
queryKey: [QueryKeys.Movies, QueryKeys.History, radarrId],
queryFn: () => {
if (radarrId) {
return api.movies.historyBy(radarrId);
}
return [];
},
});
}

@ -1,66 +1,82 @@
import { useMutation, useQuery, useQueryClient } from "react-query";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { QueryKeys } from "@/apis/queries/keys";
import api from "@/apis/raw";
export function useSystemProviders(history?: boolean) {
return useQuery(
[QueryKeys.System, QueryKeys.Providers, history ?? false],
() => api.providers.providers(history),
);
return useQuery({
queryKey: [QueryKeys.System, QueryKeys.Providers, history ?? false],
queryFn: () => api.providers.providers(history),
});
}
export function useMoviesProvider(radarrId?: number) {
return useQuery(
[QueryKeys.System, QueryKeys.Providers, QueryKeys.Movies, radarrId],
() => {
return useQuery({
queryKey: [
QueryKeys.System,
QueryKeys.Providers,
QueryKeys.Movies,
radarrId,
],
queryFn: () => {
if (radarrId) {
return api.providers.movies(radarrId);
}
return [];
},
{
staleTime: 0,
},
);
staleTime: 0,
});
}
export function useEpisodesProvider(episodeId?: number) {
return useQuery(
[QueryKeys.System, QueryKeys.Providers, QueryKeys.Episodes, episodeId],
() => {
return useQuery({
queryKey: [
QueryKeys.System,
QueryKeys.Providers,
QueryKeys.Episodes,
episodeId,
],
queryFn: () => {
if (episodeId) {
return api.providers.episodes(episodeId);
}
return [];
},
{
staleTime: 0,
},
);
staleTime: 0,
});
}
export function useResetProvider() {
const client = useQueryClient();
return useMutation(
[QueryKeys.System, QueryKeys.Providers],
() => api.providers.reset(),
{
onSuccess: () => {
client.invalidateQueries([QueryKeys.System, QueryKeys.Providers]);
},
return useMutation({
mutationKey: [QueryKeys.System, QueryKeys.Providers],
mutationFn: () => api.providers.reset(),
onSuccess: () => {
client.invalidateQueries({
queryKey: [QueryKeys.System, QueryKeys.Providers],
});
},
);
});
}
export function useDownloadEpisodeSubtitles() {
const client = useQueryClient();
return useMutation(
[
return useMutation({
mutationKey: [
QueryKeys.System,
QueryKeys.Providers,
QueryKeys.Subtitles,
QueryKeys.Episodes,
],
(param: {
mutationFn: (param: {
seriesId: number;
episodeId: number;
form: FormType.ManualDownload;
@ -70,30 +86,33 @@ export function useDownloadEpisodeSubtitles() {
param.episodeId,
param.form,
),
{
onSuccess: (_, param) => {
client.invalidateQueries([QueryKeys.Series, param.seriesId]);
},
onSuccess: (_, param) => {
client.invalidateQueries({
queryKey: [QueryKeys.Series, param.seriesId],
});
},
);
});
}
export function useDownloadMovieSubtitles() {
const client = useQueryClient();
return useMutation(
[
return useMutation({
mutationKey: [
QueryKeys.System,
QueryKeys.Providers,
QueryKeys.Subtitles,
QueryKeys.Movies,
],
(param: { radarrId: number; form: FormType.ManualDownload }) =>
mutationFn: (param: { radarrId: number; form: FormType.ManualDownload }) =>
api.providers.downloadMovieSubtitle(param.radarrId, param.form),
{
onSuccess: (_, param) => {
client.invalidateQueries([QueryKeys.Movies, param.radarrId]);
},
onSuccess: (_, param) => {
client.invalidateQueries({
queryKey: [QueryKeys.Movies, param.radarrId],
});
},
);
});
}

@ -1,9 +1,10 @@
import { useEffect } from "react";
import {
QueryClient,
useMutation,
useQuery,
useQueryClient,
} from "react-query";
} from "@tanstack/react-query";
import { usePaginationQuery } from "@/apis/queries/hooks";
import { QueryKeys } from "@/apis/queries/keys";
import api from "@/apis/raw";
@ -16,31 +17,47 @@ function cacheSeries(client: QueryClient, series: Item.Series[]) {
export function useSeriesByIds(ids: number[]) {
const client = useQueryClient();
return useQuery([QueryKeys.Series, ...ids], () => api.series.series(ids), {
onSuccess: (data) => {
cacheSeries(client, data);
},
const query = useQuery({
queryKey: [QueryKeys.Series, ...ids],
queryFn: () => api.series.series(ids),
});
useEffect(() => {
if (query.isSuccess && query.data) {
cacheSeries(client, query.data);
}
}, [query.isSuccess, query.data, client]);
return query;
}
export function useSeriesById(id: number) {
return useQuery([QueryKeys.Series, id], async () => {
const response = await api.series.series([id]);
return response.length > 0 ? response[0] : undefined;
return useQuery({
queryKey: [QueryKeys.Series, id],
queryFn: async () => {
const response = await api.series.series([id]);
return response.length > 0 ? response[0] : undefined;
},
});
}
export function useSeries() {
const client = useQueryClient();
return useQuery(
[QueryKeys.Series, QueryKeys.All],
() => api.series.series(),
{
onSuccess: (data) => {
cacheSeries(client, data);
},
},
);
const query = useQuery({
queryKey: [QueryKeys.Series, QueryKeys.All],
queryFn: () => api.series.series(),
});
useEffect(() => {
if (query.isSuccess && query.data) {
cacheSeries(client, query.data);
}
}, [query.isSuccess, query.data, client]);
return query;
}
export function useSeriesPagination() {
@ -51,29 +68,33 @@ export function useSeriesPagination() {
export function useSeriesModification() {
const client = useQueryClient();
return useMutation(
[QueryKeys.Series],
(form: FormType.ModifyItem) => api.series.modify(form),
{
onSuccess: (_, form) => {
form.id.forEach((v) => {
client.invalidateQueries([QueryKeys.Series, v]);
return useMutation({
mutationKey: [QueryKeys.Series],
mutationFn: (form: FormType.ModifyItem) => api.series.modify(form),
onSuccess: (_, form) => {
form.id.forEach((v) => {
client.invalidateQueries({
queryKey: [QueryKeys.Series, v],
});
client.invalidateQueries([QueryKeys.Series]);
},
});
client.invalidateQueries({
queryKey: [QueryKeys.Series],
});
},
);
});
}
export function useSeriesAction() {
const client = useQueryClient();
return useMutation(
[QueryKeys.Actions, QueryKeys.Series],
(form: FormType.SeriesAction) => api.series.action(form),
{
onSuccess: () => {
client.invalidateQueries([QueryKeys.Series]);
},
return useMutation({
mutationKey: [QueryKeys.Actions, QueryKeys.Series],
mutationFn: (form: FormType.SeriesAction) => api.series.action(form),
onSuccess: () => {
client.invalidateQueries({
queryKey: [QueryKeys.Series],
});
},
);
});
}

@ -1,16 +1,28 @@
import { useIsMutating } from "react-query";
import { useIsMutating } from "@tanstack/react-query";
import { QueryKeys } from "@/apis/queries/keys";
export function useIsAnyActionRunning() {
return useIsMutating([QueryKeys.Actions]) > 0;
return (
useIsMutating({
mutationKey: [QueryKeys.Actions],
}) > 0
);
}
export function useIsMovieActionRunning() {
return useIsMutating([QueryKeys.Actions, QueryKeys.Movies]) > 0;
return (
useIsMutating({
mutationKey: [QueryKeys.Actions, QueryKeys.Movies],
}) > 0
);
}
export function useIsSeriesActionRunning() {
return useIsMutating([QueryKeys.Actions, QueryKeys.Series]) > 0;
return (
useIsMutating({
mutationKey: [QueryKeys.Actions, QueryKeys.Series],
}) > 0
);
}
export function useIsAnyMutationRunning() {

@ -1,4 +1,4 @@
import { useMutation, useQuery, useQueryClient } from "react-query";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { QueryKeys } from "@/apis/queries/keys";
import api from "@/apis/raw";
@ -8,23 +8,29 @@ export function useSubtitleAction() {
action: string;
form: FormType.ModifySubtitle;
}
return useMutation(
[QueryKeys.Subtitles],
(param: Param) => api.subtitles.modify(param.action, param.form),
{
onSuccess: (_, param) => {
client.invalidateQueries([QueryKeys.History]);
// TODO: Query less
const { type, id } = param.form;
if (type === "episode") {
client.invalidateQueries([QueryKeys.Series, id]);
} else {
client.invalidateQueries([QueryKeys.Movies, id]);
}
},
return useMutation({
mutationKey: [QueryKeys.Subtitles],
mutationFn: (param: Param) =>
api.subtitles.modify(param.action, param.form),
onSuccess: (_, param) => {
client.invalidateQueries({
queryKey: [QueryKeys.History],
});
// TODO: Query less
const { type, id } = param.form;
if (type === "episode") {
client.invalidateQueries({
queryKey: [QueryKeys.Series, id],
});
} else {
client.invalidateQueries({
queryKey: [QueryKeys.Movies, id],
});
}
},
);
});
}
export function useEpisodeSubtitleModification() {
@ -36,42 +42,48 @@ export function useEpisodeSubtitleModification() {
form: T;
}
const download = useMutation(
[QueryKeys.Subtitles, QueryKeys.Episodes],
(param: Param<FormType.Subtitle>) =>
const download = useMutation({
mutationKey: [QueryKeys.Subtitles, QueryKeys.Episodes],
mutationFn: (param: Param<FormType.Subtitle>) =>
api.episodes.downloadSubtitles(
param.seriesId,
param.episodeId,
param.form,
),
{
onSuccess: (_, param) => {
client.invalidateQueries([QueryKeys.Series, param.seriesId]);
},
onSuccess: (_, param) => {
client.invalidateQueries({
queryKey: [QueryKeys.Series, param.seriesId],
});
},
);
});
const remove = useMutation(
[QueryKeys.Subtitles, QueryKeys.Episodes],
(param: Param<FormType.DeleteSubtitle>) =>
const remove = useMutation({
mutationKey: [QueryKeys.Subtitles, QueryKeys.Episodes],
mutationFn: (param: Param<FormType.DeleteSubtitle>) =>
api.episodes.deleteSubtitles(param.seriesId, param.episodeId, param.form),
{
onSuccess: (_, param) => {
client.invalidateQueries([QueryKeys.Series, param.seriesId]);
},
onSuccess: (_, param) => {
client.invalidateQueries({
queryKey: [QueryKeys.Series, param.seriesId],
});
},
);
});
const upload = useMutation(
[QueryKeys.Subtitles, QueryKeys.Episodes],
(param: Param<FormType.UploadSubtitle>) =>
const upload = useMutation({
mutationKey: [QueryKeys.Subtitles, QueryKeys.Episodes],
mutationFn: (param: Param<FormType.UploadSubtitle>) =>
api.episodes.uploadSubtitles(param.seriesId, param.episodeId, param.form),
{
onSuccess: (_, { seriesId }) => {
client.invalidateQueries([QueryKeys.Series, seriesId]);
},
onSuccess: (_, { seriesId }) => {
client.invalidateQueries({
queryKey: [QueryKeys.Series, seriesId],
});
},
);
});
return { download, remove, upload };
}
@ -84,46 +96,54 @@ export function useMovieSubtitleModification() {
form: T;
}
const download = useMutation(
[QueryKeys.Subtitles, QueryKeys.Movies],
(param: Param<FormType.Subtitle>) =>
const download = useMutation({
mutationKey: [QueryKeys.Subtitles, QueryKeys.Movies],
mutationFn: (param: Param<FormType.Subtitle>) =>
api.movies.downloadSubtitles(param.radarrId, param.form),
{
onSuccess: (_, param) => {
client.invalidateQueries([QueryKeys.Movies, param.radarrId]);
},
onSuccess: (_, param) => {
client.invalidateQueries({
queryKey: [QueryKeys.Movies, param.radarrId],
});
},
);
});
const remove = useMutation({
mutationKey: [QueryKeys.Subtitles, QueryKeys.Movies],
const remove = useMutation(
[QueryKeys.Subtitles, QueryKeys.Movies],
(param: Param<FormType.DeleteSubtitle>) =>
mutationFn: (param: Param<FormType.DeleteSubtitle>) =>
api.movies.deleteSubtitles(param.radarrId, param.form),
{
onSuccess: (_, param) => {
client.invalidateQueries([QueryKeys.Movies, param.radarrId]);
},
onSuccess: (_, param) => {
client.invalidateQueries({
queryKey: [QueryKeys.Movies, param.radarrId],
});
},
);
});
const upload = useMutation(
[QueryKeys.Subtitles, QueryKeys.Movies],
(param: Param<FormType.UploadSubtitle>) =>
const upload = useMutation({
mutationKey: [QueryKeys.Subtitles, QueryKeys.Movies],
mutationFn: (param: Param<FormType.UploadSubtitle>) =>
api.movies.uploadSubtitles(param.radarrId, param.form),
{
onSuccess: (_, { radarrId }) => {
client.invalidateQueries([QueryKeys.Movies, radarrId]);
},
onSuccess: (_, { radarrId }) => {
client.invalidateQueries({
queryKey: [QueryKeys.Movies, radarrId],
});
},
);
});
return { download, remove, upload };
}
export function useSubtitleInfos(names: string[]) {
return useQuery([QueryKeys.Subtitles, QueryKeys.Infos, names], () =>
api.subtitles.info(names),
);
return useQuery({
queryKey: [QueryKeys.Subtitles, QueryKeys.Infos, names],
queryFn: () => api.subtitles.info(names),
});
}
export function useRefTracksByEpisodeId(
@ -131,11 +151,17 @@ export function useRefTracksByEpisodeId(
sonarrEpisodeId: number,
isEpisode: boolean,
) {
return useQuery(
[QueryKeys.Episodes, sonarrEpisodeId, QueryKeys.Subtitles, subtitlesPath],
() => api.subtitles.getRefTracksByEpisodeId(subtitlesPath, sonarrEpisodeId),
{ enabled: isEpisode },
);
return useQuery({
queryKey: [
QueryKeys.Episodes,
sonarrEpisodeId,
QueryKeys.Subtitles,
subtitlesPath,
],
queryFn: () =>
api.subtitles.getRefTracksByEpisodeId(subtitlesPath, sonarrEpisodeId),
enabled: isEpisode,
});
}
export function useRefTracksByMovieId(
@ -143,9 +169,15 @@ export function useRefTracksByMovieId(
radarrMovieId: number,
isMovie: boolean,
) {
return useQuery(
[QueryKeys.Movies, radarrMovieId, QueryKeys.Subtitles, subtitlesPath],
() => api.subtitles.getRefTracksByMovieId(subtitlesPath, radarrMovieId),
{ enabled: isMovie },
);
return useQuery({
queryKey: [
QueryKeys.Movies,
radarrMovieId,
QueryKeys.Subtitles,
subtitlesPath,
],
queryFn: () =>
api.subtitles.getRefTracksByMovieId(subtitlesPath, radarrMovieId),
enabled: isMovie,
});
}

@ -1,20 +1,18 @@
import { useMemo } from "react";
import { useMutation, useQuery, useQueryClient } from "react-query";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { QueryKeys } from "@/apis/queries/keys";
import api from "@/apis/raw";
import { Environment } from "@/utilities";
import { setAuthenticated } from "@/utilities/event";
export function useBadges() {
return useQuery(
[QueryKeys.System, QueryKeys.Badges],
() => api.badges.all(),
{
refetchOnWindowFocus: "always",
refetchInterval: 1000 * 60,
staleTime: 1000 * 10,
},
);
return useQuery({
queryKey: [QueryKeys.System, QueryKeys.Badges],
queryFn: () => api.badges.all(),
refetchOnWindowFocus: "always",
refetchInterval: 1000 * 60,
staleTime: 1000 * 10,
});
}
export function useFileSystem(
@ -22,9 +20,10 @@ export function useFileSystem(
path: string,
enabled: boolean,
) {
return useQuery(
[QueryKeys.FileSystem, type, path],
() => {
return useQuery({
queryKey: [QueryKeys.FileSystem, type, path],
queryFn: () => {
if (type === "bazarr") {
return api.files.bazarr(path);
} else if (type === "radarr") {
@ -32,53 +31,63 @@ export function useFileSystem(
} else if (type === "sonarr") {
return api.files.sonarr(path);
}
return [];
},
{
enabled,
},
);
enabled,
});
}
export function useSystemSettings() {
return useQuery(
[QueryKeys.System, QueryKeys.Settings],
() => api.system.settings(),
{
staleTime: Infinity,
},
);
return useQuery({
queryKey: [QueryKeys.System, QueryKeys.Settings],
queryFn: () => api.system.settings(),
staleTime: Infinity,
});
}
export function useSettingsMutation() {
const client = useQueryClient();
return useMutation(
[QueryKeys.System, QueryKeys.Settings],
(data: LooseObject) => api.system.updateSettings(data),
{
onSuccess: () => {
client.invalidateQueries([QueryKeys.System]);
client.invalidateQueries([QueryKeys.Series]);
client.invalidateQueries([QueryKeys.Episodes]);
client.invalidateQueries([QueryKeys.Movies]);
client.invalidateQueries([QueryKeys.Wanted]);
client.invalidateQueries([QueryKeys.Badges]);
},
return useMutation({
mutationKey: [QueryKeys.System, QueryKeys.Settings],
mutationFn: (data: LooseObject) => api.system.updateSettings(data),
onSuccess: () => {
client.invalidateQueries({
queryKey: [QueryKeys.System],
});
client.invalidateQueries({
queryKey: [QueryKeys.Series],
});
client.invalidateQueries({
queryKey: [QueryKeys.Episodes],
});
client.invalidateQueries({
queryKey: [QueryKeys.Movies],
});
client.invalidateQueries({
queryKey: [QueryKeys.Wanted],
});
client.invalidateQueries({
queryKey: [QueryKeys.Badges],
});
},
);
});
}
export function useServerSearch(query: string, enabled: boolean) {
return useQuery(
[QueryKeys.System, QueryKeys.Search, query],
() => api.system.search(query),
{
enabled,
},
);
return useQuery({
queryKey: [QueryKeys.System, QueryKeys.Search, query],
queryFn: () => api.system.search(query),
enabled,
});
}
export function useSystemLogs() {
return useQuery([QueryKeys.System, QueryKeys.Logs], () => api.system.logs(), {
return useQuery({
queryKey: [QueryKeys.System, QueryKeys.Logs],
queryFn: () => api.system.logs(),
refetchOnWindowFocus: "always",
refetchInterval: 1000 * 60,
staleTime: 1000 * 10,
@ -87,171 +96,187 @@ export function useSystemLogs() {
export function useDeleteLogs() {
const client = useQueryClient();
return useMutation(
[QueryKeys.System, QueryKeys.Logs],
() => api.system.deleteLogs(),
{
onSuccess: () => {
client.invalidateQueries([QueryKeys.System, QueryKeys.Logs]);
},
return useMutation({
mutationKey: [QueryKeys.System, QueryKeys.Logs],
mutationFn: () => api.system.deleteLogs(),
onSuccess: () => {
client.invalidateQueries({
queryKey: [QueryKeys.System, QueryKeys.Logs],
});
},
);
});
}
export function useSystemAnnouncements() {
return useQuery(
[QueryKeys.System, QueryKeys.Announcements],
() => api.system.announcements(),
{
refetchOnWindowFocus: "always",
refetchInterval: 1000 * 60,
staleTime: 1000 * 10,
},
);
return useQuery({
queryKey: [QueryKeys.System, QueryKeys.Announcements],
queryFn: () => api.system.announcements(),
refetchOnWindowFocus: "always",
refetchInterval: 1000 * 60,
staleTime: 1000 * 10,
});
}
export function useSystemAnnouncementsAddDismiss() {
const client = useQueryClient();
return useMutation(
[QueryKeys.System, QueryKeys.Announcements],
(param: { hash: string }) => {
return useMutation({
mutationKey: [QueryKeys.System, QueryKeys.Announcements],
mutationFn: (param: { hash: string }) => {
const { hash } = param;
return api.system.addAnnouncementsDismiss(hash);
},
{
onSuccess: (_, { hash }) => {
client.invalidateQueries([QueryKeys.System, QueryKeys.Announcements]);
client.invalidateQueries([QueryKeys.System, QueryKeys.Badges]);
},
onSuccess: (_, { hash }) => {
client.invalidateQueries({
queryKey: [QueryKeys.System, QueryKeys.Announcements],
});
client.invalidateQueries({
queryKey: [QueryKeys.System, QueryKeys.Badges],
});
},
);
});
}
export function useSystemTasks() {
return useQuery(
[QueryKeys.System, QueryKeys.Tasks],
() => api.system.tasks(),
{
refetchOnWindowFocus: "always",
refetchInterval: 1000 * 60,
staleTime: 1000 * 10,
},
);
return useQuery({
queryKey: [QueryKeys.System, QueryKeys.Tasks],
queryFn: () => api.system.tasks(),
refetchOnWindowFocus: "always",
refetchInterval: 1000 * 60,
staleTime: 1000 * 10,
});
}
export function useRunTask() {
const client = useQueryClient();
return useMutation(
[QueryKeys.System, QueryKeys.Tasks],
(id: string) => api.system.runTask(id),
{
onSuccess: () => {
client.invalidateQueries([QueryKeys.System, QueryKeys.Tasks]);
client.invalidateQueries([QueryKeys.System, QueryKeys.Backups]);
},
return useMutation({
mutationKey: [QueryKeys.System, QueryKeys.Tasks],
mutationFn: (id: string) => api.system.runTask(id),
onSuccess: () => {
client.invalidateQueries({
queryKey: [QueryKeys.System, QueryKeys.Tasks],
});
client.invalidateQueries({
queryKey: [QueryKeys.System, QueryKeys.Backups],
});
},
);
});
}
export function useSystemBackups() {
return useQuery([QueryKeys.System, "backups"], () => api.system.backups());
return useQuery({
queryKey: [QueryKeys.System, "backups"],
queryFn: () => api.system.backups(),
});
}
export function useCreateBackups() {
const client = useQueryClient();
return useMutation(
[QueryKeys.System, QueryKeys.Backups],
() => api.system.createBackups(),
{
onSuccess: () => {
client.invalidateQueries([QueryKeys.System, QueryKeys.Backups]);
},
return useMutation({
mutationKey: [QueryKeys.System, QueryKeys.Backups],
mutationFn: () => api.system.createBackups(),
onSuccess: () => {
client.invalidateQueries({
queryKey: [QueryKeys.System, QueryKeys.Backups],
});
},
);
});
}
export function useRestoreBackups() {
const client = useQueryClient();
return useMutation(
[QueryKeys.System, QueryKeys.Backups],
(filename: string) => api.system.restoreBackups(filename),
{
onSuccess: () => {
client.invalidateQueries([QueryKeys.System, QueryKeys.Backups]);
},
return useMutation({
mutationKey: [QueryKeys.System, QueryKeys.Backups],
mutationFn: (filename: string) => api.system.restoreBackups(filename),
onSuccess: () => {
client.invalidateQueries({
queryKey: [QueryKeys.System, QueryKeys.Backups],
});
},
);
});
}
export function useDeleteBackups() {
const client = useQueryClient();
return useMutation(
[QueryKeys.System, QueryKeys.Backups],
(filename: string) => api.system.deleteBackups(filename),
{
onSuccess: () => {
client.invalidateQueries([QueryKeys.System, QueryKeys.Backups]);
},
return useMutation({
mutationKey: [QueryKeys.System, QueryKeys.Backups],
mutationFn: (filename: string) => api.system.deleteBackups(filename),
onSuccess: () => {
client.invalidateQueries({
queryKey: [QueryKeys.System, QueryKeys.Backups],
});
},
);
});
}
export function useSystemStatus() {
return useQuery([QueryKeys.System, "status"], () => api.system.status());
return useQuery({
queryKey: [QueryKeys.System, "status"],
queryFn: () => api.system.status(),
});
}
export function useSystemHealth() {
return useQuery([QueryKeys.System, "health"], () => api.system.health());
return useQuery({
queryKey: [QueryKeys.System, "health"],
queryFn: () => api.system.health(),
});
}
export function useSystemReleases() {
return useQuery([QueryKeys.System, "releases"], () => api.system.releases());
return useQuery({
queryKey: [QueryKeys.System, "releases"],
queryFn: () => api.system.releases(),
});
}
export function useSystem() {
const client = useQueryClient();
const { mutate: logout, isLoading: isLoggingOut } = useMutation(
[QueryKeys.System, QueryKeys.Actions],
() => api.system.logout(),
{
onSuccess: () => {
setAuthenticated(false);
client.clear();
},
const { mutate: logout, isPending: isLoggingOut } = useMutation({
mutationKey: [QueryKeys.System, QueryKeys.Actions],
mutationFn: () => api.system.logout(),
onSuccess: () => {
setAuthenticated(false);
client.clear();
},
);
});
const { mutate: login, isLoading: isLoggingIn } = useMutation(
[QueryKeys.System, QueryKeys.Actions],
(param: { username: string; password: string }) =>
const { mutate: login, isPending: isLoggingIn } = useMutation({
mutationKey: [QueryKeys.System, QueryKeys.Actions],
mutationFn: (param: { username: string; password: string }) =>
api.system.login(param.username, param.password),
{
onSuccess: () => {
// TODO: Hard-coded value
window.location.replace(Environment.baseUrl);
},
onSuccess: () => {
// TODO: Hard-coded value
window.location.replace(Environment.baseUrl);
},
);
});
const { mutate: shutdown, isLoading: isShuttingDown } = useMutation(
[QueryKeys.System, QueryKeys.Actions],
() => api.system.shutdown(),
{
onSuccess: () => {
client.clear();
},
const { mutate: shutdown, isPending: isShuttingDown } = useMutation({
mutationKey: [QueryKeys.System, QueryKeys.Actions],
mutationFn: () => api.system.shutdown(),
onSuccess: () => {
client.clear();
},
);
});
const { mutate: restart, isPending: isRestarting } = useMutation({
mutationKey: [QueryKeys.System, QueryKeys.Actions],
mutationFn: () => api.system.restart(),
const { mutate: restart, isLoading: isRestarting } = useMutation(
[QueryKeys.System, QueryKeys.Actions],
() => api.system.restart(),
{
onSuccess: () => {
client.clear();
},
onSuccess: () => {
client.clear();
},
);
});
return useMemo(
() => ({

@ -4,7 +4,7 @@ import {
useQuery,
useQueryClient,
UseQueryResult,
} from "react-query";
} from "@tanstack/react-query";
import { GetItemId, useOnValueChange } from "@/utilities";
import { usePageSize } from "@/utilities/storage";
import { QueryKeys } from "./keys";
@ -39,31 +39,31 @@ export function usePaginationQuery<
const start = page * pageSize;
const results = useQuery(
[...queryKey, QueryKeys.Range, { start, size: pageSize }],
() => {
const results = useQuery({
queryKey: [...queryKey, QueryKeys.Range, { start, size: pageSize }],
queryFn: () => {
const param: Parameter.Range = {
start,
length: pageSize,
};
return queryFn(param);
},
{
onSuccess: ({ data }) => {
if (cacheIndividual) {
data.forEach((item) => {
const id = GetItemId(item);
if (id) {
client.setQueryData([...queryKey, id], item);
}
});
}
},
},
);
});
const { data } = results;
useEffect(() => {
if (results.isSuccess && results.data && cacheIndividual) {
results.data.data.forEach((item) => {
const id = GetItemId(item);
if (id) {
client.setQueryData([...queryKey, id], item);
}
});
}
}, [results.isSuccess, results.data, client, cacheIndividual, queryKey]);
const totalCount = data?.total ?? 0;
const pageCount = Math.ceil(totalCount / pageSize);

@ -1,4 +1,4 @@
import { QueryClient } from "react-query";
import { QueryClient } from "@tanstack/react-query";
const queryClient = new QueryClient({
defaultOptions: {
@ -6,7 +6,11 @@ const queryClient = new QueryClient({
refetchOnWindowFocus: false,
retry: false,
staleTime: 1000 * 60,
keepPreviousData: true,
networkMode: "offlineFirst",
placeholderData: (previousData: object) => previousData,
},
mutations: {
networkMode: "offlineFirst",
},
},
});

@ -74,3 +74,9 @@ $header-height: 64px;
}
}
}
:root {
@include dark {
--mantine-color-body: var(--mantine-color-dark-8);
}
}

@ -1,25 +1,16 @@
@layer mantine {
.root {
background-color: transparent;
&[data-variant="light"] {
color: var(--mantine-color-dark-0);
}
&[data-variant="dark"] {
--ai-bg: transparent;
--ai-hover: darken(var(--mantine-color-grape-light), 0.2);
}
--ai-bg: transparent;
@include light {
&[data-variant="light"] {
background-color: var(--mantine-color-gray-1);
color: var(--mantine-color-dark-2);
}
color: var(--mantine-color-dark-2);
--ai-hover: var(--mantine-color-gray-1);
--ai-hover-color: var(--mantine-color-gray-1);
}
&[data-variant="dark"] {
--ai-color: var(--mantine-color-dark-filled);
}
@include dark {
color: var(--mantine-color-dark-0);
--ai-hover: var(--mantine-color-gray-8);
}
}
}

@ -1,7 +0,0 @@
@layer mantine {
.main {
@include dark {
background-color: var(--mantine-color-dark-8);
}
}
}

@ -3,7 +3,7 @@
background-color: transparentize($color-brand-6, 0.8);
&[data-variant="warning"] {
color: lighten($color-warning-2, 1);
color: lighten($color-warning-2, 0.8);
background-color: transparentize($color-warning-6, 0.8);
}
@ -17,6 +17,11 @@
background-color: transparentize($color-disabled-7, 0.8);
}
&[data-variant="light"] {
color: var(--mantine-color-dark-0);
background-color: transparentize($color-disabled-9, 0.8);
}
@include light {
color: $color-brand-6;
background-color: transparentize($color-brand-3, 0.8);
@ -35,6 +40,11 @@
color: darken($color-highlight-6, 1);
background-color: transparentize($color-highlight-5, 0.8);
}
&[data-variant="light"] {
color: var(--mantine-color-black);
background-color: var(--mantine-color-gray-5);
}
}
}
}

@ -1,9 +0,0 @@
.result {
@include light {
color: var(--mantine-color-dark-8);
}
@include dark {
color: var(--mantine-color-gray-1);
}
}

@ -1,16 +1,10 @@
import { FunctionComponent, useMemo, useState } from "react";
import { Link } from "react-router-dom";
import {
Anchor,
Autocomplete,
ComboboxItem,
OptionsFilter,
} from "@mantine/core";
import { useNavigate } from "react-router-dom";
import { Autocomplete, ComboboxItem, OptionsFilter, Text } from "@mantine/core";
import { faSearch } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useServerSearch } from "@/apis/hooks";
import { useDebouncedValue } from "@/utilities";
import styles from "./Search.module.scss";
type SearchResultItem = {
value: string;
@ -57,21 +51,8 @@ const optionsFilter: OptionsFilter = ({ options, search }) => {
});
};
const ResultComponent = ({ name, link }: { name: string; link: string }) => {
return (
<Anchor
component={Link}
to={link}
underline="never"
className={styles.result}
p="sm"
>
{name}
</Anchor>
);
};
const Search: FunctionComponent = () => {
const navigate = useNavigate();
const [query, setQuery] = useState("");
const results = useSearch(query);
@ -79,14 +60,7 @@ const Search: FunctionComponent = () => {
return (
<Autocomplete
leftSection={<FontAwesomeIcon icon={faSearch} />}
renderOption={(input) => (
<ResultComponent
name={input.option.value}
link={
results.find((a) => a.value === input.option.value)?.link || "/"
}
/>
)}
renderOption={(input) => <Text p="xs">{input.option.value}</Text>}
placeholder="Search"
size="sm"
data={results}
@ -96,6 +70,9 @@ const Search: FunctionComponent = () => {
onChange={setQuery}
onBlur={() => setQuery("")}
filter={optionsFilter}
onOptionSubmit={(option) =>
navigate(results.find((a) => a.value === option)?.link || "/")
}
></Autocomplete>
);
};

@ -1,5 +1,5 @@
import { useCallback, useState } from "react";
import { UseMutationResult } from "react-query";
import { UseMutationResult } from "@tanstack/react-query";
import { Action } from "@/components/inputs";
import { ActionProps } from "@/components/inputs/Action";

@ -1,6 +1,6 @@
import { useCallback, useState } from "react";
import { UseMutationResult } from "react-query";
import { Button, ButtonProps } from "@mantine/core";
import { UseMutationResult } from "@tanstack/react-query";
type MutateButtonProps<DATA, VAR> = Omit<
ButtonProps,

@ -1,6 +1,6 @@
import { FunctionComponent, ReactNode } from "react";
import { UseQueryResult } from "react-query";
import { LoadingOverlay } from "@mantine/core";
import { UseQueryResult } from "@tanstack/react-query";
import { LoadingProvider } from "@/contexts";
interface QueryOverlayProps {

@ -1,7 +1,7 @@
import { FunctionComponent, useMemo } from "react";
import { UseMutationResult } from "react-query";
import { Button, Divider, Group, LoadingOverlay, Stack } from "@mantine/core";
import { useForm } from "@mantine/form";
import { UseMutationResult } from "@tanstack/react-query";
import { useLanguageProfiles } from "@/apis/hooks";
import { MultiSelector, Selector } from "@/components/inputs";
import { useModals, withModal } from "@/modules/modals";
@ -21,7 +21,7 @@ const ItemEditForm: FunctionComponent<Props> = ({
onCancel,
}) => {
const { data, isFetching } = useLanguageProfiles();
const { isLoading, mutate } = mutation;
const { isPending, mutate } = mutation;
const modals = useModals();
const profileOptions = useSelectorOptions(
@ -47,7 +47,7 @@ const ItemEditForm: FunctionComponent<Props> = ({
(v) => v.code2,
);
const isOverlayVisible = isLoading || isFetching || item === null;
const isOverlayVisible = isPending || isFetching || item === null;
return (
<form

@ -243,7 +243,7 @@ const MovieUploadForm: FunctionComponent<Props> = ({
<Action
label="Remove"
icon={faTrash}
color="red"
c="red"
onClick={() => action.remove(index)}
></Action>
);

@ -241,7 +241,7 @@ const ProfileEditForm: FunctionComponent<Props> = ({
<Action
label="Remove"
icon={faTrash}
color="red"
c="red"
onClick={() => action.remove(row.index)}
></Action>
);

@ -309,7 +309,7 @@ const SeriesUploadForm: FunctionComponent<Props> = ({
<Action
label="Remove"
icon={faTrash}
color="red"
c="red"
onClick={() => action.remove(index)}
></Action>
);

@ -9,7 +9,11 @@ import {
useRefTracksByMovieId,
useSubtitleAction,
} from "@/apis/hooks";
import { GroupedSelector, Selector } from "@/components/inputs";
import {
GroupedSelector,
GroupedSelectorOptions,
Selector,
} from "@/components/inputs";
import { useModals, withModal } from "@/modules/modals";
import { task } from "@/modules/task";
import { syncMaxOffsetSecondsOptions } from "@/pages/Settings/Subtitles/options";
@ -17,11 +21,6 @@ import { toPython } from "@/utilities";
const TaskName = "Syncing Subtitle";
interface SelectOptions {
group: string;
items: { value: string; label: string }[];
}
function useReferencedSubtitles(
mediaType: "episode" | "movie",
mediaId: number,
@ -41,13 +40,13 @@ function useReferencedSubtitles(
const mediaData = mediaType === "episode" ? episodeData : movieData;
const subtitles: SelectOptions[] = [];
const subtitles: GroupedSelectorOptions<string>[] = [];
if (!mediaData.data) {
return [];
} else {
if (mediaData.data.audio_tracks.length > 0) {
const embeddedAudioGroup: SelectOptions = {
const embeddedAudioGroup: GroupedSelectorOptions<string> = {
group: "Embedded audio tracks",
items: [],
};
@ -63,7 +62,7 @@ function useReferencedSubtitles(
}
if (mediaData.data.embedded_subtitles_tracks.length > 0) {
const embeddedSubtitlesTrackGroup: SelectOptions = {
const embeddedSubtitlesTrackGroup: GroupedSelectorOptions<string> = {
group: "Embedded subtitles tracks",
items: [],
};
@ -79,7 +78,7 @@ function useReferencedSubtitles(
}
if (mediaData.data.external_subtitles_tracks.length > 0) {
const externalSubtitlesFilesGroup: SelectOptions = {
const externalSubtitlesFilesGroup: GroupedSelectorOptions<string> = {
group: "External Subtitles files",
items: [],
};
@ -127,11 +126,7 @@ const SyncSubtitleForm: FunctionComponent<Props> = ({
const mediaId = selections[0].id;
const subtitlesPath = selections[0].path;
const subtitles: SelectOptions[] = useReferencedSubtitles(
mediaType,
mediaId,
subtitlesPath,
);
const subtitles = useReferencedSubtitles(mediaType, mediaId, subtitlesPath);
const form = useForm<FormValues>({
initialValues: {

@ -74,6 +74,7 @@ const TimeOffsetForm: FunctionComponent<Props> = ({ selections, onSubmit }) => {
<Button
color="gray"
variant="filled"
style={{ overflow: "visible" }}
onClick={() =>
form.setValues((f) => ({ ...f, positive: !f.positive }))
}

@ -1,13 +1,13 @@
import { useCallback, useMemo, useRef } from "react";
import {
ComboboxItem,
ComboboxParsedItemGroup,
ComboboxItemGroup,
MultiSelect,
MultiSelectProps,
Select,
SelectProps,
} from "@mantine/core";
import { isNull, isUndefined } from "lodash";
import { isNull, isUndefined, noop } from "lodash";
import { LOG } from "@/utilities/console";
export type SelectorOption<T> = Override<
@ -35,9 +35,14 @@ function DefaultKeyBuilder<T>(value: T) {
}
}
export interface GroupedSelectorOptions<T> {
group: string;
items: SelectorOption<T>[];
}
export type GroupedSelectorProps<T> = Override<
{
options: ComboboxParsedItemGroup[];
options: ComboboxItemGroup[];
getkey?: (value: T) => string;
},
Omit<SelectProps, "data">
@ -47,6 +52,7 @@ export function GroupedSelector<T>({
value,
options,
getkey = DefaultKeyBuilder,
onOptionSubmit = noop,
...select
}: GroupedSelectorProps<T>) {
return (

@ -269,6 +269,7 @@ const EpisodeHistoryView: FunctionComponent<EpisodeHistoryViewProps> = ({
return (
<QueryOverlay result={history}>
<PageTable
autoScroll={false}
tableStyles={{ emptyText: "No history found", placeholder: 5 }}
columns={columns}
data={data ?? []}

@ -1,5 +1,4 @@
import { useCallback, useMemo, useState } from "react";
import { UseQueryResult } from "react-query";
import { Column } from "react-table";
import {
Alert,
@ -18,6 +17,7 @@ import {
faInfoCircle,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { UseQueryResult } from "@tanstack/react-query";
import { isString } from "lodash";
import { Action, PageTable } from "@/components";
import Language from "@/components/bazarr/Language";
@ -162,8 +162,7 @@ function ManualSearchView<T extends SupportType>(props: Props<T>) {
<Action
label="Download"
icon={faDownload}
color="brand"
variant="light"
c="brand"
disabled={item === null}
onClick={() => {
if (!item) return;

@ -1,4 +1,4 @@
import { onlineManager } from "react-query";
import { onlineManager } from "@tanstack/react-query";
import { debounce, forIn, remove, uniq } from "lodash";
import { io, Socket } from "socket.io-client";
import { Environment, isDevEnv, isTestEnv } from "@/utilities";

@ -40,13 +40,13 @@ export function createDefaultReducer(): SocketIO.Reducer[] {
update: (ids) => {
LOG("info", "Invalidating series", ids);
ids.forEach((id) => {
queryClient.invalidateQueries([QueryKeys.Series, id]);
queryClient.invalidateQueries({ queryKey: [QueryKeys.Series, id] });
});
},
delete: (ids) => {
LOG("info", "Invalidating series", ids);
ids.forEach((id) => {
queryClient.invalidateQueries([QueryKeys.Series, id]);
queryClient.invalidateQueries({ queryKey: [QueryKeys.Series, id] });
});
},
},
@ -55,13 +55,13 @@ export function createDefaultReducer(): SocketIO.Reducer[] {
update: (ids) => {
LOG("info", "Invalidating movies", ids);
ids.forEach((id) => {
queryClient.invalidateQueries([QueryKeys.Movies, id]);
queryClient.invalidateQueries({ queryKey: [QueryKeys.Movies, id] });
});
},
delete: (ids) => {
LOG("info", "Invalidating movies", ids);
ids.forEach((id) => {
queryClient.invalidateQueries([QueryKeys.Movies, id]);
queryClient.invalidateQueries({ queryKey: [QueryKeys.Movies, id] });
});
},
},
@ -78,10 +78,9 @@ export function createDefaultReducer(): SocketIO.Reducer[] {
id,
]);
if (episode !== undefined) {
queryClient.invalidateQueries([
QueryKeys.Series,
episode.sonarrSeriesId,
]);
queryClient.invalidateQueries({
queryKey: [QueryKeys.Series, episode.sonarrSeriesId],
});
}
});
},
@ -93,10 +92,9 @@ export function createDefaultReducer(): SocketIO.Reducer[] {
id,
]);
if (episode !== undefined) {
queryClient.invalidateQueries([
QueryKeys.Series,
episode.sonarrSeriesId,
]);
queryClient.invalidateQueries({
queryKey: [QueryKeys.Series, episode.sonarrSeriesId],
});
}
});
},
@ -105,83 +103,106 @@ export function createDefaultReducer(): SocketIO.Reducer[] {
key: "episode-wanted",
update: (ids) => {
// Find a better way to update wanted
queryClient.invalidateQueries([QueryKeys.Episodes, QueryKeys.Wanted]);
queryClient.invalidateQueries({
queryKey: [QueryKeys.Episodes, QueryKeys.Wanted],
});
},
delete: () => {
queryClient.invalidateQueries([QueryKeys.Episodes, QueryKeys.Wanted]);
queryClient.invalidateQueries({
queryKey: [QueryKeys.Episodes, QueryKeys.Wanted],
});
},
},
{
key: "movie-wanted",
update: (ids) => {
// Find a better way to update wanted
queryClient.invalidateQueries([QueryKeys.Movies, QueryKeys.Wanted]);
queryClient.invalidateQueries({
queryKey: [QueryKeys.Movies, QueryKeys.Wanted],
});
},
delete: () => {
queryClient.invalidateQueries([QueryKeys.Movies, QueryKeys.Wanted]);
queryClient.invalidateQueries({
queryKey: [QueryKeys.Movies, QueryKeys.Wanted],
});
},
},
{
key: "settings",
any: () => {
queryClient.invalidateQueries([QueryKeys.System]);
queryClient.invalidateQueries({ queryKey: [QueryKeys.System] });
},
},
{
key: "languages",
any: () => {
queryClient.invalidateQueries([QueryKeys.System, QueryKeys.Languages]);
queryClient.invalidateQueries({
queryKey: [QueryKeys.System, QueryKeys.Languages],
});
},
},
{
key: "badges",
any: () => {
queryClient.invalidateQueries([QueryKeys.System, QueryKeys.Badges]);
queryClient.invalidateQueries({
queryKey: [QueryKeys.System, QueryKeys.Badges],
});
},
},
{
key: "movie-history",
any: () => {
queryClient.invalidateQueries([QueryKeys.Movies, QueryKeys.History]);
queryClient.invalidateQueries({
queryKey: [QueryKeys.Movies, QueryKeys.History],
});
},
},
{
key: "movie-blacklist",
any: () => {
queryClient.invalidateQueries([QueryKeys.Movies, QueryKeys.Blacklist]);
queryClient.invalidateQueries({
queryKey: [QueryKeys.Movies, QueryKeys.Blacklist],
});
},
},
{
key: "episode-history",
any: () => {
queryClient.invalidateQueries([QueryKeys.Episodes, QueryKeys.History]);
queryClient.invalidateQueries({
queryKey: [QueryKeys.Episodes, QueryKeys.History],
});
},
},
{
key: "episode-blacklist",
any: () => {
queryClient.invalidateQueries([
QueryKeys.Episodes,
QueryKeys.Blacklist,
]);
queryClient.invalidateQueries({
queryKey: [QueryKeys.Episodes, QueryKeys.Blacklist],
});
},
},
{
key: "reset-episode-wanted",
any: () => {
queryClient.invalidateQueries([QueryKeys.Episodes, QueryKeys.Wanted]);
queryClient.invalidateQueries({
queryKey: [QueryKeys.Episodes, QueryKeys.Wanted],
});
},
},
{
key: "reset-movie-wanted",
any: () => {
queryClient.invalidateQueries([QueryKeys.Movies, QueryKeys.Wanted]);
queryClient.invalidateQueries({
queryKey: [QueryKeys.Movies, QueryKeys.Wanted],
});
},
},
{
key: "task",
any: () => {
queryClient.invalidateQueries([QueryKeys.System, QueryKeys.Tasks]);
queryClient.invalidateQueries({
queryKey: [QueryKeys.System, QueryKeys.Tasks],
});
},
},
];

@ -169,7 +169,6 @@ const Table: FunctionComponent<Props> = ({
<Action
label="Manual Search"
disabled={disabled}
variant="dark"
onClick={() => {
modals.openContextModal(EpisodeSearchModal, {
item: row.original,
@ -182,7 +181,6 @@ const Table: FunctionComponent<Props> = ({
<Action
label="History"
disabled={disabled}
variant="dark"
onClick={() => {
modals.openContextModal(
EpisodeHistoryModal,

@ -161,7 +161,6 @@ const Table: FunctionComponent<Props> = ({ movie, profile, disabled }) => {
<Action
label="Subtitle Actions"
disabled={isSubtitleTrack(path)}
variant="dark"
icon={faEllipsis}
></Action>
</SubtitleToolsMenu>

@ -87,7 +87,6 @@ const MovieView: FunctionComponent = () => {
<Action
label="Edit Movie"
tooltip={{ position: "left" }}
variant="light"
onClick={() =>
modals.openContextModal(
ItemEditModal,

@ -87,7 +87,6 @@ const SeriesView: FunctionComponent = () => {
<Action
label="Edit Series"
tooltip={{ position: "left" }}
variant="light"
onClick={() =>
modals.openContextModal(
ItemEditModal,

@ -93,9 +93,8 @@ const SettingsGeneralView: FunctionComponent = () => {
window.isSecureContext && (
<Action
label="Copy API Key"
variant="light"
settingKey={settingApiKey}
color={copied ? "green" : undefined}
c={copied ? "green" : undefined}
icon={copied ? faCheck : faClipboard}
onClick={(update, value) => {
if (value) {
@ -108,9 +107,8 @@ const SettingsGeneralView: FunctionComponent = () => {
}
<Action
label="Regenerate"
variant="light"
settingKey={settingApiKey}
color="red"
c="red"
icon={faSync}
onClick={(update) => {
update(generateApiKey());

@ -342,7 +342,7 @@ const EqualsTable: FunctionComponent<EqualsTableProps> = () => {
<Action
label="Remove"
icon={faTrash}
color="red"
c="red"
onClick={() => remove(row.index)}
></Action>
);

@ -132,6 +132,7 @@ const Table: FunctionComponent = () => {
<Action
label="Edit Profile"
icon={faWrench}
c="gray"
onClick={() => {
modals.openContextModal(ProfileEditModal, {
languages,
@ -143,7 +144,7 @@ const Table: FunctionComponent = () => {
<Action
label="Remove"
icon={faTrash}
color="red"
c="red"
onClick={() => action.remove(row.index)}
></Action>
</Group>

@ -1,5 +1,4 @@
import { FunctionComponent, useCallback, useMemo } from "react";
import { useMutation } from "react-query";
import {
Button,
Divider,
@ -9,6 +8,7 @@ import {
Textarea,
} from "@mantine/core";
import { useForm } from "@mantine/form";
import { useMutation } from "@tanstack/react-query";
import { isObject } from "lodash";
import api from "@/apis/raw";
import { Selector } from "@/components";
@ -63,7 +63,9 @@ const NotificationForm: FunctionComponent<Props> = ({
},
});
const test = useMutation((url: string) => api.system.testNotification(url));
const test = useMutation({
mutationFn: (url: string) => api.system.testNotification(url),
});
return (
<form

@ -1,12 +1,19 @@
// eslint-disable-next-line simple-import-sort/imports
import { FunctionComponent } from "react";
import { Anchor, Blockquote, Text } from "@mantine/core";
import { Check, Layout, Message, Section } from "@/pages/Settings/components";
import { NotificationView } from "./components";
import { faQuoteLeftAlt } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
const SettingsNotificationsView: FunctionComponent = () => {
return (
<Layout name="Notifications">
<Blockquote>
<Blockquote
bg="transparent"
mt="xl"
icon={<FontAwesomeIcon icon={faQuoteLeftAlt}></FontAwesomeIcon>}
>
<Text>
Thanks to caronc for his work on{" "}
<Anchor

@ -25,7 +25,7 @@ const Layout: FunctionComponent<Props> = (props) => {
const { children, name } = props;
const { data: settings, isLoading, isRefetching } = useSystemSettings();
const { mutate, isLoading: isMutating } = useSettingsMutation();
const { mutate, isPending: isMutating } = useSettingsMutation();
const form = useForm<FormValues>({
initialValues: {

@ -28,7 +28,7 @@ const LayoutModal: FunctionComponent<Props> = (props) => {
const { children, callbackModal } = props;
const { data: settings, isLoading, isRefetching } = useSystemSettings();
const { mutate, isLoading: isMutating } = useSettingsMutation();
const { mutate, isPending: isMutating } = useSettingsMutation();
const form = useForm<FormValues>({
initialValues: {

@ -10,7 +10,7 @@ import Table from "./table";
const SystemBackupsView: FunctionComponent = () => {
const backups = useSystemBackups();
const { mutate: backup, isLoading: isResetting } = useCreateBackups();
const { mutate: backup, isPending: isResetting } = useCreateBackups();
useDocumentTitle("Backups - Bazarr (System)");

@ -81,7 +81,7 @@ const Table: FunctionComponent<Props> = ({ backups }) => {
return (
<Action
label="Delete"
color="red"
c="red"
onClick={() =>
modals.openConfirmModal({
title: "Delete Backup",

@ -19,7 +19,7 @@ const SystemLogsView: FunctionComponent = () => {
const logs = useSystemLogs();
const { isFetching, data, refetch } = logs;
const { mutate, isLoading } = useDeleteLogs();
const { mutate, isPending } = useDeleteLogs();
const download = useCallback(() => {
window.open(`${Environment.baseUrl}/bazarr.log`);
@ -98,14 +98,14 @@ const SystemLogsView: FunctionComponent = () => {
Download
</Toolbox.Button>
<Toolbox.Button
loading={isLoading}
loading={isPending}
icon={faTrash}
onClick={() => mutate()}
>
Empty
</Toolbox.Button>
<Toolbox.Button
loading={isLoading}
loading={isPending}
icon={faFilter}
onClick={openFilterModal}
rightSection={

@ -12,7 +12,7 @@ const SystemProvidersView: FunctionComponent = () => {
const { isFetching, data, refetch } = providers;
const { mutate: reset, isLoading: isResetting } = useResetProvider();
const { mutate: reset, isPending: isResetting } = useResetProvider();
useDocumentTitle("Providers - Bazarr (System)");

@ -21,7 +21,7 @@ const SystemReleasesView: FunctionComponent = () => {
useDocumentTitle("Releases - Bazarr (System)");
return (
<Container size={600} py={12}>
<Container size="md" py={12}>
<QueryOverlay result={releases}>
<Stack gap="lg">
{data?.map((v, idx) => (

@ -42,7 +42,7 @@ const WantedMoviesView: FunctionComponent = () => {
<Group gap="sm">
{value.map((item, idx) => (
<Badge
color={download.isLoading ? "gray" : undefined}
color={download.isPending ? "gray" : undefined}
leftSection={<FontAwesomeIcon icon={faSearch} />}
key={BuildKey(idx, item.code2)}
style={{ cursor: "pointer" }}

@ -50,7 +50,7 @@ const WantedSeriesView: FunctionComponent = () => {
<Group gap="sm">
{value.map((item, idx) => (
<Badge
color={download.isLoading ? "gray" : undefined}
color={download.isPending ? "gray" : undefined}
leftSection={<FontAwesomeIcon icon={faSearch} />}
key={BuildKey(idx, item.code2)}
style={{ cursor: "pointer" }}

@ -200,8 +200,8 @@ const ItemBadge: FunctionComponent<ItemBadgeProps> = ({
}) => (
<Badge
leftSection={<FontAwesomeIcon icon={icon}></FontAwesomeIcon>}
variant="light"
radius="sm"
color="dark"
size="sm"
style={{ textTransform: "none" }}
aria-label={title}

@ -1,13 +1,17 @@
import { useCallback, useMemo, useState } from "react";
import { UseMutationResult } from "react-query";
import { useNavigate } from "react-router-dom";
import { Column, useRowSelect } from "react-table";
import { Box, Container } from "@mantine/core";
import { Box, Container, useCombobox } from "@mantine/core";
import { faCheck, faUndo } from "@fortawesome/free-solid-svg-icons";
import { UseMutationResult } from "@tanstack/react-query";
import { uniqBy } from "lodash";
import { useIsAnyMutationRunning, useLanguageProfiles } from "@/apis/hooks";
import { SimpleTable, Toolbox } from "@/components";
import { Selector, SelectorOption } from "@/components/inputs";
import {
GroupedSelector,
GroupedSelectorOptions,
SimpleTable,
Toolbox,
} from "@/components";
import { useCustomSelection } from "@/components/tables/plugins";
import { GetItemId, useSelectorOptions } from "@/utilities";
@ -36,10 +40,26 @@ function MassEditor<T extends Item.Base>(props: MassEditorProps<T>) {
const profileOptions = useSelectorOptions(profiles ?? [], (v) => v.name);
const profileOptionsWithAction = useMemo<SelectorOption<Language.Profile>[]>(
() => [...profileOptions.options],
[profileOptions.options],
);
const profileOptionsWithAction = useMemo<
GroupedSelectorOptions<string>[]
>(() => {
return [
{
group: "Actions",
items: [{ label: "Clear", value: "", profileId: null }],
},
{
group: "Profiles",
items: profileOptions.options.map((a) => {
return {
value: a.value.profileId.toString(),
label: a.label,
profileId: a.value.profileId,
};
}),
},
];
}, [profileOptions.options]);
const getKey = useCallback((value: Language.Profile | null) => {
if (value) {
@ -51,11 +71,20 @@ function MassEditor<T extends Item.Base>(props: MassEditorProps<T>) {
const { mutateAsync } = mutation;
/**
* Submit the form that contains the series id and the respective profile id set in chunks to prevent payloads too
* large when we have a high amount of series or movies being applied the profile. The chunks are executed in order
* since there are no much benefit on executing in parallel, also parallelism could result in high load on the server
* side if not throttled properly.
*/
const save = useCallback(() => {
const chunkSize = 1000;
const form: FormType.ModifyItem = {
id: [],
profileid: [],
};
dirties.forEach((v) => {
const id = GetItemId(v);
if (id) {
@ -63,12 +92,29 @@ function MassEditor<T extends Item.Base>(props: MassEditorProps<T>) {
form.profileid.push(v.profileId);
}
});
return mutateAsync(form);
const mutateInChunks = async (
ids: number[],
profileIds: (number | null)[],
) => {
if (ids.length === 0) return;
const chunkIds = ids.slice(0, chunkSize);
const chunkProfileIds = profileIds.slice(0, chunkSize);
await mutateAsync({
id: chunkIds,
profileid: chunkProfileIds,
});
await mutateInChunks(ids.slice(chunkSize), profileIds.slice(chunkSize));
};
return mutateInChunks(form.id, form.profileid);
}, [dirties, mutateAsync]);
const setProfiles = useCallback(
(profile: Language.Profile | null) => {
const id = profile?.profileId ?? null;
(id: number | null) => {
const newItems = selections.map((v) => ({ ...v, profileId: id }));
setDirties((dirty) => {
@ -78,18 +124,29 @@ function MassEditor<T extends Item.Base>(props: MassEditorProps<T>) {
[selections],
);
const combobox = useCombobox();
return (
<Container fluid px={0}>
<Toolbox>
<Box>
<Selector
allowDeselect
<GroupedSelector
onClick={() => combobox.openDropdown()}
onDropdownClose={() => {
combobox.resetSelectedOption();
}}
placeholder="Change Profile"
withCheckIcon={false}
options={profileOptionsWithAction}
getkey={getKey}
disabled={selections.length === 0}
onChange={setProfiles}
></Selector>
comboboxProps={{
store: combobox,
onOptionSubmit: (value) => {
setProfiles(value ? +value : null);
},
}}
></GroupedSelector>
</Box>
<Box>
<Toolbox.Button icon={faUndo} onClick={onEnded}>

@ -1,7 +1,7 @@
import { FunctionComponent, PropsWithChildren } from "react";
import { QueryClientProvider } from "react-query";
import { ReactQueryDevtools } from "react-query/devtools";
import { Notifications } from "@mantine/notifications";
import { QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import queryClient from "@/apis/queries";
import ThemeProvider from "@/App/ThemeProvider";
import { ModalsProvider } from "@/modules/modals";

Loading…
Cancel
Save