Compare commits

..

59 Commits

Author SHA1 Message Date
semantic-release-bot
c6486c3643 chore(release): 1.16.0 2021-01-07 09:58:09 +00:00
sct
f9695068a8 Merge branch 'develop' 2021-01-07 09:53:04 +00:00
Weblate (bot)
2bfe0f2bf6 feat(lang): Translations update from Weblate (#580)
* feat(lang): translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (433 of 433 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Tijuco <linuxinmotion@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/pt_BR/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (French)

Currently translated at 100.0% (454 of 454 strings)

feat(lang): translated using Weblate (French)

Currently translated at 100.0% (433 of 433 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/fr/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Italian)

Currently translated at 100.0% (454 of 454 strings)

feat(lang): translated using Weblate (Italian)

Currently translated at 100.0% (433 of 433 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/it/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (German)

Currently translated at 100.0% (454 of 454 strings)

feat(lang): translated using Weblate (German)

Currently translated at 100.0% (454 of 454 strings)

feat(lang): translated using Weblate (German)

Currently translated at 100.0% (433 of 433 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Paul <panzer1119@gmail.com>
Co-authored-by: cyb3rgh05t <flammang.yves@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/de/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Dutch)

Currently translated at 98.2% (446 of 454 strings)

Co-authored-by: Jos Vennik <josvennik@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/nl/
Translation: Overseerr/Overseerr Frontend

Co-authored-by: Tijuco <linuxinmotion@gmail.com>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Paul <panzer1119@gmail.com>
Co-authored-by: cyb3rgh05t <flammang.yves@gmail.com>
Co-authored-by: Jos Vennik <josvennik@gmail.com>
2021-01-07 18:50:51 +09:00
sct
493d82b6b0 fix(plex-sync): do not run plex sync if no admin exists 2021-01-06 00:55:53 +00:00
sct
e2c2172ac6 build(deps): batch update dependencies 2021-01-05 15:17:46 +00:00
Jakob Ankarhem
ee5d0181fc feat(notifications): add pushover integration (#574)
* feat(notifications): add pushover integration

* refactor(pushover): group i18n translations
2021-01-05 13:19:25 +09:00
sct
67f2b57f00 fix(frontend): apply same titlecard hover effect to personcard 2021-01-04 16:40:39 +00:00
sct
88810bf0a4 fix(frontend): use hardware acceleration for titlecard scale 2021-01-04 16:09:09 +00:00
sct
0ab4c3c36f fix(frontend): only animate titlecard when showDetail is true 2021-01-04 15:33:07 +00:00
sct
effc80977a fix(frontend): adjust titlecard badge styling
this commit aligns the badges, removes the border from the availability badge (replaces it with a
light shadow) and also adds a new animation to the titlecard
2021-01-04 15:21:46 +00:00
semantic-release-bot
fd90adba4d chore(release): 1.15.0 2021-01-04 12:01:25 +00:00
sct
970aa612fe Merge pull request #570 from sct/release/release-20210104
Release
2021-01-04 20:56:34 +09:00
sct
71169944dc Merge branch 'develop' into release/release-20210104 2021-01-04 11:54:45 +00:00
Weblate (bot)
b04eda6c8a feat(lang): translations update from Weblate (#495)
* feat(lang): translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (432 of 432 strings)

feat(lang): translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (432 of 432 strings)

feat(lang): translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (418 of 418 strings)

feat(lang): translated using Weblate (Portuguese (Brazil))

Currently translated at 93.7% (391 of 417 strings)

feat(lang): translated using Weblate (Portuguese (Brazil))

Currently translated at 95.3% (390 of 409 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: K. Herbert <herbert.ka@mailo.com>
Co-authored-by: Tijuco <linuxinmotion@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/pt_BR/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (French)

Currently translated at 100.0% (432 of 432 strings)

feat(lang): translated using Weblate (French)

Currently translated at 100.0% (422 of 422 strings)

feat(lang): translated using Weblate (French)

Currently translated at 100.0% (418 of 418 strings)

feat(lang): translated using Weblate (French)

Currently translated at 100.0% (409 of 409 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/fr/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Swedish)

Currently translated at 95.1% (389 of 409 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Jakob Ankarhem <jakob.ankarhem@outlook.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/sv/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Italian)

Currently translated at 100.0% (432 of 432 strings)

feat(lang): translated using Weblate (Italian)

Currently translated at 100.0% (422 of 422 strings)

feat(lang): translated using Weblate (Italian)

Currently translated at 100.0% (418 of 418 strings)

feat(lang): translated using Weblate (Italian)

Currently translated at 100.0% (409 of 409 strings)

feat(lang): translated using Weblate (Italian)

Currently translated at 100.0% (409 of 409 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Simone Chiavaccini <mazzetta86@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/it/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Spanish)

Currently translated at 100.0% (432 of 432 strings)

feat(lang): translated using Weblate (Spanish)

Currently translated at 100.0% (409 of 409 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Shutruk <cokimaya007@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/es/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (German)

Currently translated at 100.0% (432 of 432 strings)

feat(lang): translated using Weblate (German)

Currently translated at 100.0% (432 of 432 strings)

feat(lang): translated using Weblate (German)

Currently translated at 100.0% (432 of 432 strings)

feat(lang): translated using Weblate (German)

Currently translated at 100.0% (432 of 432 strings)

feat(lang): translated using Weblate (German)

Currently translated at 100.0% (422 of 422 strings)

feat(lang): translated using Weblate (German)

Currently translated at 100.0% (418 of 418 strings)

feat(lang): translated using Weblate (German)

Currently translated at 100.0% (409 of 409 strings)

feat(lang): translated using Weblate (German)

Currently translated at 100.0% (409 of 409 strings)

feat(lang): translated using Weblate (German)

Currently translated at 100.0% (409 of 409 strings)

feat(lang): translated using Weblate (German)

Currently translated at 98.7% (404 of 409 strings)

Co-authored-by: Ben Wallner <ben.david.wallner@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: K. Herbert <herbert.ka@mailo.com>
Co-authored-by: Paul <panzer1119@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/de/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Dutch)

Currently translated at 100.0% (432 of 432 strings)

feat(lang): translated using Weblate (Dutch)

Currently translated at 100.0% (432 of 432 strings)

feat(lang): translated using Weblate (Dutch)

Currently translated at 96.9% (419 of 432 strings)

feat(lang): translated using Weblate (Dutch)

Currently translated at 79.8% (345 of 432 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Jos Vennik <josvennik@gmail.com>
Co-authored-by: Kobe <kobe.boecquaert@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/nl/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Japanese)

Currently translated at 99.5% (407 of 409 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: T'ai <chivalrousjosh@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ja/
Translation: Overseerr/Overseerr Frontend

Co-authored-by: K. Herbert <herbert.ka@mailo.com>
Co-authored-by: Tijuco <linuxinmotion@gmail.com>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Jakob Ankarhem <jakob.ankarhem@outlook.com>
Co-authored-by: Simone Chiavaccini <mazzetta86@gmail.com>
Co-authored-by: Shutruk <cokimaya007@gmail.com>
Co-authored-by: Ben Wallner <ben.david.wallner@gmail.com>
Co-authored-by: Paul <panzer1119@gmail.com>
Co-authored-by: Jos Vennik <josvennik@gmail.com>
Co-authored-by: Kobe <kobe.boecquaert@gmail.com>
Co-authored-by: T'ai <chivalrousjosh@gmail.com>
2021-01-04 20:41:22 +09:00
sct
5add44cfb0 feat(frontend): filter/sorting for request list
closes #431
2021-01-03 16:02:34 +00:00
sct
a524b9c4c8 fix(frontend): add i18n for request text on titlecard 2021-01-03 14:38:05 +00:00
sct
b5a3a7a89f fix(frontend): change titlecard to only have a request button 2021-01-03 14:03:14 +00:00
sct
67ac9e075f fix(frontend): fix sliders overflowing on firefox
fixes #566
2021-01-03 13:50:28 +00:00
sct
ce0687922a fix(frontend): disable pointer-events on titlecard badges 2021-01-02 15:38:36 +00:00
sct
ba106c447d fix(frontend): increase titlecard status badge size on larger screens 2021-01-02 15:24:25 +00:00
sct
8cbf39a9d1 fix(frontend): show movie/series badges always 2021-01-02 14:40:47 +00:00
sct
78b15cad18 refactor(frontend): titlecard design experiment 2021-01-02 14:06:41 +00:00
semantic-release-bot
d77f9eb44e chore(release): 1.14.1 2021-01-02 08:57:24 +00:00
sct
8c0903393c fix(holiday): remove special holiday slider 2021-01-02 08:51:37 +00:00
sct
22f2037ea6 fix(holiday): remove special holiday slider 2021-01-02 08:33:24 +00:00
samwiseg0
3e613a3e93 docs: Update Ubuntu install instructions [skip ci] (#558)
* docs: Update Ubuntu install instructions [skip ci]

* docs: fix updating spacing [skip ci]

Co-authored-by: sct <ryan@sct.dev>
2021-01-02 16:59:38 +09:00
sct
5c847026aa fix(api): return 202 when same seasons are requested again 2021-01-02 07:48:43 +00:00
Chris Pritchard
2f017fe2cb docs: Added installation instructions for gentoo (#556) [skip ci] 2021-01-01 21:32:53 -05:00
flying-sausages
7d95fd0f30 docs: Add swizzin install instructions (#552) [skip ci] 2021-01-01 17:15:43 -05:00
flying-sausages
28b1d8cf4c docs: Update Linux instructions #554 [skip ci] 2021-01-01 17:05:03 -05:00
allcontributors[bot]
9d6970db57 docs: add flying-sausages as a contributor (#551) [skip ci]
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2021-01-02 01:37:23 +09:00
flying-sausages
f906711ee8 docs: Add native linux install instructions (#550) [skip ci]
* Add native instructions

* formatting

* Update docs/getting-started/installation.md

* Add untested upgrade instructions
2021-01-02 01:35:29 +09:00
sct
93b57a76f1 fix(frontend): handle currentLibrary possibly being null on first manual sync 2020-12-31 05:05:16 +00:00
Brandon Cohen
12db7a065a fix(frontend): full season request modal fits on a smaller mobile UI (#535)
* fix(frontend): full season request modal fits on a smaller mobile UI

* fix(frontend): added responsive variant and removed unnecessary padding

* fix(frontend): added in responsive design

* fix(frontend): applied the same spacing from the discovery page to the requests/users pages

* fix(frontend): revered change to keep tables edge to edge on mobile
2020-12-31 13:57:31 +09:00
sct
058fb65495 fix(frontend): update login/setup images 2020-12-29 11:25:55 +00:00
sct
721ed9a930 fix: correctly deal with tmdb id duplicates between movies/series
fixes #526
2020-12-29 10:26:37 +00:00
Danshil Mungur
5295f74915 docs: added reminder to check default user permissions (#527) [skip ci] 2020-12-28 19:17:29 -05:00
Danshil Mungur
7d6c6cb2a1 docs: removed duplicate page (#522) [skip ci]
* docs(reverse-proxy-examples): removed duplicate text in hint

* docs(extending-overseerr): removed duplicate page
2020-12-29 08:17:56 +09:00
Danshil Mungur
d53d61014f docs(reverse-proxy-examples): removed duplicate text in hint (#520) [skip ci] 2020-12-29 02:25:09 +09:00
sct
7c40d9131d docs: update introduction (#519)
* docs: update docs introduction

* docs: fix link in faq
2020-12-28 20:44:08 +09:00
sct
f124d732a2 fix(frontend): fix count of requests in request list 2020-12-28 11:27:24 +00:00
sct
8af6a1f566 feat(notifications): control notifcation types per agent
closes #513
2020-12-28 02:21:45 +00:00
sct
d00e470b55 fix: use new commit tag file for app version as well 2020-12-27 17:22:21 +00:00
sct
55f9e41f1b build: write commit tag to json file inside docker image 2020-12-27 17:08:25 +00:00
sct
343f466788 fix(frontend): search clear button now correctly triggers routing 2020-12-27 15:31:18 +00:00
sct
fae4818957 fix(frontend): set locale cookie expiration to be much longer 2020-12-27 14:47:39 +00:00
sct
d188f6ffad fix(frontend): combine duplicate credits on a persons detail page
fixes #504
2020-12-27 12:14:47 +00:00
sct
289864af1a fix(build): fixes build to include commit tag for app build step 2020-12-27 10:33:44 +00:00
sct
d2620281bf GitBook: [develop] 5 pages modified 2020-12-27 10:28:50 +00:00
sct
30ed82e155 docs: documentation update (#516) [skip ci]
* docs: testing docs with gitbook

* GitBook: [docs] 3 pages modified

* GitBook: [docs] one page modified

* docs: migrate more wiki docs

* docs: Update gmail. Add 4k FAQ

* GitBook: [docs] 6 pages modified

* docs: add media missing in overseerr to faq

* GitBook: [docs] one page modified

* GitBook: [docs] 5 pages modified

* GitBook: [docs] 2 pages modified

* GitBook: [docs] 5 pages modified

* GitBook: [docs] 5 pages modified

* GitBook: [docs] 5 pages modified

* GitBook: [docs] one page modified

* GitBook: [docs] 2 pages modified

Co-authored-by: samwiseg0 <2241731+samwiseg0@users.noreply.github.com>
Co-authored-by: samwiseg0 <phillipah@gmail.com>
2020-12-27 19:27:03 +09:00
sct
c034496f55 feat(frontend): add documentation link to about page
Also updates the README with new documentation links
2020-12-27 10:23:10 +00:00
sct
2dcda39d40 fix(frontend): add localized strings for status checker 2020-12-27 10:12:06 +00:00
sct
75a426437a feat: status checker to prompt users to reload their frontend when app version changes 2020-12-27 10:05:54 +00:00
Danshil Mungur
1438b08cf0 fix(frontend): center text in movie auto-approve modal on small screens (#510)
fix #507
2020-12-27 18:20:58 +09:00
Jakob Ankarhem
0185bb1a70 feat(email): add sendername to email notification (#506)
* feat(email): add sendername to email notification

* feat(email): add sendername to api + move field in form

Co-authored-by: Jakob Ankarhem <jakob.ankarhem@jetshop.se>
2020-12-27 00:54:23 +09:00
Jakob Ankarhem
c8d4d674f4 feat(frontend): add telegram integration (#491)
* feat(frontend): add telegram notification agent

* feat(telegram): add i18n keys for telegram

* style(telegram): change message formatting in notification

* feat(telegram): add short tutorial for telegram setup

* feat(telegram): add i18n keys for telegram tutorial

* style(telegram): correct grammar in infobox

Co-authored-by: sct <ryan@sct.dev>

* fix(telegram): redo i18n extraction

Co-authored-by: Jakob Ankarhem <jakob.ankarhem@jetshop.se>
Co-authored-by: sct <ryan@sct.dev>
2020-12-27 00:02:00 +09:00
Jakob Ankarhem
7434a26f76 feat(frontend): add clear-field-icon to search field (#498)
* feat(frontend): add button to clear search field

Clear input field button was not visible on all devices, this replaces native ones with an svg

* refactor(search): use tailwind css for button and change svg

* refactor(search): larger click area on reset button

Co-authored-by: Jakob Ankarhem <jakob.ankarhem@jetshop.se>
2020-12-26 23:43:22 +09:00
allcontributors[bot]
46af705014 docs: add danshilm as a contributor (#505) [skip ci]
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2020-12-26 18:22:28 +09:00
Danshil Mungur
2b27a715b0 fix(docs): fix typo in build instructions (#503) 2020-12-26 18:21:46 +09:00
102 changed files with 3662 additions and 942 deletions

View File

@@ -196,7 +196,8 @@
"avatar_url": "https://avatars2.githubusercontent.com/u/20923978?v=4",
"profile": "https://github.com/danshilm",
"contributions": [
"code"
"code",
"doc"
]
},
{
@@ -235,6 +236,15 @@
"contributions": [
"code"
]
},
{
"login": "flying-sausages",
"name": "flying-sausages",
"avatar_url": "https://avatars1.githubusercontent.com/u/23618693?v=4",
"profile": "https://github.com/flying-sausages",
"contributions": [
"doc"
]
}
],
"badgeTemplate": "<a href=\"#contributors-\"><img alt=\"All Contributors\" src=\"https://img.shields.io/badge/all_contributors-<%= contributors.length %>-orange.svg\"/></a>",

5
.gitbook.yaml Normal file
View File

@@ -0,0 +1,5 @@
root: ./docs
structure:
readme: README.md
summary: SUMMARY.md

View File

@@ -1,3 +1,66 @@
# [1.16.0](https://github.com/sct/overseerr/compare/v1.15.0...v1.16.0) (2021-01-07)
### Bug Fixes
* **frontend:** adjust titlecard badge styling ([effc809](https://github.com/sct/overseerr/commit/effc80977a4ed732092254248f82363e52233171))
* **frontend:** apply same titlecard hover effect to personcard ([67f2b57](https://github.com/sct/overseerr/commit/67f2b57f00216ded3b34965629d6fdd2f16bc25f))
* **frontend:** only animate titlecard when showDetail is true ([0ab4c3c](https://github.com/sct/overseerr/commit/0ab4c3c36fe2c1ded142b6931111516f7f990a41))
* **frontend:** use hardware acceleration for titlecard scale ([88810bf](https://github.com/sct/overseerr/commit/88810bf0a4ef74299f6541b60fa91cea3610f99c))
* **plex-sync:** do not run plex sync if no admin exists ([493d82b](https://github.com/sct/overseerr/commit/493d82b6b066d77609cf66e005fd1f1472b8e011))
### Features
* **lang:** translations update from Weblate ([#495](https://github.com/sct/overseerr/issues/495)) ([b04eda6](https://github.com/sct/overseerr/commit/b04eda6c8a3bfcaa2a14b8a29612fdf690c9fba0))
* **lang:** Translations update from Weblate ([#580](https://github.com/sct/overseerr/issues/580)) ([2bfe0f2](https://github.com/sct/overseerr/commit/2bfe0f2bf66956763ab26d5c54f26e6c456f59f7))
* **notifications:** add pushover integration ([#574](https://github.com/sct/overseerr/issues/574)) ([ee5d018](https://github.com/sct/overseerr/commit/ee5d0181fc9a673b27aefd1d09b0a78c3d2e4f55))
# [1.15.0](https://github.com/sct/overseerr/compare/v1.14.1...v1.15.0) (2021-01-04)
### Bug Fixes
* **api:** return 202 when same seasons are requested again ([5c84702](https://github.com/sct/overseerr/commit/5c847026aad79fcac4d020786ded9f867696c226))
* **build:** fixes build to include commit tag for app build step ([289864a](https://github.com/sct/overseerr/commit/289864af1a995ce04834bf8a220cc238e1954d19))
* **docs:** fix typo in build instructions ([#503](https://github.com/sct/overseerr/issues/503)) ([2b27a71](https://github.com/sct/overseerr/commit/2b27a715b07c27200ba1e5e9623629a34389276d))
* **frontend:** add i18n for request text on titlecard ([a524b9c](https://github.com/sct/overseerr/commit/a524b9c4c8968f6823d33eb270dc26069fe4a725))
* **frontend:** add localized strings for status checker ([2dcda39](https://github.com/sct/overseerr/commit/2dcda39d40d820419e098bd6f1101eb820e5b42d))
* **frontend:** center text in movie auto-approve modal on small screens ([#510](https://github.com/sct/overseerr/issues/510)) ([1438b08](https://github.com/sct/overseerr/commit/1438b08cf0b358d79c6688c64be99f1718ec2d23)), closes [#507](https://github.com/sct/overseerr/issues/507)
* **frontend:** change titlecard to only have a request button ([b5a3a7a](https://github.com/sct/overseerr/commit/b5a3a7a89fcaf86dd794dc419711677b53646577))
* **frontend:** combine duplicate credits on a persons detail page ([d188f6f](https://github.com/sct/overseerr/commit/d188f6ffadff1564c47d5f33138e35498bed29fd)), closes [#504](https://github.com/sct/overseerr/issues/504)
* **frontend:** disable pointer-events on titlecard badges ([ce06879](https://github.com/sct/overseerr/commit/ce0687922a94588b3492e8ddf2e84f54dd1a0d4e))
* **frontend:** fix count of requests in request list ([f124d73](https://github.com/sct/overseerr/commit/f124d732a2911abdccb5abc11471efe61cc20f7a))
* **frontend:** fix sliders overflowing on firefox ([67ac9e0](https://github.com/sct/overseerr/commit/67ac9e075f0ca1cfe7e4766d9168815d7ab600fa)), closes [#566](https://github.com/sct/overseerr/issues/566)
* **frontend:** full season request modal fits on a smaller mobile UI ([#535](https://github.com/sct/overseerr/issues/535)) ([12db7a0](https://github.com/sct/overseerr/commit/12db7a065ad566b47d46de4b949343290894f153))
* **frontend:** handle currentLibrary possibly being null on first manual sync ([93b57a7](https://github.com/sct/overseerr/commit/93b57a76f10a823615ca11ff59f523b67aa30fad))
* **frontend:** increase titlecard status badge size on larger screens ([ba106c4](https://github.com/sct/overseerr/commit/ba106c447d76db2f9ac70a60c5b38cc60ab554fe))
* **frontend:** search clear button now correctly triggers routing ([343f466](https://github.com/sct/overseerr/commit/343f466788abc308b91a414ef61bba816ac8875c))
* **frontend:** set locale cookie expiration to be much longer ([fae4818](https://github.com/sct/overseerr/commit/fae481895736eab81d52eb93788beb00669fb355))
* **frontend:** show movie/series badges always ([8cbf39a](https://github.com/sct/overseerr/commit/8cbf39a9d12eaee7720fa4721c350c1ef9dee856))
* **frontend:** update login/setup images ([058fb65](https://github.com/sct/overseerr/commit/058fb65495baa08a0bd4c9e0aef320c6fc7d017b))
* **holiday:** remove special holiday slider ([8c09033](https://github.com/sct/overseerr/commit/8c0903393cf2cb2a929ba70a8ab6ddcc4cba0574))
* correctly deal with tmdb id duplicates between movies/series ([721ed9a](https://github.com/sct/overseerr/commit/721ed9a93087a57ae749388bddcacf26022e3df6)), closes [#526](https://github.com/sct/overseerr/issues/526)
* use new commit tag file for app version as well ([d00e470](https://github.com/sct/overseerr/commit/d00e470b55327489b49d770144b7cfdb24045be6))
### Features
* **email:** add sendername to email notification ([#506](https://github.com/sct/overseerr/issues/506)) ([0185bb1](https://github.com/sct/overseerr/commit/0185bb1a7084c1faeb61fb1c63e34e26732711c8))
* **frontend:** add clear-field-icon to search field ([#498](https://github.com/sct/overseerr/issues/498)) ([7434a26](https://github.com/sct/overseerr/commit/7434a26f76b5e9f74918f3e1a34443d20ecfcbe4))
* **frontend:** add documentation link to about page ([c034496](https://github.com/sct/overseerr/commit/c034496f557a031aed35cd28dc7221d8cdf36643))
* **frontend:** add telegram integration ([#491](https://github.com/sct/overseerr/issues/491)) ([c8d4d67](https://github.com/sct/overseerr/commit/c8d4d674f412082ad9e9da09abd79660365cf728))
* **frontend:** filter/sorting for request list ([5add44c](https://github.com/sct/overseerr/commit/5add44cfb0379aa6fed7c3b867230292feacc684)), closes [#431](https://github.com/sct/overseerr/issues/431)
* **notifications:** control notifcation types per agent ([8af6a1f](https://github.com/sct/overseerr/commit/8af6a1f566769c583af7dd9e18d162717835b7cc)), closes [#513](https://github.com/sct/overseerr/issues/513)
* status checker to prompt users to reload their frontend when app version changes ([75a4264](https://github.com/sct/overseerr/commit/75a426437a4182e21da13684066966dd5bf8fc5e))
## [1.14.1](https://github.com/sct/overseerr/compare/v1.14.0...v1.14.1) (2021-01-02)
### Bug Fixes
* **holiday:** remove special holiday slider ([22f2037](https://github.com/sct/overseerr/commit/22f2037ea6c5a0ba2ffa4d69f2b7cf42bdcf8575))
# [1.14.0](https://github.com/sct/overseerr/compare/v1.13.0...v1.14.0) (2020-12-25)

View File

@@ -38,7 +38,7 @@ All help is welcome and greatly appreciated. If you would like to contribute to
```
yarn
yarn install
yarn dev
```
- Alternatively you can run using [Docker](https://www.docker.com/) with `docker-compose up -d`. This method does not require installing NodeJS or Yarn on your machine directly.

View File

@@ -1,5 +1,8 @@
FROM node:12.18-alpine AS BUILD_IMAGE
ARG COMMIT_TAG
ENV COMMIT_TAG=${COMMIT_TAG}
COPY . /app
WORKDIR /app
@@ -25,6 +28,8 @@ COPY --from=BUILD_IMAGE /app/dist ./dist
COPY --from=BUILD_IMAGE /app/.next ./.next
COPY --from=BUILD_IMAGE /app/node_modules ./node_modules
RUN echo "{\"commitTag\": \"${COMMIT_TAG}\"}" > committag.json
CMD yarn start
EXPOSE 5055

View File

@@ -16,7 +16,7 @@
<a href="https://lgtm.com/projects/g/sct/overseerr/context:javascript"><img alt="Language grade: JavaScript" src="https://img.shields.io/lgtm/grade/javascript/g/sct/overseerr.svg?logo=lgtm&logoWidth=18"/></a>
<img alt="GitHub" src="https://img.shields.io/github/license/sct/overseerr">
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
<a href="#contributors-"><img alt="All Contributors" src="https://img.shields.io/badge/all_contributors-25-orange.svg"/></a>
<a href="#contributors-"><img alt="All Contributors" src="https://img.shields.io/badge/all_contributors-26-orange.svg"/></a>
<!-- ALL-CONTRIBUTORS-BADGE:END -->
</p>
@@ -36,16 +36,21 @@
- User profiles.
- User settings page (to give users the ability to modify their Overseerr experience to their liking).
- Version update notifications in-app.
- 4K requests (Includes multi-radarr/sonarr management for media)
## Planned Features
- More notification types (Slack/Telegram/etc.).
- More notification types.
- Issues system. This will allow users to report issues with content on your media server.
- Local user system (for those who don't use Plex).
- Compatibility APIs (to work with existing tools in your system).
## Getting Started
Check out our documenation for steps on how to install and run Overseerr:
https://docs.overseerr.dev/getting-started/installation
## Running Overseerr
Currently, Overseerr is only distributed through Docker images. If you have Docker, you can run Overseerr as per:
@@ -70,6 +75,7 @@ After running Overseerr for the first time, configure it by visiting the web UI
## Support
- Check out the [Overseerr Documentation](https://docs.overseerr.dev/) before asking for help. Your question might already be in the [FAQ](https://docs.overseerr.dev/support/faq).
- You can get support on [Discord](https://discord.gg/PkCWJSeCk7).
- You can ask questions in the Help category of our [GitHub Discussions](https://github.com/sct/overseerr/discussions).
- Bugs/Feature Requests can be opened via a [GitHub issue](https://github.com/sct/overseerr/issues).
@@ -123,13 +129,14 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<td align="center"><a href="https://github.com/Panzer1119"><img src="https://avatars1.githubusercontent.com/u/23016343?v=4" width="100px;" alt=""/><br /><sub><b>Paul Hagedorn</b></sub></a><br /><a href="#translation-Panzer1119" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/Shagon94"><img src="https://avatars3.githubusercontent.com/u/9140783?v=4" width="100px;" alt=""/><br /><sub><b>Shagon94</b></sub></a><br /><a href="#translation-Shagon94" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/sebstrgg"><img src="https://avatars3.githubusercontent.com/u/27026694?v=4" width="100px;" alt=""/><br /><sub><b>sebstrgg</b></sub></a><br /><a href="#translation-sebstrgg" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/danshilm"><img src="https://avatars2.githubusercontent.com/u/20923978?v=4" width="100px;" alt=""/><br /><sub><b>Danshil Mungur</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=danshilm" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/danshilm"><img src="https://avatars2.githubusercontent.com/u/20923978?v=4" width="100px;" alt=""/><br /><sub><b>Danshil Mungur</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=danshilm" title="Code">💻</a> <a href="https://github.com/sct/overseerr/commits?author=danshilm" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/doob187"><img src="https://avatars1.githubusercontent.com/u/60312740?v=4" width="100px;" alt=""/><br /><sub><b>doob187</b></sub></a><br /><a href="#infra-doob187" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
<td align="center"><a href="https://github.com/johnpyp"><img src="https://avatars2.githubusercontent.com/u/20625636?v=4" width="100px;" alt=""/><br /><sub><b>johnpyp</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=johnpyp" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/ankarhem"><img src="https://avatars1.githubusercontent.com/u/14110063?v=4" width="100px;" alt=""/><br /><sub><b>Jakob Ankarhem</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=ankarhem" title="Documentation">📖</a> <a href="https://github.com/sct/overseerr/commits?author=ankarhem" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/jayesh100"><img src="https://avatars1.githubusercontent.com/u/8022175?v=4" width="100px;" alt=""/><br /><sub><b>Jayesh</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=jayesh100" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/flying-sausages"><img src="https://avatars1.githubusercontent.com/u/23618693?v=4" width="100px;" alt=""/><br /><sub><b>flying-sausages</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=flying-sausages" title="Documentation">📖</a></td>
</tr>
</table>

22
docs/README.md Normal file
View File

@@ -0,0 +1,22 @@
# Introduction
Welcome to the Overseerr Documentation.
## Features
- **Full Plex integration**. Login and manage user access with Plex.
- **Syncs to your Plex library** to show what titles you already have.
- **Integrates with Sonarr and Radarr**. With more services to come in the future.
- **Easy to use request system** allowing users to request individual seasons or movies in a friendly, clean UI.
- **Simple request management UI**. Don't dig through the app to approve recent requests.
- **Mobile-friendly design**, for when you need to approve requests on the go.
- Granular permission system.
- Localization into other languages.
## Motivation
The primary motivation for starting this project was to have an incredibly performant and easy to use application. There is a heavy focus on the user experience for both the server owner and the users. We feel requesting should be **effortless for the user**. Find the media you want, click request, and branch off efficiently into other titles that interest you, all in one seamless flow. For the server owner, Overseerr takes all the hassle out of approving your users' requests.
## We need your help!
Overseerr is an ambitious project. We have already poured a lot of work into this, with more coming. We need your valuable feedback and help with finding bugs. Also, being that this is an open-source project, anyone is welcome to contribute. Contribution includes building features, patching bugs, or even translating the application. You can find the contribution guide on our GitHub.

17
docs/SUMMARY.md Normal file
View File

@@ -0,0 +1,17 @@
# Table of contents
* [Introduction](README.md)
## Getting Started
* [Installation](getting-started/installation.md)
## Support
* [Frequently Asked Questions](support/faq.md)
* [Asking for Support](support/asking-for-support.md)
## Extending Overseerr
* [Reverse Proxy Examples](extending-overseerr/reverse-proxy-examples.md)

View File

@@ -0,0 +1,132 @@
# Reverse Proxy Examples
{% hint style="warning" %}
Base URLs cannot be configured in Overseerr. With this limitation, only subdomain configurations are supported.
{% endhint %}
## LE/SWAG
### Subdomain
Place in the `proxy-confs` folder as `overseerr.subdomain.conf`
Example Configuration:
```text
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name overseerr.*;
include /config/nginx/ssl.conf;
client_max_body_size 0;
location / {
include /config/nginx/proxy.conf;
resolver 127.0.0.11 valid=30s;
set $upstream_app overseerr;
set $upstream_port 5055;
set $upstream_proto http;
proxy_pass $upstream_proto://$upstream_app:$upstream_port;
}
}
```
## Traefik \(v2\)
Add the labels to the Overseerr service in your `docker-compose` file. A basic example for a `docker-compose` file using Traefik can be found [here](https://doc.traefik.io/traefik/user-guides/docker-compose/basic-example/).
### Subdomain
Example Configuration:
```text
labels:
- "traefik.enable=true"
## HTTP Routers
- "traefik.http.routers.overseerr-rtr.entrypoints=https"
- "traefik.http.routers.overseerr-rtr.rule=Host(`overseerr.domain.com`)"
- "traefik.http.routers.overseerr-rtr.tls=true"
## HTTP Services
- "traefik.http.routers.overseerr-rtr.service=overseerr-svc"
- "traefik.http.services.overseerr-svc.loadbalancer.server.port=5055"
```
## LE/NGINX
### Subdomain
Take the configuration below and place it in `/etc/nginx/sites-available/overseerr.example.com.conf`.
Create a symlink to `/etc/nginx/sites-enabled`:
```text
sudo ln -s /etc/nginx/sites-available/overseerr.example.com.conf /etc/nginx/sites-enabled/overseerr.example.com.conf
```
Test the configuration:
```text
sudo nginx -t
```
Reload your configuration for NGINX:
```text
sudo systemctl reload nginx
```
Example Configuration:
```text
server {
listen 80;
server_name overseerr.example.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name overseerr.example.com;
ssl_certificate /etc/letsencrypt/live/overseerr.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/overseerr.example.com/privkey.pem;
proxy_set_header Referer $http_referer;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-Port $remote_port;
proxy_set_header X-Forwarded-Host $host:$remote_port;
proxy_set_header X-Forwarded-Server $host;
proxy_set_header X-Forwarded-Port $remote_port;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Ssl on;
real_ip_header CF-Connecting-IP;
# Control the behavior of the Referer header (Referrer-Policy)
add_header Referrer-Policy "no-referrer";
# HTTP Strict Transport Security
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
# Reduce XSS risks (Content-Security-Policy)
add_header Content-Security-Policy "default-src 'self'; connect-src 'self' https://plex.tv; style-src 'self' 'unsafe-inline' https://rsms.me/inter/inter.css; script-src 'self'; img-src 'self' data: https://plex.tv https://assets.plex.tv https://secure.gravatar.com https://i2.wp.com https://image.tmdb.org; font-src 'self' https://rsms.me/inter/font-files/" always;
# Prevent some categories of XSS attacks (X-XSS-Protection)
add_header X-XSS-Protection "1; mode=block" always;
# Provide clickjacking protection (X-Frame-Options)
add_header X-Frame-Options "SAMEORIGIN" always;
# Prevent Sniff Mimetype (X-Content-Type-Options)
add_header X-Content-Type-Options "nosniff" always;
access_log /var/log/nginx/overseerr.example.com-access.log;
error_log /var/log/nginx/overseerr.example.com-error.log;
location / {
proxy_pass http://127.0.0.1:5055;
}
}
```

View File

@@ -0,0 +1,196 @@
# Installation
{% hint style="danger" %}
Overseerr is currently under very heavy, rapid development and things are likely to break often. We need all the help we can get to find bugs and get them fixed to hit a more stable release. If you would like to help test the bleeding edge, please use the image **`sctx/overseerr:develop`** instead!
{% endhint %}
{% hint style="info" %}
After running Overseerr for the first time, configure it by visiting the web UI at `http://[address]:5055` and completing the setup steps.
{% endhint %}
## Docker
{% tabs %}
{% tab title="Basic" %}
```bash
docker run -d \
-e LOG_LEVEL=info \
-e TZ=Asia/Tokyo \
-p 5055:5055 \
-v /path/to/appdata/config:/app/config \
--restart unless-stopped \
sctx/overseerr
```
{% endtab %}
{% tab title="UID/GID" %}
```text
docker run -d \
--user=[ user | user:group | uid | uid:gid | user:gid | uid:group ] \
-e LOG_LEVEL=info \
-e TZ=Asia/Tokyo \
-p 5055:5055 \
-v /path/to/appdata/config:/app/config \
--restart unless-stopped \
sctx/overseerr
```
{% endtab %}
{% tab title="Manual Update" %}
```text
# Stop the Overseerr container
docker stop overseerr
# Remove the Overseerr container
docker rm overseerr
# Pull the latest update
docker pull sctx/overseerr
# Run the Overseerr container with the same parameters as before
docker run -d ...
```
{% endtab %}
{% endtabs %}
{% hint style="info" %}
Use a 3rd party updating mechanism such as [Watchtower](https://github.com/containrrr/watchtower) or [Ouroboros](https://github.com/pyouroboros/ouroboros) to keep Overseerr up-to-date automatically.
{% endhint %}
## Unraid
1. Ensure you have the **Community Applications** plugin installed.
2. Inside the **Communtiy Applications** app store, search for **Overseerr**.
3. Click the **Install Button**.
4. On the following **Add Container** screen, make changes to the **Host Port** and **Host Path 1**\(Appdata\) as needed.
5. Click apply and access "Overseerr" at your `<ServerIP:HostPort>` in a web browser.
## Windows
Please refer to the [docker for windows documentation](https://docs.docker.com/docker-for-windows/) for installation.
{% hint style="danger" %}
**WSL2 will need to be installed to prevent DB corruption! Please see** [**Docker Desktop WSL 2 backend**](https://docs.docker.com/docker-for-windows/wsl/) **on how to enable WSL2. The command below will only work with WSL2 installed! Details below.**
{% endhint %}
```bash
docker run -d -e LOG_LEVEL=info -e TZ=Asia/Tokyo -p 5055:5055 -v "/your/path/here:/app/config" --restart unless-stopped sctx/overseerr
```
{% hint style="info" %}
Docker on Windows works differently than it does on Linux; it uses a VM to run a stripped-down Linux and then runs docker within that. The volume mounts are exposed to the docker in this VM via SMB mounts. While this is fine for media, it is unacceptable for the `/app/config` directory because SMB does not support file locking. This will eventually corrupt your database which can lead to slow behavior and crashes. If you must run in docker on Windows, you should put the `/app/config` directory mount inside the VM and not on the Windows host. It's worth noting that this warning also extends to other containers which use SQLite databases.
{% endhint %}
## Linux \(Unsupported\)
{% tabs %}
{% tab title="Ubuntu 16.04+/Debian" %}
{% hint style="danger" %}
This install method is **not currently supported**. Docker is the only install method supported. Do not create issues or ask for support unless you are able to reproduce the issue with Docker.
{% endhint %}
```bash
# Install nodejs
sudo apt-get install -y curl git gnupg2
curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -
sudo apt-get install -y nodejs
# Install yarn
curl -sL https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
sudo apt-get update && sudo apt-get install yarn
# Install Overseerr
cd ~ && git clone https://github.com/sct/overseerr.git
cd overseerr
yarn install
yarn build
yarn start
```
**Updating**
In order to update, you will need to re-build overseer.
```bash
cd ~/.overseerr
git pull
yarn install
yarn build
yarn start
```
{% endtab %}
{% tab title="Ubuntu ARM" %}
{% hint style="danger" %}
This install method is **not currently supported**. Docker is the only install method supported. Do not create issues or ask for support unless you are able to reproduce the issue with Docker.
{% endhint %}
```bash
# Install nodejs
sudo apt-get install -y curl git gnupg2 build-essential
curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -
sudo apt-get install -y nodejs
# Install yarn
curl -sL https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
sudo apt-get update && sudo apt-get install yarn
# Install Overseerr
cd ~ && git clone https://github.com/sct/overseerr.git
cd overseerr
npm config set python "$(which python3)"
yarn install
yarn build
yarn start
```
**Updating**
In order to update, you will need to re-build overseer.
```bash
cd ~/.overseerr
git pull
yarn install
yarn build
yarn start
```
{% endtab %}
{% tab title="ArchLinux \(3rd Party\)" %}
Built from tag \(master\): [https://aur.archlinux.org/packages/overseerr/](https://aur.archlinux.org/packages/overseerr/)
Built from latest \(develop\): [aur.archlinux.org/packages/overseerr-git](https://aur.archlinux.org/packages/overseerr-git/)
**To install these just use your favorite AUR package manager:**
```bash
yay -S overseer
```
{% endtab %}
{% tab title="Gentoo \(3rd Party\)" %}
Portage overlay [GitHub Repository](https://github.com/chriscpritchard/overseerr-overlay)
Efforts will be made to keep up to date with the latest releases, however, this cannot be guaranteed.
To enable using eselect repository, run:
```bash
eselect repository add overseerr-overlay git https://github.com/chriscpritchard/overseerr-overlay.git
```
Once complete, you can just run:
```bash
emerge www-apps/overseerr
```
{% endtab %}
{% endtabs %}
## Swizzin \(Third party\)
The installation is not implemented via docker, but barebones. The latest released version of overseerr will be used.
Please see the [swizzin documentation](https://swizzin.ltd/applications/overseerr) for more information.
To install, run the following:
```bash
box install overseerr
```
To upgrade, run the following:
```bash
box upgrade overseerr
```

View File

@@ -0,0 +1,34 @@
# Asking for Support
## Before asking for support, make sure you try these things first
* Make sure you have **updated** to the latest version.
* ["Have you tried turning it off and on again?"](https://www.youtube.com/watch?v=nn2FB1P_Mn8)
* **Analyzing** your logs, you just might find the solution yourself!
* **Search** the [Wiki](../), [Installation Guides](../getting-started/installation.md), and [FAQs](faq.md).
* If you have questions, feel free to ask them on [Discord](https://discord.gg/PkCWJSeCk7) \(Please review our [Code of Conduct](https://github.com/sct/overseerr/blob/develop/CODE_OF_CONDUCT.md)\). Please include a link to your logs. See [How can I share my logs?](asking-for-support.md#how-can-i-share-my-logs) for more details.
## What should I include when asking for support?
When you contact support saying something like "it doesn't work" leaves little to go on to figure out what is wrong for you. When contacting support try to include information such as the following:
* What did you try to do? When you describe what you did to reach the state you are in we may notice something you did different from the instructions, or something that your unique setup requires in addition. Some examples of what to provide here:
* What command did you enter?
* What did you click on?
* What settings did you change?
* Provide a step-by-step list of what you tried.
* What do you see? We cannot see your screen so some of the following is necessary for us to know what is going on:
* Did something happen?
* Did something not happen?
* Are there any error messages showing?
* Screenshots can help us see what you are seeing
* The Overseerr logs show exactly what happened and are often critical for identifying issues \(see [How can I share my logs?](asking-for-support.md#how-can-i-share-my-logs) below\).
## How can I share my logs?
First you will need to gather your logs from the install directory.
1. Collect the log file from `<Overseeerr-install-directory>/logs/overseerr.log`
2. Open the log file and **upload the text** by going to [gist.github.com](https://gist.github.com/) and creating a new secret Gist of the contents.
3. **Share the link** with support in [Discord](https://discord.gg/PkCWJSeCk7) by copying the URL of the page.

95
docs/support/faq.md Normal file
View File

@@ -0,0 +1,95 @@
# Frequently Asked Questions
{% hint style="info" %}
If you can't find a solution here, please ask on [Discord](https://discord.gg/PkCWJSeCk7). Please do not post questions on the GitHub issues tracker.
{% endhint %}
## General
### I receive 409 or 400 errors when requesting a movie or tv show!
**A:** Verify your are running radarr and sonarr v3. Overseerr was developed for v3 and is not currently backward compatible.
### How do I keep Overseerr up-to-date?
**A:** Use a 3rd party updating mechanism such as [Watchtower](https://github.com/containrrr/watchtower) or [Ouroboros](https://github.com/pyouroboros/ouroboros) to keep Overseerr up-to-date automatically.
### How can I access Overseerr outside my home network?
**A:** The easy and least secure method is to forward an external port \(`5055`\) on your router to the internal port used by Overseerr \(default is TCP `5055`\). Visit [Port Forward](http://portforward.com/) for instructions for your particular router. You will then be able to access Overseerr via `http://EXTERNAL-IP-ADDRESS:5055`.
The more advanced and most preferred method \(and more secure if you use SSL\) is to set up a web server with NGINX/Apache, and use a reverse proxy to access Overseerr. You can lookup many guides on the internet to find out how to do this. There are several reverse proxy config examples located [here](../extending-overseerr/reverse-proxy-examples.md).
The most secure method, but also the most inconvenient, is to set up a VPN tunnel to your home server, then you can access Overseerr as if it is on a local network via `http://LOCAL-IP-ADDRESS:5055`.
### Overseerr is amazing! But it is not translated in my language yet! Can I help with translations?
**A:** You sure can! We are using Weblate for translations! Check it out [here](https://hosted.weblate.org/engage/overseerr/). If your language is not listed please open an [enhancement request in issues](https://github.com/sct/overseerr/issues/new/choose).
### Where can I find the changelog?
**A:** You can find the changelog in the **Settings -&gt; About** page in your instance. You can also find it on github [here](https://github.com/sct/overseerr/releases).
### Can I make 4K requests?
**A:** 4K requests are not supported just yet but they will be supported in the future!
### Some media is missing from Overseerr that I know is in Plex!
**A:** Overseerr supports the new Plex Movie, Legacy Plex Movie, TheTVDB agent, and the TMDb agent. Please verify that your library is using one of the agents previously listed. If you are changing agents, a full metadata refresh will need to be performed. Caution, this can take a long time depending on how many items you have in your movie library.
**Troubleshooting Steps:**
Check the Overseerr logs for media items that are missing. The logs will contain an error as to why that item could not be matched. One example might be `errorMessage":"SQLITE_CONSTRAINT: NOT NULL`. This means that the TMDb ID is missing from the Plex XML for that item.
1. Verify that you are using one of the agents mentioned above.
2. Refresh the metadata for just that item.
3. Run a full scan in Overseerr to see if that item is now matched properly.
4. If the item is now seen by Overseerr then repeat step 2 for each missing item. If you have a large amount of items missing then a full metadata refresh is recommended for that library.
5. Run a full scan on Overseerr after refreshing all unmatched items.
Perform these steps to verify the media item has a guid Overseerr can match.
1. Go to the media item in Plex and **"Get info"** and click on **"View XML"**.
2. Verify that the media item has the same format of one of the examples below.
**Examples:**
1. TMDB agent `guid="com.plexapp.agents.themoviedb://1705"`
2. The new Plex Movie agent `<Guid id="tmdb://464052"/>`
3. TheTVDB agent `guid="com.plexapp.agents.thetvdb://78874/1/1"`
4. Legacy Plex Movie agent `guid="com.plexapp.agents.imdb://tt0765446"`
### Where can I find the logs?
**A:** The logs are located at `<Overseeerr-install-directory>/logs/overseerr.log`
## User Management
### Why can't I see all my Plex users?
**A:** Navigate to your **User List** in Overseerr and click **Import Users From Plex** button. Don't forget to check the default user permissions in the **Settings -&gt; General Settings** page beforehand.
### Can I create local users in Overseerr?
**A:** Not at this time. But it is a planned feature!
### Is is possible to set user roles in Overseerr?
**A:** Unfortunately, this is not possible yet. It is planned!
## Requests
### I approved a requested movie and radarr didn't search for it!
**A:** Check your minimum availability in radarr. If an added item does not meet the minimum availability, no search will be performed. Also verify that radarr did not search for it by checking the radarr logs. Lastly, verify the item was not already being monitored by radarr. Currently there is no state sync with radarr.
### Help! My request still shows "requested" even though it's in Plex!?!
**A:** See "[Some media is missing from Overseerr that I know is in Plex!](./faq.md#some-media-is-missing-from-overseerr-that-i-know-is-in-plex)" for troubleshooting steps.
## Notifications
### I am getting "Username and Password not accepted" when sending email notifications to gmail!
**A:** If you have 2-Step Verification enabled on your account you will need to create an app password. More details can be found [here](https://support.google.com/mail/answer/185833).

View File

@@ -1,4 +1,7 @@
module.exports = {
env: {
commitTag: process.env.COMMIT_TAG || 'local',
},
webpack(config) {
config.module.rules.push({
test: /\.svg$/,

View File

@@ -855,6 +855,42 @@ components:
properties:
webhookUrl:
type: string
TelegramSettings:
type: object
properties:
enabled:
type: boolean
example: false
types:
type: number
example: 2
options:
type: object
properties:
botAPI:
type: string
chatId:
type: string
PushoverSettings:
type: object
properties:
enabled:
type: boolean
example: false
types:
type: number
example: 2
options:
type: object
properties:
accessToken:
type: string
userToken:
type: string
priority:
type: number
sound:
type: string
NotificationEmailSettings:
type: object
properties:
@@ -870,6 +906,9 @@ components:
emailFrom:
type: string
example: no-reply@example.com
senderName:
type: string
example: Overseerr
smtpHost:
type: string
example: 127.0.0.1
@@ -1083,6 +1122,26 @@ components:
name: X-Api-Key
paths:
/status:
get:
summary: Return Overseerr version
description: Returns the current Overseerr version in JSON format
security: []
tags:
- public
responses:
'200':
description: Returned version
content:
application/json:
schema:
type: object
properties:
version:
type: string
example: 1.0.0
commitTag:
type: string
/settings/main:
get:
summary: Returns main settings
@@ -1635,6 +1694,98 @@ paths:
responses:
'204':
description: Test notification attempted
/settings/notifications/telegram:
get:
summary: Return current telegram notification settings
description: Returns current telegram notification settings in JSON format
tags:
- settings
responses:
'200':
description: Returned telegram settings
content:
application/json:
schema:
$ref: '#/components/schemas/TelegramSettings'
post:
summary: Update telegram notification settings
description: Update current telegram notification settings with provided values
tags:
- settings
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/TelegramSettings'
responses:
'200':
description: 'Values were sucessfully updated'
content:
application/json:
schema:
$ref: '#/components/schemas/TelegramSettings'
/settings/notifications/telegram/test:
post:
summary: Test the provided telegram settings
description: Sends a test notification to the telegram agent
tags:
- settings
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/TelegramSettings'
responses:
'204':
description: Test notification attempted
/settings/notifications/pushover:
get:
summary: Return current pushover notification settings
description: Returns current pushover notification settings in JSON format
tags:
- settings
responses:
'200':
description: Returned pushover settings
content:
application/json:
schema:
$ref: '#/components/schemas/PushoverSettings'
post:
summary: Update pushover notification settings
description: Update current pushover notification settings with provided values
tags:
- settings
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/PushoverSettings'
responses:
'200':
description: 'Values were sucessfully updated'
content:
application/json:
schema:
$ref: '#/components/schemas/PushoverSettings'
/settings/notifications/pushover/test:
post:
summary: Test the provided pushover settings
description: Sends a test notification to the pushover agent
tags:
- settings
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/PushoverSettings'
responses:
'204':
description: Test notification attempted
/settings/notifications/slack:
get:
summary: Return current slack notification settings

View File

@@ -1,6 +1,6 @@
{
"name": "overseerr",
"version": "1.14.0",
"version": "1.16.0",
"private": true,
"scripts": {
"dev": "nodemon -e ts --watch server --watch overseerr-api.yml -e .json,.ts,.yml -x ts-node --files --project server/tsconfig.json server/index.ts",
@@ -18,19 +18,19 @@
"license": "MIT",
"dependencies": {
"@svgr/webpack": "^5.5.0",
"axios": "^0.21.0",
"axios": "^0.21.1",
"body-parser": "^1.19.0",
"bowser": "^2.11.0",
"connect-typeorm": "^1.1.4",
"cookie-parser": "^1.4.5",
"email-templates": "^8.0.2",
"express": "^4.17.1",
"express-openapi-validator": "^4.9.4",
"express-openapi-validator": "^4.10.2",
"express-session": "^1.17.1",
"formik": "^2.2.6",
"intl": "^1.2.5",
"lodash": "^4.17.20",
"next": "^10.0.3",
"next": "^10.0.4",
"node-schedule": "^1.3.2",
"nodemailer": "^6.4.17",
"nookies": "^2.5.0",
@@ -39,16 +39,16 @@
"react": "17.0.1",
"react-dom": "17.0.1",
"react-intersection-observer": "^8.31.0",
"react-intl": "^5.10.9",
"react-intl": "^5.10.11",
"react-markdown": "^5.0.3",
"react-spring": "^8.0.27",
"react-toast-notifications": "^2.4.0",
"react-transition-group": "^4.4.1",
"react-truncate-markup": "^5.0.1",
"react-use-clipboard": "1.0.2",
"react-use-clipboard": "1.0.7",
"reflect-metadata": "^0.1.13",
"sqlite3": "^5.0.0",
"swagger-ui-express": "^4.1.5",
"swagger-ui-express": "^4.1.6",
"swr": "^0.3.11",
"typeorm": "^0.2.29",
"uuid": "^8.3.2",
@@ -71,11 +71,11 @@
"@tailwindcss/typography": "^0.3.1",
"@types/body-parser": "^1.19.0",
"@types/cookie-parser": "^1.4.2",
"@types/email-templates": "^7.1.0",
"@types/email-templates": "^8.0.0",
"@types/express": "^4.17.9",
"@types/express-session": "^1.17.0",
"@types/lodash": "^4.14.165",
"@types/node": "^14.14.14",
"@types/lodash": "^4.14.167",
"@types/node": "^14.14.20",
"@types/node-schedule": "^1.3.1",
"@types/nodemailer": "^6.4.0",
"@types/react": "^17.0.0",
@@ -87,20 +87,20 @@
"@types/xml2js": "^0.4.7",
"@types/yamljs": "^0.2.31",
"@types/yup": "^0.29.11",
"@typescript-eslint/eslint-plugin": "^4.10.0",
"@typescript-eslint/parser": "^4.10.0",
"@typescript-eslint/eslint-plugin": "^4.12.0",
"@typescript-eslint/parser": "^4.12.0",
"autoprefixer": "^9",
"babel-plugin-react-intl": "^8.2.22",
"babel-plugin-react-intl": "^8.2.25",
"babel-plugin-react-intl-auto": "^3.3.0",
"commitizen": "^4.2.2",
"copyfiles": "^2.4.1",
"cz-conventional-changelog": "^3.3.0",
"eslint": "^7.16.0",
"eslint": "^7.17.0",
"eslint-config-prettier": "^7.1.0",
"eslint-plugin-formatjs": "^2.9.11",
"eslint-plugin-formatjs": "^2.10.2",
"eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-prettier": "^3.3.0",
"eslint-plugin-react": "^7.21.5",
"eslint-plugin-prettier": "^3.3.1",
"eslint-plugin-react": "^7.22.0",
"eslint-plugin-react-hooks": "^4.2.0",
"extract-react-intl-messages": "^4.1.1",
"husky": "^4.3.6",
@@ -109,7 +109,7 @@
"postcss": "^7",
"postcss-preset-env": "^6.7.0",
"prettier": "^2.2.1",
"semantic-release": "^17.3.0",
"semantic-release": "^17.3.1",
"semantic-release-docker": "^2.2.0",
"tailwindcss": "npm:@tailwindcss/postcss7-compat",
"ts-node": "^9.1.1",
@@ -171,7 +171,7 @@
[
"@semantic-release/exec",
{
"prepareCmd": "docker build -t sctx/overseerr ."
"prepareCmd": "docker build --build-arg COMMIT_TAG=$GITHUB_SHA -t sctx/overseerr ."
}
],
"semantic-release-docker",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 315 KiB

After

Width:  |  Height:  |  Size: 382 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 249 KiB

After

Width:  |  Height:  |  Size: 407 KiB

BIN
public/images/rotate5.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 384 KiB

BIN
public/images/rotate6.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 KiB

View File

@@ -40,12 +40,15 @@ class Media {
}
}
public static async getMedia(id: number): Promise<Media | undefined> {
public static async getMedia(
id: number,
mediaType: MediaType
): Promise<Media | undefined> {
const mediaRepository = getRepository(Media);
try {
const media = await mediaRepository.findOne({
where: { tmdbId: id },
where: { tmdbId: id, mediaType },
relations: ['requests'],
});
@@ -62,7 +65,7 @@ class Media {
@Column({ type: 'varchar' })
public mediaType: MediaType;
@Column({ unique: true })
@Column()
@Index()
public tmdbId: number;
@@ -70,7 +73,7 @@ class Media {
@Index()
public tvdbId?: number;
@Column({ unique: true, nullable: true })
@Column({ nullable: true })
@Index()
public imdbId?: string;

View File

@@ -17,8 +17,10 @@ import { startJobs } from './job/schedule';
import notificationManager from './lib/notifications';
import DiscordAgent from './lib/notifications/agents/discord';
import EmailAgent from './lib/notifications/agents/email';
import TelegramAgent from './lib/notifications/agents/telegram';
import { getAppVersion } from './utils/appVersion';
import SlackAgent from './lib/notifications/agents/slack';
import PushoverAgent from './lib/notifications/agents/pushover';
const API_SPEC_PATH = path.join(__dirname, '../overseerr-api.yml');
@@ -47,6 +49,8 @@ app
new DiscordAgent(),
new EmailAgent(),
new SlackAgent(),
new TelegramAgent(),
new PushoverAgent(),
]);
// Start Jobs

View File

@@ -44,11 +44,11 @@ class JobPlexSync {
this.isRecentOnly = isRecentOnly ?? false;
}
private async getExisting(tmdbId: number) {
private async getExisting(tmdbId: number, mediaType: MediaType) {
const mediaRepository = getRepository(Media);
const existing = await mediaRepository.findOne({
where: { tmdbId: tmdbId },
where: { tmdbId: tmdbId, mediaType },
});
return existing;
@@ -78,7 +78,10 @@ class JobPlexSync {
}
});
const existing = await this.getExisting(newMedia.tmdbId);
const existing = await this.getExisting(
newMedia.tmdbId,
MediaType.MOVIE
);
if (existing && existing.status === MediaStatus.AVAILABLE) {
this.log(`Title exists and is already available ${metadata.title}`);
@@ -115,7 +118,7 @@ class JobPlexSync {
throw new Error('Unable to find TMDB ID');
}
const existing = await this.getExisting(tmdbMovieId);
const existing = await this.getExisting(tmdbMovieId, MediaType.MOVIE);
if (existing && existing.status === MediaStatus.AVAILABLE) {
this.log(`Title exists and is already available ${plexitem.title}`);
} else if (existing && existing.status !== MediaStatus.AVAILABLE) {
@@ -184,9 +187,7 @@ class JobPlexSync {
if (tvShow && metadata) {
// Lets get the available seasons from plex
const seasons = tvShow.seasons;
const media = await mediaRepository.findOne({
where: { tmdbId: tvShow.id, mediaType: MediaType.TV },
});
const media = await this.getExisting(tvShow.id, MediaType.TV);
const newSeasons: Season[] = [];
@@ -344,7 +345,7 @@ class JobPlexSync {
private log(
message: string,
level: 'info' | 'error' | 'debug' = 'debug',
level: 'info' | 'error' | 'debug' | 'warn' = 'debug',
optional?: Record<string, unknown>
): void {
logger[level](message, { label: 'Plex Sync', ...optional });
@@ -355,11 +356,15 @@ class JobPlexSync {
if (!this.running) {
this.running = true;
const userRepository = getRepository(User);
const admin = await userRepository.findOneOrFail({
const admin = await userRepository.findOne({
select: ['id', 'plexToken'],
order: { id: 'ASC' },
});
if (!admin) {
return this.log('No admin configured. Plex sync skipped.', 'warn');
}
this.plexClient = new PlexAPI({ plexToken: admin.plexToken });
this.libraries = settings.plex.libraries.filter(

View File

@@ -1,5 +1,5 @@
import axios from 'axios';
import { Notification } from '..';
import { hasNotificationType, Notification } from '..';
import logger from '../../../logger';
import { getSettings, NotificationAgentDiscord } from '../../settings';
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
@@ -196,10 +196,12 @@ class DiscordAgent
};
}
// TODO: Add checking for type here once we add notification type filters for agents
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public shouldSend(_type: Notification): boolean {
if (this.getSettings().enabled && this.getSettings().options.webhookUrl) {
public shouldSend(type: Notification): boolean {
if (
this.getSettings().enabled &&
this.getSettings().options.webhookUrl &&
hasNotificationType(type, this.getSettings().types)
) {
return true;
}

View File

@@ -1,5 +1,5 @@
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
import { Notification } from '..';
import { hasNotificationType, Notification } from '..';
import path from 'path';
import { getSettings, NotificationAgentEmail } from '../../settings';
import nodemailer from 'nodemailer';
@@ -22,12 +22,13 @@ class EmailAgent
return settings.notifications.agents.email;
}
// TODO: Add checking for type here once we add notification type filters for agents
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public shouldSend(_type: Notification): boolean {
public shouldSend(type: Notification): boolean {
const settings = this.getSettings();
if (settings.enabled) {
if (
settings.enabled &&
hasNotificationType(type, this.getSettings().types)
) {
return true;
}
@@ -60,7 +61,10 @@ class EmailAgent
const settings = this.getSettings();
return new Email({
message: {
from: settings.options.emailFrom,
from: {
name: settings.options.senderName,
address: settings.options.emailFrom,
},
},
send: true,
transport: this.getSmtpTransport(),

View File

@@ -0,0 +1,122 @@
import axios from 'axios';
import { hasNotificationType, Notification } from '..';
import logger from '../../../logger';
import { getSettings, NotificationAgentPushover } from '../../settings';
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
interface PushoverPayload {
token: string;
user: string;
title: string;
message: string;
html: number;
}
class PushoverAgent
extends BaseAgent<NotificationAgentPushover>
implements NotificationAgent {
protected getSettings(): NotificationAgentPushover {
if (this.settings) {
return this.settings;
}
const settings = getSettings();
return settings.notifications.agents.pushover;
}
public shouldSend(type: Notification): boolean {
if (
this.getSettings().enabled &&
this.getSettings().options.accessToken &&
this.getSettings().options.userToken &&
hasNotificationType(type, this.getSettings().types)
) {
return true;
}
return false;
}
private constructMessageDetails(
type: Notification,
payload: NotificationPayload
): { title: string; message: string } {
const settings = getSettings();
let messageTitle = '';
let message = '';
const title = payload.subject;
const plot = payload.message;
const user = payload.notifyUser.username;
switch (type) {
case Notification.MEDIA_PENDING:
messageTitle = 'New Request';
message += `${title}\n\n`;
message += `${plot}\n\n`;
message += `<b>Requested By</b>\n${user}\n\n`;
message += `<b>Status</b>\nPending Approval\n`;
break;
case Notification.MEDIA_APPROVED:
messageTitle = 'Request Approved';
message += `${title}\n\n`;
message += `${plot}\n\n`;
message += `<b>Requested By</b>\n${user}\n\n`;
message += `<b>Status</b>\nProcessing Request\n`;
break;
case Notification.MEDIA_AVAILABLE:
messageTitle = 'Now available!';
message += `${title}\n\n`;
message += `${plot}\n\n`;
message += `<b>Requested By</b>\n${user}\n\n`;
message += `<b>Status</b>\nAvailable\n`;
break;
case Notification.TEST_NOTIFICATION:
messageTitle = 'Test Notification';
message += `${title}\n\n`;
message += `${plot}\n\n`;
message += `<b>Requested By</b>\n${user}\n`;
break;
}
if (settings.main.applicationUrl && payload.media) {
const actionUrl = `${settings.main.applicationUrl}/${payload.media.mediaType}/${payload.media.tmdbId}`;
message += `<a href="${actionUrl}">Open in Overseerr</a>`;
}
return { title: messageTitle, message };
}
public async send(
type: Notification,
payload: NotificationPayload
): Promise<boolean> {
logger.debug('Sending Pushover notification', { label: 'Notifications' });
try {
const endpoint = 'https://api.pushover.net/1/messages.json';
const { accessToken, userToken } = this.getSettings().options;
const { title, message } = this.constructMessageDetails(type, payload);
await axios.post(endpoint, {
token: accessToken,
user: userToken,
title: title,
message: message,
html: 1,
} as PushoverPayload);
return true;
} catch (e) {
logger.error('Error sending Pushover notification', {
label: 'Notifications',
message: e.message,
});
return false;
}
}
}
export default PushoverAgent;

View File

@@ -1,5 +1,5 @@
import axios from 'axios';
import { Notification } from '..';
import { hasNotificationType, Notification } from '..';
import logger from '../../../logger';
import { getSettings, NotificationAgentSlack } from '../../settings';
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
@@ -187,10 +187,12 @@ class SlackAgent
};
}
// TODO: Add checking for type here once we add notification type filters for agents
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public shouldSend(_type: Notification): boolean {
if (this.getSettings().enabled && this.getSettings().options.webhookUrl) {
public shouldSend(type: Notification): boolean {
if (
this.getSettings().enabled &&
this.getSettings().options.webhookUrl &&
hasNotificationType(type, this.getSettings().types)
) {
return true;
}

View File

@@ -0,0 +1,127 @@
import axios from 'axios';
import { hasNotificationType, Notification } from '..';
import logger from '../../../logger';
import { getSettings, NotificationAgentTelegram } from '../../settings';
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
interface TelegramPayload {
text: string;
parse_mode: string;
chat_id: string;
}
class TelegramAgent
extends BaseAgent<NotificationAgentTelegram>
implements NotificationAgent {
private baseUrl = 'https://api.telegram.org/';
protected getSettings(): NotificationAgentTelegram {
if (this.settings) {
return this.settings;
}
const settings = getSettings();
return settings.notifications.agents.telegram;
}
public shouldSend(type: Notification): boolean {
if (
this.getSettings().enabled &&
this.getSettings().options.botAPI &&
this.getSettings().options.chatId &&
hasNotificationType(type, this.getSettings().types)
) {
return true;
}
return false;
}
private escapeText(text: string | undefined): string {
return text ? text.replace(/[_*[\]()~>#+=|{}.!-]/gi, (x) => '\\' + x) : '';
}
private buildMessage(
type: Notification,
payload: NotificationPayload
): string {
const settings = getSettings();
let message = '';
const title = this.escapeText(payload.subject);
const plot = this.escapeText(payload.message);
const user = this.escapeText(payload.notifyUser.username);
/* eslint-disable no-useless-escape */
switch (type) {
case Notification.MEDIA_PENDING:
message += `\*New Request\*\n`;
message += `${title}\n\n`;
message += `${plot}\n\n`;
message += `\*Requested By\*\n${user}\n\n`;
message += `\*Status\*\nPending Approval\n`;
break;
case Notification.MEDIA_APPROVED:
message += `\*Request Approved\*\n`;
message += `${title}\n\n`;
message += `${plot}\n\n`;
message += `\*Requested By\*\n${user}\n\n`;
message += `\*Status\*\nProcessing Request\n`;
break;
case Notification.MEDIA_AVAILABLE:
message += `\*Now available\\!\*\n`;
message += `${title}\n\n`;
message += `${plot}\n\n`;
message += `\*Requested By\*\n${user}\n\n`;
message += `\*Status\*\nAvailable\n`;
break;
case Notification.TEST_NOTIFICATION:
message += `\*Test Notification\*\n`;
message += `${title}\n\n`;
message += `${plot}\n\n`;
message += `\*Requested By\*\n${user}\n`;
break;
}
if (settings.main.applicationUrl && payload.media) {
const actionUrl = `${settings.main.applicationUrl}/${payload.media.mediaType}/${payload.media.tmdbId}`;
message += `\[Open in Overseerr\]\(${actionUrl}\)`;
}
/* eslint-enable */
return message;
}
public async send(
type: Notification,
payload: NotificationPayload
): Promise<boolean> {
logger.debug('Sending telegram notification', { label: 'Notifications' });
try {
const endpoint = `${this.baseUrl}bot${
this.getSettings().options.botAPI
}/sendMessage`;
await axios.post(endpoint, {
text: this.buildMessage(type, payload),
parse_mode: 'MarkdownV2',
chat_id: `${this.getSettings().options.chatId}`,
} as TelegramPayload);
return true;
} catch (e) {
logger.error('Error sending Telegram notification', {
label: 'Notifications',
message: e.message,
});
return false;
}
}
}
export default TelegramAgent;

View File

@@ -9,6 +9,27 @@ export enum Notification {
TEST_NOTIFICATION = 32,
}
export const hasNotificationType = (
types: Notification | Notification[],
value: number
): boolean => {
let total = 0;
// If we are not checking any notifications, bail out and return true
if (types === 0) {
return true;
}
if (Array.isArray(types)) {
// Combine all notification values into one
total = types.reduce((a, v) => a + v, 0);
} else {
total = types;
}
return !!(value & total);
};
class NotificationManager {
private activeAgents: NotificationAgent[] = [];

View File

@@ -81,6 +81,23 @@ export interface NotificationAgentEmail extends NotificationAgentConfig {
authUser?: string;
authPass?: string;
allowSelfSigned: boolean;
senderName: string;
};
}
export interface NotificationAgentTelegram extends NotificationAgentConfig {
options: {
botAPI: string;
chatId: string;
};
}
export interface NotificationAgentPushover extends NotificationAgentConfig {
options: {
accessToken: string;
userToken: string;
priority: number;
sound: string;
};
}
@@ -88,6 +105,8 @@ interface NotificationAgents {
email: NotificationAgentEmail;
discord: NotificationAgentDiscord;
slack: NotificationAgentSlack;
telegram: NotificationAgentTelegram;
pushover: NotificationAgentPushover;
}
interface NotificationSettings {
@@ -140,6 +159,7 @@ class Settings {
smtpPort: 587,
secure: false,
allowSelfSigned: false,
senderName: 'Overseerr',
},
},
discord: {
@@ -156,6 +176,24 @@ class Settings {
webhookUrl: '',
},
},
telegram: {
enabled: false,
types: 0,
options: {
botAPI: '',
chatId: '',
},
},
pushover: {
enabled: false,
types: 0,
options: {
accessToken: '',
userToken: '',
priority: 0,
sound: '',
},
},
},
},
};

View File

@@ -0,0 +1,52 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class RemoveTmdbIdUniqueConstraint1609236552057
implements MigrationInterface {
name = 'RemoveTmdbIdUniqueConstraint1609236552057';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX "IDX_7ff2d11f6a83cb52386eaebe74"`);
await queryRunner.query(`DROP INDEX "IDX_41a289eb1fa489c1bc6f38d9c3"`);
await queryRunner.query(`DROP INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5"`);
await queryRunner.query(
`CREATE TABLE "temporary_media" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "mediaType" varchar NOT NULL, "tmdbId" integer NOT NULL, "tvdbId" integer, "imdbId" varchar, "status" integer NOT NULL DEFAULT (1), "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "lastSeasonChange" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), CONSTRAINT "UQ_41a289eb1fa489c1bc6f38d9c3c" UNIQUE ("tvdbId"))`
);
await queryRunner.query(
`INSERT INTO "temporary_media"("id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "createdAt", "updatedAt", "lastSeasonChange") SELECT "id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "createdAt", "updatedAt", "lastSeasonChange" FROM "media"`
);
await queryRunner.query(`DROP TABLE "media"`);
await queryRunner.query(`ALTER TABLE "temporary_media" RENAME TO "media"`);
await queryRunner.query(
`CREATE INDEX "IDX_7ff2d11f6a83cb52386eaebe74" ON "media" ("imdbId") `
);
await queryRunner.query(
`CREATE INDEX "IDX_41a289eb1fa489c1bc6f38d9c3" ON "media" ("tvdbId") `
);
await queryRunner.query(
`CREATE INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5" ON "media" ("tmdbId") `
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5"`);
await queryRunner.query(`DROP INDEX "IDX_41a289eb1fa489c1bc6f38d9c3"`);
await queryRunner.query(`DROP INDEX "IDX_7ff2d11f6a83cb52386eaebe74"`);
await queryRunner.query(`ALTER TABLE "media" RENAME TO "temporary_media"`);
await queryRunner.query(
`CREATE TABLE "media" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "mediaType" varchar NOT NULL, "tmdbId" integer NOT NULL, "tvdbId" integer, "imdbId" varchar, "status" integer NOT NULL DEFAULT (1), "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "lastSeasonChange" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), CONSTRAINT "UQ_41a289eb1fa489c1bc6f38d9c3c" UNIQUE ("tvdbId"), CONSTRAINT "UQ_7157aad07c73f6a6ae3bbd5ef5e" UNIQUE ("tmdbId"))`
);
await queryRunner.query(
`INSERT INTO "media"("id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "createdAt", "updatedAt", "lastSeasonChange") SELECT "id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "createdAt", "updatedAt", "lastSeasonChange" FROM "temporary_media"`
);
await queryRunner.query(`DROP TABLE "temporary_media"`);
await queryRunner.query(
`CREATE INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5" ON "media" ("tmdbId") `
);
await queryRunner.query(
`CREATE INDEX "IDX_41a289eb1fa489c1bc6f38d9c3" ON "media" ("tvdbId") `
);
await queryRunner.query(
`CREATE INDEX "IDX_7ff2d11f6a83cb52386eaebe74" ON "media" ("imdbId") `
);
}
}

View File

@@ -1,4 +1,5 @@
import { TmdbCollection } from '../api/themoviedb';
import { MediaType } from '../constants/media';
import Media from '../entity/Media';
import { mapMovieResult, MovieResult } from './Search';
@@ -23,7 +24,9 @@ export const mapCollection = (
parts: collection.parts.map((part) =>
mapMovieResult(
part,
media?.find((req) => req.tmdbId === part.id)
media?.find(
(req) => req.tmdbId === part.id && req.mediaType === MediaType.MOVIE
)
)
),
});

View File

@@ -3,6 +3,7 @@ import type {
TmdbPersonResult,
TmdbTvResult,
} from '../api/themoviedb';
import { MediaType as MainMediaType } from '../constants/media';
import Media from '../entity/Media';
export type MediaType = 'tv' | 'movie' | 'person';
@@ -122,12 +123,18 @@ export const mapSearchResults = (
case 'movie':
return mapMovieResult(
result,
media?.find((req) => req.tmdbId === result.id)
media?.find(
(req) =>
req.tmdbId === result.id && req.mediaType === MainMediaType.MOVIE
)
);
case 'tv':
return mapTvResult(
result,
media?.find((req) => req.tmdbId === result.id)
media?.find(
(req) =>
req.tmdbId === result.id && req.mediaType === MainMediaType.TV
)
);
default:
return mapPersonResult(result);

View File

@@ -26,7 +26,9 @@ discoverRoutes.get('/movies', async (req, res) => {
results: data.results.map((result) =>
mapMovieResult(
result,
media.find((req) => req.tmdbId === result.id)
media.find(
(req) => req.tmdbId === result.id && req.mediaType === MediaType.MOVIE
)
)
),
});

View File

@@ -13,10 +13,19 @@ import tvRoutes from './tv';
import mediaRoutes from './media';
import personRoutes from './person';
import collectionRoutes from './collection';
import { getAppVersion, getCommitTag } from '../utils/appVersion';
const router = Router();
router.use(checkUser);
router.get('/status', (req, res) => {
return res.status(200).json({
version: getAppVersion(),
commitTag: getCommitTag(),
});
});
router.use('/user', isAuthenticated(Permission.MANAGE_USERS), user);
router.get('/settings/public', (_req, res) => {
const settings = getSettings();

View File

@@ -5,6 +5,7 @@ import { mapMovieResult } from '../models/Search';
import Media from '../entity/Media';
import RottenTomatoes from '../api/rottentomatoes';
import logger from '../logger';
import { MediaType } from '../constants/media';
const movieRoutes = Router();
@@ -17,7 +18,7 @@ movieRoutes.get('/:id', async (req, res, next) => {
language: req.query.language as string,
});
const media = await Media.getMedia(tmdbMovie.id);
const media = await Media.getMedia(tmdbMovie.id, MediaType.MOVIE);
return res.status(200).json(mapMovieDetails(tmdbMovie, media));
} catch (e) {
@@ -49,7 +50,9 @@ movieRoutes.get('/:id/recommendations', async (req, res) => {
results: results.results.map((result) =>
mapMovieResult(
result,
media.find((req) => req.tmdbId === result.id)
media.find(
(req) => req.tmdbId === result.id && req.mediaType === MediaType.MOVIE
)
)
),
});
@@ -75,7 +78,9 @@ movieRoutes.get('/:id/similar', async (req, res) => {
results: results.results.map((result) =>
mapMovieResult(
result,
media.find((req) => req.tmdbId === result.id)
media.find(
(req) => req.tmdbId === result.id && req.mediaType === MediaType.MOVIE
)
)
),
});

View File

@@ -45,13 +45,19 @@ personRoutes.get('/:id/combined_credits', async (req, res) => {
cast: combinedCredits.cast.map((result) =>
mapCastCredits(
result,
castMedia.find((med) => med.tmdbId === result.id)
castMedia.find(
(med) =>
med.tmdbId === result.id && med.mediaType === result.media_type
)
)
),
crew: combinedCredits.crew.map((result) =>
mapCrewCredits(
result,
crewMedia.find((med) => med.tmdbId === result.id)
crewMedia.find(
(med) =>
med.tmdbId === result.id && med.mediaType === result.media_type
)
)
),
id: combinedCredits.id,

View File

@@ -102,7 +102,7 @@ requestRoutes.post(
: await tmdb.getTvShow({ tvId: req.body.mediaId });
let media = await mediaRepository.findOne({
where: { tmdbId: req.body.mediaId },
where: { tmdbId: req.body.mediaId, mediaType: req.body.mediaType },
relations: ['requests'],
});
@@ -164,7 +164,7 @@ requestRoutes.post(
if (finalSeasons.length === 0) {
return next({
status: 500,
status: 202,
message: 'No seasons available to request',
});
}

View File

@@ -25,6 +25,8 @@ import { Notification } from '../lib/notifications';
import DiscordAgent from '../lib/notifications/agents/discord';
import EmailAgent from '../lib/notifications/agents/email';
import SlackAgent from '../lib/notifications/agents/slack';
import TelegramAgent from '../lib/notifications/agents/telegram';
import PushoverAgent from '../lib/notifications/agents/pushover';
const settingsRoutes = Router();
@@ -503,6 +505,74 @@ settingsRoutes.post('/notifications/slack/test', (req, res, next) => {
return res.status(204).send();
});
settingsRoutes.get('/notifications/telegram', (_req, res) => {
const settings = getSettings();
res.status(200).json(settings.notifications.agents.telegram);
});
settingsRoutes.post('/notifications/telegram', (req, res) => {
const settings = getSettings();
settings.notifications.agents.telegram = req.body;
settings.save();
res.status(200).json(settings.notifications.agents.telegram);
});
settingsRoutes.post('/notifications/telegram/test', (req, res, next) => {
if (!req.user) {
return next({
status: 500,
message: 'User information missing from request',
});
}
const telegramAgent = new TelegramAgent(req.body);
telegramAgent.send(Notification.TEST_NOTIFICATION, {
notifyUser: req.user,
subject: 'Test Notification',
message:
'This is a test notification! Check check, 1, 2, 3. Are we coming in clear?',
});
return res.status(204).send();
});
settingsRoutes.get('/notifications/pushover', (_req, res) => {
const settings = getSettings();
res.status(200).json(settings.notifications.agents.pushover);
});
settingsRoutes.post('/notifications/pushover', (req, res) => {
const settings = getSettings();
settings.notifications.agents.pushover = req.body;
settings.save();
res.status(200).json(settings.notifications.agents.pushover);
});
settingsRoutes.post('/notifications/pushover/test', (req, res, next) => {
if (!req.user) {
return next({
status: 500,
message: 'User information missing from request',
});
}
const pushoverAgent = new PushoverAgent(req.body);
pushoverAgent.send(Notification.TEST_NOTIFICATION, {
notifyUser: req.user,
subject: 'Test Notification',
message:
'This is a test notification! Check check, 1, 2, 3. Are we coming in clear?',
});
return res.status(204).send();
});
settingsRoutes.get('/notifications/email', (_req, res) => {
const settings = getSettings();

View File

@@ -5,6 +5,7 @@ import { mapTvResult } from '../models/Search';
import Media from '../entity/Media';
import RottenTomatoes from '../api/rottentomatoes';
import logger from '../logger';
import { MediaType } from '../constants/media';
const tvRoutes = Router();
@@ -16,7 +17,7 @@ tvRoutes.get('/:id', async (req, res, next) => {
language: req.query.language as string,
});
const media = await Media.getMedia(tv.id);
const media = await Media.getMedia(tv.id, MediaType.TV);
return res.status(200).json(mapTvDetails(tv, media));
} catch (e) {
@@ -60,7 +61,9 @@ tvRoutes.get('/:id/recommendations', async (req, res) => {
results: results.results.map((result) =>
mapTvResult(
result,
media.find((req) => req.tmdbId === result.id)
media.find(
(req) => req.tmdbId === result.id && req.mediaType === MediaType.TV
)
)
),
});
@@ -86,7 +89,9 @@ tvRoutes.get('/:id/similar', async (req, res) => {
results: results.results.map((result) =>
mapTvResult(
result,
media.find((req) => req.tmdbId === result.id)
media.find(
(req) => req.tmdbId === result.id && req.mediaType === MediaType.TV
)
)
),
});

View File

@@ -1,3 +1,20 @@
import { existsSync } from 'fs';
import path from 'path';
import logger from '../logger';
const COMMIT_TAG_PATH = path.join(__dirname, '../../committag.json');
let commitTag = 'local';
if (existsSync(COMMIT_TAG_PATH)) {
// eslint-disable-next-line @typescript-eslint/no-var-requires
commitTag = require(COMMIT_TAG_PATH).commitTag;
logger.info(`Commit Tag: ${commitTag}`);
}
export const getCommitTag = (): string => {
return commitTag;
};
export const getAppVersion = (): string => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { version } = require('../../package.json');
@@ -5,7 +22,7 @@ export const getAppVersion = (): string => {
let finalVersion = version;
if (version === '0.1.0') {
finalVersion = `develop-${process.env.COMMIT_TAG ?? 'local'}`;
finalVersion = `develop-${getCommitTag()}`;
}
return finalVersion;

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="57 57 602 602" version="1.1">
<g id="layer1" stroke="none" stroke-width="1" fill="rgb(31, 41, 55)" fill-rule="evenodd" transform="translate(58.964119, 58.887520)" opacity="0.91">
<ellipse style="fill: rgb(255, 255, 255); fill-rule: evenodd; stroke: rgb(255, 255, 255); stroke-width: 0;" transform="matrix(-0.674571, 0.73821, -0.73821, -0.674571, 556.833239, 241.613465)" cx="216.308" cy="152.076" rx="296.855" ry="296.855"></ellipse>
<path d="M 280.949 172.514 L 355.429 162.714 L 282.909 326.374 L 282.909 326.374 C 295.649 325.394 308.142 321.067 320.389 313.394 L 320.389 313.394 L 320.389 313.394 C 332.642 305.714 343.916 296.077 354.209 284.484 L 354.209 284.484 L 354.209 284.484 C 364.496 272.884 373.396 259.981 380.909 245.774 L 380.909 245.774 L 380.909 245.774 C 388.422 231.561 393.812 217.594 397.079 203.874 L 397.079 203.874 L 397.079 203.874 C 399.039 195.381 399.939 187.214 399.779 179.374 L 399.779 179.374 L 399.779 179.374 C 399.612 171.534 397.569 164.674 393.649 158.794 L 393.649 158.794 L 393.649 158.794 C 389.729 152.914 383.766 148.177 375.759 144.584 L 375.759 144.584 L 375.759 144.584 C 367.759 140.991 356.899 139.194 343.179 139.194 L 343.179 139.194 L 343.179 139.194 C 327.172 139.194 311.409 141.807 295.889 147.034 L 295.889 147.034 L 295.889 147.034 C 280.376 152.261 266.002 159.857 252.769 169.824 L 252.769 169.824 L 252.769 169.824 C 239.542 179.784 228.029 192.197 218.229 207.064 L 218.229 207.064 L 218.229 207.064 C 208.429 221.924 201.406 238.827 197.159 257.774 L 197.159 257.774 L 197.159 257.774 C 195.526 263.981 194.546 268.961 194.219 272.714 L 194.219 272.714 L 194.219 272.714 C 193.892 276.474 193.812 279.577 193.979 282.024 L 193.979 282.024 L 193.979 282.024 C 194.139 284.477 194.462 286.357 194.949 287.664 L 194.949 287.664 L 194.949 287.664 C 195.442 288.971 195.852 290.277 196.179 291.584 L 196.179 291.584 L 196.179 291.584 C 179.519 291.584 167.349 288.234 159.669 281.534 L 159.669 281.534 L 159.669 281.534 C 151.996 274.841 150.119 263.164 154.039 246.504 L 154.039 246.504 L 154.039 246.504 C 157.959 229.191 166.862 212.694 180.749 197.014 L 180.749 197.014 L 180.749 197.014 C 194.629 181.334 211.122 167.531 230.229 155.604 L 230.229 155.604 L 230.229 155.604 C 249.342 143.684 270.249 134.214 292.949 127.194 L 292.949 127.194 L 292.949 127.194 C 315.656 120.167 337.789 116.654 359.349 116.654 L 359.349 116.654 L 359.349 116.654 C 378.296 116.654 394.219 119.347 407.119 124.734 L 407.119 124.734 L 407.119 124.734 C 420.026 130.127 430.072 137.234 437.259 146.054 L 437.259 146.054 L 437.259 146.054 C 444.446 154.874 448.936 165.164 450.729 176.924 L 450.729 176.924 L 450.729 176.924 C 452.529 188.684 451.959 200.934 449.019 213.674 L 449.019 213.674 L 449.019 213.674 C 445.426 229.027 438.646 244.464 428.679 259.984 L 428.679 259.984 L 428.679 259.984 C 418.719 275.497 406.226 289.544 391.199 302.124 L 391.199 302.124 L 391.199 302.124 C 376.172 314.697 358.939 324.904 339.499 332.744 L 339.499 332.744 L 339.499 332.744 C 320.066 340.584 299.406 344.504 277.519 344.504 L 277.519 344.504 L 275.069 344.504 L 212.839 484.154 L 142.279 484.154 L 280.949 172.514 Z" transform="matrix(1, 0, 0, 1, 0, 0)" style="fill-rule: nonzero; white-space: pre;"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 24C0 37.2548 10.7452 48 24 48C37.2548 48 48 37.2548 48 24C48 10.7452 37.2548 0 24 0C10.7452 0 0 10.7452 0 24ZM19.6 35L20.0083 28.8823L20.008 28.882L31.1369 18.839C31.6253 18.4055 31.0303 18.1941 30.3819 18.5873L16.6473 27.2523L10.7147 25.4007C9.4335 25.0084 9.4243 24.128 11.0023 23.4951L34.1203 14.5809C35.1762 14.1015 36.1953 14.8345 35.7922 16.4505L31.8552 35.0031C31.5803 36.3215 30.7837 36.6368 29.68 36.0278L23.6827 31.5969L20.8 34.4C20.7909 34.4088 20.7819 34.4176 20.7729 34.4264C20.4505 34.7403 20.1837 35 19.6 35Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 679 B

1
src/assets/xcircle.svg Normal file
View File

@@ -0,0 +1 @@
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>

After

Width:  |  Height:  |  Size: 267 B

View File

@@ -102,7 +102,7 @@ const CollectionDetails: React.FC<CollectionDetailsProps> = ({
return (
<div
className="px-4 pt-4 -mx-4 -mt-2 bg-center bg-cover sm:px-8 "
className="px-4 pt-4 -mx-4 -mt-2 bg-center bg-cover"
style={{
height: 493,
backgroundImage: `linear-gradient(180deg, rgba(17, 24, 39, 0.47) 0%, rgba(17, 24, 39, 1) 100%), url(//image.tmdb.org/t/p/w1920_and_h800_multi_faces/${data.backdropPath})`,

View File

@@ -7,7 +7,7 @@ interface ListItemProps {
const ListItem: React.FC<ListItemProps> = ({ title, children }) => {
return (
<div className="py-4 sm:py-5 sm:grid sm:grid-cols-3 sm:gap-4">
<div className="py-4 sm:grid sm:grid-cols-3 sm:gap-4">
<dt className="text-sm font-medium text-gray-200">{title}</dt>
<dd className="mt-1 flex text-sm text-gray-400 sm:mt-0 sm:col-span-2">
<span className="flex-grow">{children}</span>

View File

@@ -98,7 +98,7 @@ const Modal: React.FC<ModalProps> = ({
show={!loading}
>
<div
className="inline-block align-bottom bg-gray-700 sm:rounded-lg px-4 pt-5 pb-4 text-left overflow-auto shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-3xl w-full sm:p-6 max-h-full"
className="inline-block align-bottom bg-gray-700 sm:rounded-lg px-4 pt-5 pb-4 text-left overflow-auto shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-3xl w-full max-h-full"
role="dialog"
aria-modal="true"
aria-labelledby="modal-headline"
@@ -166,7 +166,7 @@ const Modal: React.FC<ModalProps> = ({
<Button
buttonType={cancelButtonType}
onClick={onCancel}
className="ml-3 sm:ml-0 sm:px-4"
className="ml-3 sm:ml-0"
>
{cancelText
? cancelText

View File

@@ -61,7 +61,7 @@ const SlideOver: React.FC<SlideOverProps> = ({
>
<div className="w-screen max-w-md" ref={slideoverRef}>
<div className="flex flex-col h-full overflow-y-scroll bg-gray-700 shadow-xl">
<header className="px-4 py-6 space-y-1 bg-indigo-600 sm:px-6">
<header className="px-4 py-6 space-y-1 bg-indigo-600">
<div className="flex items-center justify-between space-x-3">
<h2 className="text-lg font-medium leading-7 text-white">
{title}
@@ -97,7 +97,7 @@ const SlideOver: React.FC<SlideOverProps> = ({
</div>
)}
</header>
<div className="relative flex-1 px-4 py-6 text-white sm:px-6">
<div className="relative flex-1 px-4 py-6 text-white">
{children}
</div>
</div>

View File

@@ -71,8 +71,8 @@ const TD: React.FC<TDProps> = ({
const Table: React.FC = ({ children }) => {
return (
<div className="flex flex-col">
<div className="my-2 overflow-x-auto -mx-6 sm:-mx-6 md:mx-4 lg:mx-4">
<div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
<div className="my-2 overflow-x-auto -mx-6 md:mx-0 lg:mx-0">
<div className="py-2 align-middle inline-block min-w-full">
<div className="shadow overflow-hidden sm:rounded-lg">
<table className="min-w-full">{children}</table>
</div>

View File

@@ -1,70 +0,0 @@
import React, { useContext } from 'react';
import { useSWRInfinite } from 'swr';
import type { MovieResult } from '../../../../server/models/Search';
import ListView from '../../Common/ListView';
import { LanguageContext } from '../../../context/LanguageContext';
import Header from '../../Common/Header';
interface SearchResult {
page: number;
totalResults: number;
totalPages: number;
results: MovieResult[];
}
const Holiday: React.FC = () => {
const { locale } = useContext(LanguageContext);
const { data, error, size, setSize } = useSWRInfinite<SearchResult>(
(pageIndex: number, previousPageData: SearchResult | null) => {
if (previousPageData && pageIndex + 1 > previousPageData.totalPages) {
return null;
}
return `/api/v1/discover/keyword/207317/movies?page=${
pageIndex + 1
}&language=${locale}`;
},
{
initialSize: 3,
}
);
const isLoadingInitialData = !data && !error;
const isLoadingMore =
isLoadingInitialData ||
(size > 0 && data && typeof data[size - 1] === 'undefined');
const fetchMore = () => {
setSize(size + 1);
};
if (error) {
return <div>{error}</div>;
}
const titles = data?.reduce(
(a, v) => [...a, ...v.results],
[] as MovieResult[]
);
const isEmpty = !isLoadingInitialData && titles?.length === 0;
const isReachingEnd =
isEmpty || (data && data[data.length - 1]?.results.length < 20);
return (
<>
<Header>Happy Holidays!</Header>
<ListView
items={titles}
isEmpty={isEmpty}
isLoading={
isLoadingInitialData || (isLoadingMore && (titles?.length ?? 0) > 0)
}
isReachingEnd={isReachingEnd}
onScrollBottom={fetchMore}
/>
</>
);
};
export default Holiday;

View File

@@ -63,12 +63,6 @@ const Discover: React.FC = () => {
} = useSWR<MovieDiscoverResult>(
`/api/v1/discover/movies/upcoming?language=${locale}`
);
const {
data: holUpcomingData,
error: holUpcomingError,
} = useSWR<MovieDiscoverResult>(
`/api/v1/discover/keyword/207317/movies?language=${locale}`
);
const { data: trendingData, error: trendingError } = useSWR<MixedResult>(
`/api/v1/discover/trending?language=${locale}`
@@ -87,9 +81,9 @@ const Discover: React.FC = () => {
return (
<>
<div className="md:flex md:items-center md:justify-between mb-4 mt-6">
<div className="mt-6 mb-4 md:flex md:items-center md:justify-between">
<div className="flex-1 min-w-0">
<div className="inline-flex text-xl leading-7 text-gray-300 hover:text-white sm:text-2xl sm:leading-9 sm:truncate items-center">
<div className="inline-flex items-center text-xl leading-7 text-gray-300 hover:text-white sm:text-2xl sm:leading-9 sm:truncate">
<span>
<FormattedMessage {...messages.recentlyAdded} />
</span>
@@ -108,10 +102,10 @@ const Discover: React.FC = () => {
/>
))}
/>
<div className="md:flex md:items-center md:justify-between mb-4 mt-6">
<div className="mt-6 mb-4 md:flex md:items-center md:justify-between">
<div className="flex-1 min-w-0">
<Link href="/requests">
<a className="inline-flex text-xl leading-7 text-gray-300 hover:text-white sm:text-2xl sm:leading-9 sm:truncate items-center">
<a className="inline-flex items-center text-xl leading-7 text-gray-300 hover:text-white sm:text-2xl sm:leading-9 sm:truncate">
<span>
<FormattedMessage {...messages.recentrequests} />
</span>
@@ -146,61 +140,10 @@ const Discover: React.FC = () => {
placeholder={<RequestCard.Placeholder />}
emptyMessage={intl.formatMessage(messages.nopending)}
/>
{/* Special Temporary Slider */}
<div className="md:flex md:items-center md:justify-between mb-4 mt-6">
<div className="flex-1 min-w-0">
<Link href="/discover/holiday">
<a className="inline-flex text-xl leading-7 text-gray-300 hover:text-white sm:text-2xl sm:leading-9 sm:truncate items-center">
<span>
<span role="img" aria-label="Christmas Tree" className="mr-2">
🎄
</span>
Happy Holidays!
<span role="img" aria-label="Christmas Tree" className="ml-2">
🎄
</span>
</span>
<svg
className="w-6 h-6 ml-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13 9l3 3m0 0l-3 3m3-3H8m13 0a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</a>
</Link>
</div>
</div>
<Slider
sliderKey="holiday"
isLoading={!holUpcomingData && !holUpcomingError}
isEmpty={false}
items={holUpcomingData?.results.map((title) => (
<TitleCard
key={`holiday-movie-slider-${title.id}`}
id={title.id}
image={title.posterPath}
status={title.mediaInfo?.status}
summary={title.overview}
title={title.title}
userScore={title.voteAverage}
year={title.releaseDate}
mediaType={title.mediaType}
/>
))}
/>
{/* End Special Temporary Slider */}
<div className="md:flex md:items-center md:justify-between mb-4 mt-6">
<div className="mt-6 mb-4 md:flex md:items-center md:justify-between">
<div className="flex-1 min-w-0">
<Link href="/discover/movies/upcoming">
<a className="inline-flex text-xl leading-7 text-gray-300 hover:text-white sm:text-2xl sm:leading-9 sm:truncate items-center">
<a className="inline-flex items-center text-xl leading-7 text-gray-300 hover:text-white sm:text-2xl sm:leading-9 sm:truncate">
<span>
<FormattedMessage {...messages.upcoming} />
</span>
@@ -240,10 +183,10 @@ const Discover: React.FC = () => {
/>
))}
/>
<div className="md:flex md:items-center md:justify-between mb-4 mt-6">
<div className="mt-6 mb-4 md:flex md:items-center md:justify-between">
<div className="flex-1 min-w-0">
<Link href="/discover/trending">
<a className="inline-flex text-xl leading-7 text-gray-300 hover:text-white sm:text-2xl sm:leading-9 sm:truncate items-center">
<a className="inline-flex items-center text-xl leading-7 text-gray-300 hover:text-white sm:text-2xl sm:leading-9 sm:truncate">
<span>
<FormattedMessage {...messages.trending} />
</span>
@@ -308,10 +251,10 @@ const Discover: React.FC = () => {
}
})}
/>
<div className="md:flex md:items-center md:justify-between mb-4 mt-6">
<div className="mt-6 mb-4 md:flex md:items-center md:justify-between">
<div className="flex-1 min-w-0">
<Link href="/discover/movies">
<a className="inline-flex text-xl leading-7 text-gray-300 hover:text-white sm:text-2xl sm:leading-9 sm:truncate items-center">
<a className="inline-flex items-center text-xl leading-7 text-gray-300 hover:text-white sm:text-2xl sm:leading-9 sm:truncate">
<span>
<FormattedMessage {...messages.popularmovies} />
</span>
@@ -351,10 +294,10 @@ const Discover: React.FC = () => {
/>
))}
/>
<div className="md:flex md:items-center md:justify-between mb-4 mt-4">
<div className="mt-4 mb-4 md:flex md:items-center md:justify-between">
<div className="flex-1 min-w-0">
<Link href="/discover/tv">
<a className="inline-flex text-xl leading-7 text-gray-300 hover:text-white sm:text-2xl sm:leading-9 sm:truncate items-center">
<a className="inline-flex items-center text-xl leading-7 text-gray-300 hover:text-white sm:text-2xl sm:leading-9 sm:truncate">
<span>
<FormattedMessage {...messages.populartv} />
</span>

View File

@@ -1,6 +1,7 @@
import React from 'react';
import useSearchInput from '../../../hooks/useSearchInput';
import { defineMessages, useIntl } from 'react-intl';
import ClearButton from '../../../assets/xcircle.svg';
const messages = defineMessages({
searchPlaceholder: 'Search Movies & TV',
@@ -8,16 +9,16 @@ const messages = defineMessages({
const SearchInput: React.FC = () => {
const intl = useIntl();
const { searchValue, setSearchValue, setIsOpen } = useSearchInput();
const { searchValue, setSearchValue, setIsOpen, clear } = useSearchInput();
return (
<div className="flex-1 flex">
<div className="w-full flex md:ml-0">
<div className="flex flex-1">
<div className="flex w-full md:ml-0">
<label htmlFor="search_field" className="sr-only">
Search
</label>
<div className="relative w-full text-white focus-within:text-gray-200">
<div className="absolute inset-y-0 left-0 flex items-center pointer-events-none">
<svg className="h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path
fillRule="evenodd"
clipRule="evenodd"
@@ -27,14 +28,27 @@ const SearchInput: React.FC = () => {
</div>
<input
id="search_field"
className="block w-full h-full pl-8 pr-1 py-2 rounded-md border-transparent focus:border-transparent bg-gray-600 text-white placeholder-gray-300 focus:outline-none focus:ring-0 focus:placeholder-gray-400 sm:text-base"
style={{ paddingRight: searchValue.length > 0 ? '1.75rem' : '' }}
className="block w-full h-full py-2 pl-8 text-white placeholder-gray-300 bg-gray-600 border-transparent rounded-md focus:border-transparent focus:outline-none focus:ring-0 focus:placeholder-gray-400 sm:text-base"
placeholder={intl.formatMessage(messages.searchPlaceholder)}
type="search"
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
onFocus={() => setIsOpen(true)}
onBlur={() => setIsOpen(false)}
onBlur={() => {
if (searchValue === '') {
setIsOpen(false);
}
}}
/>
{searchValue.length > 0 && (
<button
className="absolute inset-y-0 right-0 p-1 m-auto text-gray-400 transition border-none outline-none h-7 w-7 focus:outline-none focus:border-none hover:text-white"
onClick={() => clear()}
>
<ClearButton />
</button>
)}
</div>
</div>
</div>

View File

@@ -175,11 +175,9 @@ const Sidebar: React.FC<SidebarProps> = ({ open, setClosed }) => {
>
<div className="flex-shrink-0 flex items-center px-4">
<span className="text-xl text-gray-50">
<Link href="/">
<a>
<img src="/logo.png" alt="Overseerr Logo" />
</a>
</Link>
<a href="/">
<img src="/logo.png" alt="Overseerr Logo" />
</a>
</span>
</div>
<nav className="mt-5 px-2 space-y-1">
@@ -239,11 +237,9 @@ const Sidebar: React.FC<SidebarProps> = ({ open, setClosed }) => {
<div className="flex-1 flex flex-col pt-5 pb-4 overflow-y-auto">
<div className="flex items-center flex-shrink-0 px-4">
<span className="text-2xl text-gray-50">
<Link href="/">
<a>
<img src="/logo.png" alt="Overseerr Logo" />
</a>
</Link>
<a href="/">
<img src="/logo.png" alt="Overseerr Logo" />
</a>
</span>
</div>
<nav className="mt-5 flex-1 px-2 bg-gray-800 space-y-1">

View File

@@ -18,18 +18,18 @@ const Layout: React.FC = ({ children }) => {
const router = useRouter();
return (
<div className="min-h-full h-full flex bg-gray-900">
<div className="flex h-full min-w-0 min-h-full bg-gray-900">
<Sidebar open={isSidebarOpen} setClosed={() => setSidebarOpen(false)} />
<div className="flex flex-col w-0 flex-1 md:ml-64 relative mb-16">
<div className="z-10 flex-shrink-0 flex h-16 bg-gray-600 shadow fixed right-0 left-0 md:left-64">
<div className="relative flex flex-col flex-1 w-0 min-w-0 mb-16 md:ml-64">
<div className="fixed left-0 right-0 z-10 flex flex-shrink-0 h-16 bg-gray-600 shadow md:left-64">
<button
className="px-4 border-r border-gray-800 text-gray-200 focus:outline-none focus:bg-gray-300 focus:text-gray-600 md:hidden"
className="px-4 text-gray-200 border-r border-gray-800 focus:outline-none focus:bg-gray-300 focus:text-gray-600 md:hidden"
aria-label="Open sidebar"
onClick={() => setSidebarOpen(true)}
>
<svg
className="h-6 w-6"
className="w-6 h-6"
stroke="currentColor"
fill="none"
viewBox="0 0 24 24"
@@ -42,7 +42,7 @@ const Layout: React.FC = ({ children }) => {
/>
</svg>
</button>
<div className="flex-1 px-4 flex justify-between">
<div className="flex justify-between flex-1 px-4">
<SearchInput />
<div className="flex items-center md:ml-6">
<LanguagePicker />
@@ -51,18 +51,15 @@ const Layout: React.FC = ({ children }) => {
</div>
</div>
<main
className="relative z-0 top-16 focus:outline-none right-0"
tabIndex={0}
>
<div className="pt-2 pb-6 md:py-6">
<div className="max-w-8xl mx-auto px-4 sm:px-6 md:px-8">
<main className="relative z-0 top-16 focus:outline-none" tabIndex={0}>
<div className="pt-2 pb-6">
<div className="px-4 mx-auto max-w-8xl">
{router.pathname === '/' && hasPermission(Permission.ADMIN) && (
<div className="rounded-md bg-indigo-700 p-4 mt-2">
<div className="p-4 mt-2 bg-indigo-700 rounded-md">
<div className="flex">
<div className="flex-shrink-0">
<svg
className="h-5 w-5 text-white"
className="w-5 h-5 text-white"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
@@ -74,14 +71,14 @@ const Layout: React.FC = ({ children }) => {
/>
</svg>
</div>
<div className="ml-3 flex-1 md:flex md:justify-between">
<div className="flex-1 ml-3 md:flex md:justify-between">
<p className="text-sm leading-5 text-white">
<FormattedMessage {...messages.alphawarning} />
</p>
<p className="mt-3 text-sm leading-5 md:mt-0 md:ml-6">
<a
href="http://github.com/sct/overseerr"
className="whitespace-nowrap font-medium text-indigo-100 hover:text-white transition ease-in-out duration-150"
className="font-medium text-indigo-100 transition duration-150 ease-in-out whitespace-nowrap hover:text-white"
target="_blank"
rel="noreferrer"
>

View File

@@ -51,31 +51,33 @@ const Login: React.FC = () => {
}, [user, router]);
return (
<div className="min-h-screen bg-gray-900 flex flex-col justify-center py-12 sm:px-6 lg:px-8 relative">
<div className="relative flex flex-col justify-center min-h-screen py-12 bg-gray-900">
<ImageFader
backgroundImages={[
'/images/rotate1.jpg',
'/images/rotate2.jpg',
'/images/rotate3.jpg',
'/images/rotate4.jpg',
'/images/rotate5.jpg',
'/images/rotate6.jpg',
]}
/>
<div className="absolute top-4 right-4 z-50">
<div className="absolute z-50 top-4 right-4">
<LanguagePicker />
</div>
<div className="px-4 sm:px-2 md:px-0 sm:mx-auto sm:w-full sm:max-w-md relative z-40">
<div className="relative z-40 px-4 sm:mx-auto sm:w-full sm:max-w-md">
<img
src="/logo.png"
className="mx-auto max-h-32 w-auto"
className="w-auto mx-auto max-h-32"
alt="Overseerr Logo"
/>
<h2 className="mt-2 text-center text-3xl leading-9 font-extrabold text-gray-100">
<h2 className="mt-2 text-3xl font-extrabold leading-9 text-center text-gray-100">
<FormattedMessage {...messages.signinplex} />
</h2>
</div>
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md relative z-50">
<div className="relative z-50 mt-8 sm:mx-auto sm:w-full sm:max-w-md">
<div
className="bg-gray-800 bg-opacity-50 py-8 px-4 shadow sm:rounded-lg sm:px-10"
className="px-4 py-8 bg-gray-800 bg-opacity-50 shadow sm:rounded-lg"
style={{ backdropFilter: 'blur(5px)' }}
>
<Transition
@@ -87,11 +89,11 @@ const Login: React.FC = () => {
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="rounded-md bg-red-600 p-4 mb-4">
<div className="p-4 mb-4 bg-red-600 rounded-md">
<div className="flex">
<div className="flex-shrink-0">
<svg
className="h-5 w-5 text-red-300"
className="w-5 h-5 text-red-300"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"

View File

@@ -146,7 +146,7 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
return (
<div
className="px-4 pt-4 -mx-4 -mt-2 bg-center bg-cover sm:px-8 "
className="px-4 pt-4 -mx-4 -mt-2 bg-center bg-cover"
style={{
height: 493,
backgroundImage: `linear-gradient(180deg, rgba(17, 24, 39, 0.47) 0%, rgba(17, 24, 39, 1) 100%), url(//image.tmdb.org/t/p/w1920_and_h800_multi_faces/${data.backdropPath})`,

View File

@@ -0,0 +1,70 @@
import React from 'react';
import { NotificationItem, hasNotificationType } from '..';
interface NotificationTypeProps {
option: NotificationItem;
currentTypes: number;
parent?: NotificationItem;
onUpdate: (newTypes: number) => void;
}
const NotificationType: React.FC<NotificationTypeProps> = ({
option,
currentTypes,
onUpdate,
parent,
}) => {
return (
<>
<div
className={`relative flex items-start first:mt-0 mt-4 ${
!!parent?.value && hasNotificationType(parent.value, currentTypes)
? 'opacity-50'
: ''
}`}
>
<div className="flex items-center h-5">
<input
id={option.id}
name="permissions"
type="checkbox"
className="w-4 h-4 text-indigo-600 transition duration-150 ease-in-out rounded-md form-checkbox"
disabled={
!!parent?.value && hasNotificationType(parent.value, currentTypes)
}
onClick={() => {
onUpdate(
hasNotificationType(option.value, currentTypes)
? currentTypes - option.value
: currentTypes + option.value
);
}}
defaultChecked={
hasNotificationType(option.value, currentTypes) ||
(!!parent?.value &&
hasNotificationType(parent.value, currentTypes))
}
/>
</div>
<div className="ml-3 text-sm leading-5">
<label htmlFor={option.id} className="font-medium">
{option.name}
</label>
<p className="text-gray-500">{option.description}</p>
</div>
</div>
{(option.children ?? []).map((child) => (
<div key={`notification-type-child-${child.id}`} className="pl-6 mt-4">
<NotificationType
option={child}
currentTypes={currentTypes}
onUpdate={(newTypes) => onUpdate(newTypes)}
parent={option}
/>
</div>
))}
</>
);
};
export default NotificationType;

View File

@@ -0,0 +1,106 @@
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import NotificationType from './NotificationType';
const messages = defineMessages({
mediarequested: 'Media Requested',
mediarequestedDescription:
'Sends a notification when new media is requested. For certain agents, this will only send the notification to admins or users with the "Manage Requests" permission.',
mediaapproved: 'Media Approved',
mediaapprovedDescription: 'Sends a notification when media is approved.',
mediaavailable: 'Media Available',
mediaavailableDescription:
'Sends a notification when media becomes available.',
mediafailed: 'Media Failed',
mediafailedDescription:
'Sends a notification when media fails to be added to services (Radarr/Sonarr). For certain agents, this will only send the notification to admins or users with the "Manage Requests" permission.',
});
export const hasNotificationType = (
types: Notification | Notification[],
value: number
): boolean => {
let total = 0;
if (types === 0) {
return true;
}
if (Array.isArray(types)) {
total = types.reduce((a, v) => a + v, 0);
} else {
total = types;
}
return !!(value & total);
};
export enum Notification {
MEDIA_PENDING = 2,
MEDIA_APPROVED = 4,
MEDIA_AVAILABLE = 8,
MEDIA_FAILED = 16,
TEST_NOTIFICATION = 32,
}
export interface NotificationItem {
id: string;
name: string;
description: string;
value: Notification;
children?: NotificationItem[];
}
interface NotificationTypeSelectorProps {
currentTypes: number;
onUpdate: (newTypes: number) => void;
}
const NotificationTypeSelector: React.FC<NotificationTypeSelectorProps> = ({
currentTypes,
onUpdate,
}) => {
const intl = useIntl();
const types: NotificationItem[] = [
{
id: 'media-requested',
name: intl.formatMessage(messages.mediarequested),
description: intl.formatMessage(messages.mediarequestedDescription),
value: Notification.MEDIA_PENDING,
},
{
id: 'media-approved',
name: intl.formatMessage(messages.mediaapproved),
description: intl.formatMessage(messages.mediaapprovedDescription),
value: Notification.MEDIA_APPROVED,
},
{
id: 'media-available',
name: intl.formatMessage(messages.mediaavailable),
description: intl.formatMessage(messages.mediaavailableDescription),
value: Notification.MEDIA_AVAILABLE,
},
{
id: 'media-failed',
name: intl.formatMessage(messages.mediafailed),
description: intl.formatMessage(messages.mediafailedDescription),
value: Notification.MEDIA_FAILED,
},
];
return (
<>
{types.map((type) => (
<NotificationType
key={`notification-type-${type.id}`}
option={type}
currentTypes={currentTypes}
onUpdate={onUpdate}
/>
))}
</>
);
};
export default NotificationTypeSelector;

View File

@@ -1,5 +1,5 @@
import Link from 'next/link';
import React from 'react';
import React, { useState } from 'react';
interface PersonCardProps {
personId: number;
@@ -16,13 +16,30 @@ const PersonCard: React.FC<PersonCardProps> = ({
profilePath,
canExpand = false,
}) => {
const [isHovered, setHovered] = useState(false);
return (
<Link href={`/person/${personId}`}>
<a className={canExpand ? 'w-full' : 'w-36 sm:w-36 md:w-44'}>
<a
className={canExpand ? 'w-full' : 'w-36 sm:w-36 md:w-44'}
onMouseEnter={() => {
setHovered(true);
}}
onMouseLeave={() => setHovered(false)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
setHovered(true);
}
}}
role="link"
tabIndex={0}
>
<div
className={`relative ${
canExpand ? 'w-full' : 'w-36 sm:w-36 md:w-44'
} bg-gray-600 rounded-lg text-white shadow-lg hover:bg-gray-500 transition ease-in-out duration-150 cursor-pointer`}
} rounded-lg text-white shadow-lg transition ease-in-out duration-150 cursor-pointer transform-gpu ${
isHovered ? 'bg-gray-500 scale-105' : 'bg-gray-600 scale-100'
}`}
>
<div style={{ paddingBottom: '150%' }}>
<div className="absolute inset-0 flex flex-col items-center w-full h-full p-2">

View File

@@ -1,5 +1,5 @@
import { useRouter } from 'next/router';
import React, { useContext, useState } from 'react';
import React, { useContext, useMemo, useState } from 'react';
import TruncateMarkup from 'react-truncate-markup';
import useSWR from 'swr';
import type { PersonDetail } from '../../../server/models/Person';
@@ -11,6 +11,7 @@ import { defineMessages, useIntl } from 'react-intl';
import { LanguageContext } from '../../context/LanguageContext';
import ImageFader from '../Common/ImageFader';
import Ellipsis from '../../assets/ellipsis.svg';
import { groupBy } from 'lodash';
const messages = defineMessages({
appearsin: 'Appears in',
@@ -35,6 +36,42 @@ const PersonDetails: React.FC = () => {
`/api/v1/person/${router.query.personId}/combined_credits?language=${locale}`
);
const sortedCast = useMemo(() => {
const grouped = groupBy(combinedCredits?.cast ?? [], 'id');
const reduced = Object.values(grouped).map((objs) => ({
...objs[0],
character: objs.map((pos) => pos.character).join(', '),
}));
return reduced.sort((a, b) => {
const aVotes = a.voteCount ?? 0;
const bVotes = b.voteCount ?? 0;
if (aVotes > bVotes) {
return -1;
}
return 1;
});
}, [combinedCredits]);
const sortedCrew = useMemo(() => {
const grouped = groupBy(combinedCredits?.crew ?? [], 'id');
const reduced = Object.values(grouped).map((objs) => ({
...objs[0],
job: objs.map((pos) => pos.job).join(', '),
}));
return reduced.sort((a, b) => {
const aVotes = a.voteCount ?? 0;
const bVotes = b.voteCount ?? 0;
if (aVotes > bVotes) {
return -1;
}
return 1;
});
}, [combinedCredits]);
if (!data && !error) {
return <LoadingSpinner />;
}
@@ -43,24 +80,6 @@ const PersonDetails: React.FC = () => {
return <Error statusCode={404} />;
}
const sortedCast = combinedCredits?.cast.sort((a, b) => {
const aVotes = a.voteCount ?? 0;
const bVotes = b.voteCount ?? 0;
if (aVotes > bVotes) {
return -1;
}
return 1;
});
const sortedCrew = combinedCredits?.crew.sort((a, b) => {
const aVotes = a.voteCount ?? 0;
const bVotes = b.voteCount ?? 0;
if (aVotes > bVotes) {
return -1;
}
return 1;
});
const isLoading = !combinedCredits && !errorCombinedCredits;
const cast = (sortedCast ?? []).length > 0 && (

View File

@@ -43,7 +43,7 @@ const RequestBlock: React.FC<RequestBlockProps> = ({ request, onUpdate }) => {
return (
<div className="block">
<div className="px-4 py-4 sm:px-6">
<div className="px-4 py-4">
<div className="flex items-center justify-between">
<div className="mr-6 flex-col items-center text-sm leading-5 text-gray-300 flex-1 min-w-0">
<div className="flex flex-nowrap mb-1 white">

View File

@@ -100,7 +100,7 @@ const RequestItem: React.FC<RequestItemProps> = ({
if (!title && !error) {
return (
<tr className="w-full bg-gray-800 animate-pulse h-24" ref={ref}>
<tr className="w-full h-24 bg-gray-800 animate-pulse" ref={ref}>
<td colSpan={6}></td>
</tr>
);
@@ -108,61 +108,62 @@ const RequestItem: React.FC<RequestItemProps> = ({
if (!title || !requestData) {
return (
<tr className="w-full bg-gray-800 animate-pulse h-24">
<tr className="w-full h-24 bg-gray-800 animate-pulse">
<td colSpan={6}></td>
</tr>
);
}
return (
<tr className="w-full bg-gray-800 h-24 p-2 relative text-white">
<Table.TD
noPadding
className="w-20 px-4 relative hidden sm:table-cell align-middle"
>
<Link
href={
request.type === 'movie'
? `/movie/${request.media.tmdbId}`
: `/tv/${request.media.tmdbId}`
}
>
<a>
<img
src={`//image.tmdb.org/t/p/w600_and_h900_bestv2${title.posterPath}`}
alt=""
className="rounded-md shadow-sm cursor-pointer transition transform-gpu duration-300 scale-100 hover:scale-105 hover:shadow-md"
/>
</a>
</Link>
</Table.TD>
<tr className="relative w-full h-24 p-2 text-white bg-gray-800">
<Table.TD>
<Link
href={
requestData.type === 'movie'
? `/movie/${requestData.media.tmdbId}`
: `/tv/${requestData.media.tmdbId}`
}
>
<a className="text-white text-xl mr-2 hover:underline">
{isMovie(title) ? title.title : title.name}
</a>
</Link>
<div className="text-sm">
{intl.formatMessage(messages.requestedby, {
username: requestData.requestedBy.username,
})}
</div>
{requestData.seasons.length > 0 && (
<div className="hidden mt-2 text-sm sm:flex items-center">
<span className="mr-2">{intl.formatMessage(messages.seasons)}</span>
{requestData.seasons.map((season) => (
<span key={`season-${season.id}`} className="mr-2">
<Badge>{season.seasonNumber}</Badge>
</span>
))}
<div className="flex items-center">
<Link
href={
request.type === 'movie'
? `/movie/${request.media.tmdbId}`
: `/tv/${request.media.tmdbId}`
}
>
<a className="flex-shrink-0 hidden mr-4 sm:block">
<img
src={`//image.tmdb.org/t/p/w600_and_h900_bestv2${title.posterPath}`}
alt=""
className="w-12 transition duration-300 scale-100 rounded-md shadow-sm cursor-pointer transform-gpu hover:scale-105 hover:shadow-md"
/>
</a>
</Link>
<div className="flex-shrink overflow-hidden">
<Link
href={
requestData.type === 'movie'
? `/movie/${requestData.media.tmdbId}`
: `/tv/${requestData.media.tmdbId}`
}
>
<a className="min-w-0 mr-2 text-xl text-white truncate hover:underline">
{isMovie(title) ? title.title : title.name}
</a>
</Link>
<div className="text-sm">
{intl.formatMessage(messages.requestedby, {
username: requestData.requestedBy.username,
})}
</div>
{requestData.seasons.length > 0 && (
<div className="items-center hidden mt-2 text-sm sm:flex">
<span className="mr-2">
{intl.formatMessage(messages.seasons)}
</span>
{requestData.seasons.map((season) => (
<span key={`season-${season.id}`} className="mr-2">
<Badge>{season.seasonNumber}</Badge>
</span>
))}
</div>
)}
</div>
)}
</div>
</Table.TD>
<Table.TD>
{requestData.media.status === MediaStatus.UNKNOWN ? (

View File

@@ -18,14 +18,28 @@ const messages = defineMessages({
'Showing <strong>{from}</strong> to <strong>{to}</strong> of <strong>{total}</strong> results',
next: 'Next',
previous: 'Previous',
filterAll: 'All',
filterPending: 'Pending',
filterApproved: 'Approved',
noresults: 'No Results.',
showallrequests: 'Show All Requests',
sortAdded: 'Request Date',
sortModified: 'Last Modified',
});
type Filter = 'all' | 'approved' | 'pending';
type Sort = 'added' | 'modified';
const RequestList: React.FC = () => {
const intl = useIntl();
const [pageIndex, setPageIndex] = useState(0);
const [currentFilter, setCurrentFilter] = useState<Filter>('pending');
const [currentSort, setCurrentSort] = useState<Sort>('added');
const { data, error, revalidate } = useSWR<RequestResultsResponse>(
`/api/v1/request?take=10&skip=${pageIndex * 10}`
`/api/v1/request?take=10&skip=${
pageIndex * 10
}&filter=${currentFilter}&sort=${currentSort}`
);
if (!data && !error) {
return <LoadingSpinner />;
@@ -40,10 +54,86 @@ const RequestList: React.FC = () => {
return (
<>
<Header>{intl.formatMessage(messages.requests)}</Header>
<div className="flex flex-col justify-between md:items-end md:flex-row">
<Header>{intl.formatMessage(messages.requests)}</Header>
<div className="flex flex-col md:flex-row">
<div className="flex mb-2 md:mb-0 md:mr-2">
<span className="inline-flex items-center px-3 text-gray-100 bg-gray-800 border border-r-0 border-gray-500 cursor-default rounded-l-md sm:text-sm">
<svg
className="w-6 h-6"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z"
clipRule="evenodd"
/>
</svg>
</span>
<select
id="filter"
name="filter"
onChange={(e) => {
setPageIndex(0);
setCurrentFilter(e.target.value as Filter);
}}
onBlur={(e) => {
setPageIndex(0);
setCurrentFilter(e.target.value as Filter);
}}
value={currentFilter}
className="flex-1 block w-full py-2 pl-3 pr-10 text-base leading-6 text-white bg-gray-700 border-gray-500 rounded-r-md form-select focus:outline-none focus:ring-blue focus:border-gray-500 sm:text-sm sm:leading-5 disabled:opacity-50"
>
<option value="all">
{intl.formatMessage(messages.filterAll)}
</option>
<option value="pending">
{intl.formatMessage(messages.filterPending)}
</option>
<option value="approved">
{intl.formatMessage(messages.filterApproved)}
</option>
</select>
</div>
<div className="flex">
<span className="inline-flex items-center px-3 text-gray-100 bg-gray-800 border border-r-0 border-gray-500 cursor-default rounded-l-md sm:text-sm">
<svg
className="w-6 h-6"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M3 3a1 1 0 000 2h11a1 1 0 100-2H3zM3 7a1 1 0 000 2h7a1 1 0 100-2H3zM3 11a1 1 0 100 2h4a1 1 0 100-2H3zM15 8a1 1 0 10-2 0v5.586l-1.293-1.293a1 1 0 00-1.414 1.414l3 3a1 1 0 001.414 0l3-3a1 1 0 00-1.414-1.414L15 13.586V8z" />
</svg>
</span>
<select
id="sort"
name="sort"
onChange={(e) => {
setPageIndex(0);
setCurrentSort(e.target.value as Sort);
}}
onBlur={(e) => {
setPageIndex(0);
setCurrentSort(e.target.value as Sort);
}}
value={currentSort}
className="flex-1 block w-full py-2 pl-3 pr-10 text-base leading-6 text-white bg-gray-700 border-gray-500 rounded-r-md form-select focus:outline-none focus:ring-blue focus:border-gray-500 sm:text-sm sm:leading-5 disabled:opacity-50"
>
<option value="added">
{intl.formatMessage(messages.sortAdded)}
</option>
<option value="modified">
{intl.formatMessage(messages.sortModified)}
</option>
</select>
</div>
</div>
</div>
<Table>
<thead>
<Table.TH className="hidden sm:table-cell"></Table.TH>
<Table.TH>{intl.formatMessage(messages.mediaInfo)}</Table.TH>
<Table.TH>{intl.formatMessage(messages.status)}</Table.TH>
<Table.TH>{intl.formatMessage(messages.requestedAt)}</Table.TH>
@@ -60,10 +150,31 @@ const RequestList: React.FC = () => {
/>
);
})}
{data.results.length === 0 && (
<tr className="relative w-full h-24 p-2 text-white bg-gray-800">
<Table.TD colSpan={6} noPadding>
<div className="flex flex-col items-center justify-center p-4">
<span>{intl.formatMessage(messages.noresults)}</span>
{currentFilter !== 'all' && (
<div className="mt-4">
<Button
buttonSize="sm"
buttonType="primary"
onClick={() => setCurrentFilter('all')}
>
{intl.formatMessage(messages.showallrequests)}
</Button>
</div>
)}
</div>
</Table.TD>
</tr>
)}
<tr>
<Table.TD colSpan={6} noPadding>
<nav
className="bg-gray-700 px-4 py-3 flex items-center justify-between text-white sm:px-6"
className="flex items-center justify-between px-4 py-3 text-white bg-gray-700"
aria-label="Pagination"
>
<div className="hidden sm:block">
@@ -73,7 +184,7 @@ const RequestList: React.FC = () => {
to:
data.results.length < 10
? pageIndex * 10 + data.results.length
: pageIndex + 1 * 10,
: (pageIndex + 1) * 10,
total: data.pageInfo.results,
strong: function strong(msg) {
return <span className="font-medium">{msg}</span>;
@@ -81,7 +192,7 @@ const RequestList: React.FC = () => {
})}
</p>
</div>
<div className="flex-1 flex justify-start sm:justify-end">
<div className="flex justify-start flex-1 sm:justify-end">
<span className="mr-2">
<Button
disabled={!hasPrevPage}

View File

@@ -168,7 +168,7 @@ const MovieRequestModal: React.FC<RequestModalProps> = ({
okButtonType={'primary'}
iconSvg={<DownloadIcon className="w-6 h-6" />}
>
{text}
<p className="text-center md:text-left">{text}</p>
</Modal>
);
};

View File

@@ -224,7 +224,7 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
>
<div className="flex flex-col">
<div className="-mx-4 overflow-auto sm:mx-0 max-h-96">
<div className="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
<div className="inline-block min-w-full py-2 align-middle">
<div className="overflow-hidden shadow sm:rounded-lg">
<table className="min-w-full">
<thead>
@@ -256,13 +256,13 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
></span>
</span>
</th>
<th className="px-6 py-3 text-xs font-medium leading-4 tracking-wider text-left text-gray-200 uppercase bg-gray-500">
<th className="px-1 md:px-6 py-3 text-xs font-medium leading-4 tracking-wider text-left text-gray-200 uppercase bg-gray-500">
{intl.formatMessage(messages.season)}
</th>
<th className="px-6 py-3 text-xs font-medium leading-4 tracking-wider text-left text-gray-200 uppercase bg-gray-500">
<th className="px-5 md:px-6 py-3 text-xs font-medium leading-4 tracking-wider text-left text-gray-200 uppercase bg-gray-500">
{intl.formatMessage(messages.numberofepisodes)}
</th>
<th className="px-6 py-3 text-xs font-medium leading-4 tracking-wider text-left text-gray-200 uppercase bg-gray-500">
<th className="px-2 md:px-6 py-3 text-xs font-medium leading-4 tracking-wider text-left text-gray-200 uppercase bg-gray-500">
{intl.formatMessage(messages.status)}
</th>
</tr>
@@ -320,17 +320,17 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
></span>
</span>
</td>
<td className="px-6 py-4 text-sm font-medium leading-5 text-gray-100 whitespace-nowrap">
<td className="px-1 md:px-6 py-4 text-sm font-medium leading-5 text-gray-100 whitespace-nowrap">
{season.seasonNumber === 0
? intl.formatMessage(messages.extras)
: intl.formatMessage(messages.seasonnumber, {
number: season.seasonNumber,
})}
</td>
<td className="px-6 py-4 text-sm leading-5 text-gray-200 whitespace-nowrap">
<td className="px-5 md:px-6 py-4 text-sm leading-5 text-gray-200 whitespace-nowrap">
{season.episodeCount}
</td>
<td className="px-6 py-4 text-sm leading-5 text-gray-200 whitespace-nowrap">
<td className="pr-2 md:px-6 py-4 text-sm leading-5 text-gray-200 whitespace-nowrap">
{!seasonRequest && !mediaSeason && (
<Badge>
{intl.formatMessage(messages.notrequested)}

View File

@@ -7,6 +7,7 @@ import { defineMessages, useIntl } from 'react-intl';
import axios from 'axios';
import * as Yup from 'yup';
import { useToasts } from 'react-toast-notifications';
import NotificationTypeSelector from '../../NotificationTypeSelector';
const messages = defineMessages({
save: 'Save Changes',
@@ -19,6 +20,7 @@ const messages = defineMessages({
discordsettingsfailed: 'Discord notification settings failed to save.',
testsent: 'Test notification sent!',
test: 'Test',
notificationtypes: 'Notification Types',
});
const NotificationsDiscord: React.FC = () => {
@@ -69,7 +71,7 @@ const NotificationsDiscord: React.FC = () => {
}
}}
>
{({ errors, touched, isSubmitting, values, isValid }) => {
{({ errors, touched, isSubmitting, values, isValid, setFieldValue }) => {
const testSettings = async () => {
await axios.post('/api/v1/settings/notifications/discord/test', {
enabled: true,
@@ -87,10 +89,10 @@ const NotificationsDiscord: React.FC = () => {
return (
<Form>
<div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5">
<div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200">
<label
htmlFor="isDefault"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
htmlFor="enabled"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
>
{intl.formatMessage(messages.agentenabled)}
</label>
@@ -99,19 +101,19 @@ const NotificationsDiscord: React.FC = () => {
type="checkbox"
id="enabled"
name="enabled"
className="form-checkbox rounded-md h-6 w-6 text-indigo-600 transition duration-150 ease-in-out"
className="w-6 h-6 text-indigo-600 transition duration-150 ease-in-out rounded-md form-checkbox"
/>
</div>
</div>
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800 sm:pt-5">
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800">
<label
htmlFor="name"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
>
{intl.formatMessage(messages.webhookUrl)}
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<div className="max-w-lg flex rounded-md shadow-sm">
<div className="flex max-w-lg rounded-md shadow-sm">
<Field
id="webhookUrl"
name="webhookUrl"
@@ -119,17 +121,41 @@ const NotificationsDiscord: React.FC = () => {
placeholder={intl.formatMessage(
messages.webhookUrlPlaceholder
)}
className="flex-1 form-input block w-full min-w-0 rounded-md transition duration-150 ease-in-out sm:text-sm sm:leading-5 bg-gray-700 border border-gray-500"
className="flex-1 block w-full min-w-0 transition duration-150 ease-in-out bg-gray-700 border border-gray-500 rounded-md form-input sm:text-sm sm:leading-5"
/>
</div>
{errors.webhookUrl && touched.webhookUrl && (
<div className="text-red-500 mt-2">{errors.webhookUrl}</div>
<div className="mt-2 text-red-500">{errors.webhookUrl}</div>
)}
</div>
</div>
<div className="mt-8 border-t border-gray-700 pt-5">
<div className="mt-6">
<div role="group" aria-labelledby="label-permissions">
<div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-baseline">
<div>
<div
className="text-base font-medium leading-6 text-gray-400 sm:text-sm sm:leading-5"
id="label-types"
>
{intl.formatMessage(messages.notificationtypes)}
</div>
</div>
<div className="mt-4 sm:mt-0 sm:col-span-2">
<div className="max-w-lg">
<NotificationTypeSelector
currentTypes={values.types}
onUpdate={(newTypes) =>
setFieldValue('types', newTypes)
}
/>
</div>
</div>
</div>
</div>
</div>
<div className="pt-5 mt-8 border-t border-gray-700">
<div className="flex justify-end">
<span className="ml-3 inline-flex rounded-md shadow-sm">
<span className="inline-flex ml-3 rounded-md shadow-sm">
<Button
buttonType="warning"
disabled={isSubmitting || !isValid}
@@ -142,7 +168,7 @@ const NotificationsDiscord: React.FC = () => {
{intl.formatMessage(messages.test)}
</Button>
</span>
<span className="ml-3 inline-flex rounded-md shadow-sm">
<span className="inline-flex ml-3 rounded-md shadow-sm">
<Button
buttonType="primary"
type="submit"

View File

@@ -7,6 +7,7 @@ import { defineMessages, useIntl } from 'react-intl';
import axios from 'axios';
import * as Yup from 'yup';
import { useToasts } from 'react-toast-notifications';
import NotificationTypeSelector from '../../NotificationTypeSelector';
const messages = defineMessages({
save: 'Save Changes',
@@ -28,6 +29,8 @@ const messages = defineMessages({
allowselfsigned: 'Allow Self-Signed Certificates',
ssldisabletip:
'SSL should be disabled on standard TLS connections (Port 587)',
senderName: 'Sender Name',
notificationtypes: 'Notification Types',
});
const NotificationsEmail: React.FC = () => {
@@ -37,7 +40,7 @@ const NotificationsEmail: React.FC = () => {
'/api/v1/settings/notifications/email'
);
const NotificationsDiscordSchema = Yup.object().shape({
const NotificationsEmailSchema = Yup.object().shape({
emailFrom: Yup.string().required(
intl.formatMessage(messages.validationFromRequired)
),
@@ -65,8 +68,9 @@ const NotificationsEmail: React.FC = () => {
authUser: data.options.authUser,
authPass: data.options.authPass,
allowSelfSigned: data.options.allowSelfSigned,
senderName: data.options.senderName,
}}
validationSchema={NotificationsDiscordSchema}
validationSchema={NotificationsEmailSchema}
onSubmit={async (values) => {
try {
await axios.post('/api/v1/settings/notifications/email', {
@@ -80,6 +84,7 @@ const NotificationsEmail: React.FC = () => {
authUser: values.authUser,
authPass: values.authPass,
allowSelfSigned: values.allowSelfSigned,
senderName: values.senderName,
},
});
addToast(intl.formatMessage(messages.emailsettingssaved), {
@@ -96,7 +101,7 @@ const NotificationsEmail: React.FC = () => {
}
}}
>
{({ errors, touched, isSubmitting, values, isValid }) => {
{({ errors, touched, isSubmitting, values, isValid, setFieldValue }) => {
const testSettings = async () => {
await axios.post('/api/v1/settings/notifications/email/test', {
enabled: true,
@@ -108,6 +113,7 @@ const NotificationsEmail: React.FC = () => {
secure: values.secure,
authUser: values.authUser,
authPass: values.authPass,
senderName: values.senderName,
},
});
@@ -119,10 +125,10 @@ const NotificationsEmail: React.FC = () => {
return (
<Form>
<div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5">
<div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200">
<label
htmlFor="enabled"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
>
{intl.formatMessage(messages.agentenabled)}
</label>
@@ -135,10 +141,10 @@ const NotificationsEmail: React.FC = () => {
/>
</div>
</div>
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800 sm:pt-5">
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800">
<label
htmlFor="emailFrom"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
>
{intl.formatMessage(messages.emailsender)}
</label>
@@ -157,10 +163,29 @@ const NotificationsEmail: React.FC = () => {
)}
</div>
</div>
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800 sm:pt-5">
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800">
<label
htmlFor="senderName"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
>
{intl.formatMessage(messages.senderName)}
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<div className="flex max-w-lg rounded-md shadow-sm">
<Field
id="senderName"
name="senderName"
placeholder="Overseerr"
type="text"
className="flex-1 block w-full min-w-0 transition duration-150 ease-in-out bg-gray-700 border border-gray-500 rounded-md form-input sm:text-sm sm:leading-5"
/>
</div>
</div>
</div>
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800">
<label
htmlFor="smtpHost"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
>
{intl.formatMessage(messages.smtpHost)}
</label>
@@ -179,10 +204,10 @@ const NotificationsEmail: React.FC = () => {
)}
</div>
</div>
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800 sm:pt-5">
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800">
<label
htmlFor="smtpPort"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
>
{intl.formatMessage(messages.smtpPort)}
</label>
@@ -201,10 +226,10 @@ const NotificationsEmail: React.FC = () => {
)}
</div>
</div>
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5">
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200">
<label
htmlFor="secure"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
>
<div className="flex flex-col">
<span>{intl.formatMessage(messages.enableSsl)}</span>
@@ -222,10 +247,10 @@ const NotificationsEmail: React.FC = () => {
/>
</div>
</div>
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5">
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200">
<label
htmlFor="allowSelfSigned"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
>
{intl.formatMessage(messages.allowselfsigned)}
</label>
@@ -238,10 +263,10 @@ const NotificationsEmail: React.FC = () => {
/>
</div>
</div>
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800 sm:pt-5">
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800">
<label
htmlFor="authUser"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
>
{intl.formatMessage(messages.authUser)}
</label>
@@ -256,10 +281,10 @@ const NotificationsEmail: React.FC = () => {
</div>
</div>
</div>
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800 sm:pt-5">
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800">
<label
htmlFor="authPass"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
>
{intl.formatMessage(messages.authPass)}
</label>
@@ -269,11 +294,36 @@ const NotificationsEmail: React.FC = () => {
id="authPass"
name="authPass"
type="password"
autoComplete="off"
className="flex-1 block w-full min-w-0 transition duration-150 ease-in-out bg-gray-700 border border-gray-500 rounded-md form-input sm:text-sm sm:leading-5"
/>
</div>
</div>
</div>
<div className="mt-6">
<div role="group" aria-labelledby="label-permissions">
<div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-baseline">
<div>
<div
className="text-base font-medium leading-6 text-gray-400 sm:text-sm sm:leading-5"
id="label-types"
>
{intl.formatMessage(messages.notificationtypes)}
</div>
</div>
<div className="mt-4 sm:mt-0 sm:col-span-2">
<div className="max-w-lg">
<NotificationTypeSelector
currentTypes={values.types}
onUpdate={(newTypes) =>
setFieldValue('types', newTypes)
}
/>
</div>
</div>
</div>
</div>
</div>
<div className="pt-5 mt-8 border-t border-gray-700">
<div className="flex justify-end">
<span className="inline-flex ml-3 rounded-md shadow-sm">

View File

@@ -0,0 +1,257 @@
import React from 'react';
import { Field, Form, Formik } from 'formik';
import useSWR from 'swr';
import LoadingSpinner from '../../../Common/LoadingSpinner';
import Button from '../../../Common/Button';
import { defineMessages, useIntl } from 'react-intl';
import axios from 'axios';
import * as Yup from 'yup';
import { useToasts } from 'react-toast-notifications';
import Alert from '../../../Common/Alert';
import NotificationTypeSelector from '../../../NotificationTypeSelector';
const messages = defineMessages({
save: 'Save Changes',
saving: 'Saving...',
agentenabled: 'Agent Enabled',
accessToken: 'Access Token',
userToken: 'User Token',
validationAccessTokenRequired: 'You must provide an access token.',
validationUserTokenRequired: 'You must provide a user token.',
pushoversettingssaved: 'Pushover notification settings saved!',
pushoversettingsfailed: 'Pushover notification settings failed to save.',
testsent: 'Test notification sent!',
test: 'Test',
settinguppushover: 'Setting up Pushover Notifications',
settinguppushoverDescription:
'To setup Pushover you need to <RegisterApplicationLink>register an application</RegisterApplicationLink> and get the access token.\
When setting up the application you can use one of the icons in the <IconLink>public folder</IconLink> on github.\
You also need the pushover user token which can be found on the start page when you log in.',
notificationtypes: 'Notification Types',
});
const NotificationsPushover: React.FC = () => {
const intl = useIntl();
const { addToast } = useToasts();
const { data, error, revalidate } = useSWR(
'/api/v1/settings/notifications/pushover'
);
const NotificationsPushoverSchema = Yup.object().shape({
accessToken: Yup.string().required(
intl.formatMessage(messages.validationAccessTokenRequired)
),
userToken: Yup.string().required(
intl.formatMessage(messages.validationUserTokenRequired)
),
});
if (!data && !error) {
return <LoadingSpinner />;
}
return (
<Formik
initialValues={{
enabled: data?.enabled,
types: data?.types,
accessToken: data?.options.accessToken,
userToken: data?.options.userToken,
}}
validationSchema={NotificationsPushoverSchema}
onSubmit={async (values) => {
try {
await axios.post('/api/v1/settings/notifications/pushover', {
enabled: values.enabled,
types: values.types,
options: {
accessToken: values.accessToken,
userToken: values.userToken,
},
});
addToast(intl.formatMessage(messages.pushoversettingssaved), {
appearance: 'success',
autoDismiss: true,
});
} catch (e) {
addToast(intl.formatMessage(messages.pushoversettingsfailed), {
appearance: 'error',
autoDismiss: true,
});
} finally {
revalidate();
}
}}
>
{({ errors, touched, isSubmitting, values, isValid, setFieldValue }) => {
const testSettings = async () => {
await axios.post('/api/v1/settings/notifications/pushover/test', {
enabled: true,
types: values.types,
options: {
accessToken: values.accessToken,
userToken: values.userToken,
},
});
addToast(intl.formatMessage(messages.testsent), {
appearance: 'info',
autoDismiss: true,
});
};
return (
<>
<Alert
title={intl.formatMessage(messages.settinguppushover)}
type="info"
>
{intl.formatMessage(messages.settinguppushoverDescription, {
RegisterApplicationLink: function RegisterApplicationLink(msg) {
return (
<a
href="https://pushover.net/apps/build"
className="text-indigo-100 hover:text-white hover:underline"
target="_blank"
rel="noreferrer"
>
{msg}
</a>
);
},
IconLink: function IconLink(msg) {
return (
<a
href="https://github.com/sct/overseerr/tree/develop/public"
className="text-indigo-100 hover:text-white hover:underline"
target="_blank"
rel="noreferrer"
>
{msg}
</a>
);
},
})}
</Alert>
<Form>
<div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200">
<label
htmlFor="enabled"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
>
{intl.formatMessage(messages.agentenabled)}
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<Field
type="checkbox"
id="enabled"
name="enabled"
className="w-6 h-6 text-indigo-600 transition duration-150 ease-in-out rounded-md form-checkbox"
/>
</div>
</div>
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800">
<label
htmlFor="accessToken"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
>
{intl.formatMessage(messages.accessToken)}
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<div className="flex max-w-lg rounded-md shadow-sm">
<Field
id="accessToken"
name="accessToken"
type="text"
placeholder={intl.formatMessage(messages.accessToken)}
className="flex-1 block w-full min-w-0 transition duration-150 ease-in-out bg-gray-700 border border-gray-500 rounded-md form-input sm:text-sm sm:leading-5"
/>
</div>
{errors.accessToken && touched.accessToken && (
<div className="mt-2 text-red-500">
{errors.accessToken}
</div>
)}
</div>
<label
htmlFor="userToken"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
>
{intl.formatMessage(messages.userToken)}
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<div className="flex max-w-lg rounded-md shadow-sm">
<Field
id="userToken"
name="userToken"
type="text"
placeholder={intl.formatMessage(messages.userToken)}
className="flex-1 block w-full min-w-0 transition duration-150 ease-in-out bg-gray-700 border border-gray-500 rounded-md form-input sm:text-sm sm:leading-5"
/>
</div>
{errors.userToken && touched.userToken && (
<div className="mt-2 text-red-500">{errors.userToken}</div>
)}
</div>
</div>
<div className="mt-6">
<div role="group" aria-labelledby="label-permissions">
<div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-baseline">
<div>
<div
className="text-base font-medium leading-6 text-gray-400 sm:text-sm sm:leading-5"
id="label-types"
>
{intl.formatMessage(messages.notificationtypes)}
</div>
</div>
<div className="mt-4 sm:mt-0 sm:col-span-2">
<div className="max-w-lg">
<NotificationTypeSelector
currentTypes={values.types}
onUpdate={(newTypes) =>
setFieldValue('types', newTypes)
}
/>
</div>
</div>
</div>
</div>
</div>
<div className="pt-5 mt-8 border-t border-gray-700">
<div className="flex justify-end">
<span className="inline-flex ml-3 rounded-md shadow-sm">
<Button
buttonType="warning"
disabled={isSubmitting || !isValid}
onClick={(e) => {
e.preventDefault();
testSettings();
}}
>
{intl.formatMessage(messages.test)}
</Button>
</span>
<span className="inline-flex ml-3 rounded-md shadow-sm">
<Button
buttonType="primary"
type="submit"
disabled={isSubmitting || !isValid}
>
{isSubmitting
? intl.formatMessage(messages.saving)
: intl.formatMessage(messages.save)}
</Button>
</span>
</div>
</div>
</Form>
</>
);
}}
</Formik>
);
};
export default NotificationsPushover;

View File

@@ -8,6 +8,7 @@ import axios from 'axios';
import * as Yup from 'yup';
import { useToasts } from 'react-toast-notifications';
import Alert from '../../../Common/Alert';
import NotificationTypeSelector from '../../../NotificationTypeSelector';
const messages = defineMessages({
save: 'Save Changes',
@@ -23,6 +24,7 @@ const messages = defineMessages({
settingupslack: 'Setting up Slack Notifications',
settingupslackDescription:
'To use Slack notifications, you will need to create an <WebhookLink>Incoming Webhook</WebhookLink> integration and use the provided webhook URL below.',
notificationtypes: 'Notification Types',
});
const NotificationsSlack: React.FC = () => {
@@ -44,24 +46,22 @@ const NotificationsSlack: React.FC = () => {
return (
<>
<p className="mb-">
<Alert title={intl.formatMessage(messages.settingupslack)} type="info">
{intl.formatMessage(messages.settingupslackDescription, {
WebhookLink: function WebhookLink(msg) {
return (
<a
href="https://my.slack.com/services/new/incoming-webhook/"
className="text-indigo-100 hover:text-white hover:underline"
target="_blank"
rel="noreferrer"
>
{msg}
</a>
);
},
})}
</Alert>
</p>
<Alert title={intl.formatMessage(messages.settingupslack)} type="info">
{intl.formatMessage(messages.settingupslackDescription, {
WebhookLink: function WebhookLink(msg) {
return (
<a
href="https://my.slack.com/services/new/incoming-webhook/"
className="text-indigo-100 hover:text-white hover:underline"
target="_blank"
rel="noreferrer"
>
{msg}
</a>
);
},
})}
</Alert>
<Formik
initialValues={{
enabled: data.enabled,
@@ -92,7 +92,14 @@ const NotificationsSlack: React.FC = () => {
}
}}
>
{({ errors, touched, isSubmitting, values, isValid }) => {
{({
errors,
touched,
isSubmitting,
values,
isValid,
setFieldValue,
}) => {
const testSettings = async () => {
await axios.post('/api/v1/settings/notifications/slack/test', {
enabled: true,
@@ -110,10 +117,10 @@ const NotificationsSlack: React.FC = () => {
return (
<Form>
<div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5">
<div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200">
<label
htmlFor="isDefault"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
>
{intl.formatMessage(messages.agentenabled)}
</label>
@@ -126,10 +133,10 @@ const NotificationsSlack: React.FC = () => {
/>
</div>
</div>
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800 sm:pt-5">
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800">
<label
htmlFor="name"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
>
{intl.formatMessage(messages.webhookUrl)}
</label>
@@ -150,6 +157,30 @@ const NotificationsSlack: React.FC = () => {
)}
</div>
</div>
<div className="mt-6">
<div role="group" aria-labelledby="label-permissions">
<div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-baseline">
<div>
<div
className="text-base font-medium leading-6 text-gray-400 sm:text-sm sm:leading-5"
id="label-types"
>
{intl.formatMessage(messages.notificationtypes)}
</div>
</div>
<div className="mt-4 sm:mt-0 sm:col-span-2">
<div className="max-w-lg">
<NotificationTypeSelector
currentTypes={values.types}
onUpdate={(newTypes) =>
setFieldValue('types', newTypes)
}
/>
</div>
</div>
</div>
</div>
</div>
<div className="pt-5 mt-8 border-t border-gray-700">
<div className="flex justify-end">
<span className="inline-flex ml-3 rounded-md shadow-sm">

View File

@@ -0,0 +1,255 @@
import React from 'react';
import { Field, Form, Formik } from 'formik';
import useSWR from 'swr';
import LoadingSpinner from '../../Common/LoadingSpinner';
import Button from '../../Common/Button';
import { defineMessages, useIntl } from 'react-intl';
import axios from 'axios';
import * as Yup from 'yup';
import { useToasts } from 'react-toast-notifications';
import Alert from '../../Common/Alert';
import NotificationTypeSelector from '../../NotificationTypeSelector';
const messages = defineMessages({
save: 'Save Changes',
saving: 'Saving...',
agentenabled: 'Agent Enabled',
botAPI: 'Bot API',
chatId: 'Chat Id',
validationBotAPIRequired: 'You must provide a Bot API key.',
validationChatIdRequired: 'You must provide a Chat id.',
telegramsettingssaved: 'Telegram notification settings saved!',
telegramsettingsfailed: 'Telegram notification settings failed to save.',
testsent: 'Test notification sent!',
test: 'Test',
settinguptelegram: 'Setting up Telegram Notifications',
settinguptelegramDescription:
'To setup Telegram you need to <CreateBotLink>create a bot</CreateBotLink> and get the bot API key.\
Additionally, you need the chat id for the chat you want the bot to send notifications to.\
You can do this by adding <GetIdBotLink>@get_id_bot</GetIdBotLink> to the chat or group chat.',
notificationtypes: 'Notification Types',
});
const NotificationsTelegram: React.FC = () => {
const intl = useIntl();
const { addToast } = useToasts();
const { data, error, revalidate } = useSWR(
'/api/v1/settings/notifications/telegram'
);
const NotificationsTelegramSchema = Yup.object().shape({
botAPI: Yup.string().required(
intl.formatMessage(messages.validationBotAPIRequired)
),
chatId: Yup.string().required(
intl.formatMessage(messages.validationChatIdRequired)
),
});
if (!data && !error) {
return <LoadingSpinner />;
}
return (
<Formik
initialValues={{
enabled: data?.enabled,
types: data?.types,
botAPI: data?.options.botAPI,
chatId: data?.options.chatId,
}}
validationSchema={NotificationsTelegramSchema}
onSubmit={async (values) => {
try {
await axios.post('/api/v1/settings/notifications/telegram', {
enabled: values.enabled,
types: values.types,
options: {
botAPI: values.botAPI,
chatId: values.chatId,
},
});
addToast(intl.formatMessage(messages.telegramsettingssaved), {
appearance: 'success',
autoDismiss: true,
});
} catch (e) {
addToast(intl.formatMessage(messages.telegramsettingsfailed), {
appearance: 'error',
autoDismiss: true,
});
} finally {
revalidate();
}
}}
>
{({ errors, touched, isSubmitting, values, isValid, setFieldValue }) => {
const testSettings = async () => {
await axios.post('/api/v1/settings/notifications/telegram/test', {
enabled: true,
types: values.types,
options: {
botAPI: values.botAPI,
chatId: values.chatId,
},
});
addToast(intl.formatMessage(messages.testsent), {
appearance: 'info',
autoDismiss: true,
});
};
return (
<>
<Alert
title={intl.formatMessage(messages.settinguptelegram)}
type="info"
>
{intl.formatMessage(messages.settinguptelegramDescription, {
CreateBotLink: function CreateBotLink(msg) {
return (
<a
href="https://core.telegram.org/bots#6-botfather"
className="text-indigo-100 hover:text-white hover:underline"
target="_blank"
rel="noreferrer"
>
{msg}
</a>
);
},
GetIdBotLink: function GetIdBotLink(msg) {
return (
<a
href="https://telegram.me/get_id_bot"
className="text-indigo-100 hover:text-white hover:underline"
target="_blank"
rel="noreferrer"
>
{msg}
</a>
);
},
})}
</Alert>
<Form>
<div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200">
<label
htmlFor="enabled"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
>
{intl.formatMessage(messages.agentenabled)}
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<Field
type="checkbox"
id="enabled"
name="enabled"
className="w-6 h-6 text-indigo-600 transition duration-150 ease-in-out rounded-md form-checkbox"
/>
</div>
</div>
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800">
<label
htmlFor="botAPI"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
>
{intl.formatMessage(messages.botAPI)}
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<div className="flex max-w-lg rounded-md shadow-sm">
<Field
id="botAPI"
name="botAPI"
type="text"
placeholder={intl.formatMessage(messages.botAPI)}
className="flex-1 block w-full min-w-0 transition duration-150 ease-in-out bg-gray-700 border border-gray-500 rounded-md form-input sm:text-sm sm:leading-5"
/>
</div>
{errors.botAPI && touched.botAPI && (
<div className="mt-2 text-red-500">{errors.botAPI}</div>
)}
</div>
<label
htmlFor="chatId"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
>
{intl.formatMessage(messages.chatId)}
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<div className="flex max-w-lg rounded-md shadow-sm">
<Field
id="chatId"
name="chatId"
type="text"
placeholder={intl.formatMessage(messages.chatId)}
className="flex-1 block w-full min-w-0 transition duration-150 ease-in-out bg-gray-700 border border-gray-500 rounded-md form-input sm:text-sm sm:leading-5"
/>
</div>
{errors.chatId && touched.chatId && (
<div className="mt-2 text-red-500">{errors.chatId}</div>
)}
</div>
</div>
<div className="mt-6">
<div role="group" aria-labelledby="label-permissions">
<div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-baseline">
<div>
<div
className="text-base font-medium leading-6 text-gray-400 sm:text-sm sm:leading-5"
id="label-types"
>
{intl.formatMessage(messages.notificationtypes)}
</div>
</div>
<div className="mt-4 sm:mt-0 sm:col-span-2">
<div className="max-w-lg">
<NotificationTypeSelector
currentTypes={values.types}
onUpdate={(newTypes) =>
setFieldValue('types', newTypes)
}
/>
</div>
</div>
</div>
</div>
</div>
<div className="pt-5 mt-8 border-t border-gray-700">
<div className="flex justify-end">
<span className="inline-flex ml-3 rounded-md shadow-sm">
<Button
buttonType="warning"
disabled={isSubmitting || !isValid}
onClick={(e) => {
e.preventDefault();
testSettings();
}}
>
{intl.formatMessage(messages.test)}
</Button>
</span>
<span className="inline-flex ml-3 rounded-md shadow-sm">
<Button
buttonType="primary"
type="submit"
disabled={isSubmitting || !isValid}
>
{isSubmitting
? intl.formatMessage(messages.saving)
: intl.formatMessage(messages.save)}
</Button>
</span>
</div>
</div>
</Form>
</>
);
}}
</Formik>
);
};
export default NotificationsTelegram;

View File

@@ -274,10 +274,10 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
}
>
<div className="mb-6">
<div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5">
<div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200">
<label
htmlFor="isDefault"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
>
{intl.formatMessage(messages.defaultserver)}
</label>
@@ -290,10 +290,10 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
/>
</div>
</div>
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800 sm:pt-5">
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800">
<label
htmlFor="name"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
>
{intl.formatMessage(messages.servername)}
</label>
@@ -318,10 +318,10 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
)}
</div>
</div>
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800 sm:pt-5">
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800">
<label
htmlFor="hostname"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
>
{intl.formatMessage(messages.hostname)}
</label>
@@ -347,10 +347,10 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
)}
</div>
</div>
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5">
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200">
<label
htmlFor="port"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
>
{intl.formatMessage(messages.port)}
</label>
@@ -371,10 +371,10 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
)}
</div>
</div>
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5">
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200">
<label
htmlFor="ssl"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
>
{intl.formatMessage(messages.ssl)}
</label>
@@ -391,10 +391,10 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
/>
</div>
</div>
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800 sm:pt-5">
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800">
<label
htmlFor="apiKey"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
>
{intl.formatMessage(messages.apiKey)}
</label>
@@ -419,10 +419,10 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
)}
</div>
</div>
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800 sm:pt-5">
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800">
<label
htmlFor="baseUrl"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
>
{intl.formatMessage(messages.baseUrl)}
</label>
@@ -447,10 +447,10 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
)}
</div>
</div>
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800 sm:pt-5">
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800">
<label
htmlFor="activeProfileId"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
>
{intl.formatMessage(messages.qualityprofile)}
</label>
@@ -490,10 +490,10 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
)}
</div>
</div>
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800 sm:pt-5">
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-8005">
<label
htmlFor="rootFolder"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
>
{intl.formatMessage(messages.rootfolder)}
</label>
@@ -531,10 +531,10 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
)}
</div>
</div>
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800 sm:pt-5">
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800">
<label
htmlFor="minimumAvailability"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
>
{intl.formatMessage(messages.minimumAvailability)}
</label>
@@ -560,10 +560,10 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
)}
</div>
</div>
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5">
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200">
<label
htmlFor="is4k"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
>
{intl.formatMessage(messages.server4k)}
</label>

View File

@@ -18,6 +18,7 @@ const messages = defineMessages({
timezone: 'Timezone',
supportoverseerr: 'Support Overseerr',
helppaycoffee: 'Help pay for coffee',
documentation: 'Documentation',
});
const SettingsAbout: React.FC = () => {
@@ -56,6 +57,16 @@ const SettingsAbout: React.FC = () => {
</div>
<div className="mb-8">
<List title={intl.formatMessage(messages.gettingsupport)}>
<List.Item title={intl.formatMessage(messages.documentation)}>
<a
href="https://docs.overseerr.dev"
target="_blank"
rel="noreferrer"
className="text-indigo-500 hover:underline"
>
https://docs.overseerr.dev
</a>
</List.Item>
<List.Item title={intl.formatMessage(messages.githubdiscussions)}>
<a
href="https://github.com/sct/overseerr/discussions"

View File

@@ -159,10 +159,10 @@ const SettingsMain: React.FC = () => {
return (
<Form>
{userHasPermission(Permission.ADMIN) && (
<div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800 sm:pt-5">
<div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800">
<label
htmlFor="username"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
>
{intl.formatMessage(messages.apikey)}
</label>
@@ -203,10 +203,10 @@ const SettingsMain: React.FC = () => {
</div>
</div>
)}
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800 sm:pt-5">
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800">
<label
htmlFor="name"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
>
{intl.formatMessage(messages.applicationurl)}
</label>

View File

@@ -4,6 +4,8 @@ import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import DiscordLogo from '../../assets/extlogos/discord_white.svg';
import SlackLogo from '../../assets/extlogos/slack.svg';
import TelegramLogo from '../../assets/extlogos/telegram.svg';
import PushoverLogo from '../../assets/extlogos/pushover.svg';
const messages = defineMessages({
notificationsettings: 'Notification Settings',
@@ -65,6 +67,28 @@ const settingsRoutes: SettingsRoute[] = [
route: '/settings/notifications/slack',
regex: /^\/settings\/notifications\/slack/,
},
{
text: 'Telegram',
content: (
<span className="flex items-center">
<TelegramLogo className="h-4 mr-2" />
Telegram
</span>
),
route: '/settings/notifications/telegram',
regex: /^\/settings\/notifications\/telegram/,
},
{
text: 'Pushover',
content: (
<span className="flex items-center">
<PushoverLogo className="h-4 mr-2" />
Pushover
</span>
),
route: '/settings/notifications/pushover',
regex: /^\/settings\/notifications\/pushover/,
},
];
const SettingsNotifications: React.FC = ({ children }) => {

View File

@@ -48,7 +48,7 @@ interface SyncStatus {
running: boolean;
progress: number;
total: number;
currentLibrary: Library;
currentLibrary?: Library;
libraries: Library[];
}
@@ -86,12 +86,17 @@ const SettingsPlex: React.FC<SettingsPlexProps> = ({ onComplete }) => {
const syncLibraries = async () => {
setIsSyncing(true);
const params: { sync: boolean; enable?: string } = {
sync: true,
};
if (activeLibraries.length > 0) {
params.enable = activeLibraries.join(',');
}
await axios.get('/api/v1/settings/plex/library', {
params: {
sync: true,
enable:
activeLibraries.length > 0 ? activeLibraries.join(',') : undefined,
},
params,
});
setIsSyncing(false);
revalidate();
@@ -118,13 +123,16 @@ const SettingsPlex: React.FC<SettingsPlexProps> = ({ onComplete }) => {
const toggleLibrary = async (libraryId: string) => {
setIsSyncing(true);
if (activeLibraries.includes(libraryId)) {
const params: { enable?: string } = {};
if (activeLibraries.length > 1) {
params.enable = activeLibraries
.filter((id) => id !== libraryId)
.join(',');
}
await axios.get('/api/v1/settings/plex/library', {
params: {
enable:
activeLibraries.length > 0
? activeLibraries.filter((id) => id !== libraryId).join(',')
: undefined,
},
params,
});
} else {
await axios.get('/api/v1/settings/plex/library', {
@@ -192,10 +200,10 @@ const SettingsPlex: React.FC<SettingsPlexProps> = ({ onComplete }) => {
{submitError}
</div>
)}
<div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800 sm:pt-5">
<div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800">
<label
htmlFor="name"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
>
<FormattedMessage {...messages.servername} />
</label>
@@ -215,10 +223,10 @@ const SettingsPlex: React.FC<SettingsPlexProps> = ({ onComplete }) => {
</div>
</div>
</div>
<div className="mt-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800 sm:pt-5">
<div className="mt-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800">
<label
htmlFor="hostname"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
>
<FormattedMessage {...messages.hostname} />
</label>
@@ -240,10 +248,10 @@ const SettingsPlex: React.FC<SettingsPlexProps> = ({ onComplete }) => {
)}
</div>
</div>
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5">
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200">
<label
htmlFor="port"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
>
<FormattedMessage {...messages.port} />
</label>
@@ -263,10 +271,10 @@ const SettingsPlex: React.FC<SettingsPlexProps> = ({ onComplete }) => {
</div>
</div>
</div>
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5">
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200">
<label
htmlFor="ssl"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
>
{intl.formatMessage(messages.ssl)}
</label>
@@ -369,25 +377,29 @@ const SettingsPlex: React.FC<SettingsPlexProps> = ({ onComplete }) => {
<div className="flex flex-col w-full sm:flex-row">
{dataSync?.running && (
<>
<div className="flex items-center mb-2 mr-0 sm:mb-0 sm:mr-2">
<Badge>
<FormattedMessage
{...messages.currentlibrary}
values={{ name: dataSync.currentLibrary.name }}
/>
</Badge>
</div>
{dataSync.currentLibrary && (
<div className="flex items-center mb-2 mr-0 sm:mb-0 sm:mr-2">
<Badge>
<FormattedMessage
{...messages.currentlibrary}
values={{ name: dataSync.currentLibrary.name }}
/>
</Badge>
</div>
)}
<div className="flex items-center">
<Badge badgeType="warning">
<FormattedMessage
{...messages.librariesRemaining}
values={{
count: dataSync.libraries.slice(
dataSync.libraries.findIndex(
(library) =>
library.id === dataSync.currentLibrary.id
) + 1
).length,
count: dataSync.currentLibrary
? dataSync.libraries.slice(
dataSync.libraries.findIndex(
(library) =>
library.id === dataSync.currentLibrary?.id
) + 1
).length
: 0,
}}
/>
</Badge>

View File

@@ -283,10 +283,10 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
}
>
<div className="mb-6">
<div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5">
<div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200">
<label
htmlFor="isDefault"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
>
{intl.formatMessage(messages.defaultserver)}
</label>
@@ -299,10 +299,10 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
/>
</div>
</div>
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800 sm:pt-5">
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800">
<label
htmlFor="name"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
>
{intl.formatMessage(messages.servername)}
</label>
@@ -327,10 +327,10 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
)}
</div>
</div>
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800 sm:pt-5">
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800">
<label
htmlFor="hostname"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
>
{intl.formatMessage(messages.hostname)}
</label>
@@ -356,10 +356,10 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
)}
</div>
</div>
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5">
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200">
<label
htmlFor="port"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
>
{intl.formatMessage(messages.port)}
</label>
@@ -380,10 +380,10 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
)}
</div>
</div>
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5">
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200">
<label
htmlFor="ssl"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
>
{intl.formatMessage(messages.ssl)}
</label>
@@ -400,10 +400,10 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
/>
</div>
</div>
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800 sm:pt-5">
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800">
<label
htmlFor="apiKey"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
>
{intl.formatMessage(messages.apiKey)}
</label>
@@ -428,10 +428,10 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
)}
</div>
</div>
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800 sm:pt-5">
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800">
<label
htmlFor="baseUrl"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
>
{intl.formatMessage(messages.baseUrl)}
</label>
@@ -456,10 +456,10 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
)}
</div>
</div>
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800 sm:pt-5">
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800">
<label
htmlFor="activeProfileId"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
>
{intl.formatMessage(messages.qualityprofile)}
</label>
@@ -499,10 +499,10 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
)}
</div>
</div>
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800 sm:pt-5">
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800">
<label
htmlFor="rootFolder"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
>
{intl.formatMessage(messages.rootfolder)}
</label>
@@ -540,10 +540,10 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
)}
</div>
</div>
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800 sm:pt-5">
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800">
<label
htmlFor="activeAnimeProfileId"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
>
{intl.formatMessage(messages.animequalityprofile)}
</label>
@@ -584,10 +584,10 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
)}
</div>
</div>
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800 sm:pt-5">
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800">
<label
htmlFor="activeAnimeRootFolder"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
>
{intl.formatMessage(messages.animerootfolder)}
</label>
@@ -626,10 +626,10 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
)}
</div>
</div>
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5">
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200">
<label
htmlFor="is4k"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
>
{intl.formatMessage(messages.server4k)}
</label>
@@ -642,10 +642,10 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
/>
</div>
</div>
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5">
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200">
<label
htmlFor="enableSeasonFolders"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px"
>
{intl.formatMessage(messages.seasonfolders)}
</label>

View File

@@ -43,27 +43,29 @@ const Setup: React.FC = () => {
};
return (
<div className="min-h-screen bg-gray-900 flex flex-col justify-center py-12 sm:px-6 lg:px-8 relative">
<div className="relative flex flex-col justify-center min-h-screen py-12 bg-gray-900">
<ImageFader
backgroundImages={[
'/images/rotate1.jpg',
'/images/rotate2.jpg',
'/images/rotate3.jpg',
'/images/rotate4.jpg',
'/images/rotate5.jpg',
'/images/rotate6.jpg',
]}
/>
<div className="absolute top-4 right-4 z-50">
<div className="absolute z-50 top-4 right-4">
<LanguagePicker />
</div>
<div className="px-4 sm:px-2 md:px-0 sm:mx-auto sm:w-full sm:max-w-4xl relative z-40">
<div className="relative z-40 px-4 sm:mx-auto sm:w-full sm:max-w-4xl">
<img
src="/logo.png"
className="mx-auto max-h-32 w-auto mb-10"
className="w-auto mx-auto mb-10 max-h-32"
alt="Overseerr Logo"
/>
<nav className="relative z-50">
<ul
className=" bg-gray-800 bg-opacity-50 border border-gray-600 rounded-md divide-y divide-gray-600 md:flex md:divide-y-0"
className="bg-gray-800 bg-opacity-50 border border-gray-600 divide-y divide-gray-600 rounded-md md:flex md:divide-y-0"
style={{ backdropFilter: 'blur(5px)' }}
>
<SetupSteps
@@ -86,22 +88,22 @@ const Setup: React.FC = () => {
/>
</ul>
</nav>
<div className="w-full mt-10 p-4 text-white bg-gray-800 bg-opacity-50 border border-gray-600 rounded-md">
<div className="w-full p-4 mt-10 text-white bg-gray-800 bg-opacity-50 border border-gray-600 rounded-md">
{currentStep === 1 && (
<LoginWithPlex onComplete={() => setCurrentStep(2)} />
)}
{currentStep === 2 && (
<div>
<SettingsPlex onComplete={() => setPlexSettingsComplete(true)} />
<div className="mt-4 text-gray-500 text-sm">
<div className="mt-4 text-sm text-gray-500">
<span className="mr-2">
<Badge>{intl.formatMessage(messages.tip)}</Badge>
</span>
{intl.formatMessage(messages.syncingbackground)}
</div>
<div className="mt-8 border-t border-gray-700 pt-5">
<div className="pt-5 mt-8 border-t border-gray-700">
<div className="flex justify-end">
<span className="ml-3 inline-flex rounded-md shadow-sm">
<span className="inline-flex ml-3 rounded-md shadow-sm">
<Button
buttonType="primary"
disabled={!plexSettingsComplete}
@@ -117,9 +119,9 @@ const Setup: React.FC = () => {
{currentStep === 3 && (
<div>
<SettingsServices />
<div className="mt-8 border-t border-gray-700 pt-5">
<div className="pt-5 mt-8 border-t border-gray-700">
<div className="flex justify-end">
<span className="ml-3 inline-flex rounded-md shadow-sm">
<span className="inline-flex ml-3 rounded-md shadow-sm">
<Button
buttonType="primary"
onClick={() => finishSetup()}

View File

@@ -160,7 +160,7 @@ const Slider: React.FC<SliderProps> = ({
return (
<div className="relative">
<div className="absolute flex text-gray-400 right-0 -mt-10">
<div className="absolute right-0 flex -mt-10 text-gray-400">
<button
className={`${
scrollPos.isStart ? 'cursor-not-allowed text-gray-800' : ''
@@ -207,14 +207,14 @@ const Slider: React.FC<SliderProps> = ({
</button>
</div>
<div
className="relative overflow-x-scroll whitespace-nowrap hide-scrollbar overscroll-x-contain -ml-4 -mr-4 px-2 overflow-y-auto"
className="relative px-2 py-2 -my-2 -ml-4 -mr-4 overflow-x-scroll overflow-y-auto whitespace-nowrap hide-scrollbar overscroll-x-contain"
ref={containerRef}
onScroll={onScroll}
>
{items?.map((item, index) => (
<div
key={`${sliderKey}-${index}`}
className="px-2 inline-block align-top"
className="inline-block px-2 align-top"
>
{item}
</div>
@@ -223,13 +223,13 @@ const Slider: React.FC<SliderProps> = ({
[...Array(10)].map((_item, i) => (
<div
key={`placeholder-${i}`}
className="px-2 inline-block align-top"
className="inline-block px-2 align-top"
>
{placeholder}
</div>
))}
{isEmpty && (
<div className="text-center text-white mt-16 mb-16">
<div className="mt-16 mb-16 text-center text-white">
{emptyMessage ? (
emptyMessage
) : (

View File

@@ -0,0 +1,70 @@
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr';
import Modal from '../Common/Modal';
import Transition from '../Transition';
const messages = defineMessages({
newversionavailable: 'New Version Available',
newversionDescription:
'An update is now available. Click the button below to reload the application.',
reloadOverseerr: 'Reload Overseerr',
});
const StatusChecker: React.FC = () => {
const intl = useIntl();
const { data, error } = useSWR<{ version: string; commitTag: string }>(
'/api/v1/status',
{
refreshInterval: 60 * 1000,
}
);
if (!data && !error) {
return null;
}
if (!data) {
return null;
}
return (
<Transition
enter="opacity-0 transition duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="opacity-100 transition duration-300"
leaveFrom="opacity-100"
leaveTo="opacity-0"
appear
show={data.commitTag !== process.env.commitTag}
>
<Modal
iconSvg={
<svg
className="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z"
/>
</svg>
}
title={intl.formatMessage(messages.newversionavailable)}
onOk={() => location.reload()}
okText={intl.formatMessage(messages.reloadOverseerr)}
backgroundClickable={false}
>
{intl.formatMessage(messages.newversionDescription)}
</Modal>
</Transition>
);
};
export default StatusChecker;

View File

@@ -1,8 +1,5 @@
import React, { useState, useCallback, useEffect } from 'react';
import type { MediaType } from '../../../server/models/Search';
import Available from '../../assets/available.svg';
import Requested from '../../assets/requested.svg';
import Unavailable from '../../assets/unavailable.svg';
import { withProperties } from '../../utils/typeHelpers';
import Transition from '../Transition';
import Placeholder from './Placeholder';
@@ -11,6 +8,7 @@ import { MediaStatus } from '../../../server/constants/media';
import RequestModal from '../RequestModal';
import { defineMessages, useIntl } from 'react-intl';
import { useIsTouch } from '../../hooks/useIsTouch';
import globalMessages from '../../i18n/globalMessages';
const messages = defineMessages({
movie: 'Movie',
@@ -78,7 +76,9 @@ const TitleCard: React.FC<TitleCardProps> = ({
onCancel={closeModal}
/>
<div
className="outline-none cursor-default titleCard"
className={`transition duration-300 transform-gpu scale-100 outline-none cursor-default titleCard ${
showDetail ? 'scale-105' : ''
}`}
style={{
backgroundImage: `url(//image.tmdb.org/t/p/w300_and_h450_face${image})`,
}}
@@ -97,35 +97,68 @@ const TitleCard: React.FC<TitleCardProps> = ({
role="link"
tabIndex={0}
>
<div className="absolute top-0 bottom-0 left-0 right-0 w-full h-full overflow-hidden shadow-xl">
<div
className={`absolute left-0 top-0 rounded-tl-md rounded-br-md z-40 ${
mediaType === 'movie' ? 'bg-blue-500' : 'bg-purple-600'
}`}
>
<div className="flex items-center h-4 px-2 py-1 text-xs font-normal text-center text-white uppercase">
{mediaType === 'movie'
? intl.formatMessage(messages.movie)
: intl.formatMessage(messages.tvshow)}
<div className="absolute top-0 bottom-0 left-0 right-0 w-full h-full overflow-hidden shadow">
<div className="absolute left-0 right-0 flex items-center justify-between p-2">
<div
className={`rounded-full z-40 pointer-events-none shadow ${
mediaType === 'movie' ? 'bg-blue-500' : 'bg-purple-600'
}`}
>
<div className="flex items-center h-4 px-2 py-2 text-xs font-normal tracking-wider text-center text-white uppercase sm:h-5">
{mediaType === 'movie'
? intl.formatMessage(messages.movie)
: intl.formatMessage(messages.tvshow)}
</div>
</div>
<div className="z-40 pointer-events-none">
{(currentStatus === MediaStatus.AVAILABLE ||
currentStatus === MediaStatus.PARTIALLY_AVAILABLE) && (
<div className="flex items-center justify-center w-4 h-4 text-white bg-green-400 rounded-full shadow sm:w-5 sm:h-5">
<svg
className="w-3 h-3 sm:w-4 sm:h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
</div>
)}
{currentStatus === MediaStatus.PENDING && (
<div className="flex items-center justify-center w-4 h-4 text-white bg-yellow-500 rounded-full shadow sm:w-5 sm:h-5">
<svg
className="w-3 h-3 sm:w-4 sm:h-4"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M10 2a6 6 0 00-6 6v3.586l-.707.707A1 1 0 004 14h12a1 1 0 00.707-1.707L16 11.586V8a6 6 0 00-6-6zM10 18a3 3 0 01-3-3h6a3 3 0 01-3 3z" />
</svg>
</div>
)}
{currentStatus === MediaStatus.PROCESSING && (
<div className="flex items-center justify-center w-4 h-4 text-white bg-indigo-500 rounded-full shadow sm:w-5 sm:h-5">
<svg
className="w-3 h-3 sm:w-4 sm:h-4"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z"
clipRule="evenodd"
/>
</svg>
</div>
)}
</div>
</div>
<div
className="absolute top-0 right-0 z-40"
style={{
right: '-1px',
}}
>
{(currentStatus === MediaStatus.AVAILABLE ||
currentStatus === MediaStatus.PARTIALLY_AVAILABLE) && (
<Available className="rounded-tr-md" />
)}
{currentStatus === MediaStatus.PENDING && (
<Requested className="rounded-tr-md" />
)}
{currentStatus === MediaStatus.PROCESSING && (
<Unavailable className="rounded-tr-md" />
)}
</div>
<Transition
show={isUpdating}
@@ -173,7 +206,13 @@ const TitleCard: React.FC<TitleCardProps> = ({
}}
>
<div className="flex items-end w-full h-full">
<div className="px-2 text-white pb-11">
<div
className={`px-2 text-white ${
currentStatus && currentStatus !== MediaStatus.UNKNOWN
? 'pb-2'
: 'pb-11'
}`}
>
{year && <div className="text-sm">{year}</div>}
<h1 className="text-xl leading-tight whitespace-normal">
@@ -182,7 +221,11 @@ const TitleCard: React.FC<TitleCardProps> = ({
<div
className="text-xs whitespace-normal"
style={{
WebkitLineClamp: 3,
WebkitLineClamp:
currentStatus &&
currentStatus !== MediaStatus.UNKNOWN
? 5
: 3,
display: '-webkit-box',
overflow: 'hidden',
WebkitBoxOrient: 'vertical',
@@ -196,42 +239,16 @@ const TitleCard: React.FC<TitleCardProps> = ({
</Link>
<div className="absolute bottom-0 left-0 right-0 flex justify-between px-2 py-2">
<Link
href={mediaType === 'movie' ? `/movie/${id}` : `/tv/${id}`}
>
<a className="flex w-full text-center text-white transition duration-150 ease-in-out bg-indigo-500 rounded-sm cursor-pointer h-7 hover:bg-indigo-400 focus:border-indigo-700 focus:ring-indigo active:bg-indigo-700">
<svg
className="w-4 mx-auto"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
/>
</svg>
</a>
</Link>
{(!currentStatus || currentStatus === MediaStatus.UNKNOWN) && (
<button
onClick={(e) => {
e.preventDefault();
setShowRequestModal(true);
}}
className="w-full ml-2 text-center text-white transition duration-150 ease-in-out bg-indigo-500 rounded-sm h-7 hover:bg-indigo-400 focus:border-indigo-700 focus:ring-indigo active:bg-indigo-700"
className="flex items-center justify-center w-full text-white transition duration-150 ease-in-out bg-indigo-500 rounded-sm h-7 hover:bg-indigo-400 focus:border-indigo-700 focus:ring-indigo active:bg-indigo-700"
>
<svg
className="w-4 mx-auto"
className="w-4 mr-1"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
@@ -244,70 +261,9 @@ const TitleCard: React.FC<TitleCardProps> = ({
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
/>
</svg>
</button>
)}
{currentStatus === MediaStatus.PENDING && (
<button
className="w-full ml-2 text-center text-yellow-500 border border-yellow-500 rounded-sm cursor-default h-7"
disabled
>
<svg
className="w-4 mx-auto"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"
/>
</svg>
</button>
)}
{currentStatus === MediaStatus.PROCESSING && (
<button
className="w-full ml-2 text-center text-indigo-500 border border-indigo-500 rounded-sm cursor-default h-7"
disabled
>
<svg
className="w-4 mx-auto"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</button>
)}
{(currentStatus === MediaStatus.AVAILABLE ||
currentStatus === MediaStatus.PARTIALLY_AVAILABLE) && (
<button
className="w-full ml-2 text-center text-green-400 border border-green-400 rounded-sm cursor-default h-7"
disabled
>
<svg
className="w-4 mx-auto"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
<span className="text-xs">
{intl.formatMessage(globalMessages.request)}
</span>
</button>
)}
</div>

View File

@@ -3,7 +3,7 @@ import type { ToastProps } from 'react-toast-notifications';
const Toast: React.FC<ToastProps> = ({ appearance, children, onDismiss }) => {
return (
<div className="toast flex items-end justify-center px-2 py-2 pointer-events-none sm:p-6 sm:items-start sm:justify-end">
<div className="toast flex items-end justify-center px-2 py-2 pointer-events-none sm:items-start sm:justify-end">
<div className="max-w-sm w-full bg-gray-700 shadow-lg rounded-lg pointer-events-auto">
<div className="rounded-lg ring-1 ring-black ring-opacity-5 overflow-hidden">
<div className="p-4">

View File

@@ -166,7 +166,7 @@ const TvDetails: React.FC<TvDetailsProps> = ({ tv }) => {
return (
<div
className="px-4 pt-4 -mx-4 -mt-2 bg-center bg-cover sm:px-8 "
className="px-4 pt-4 -mx-4 -mt-2 bg-center bg-cover"
style={{
height: 493,
backgroundImage: `linear-gradient(180deg, rgba(17, 24, 39, 0.47) 0%, rgba(17, 24, 39, 1) 100%), url(//image.tmdb.org/t/p/w1920_and_h800_multi_faces/${data.backdropPath})`,

View File

@@ -155,10 +155,10 @@ const UserEdit: React.FC = () => {
return (
<>
<Header extraMargin={4}>
<Header>
<FormattedMessage {...messages.edituser} />
</Header>
<div className="px-4 space-y-6 sm:p-6 lg:pb-8">
<div className="space-y-6">
<div className="flex flex-col space-y-6 text-white lg:flex-row lg:space-y-0 lg:space-x-6">
<div className="flex-grow space-y-6">
<div className="space-y-1">
@@ -229,7 +229,7 @@ const UserEdit: React.FC = () => {
</div>
</div>
<div className="text-white">
<div className="sm:border-t sm:border-gray-200 sm:pt-5">
<div className="sm:border-t sm:border-gray-200">
<div role="group" aria-labelledby="label-permissions">
<div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-baseline">
<div>

View File

@@ -150,7 +150,7 @@ const UserList: React.FC = () => {
</Modal>
</Transition>
<div className="flex items-center justify-between">
<Header extraMargin={4}>{intl.formatMessage(messages.userlist)}</Header>
<Header>{intl.formatMessage(messages.userlist)}</Header>
<Button
className="mx-4 my-8"
buttonType="primary"

View File

@@ -6,6 +6,7 @@ const globalMessages = defineMessages({
processing: 'Processing',
unavailable: 'Unavailable',
requested: 'Requested',
request: 'Request',
failed: 'Failed',
pending: 'Pending',
declined: 'Declined',

View File

@@ -74,10 +74,10 @@
"components.RequestModal.notrequested": "Nicht angefragt",
"components.RequestModal.numberofepisodes": "Anzahl der Folgen",
"components.RequestModal.pendingrequest": "Ausstehende Anfrage für {title}",
"components.RequestModal.request": "Anfrage",
"components.RequestModal.request": "Anfragen",
"components.RequestModal.requestCancel": "Anfrage für <strong>{title}</strong> abgebrochen",
"components.RequestModal.requestSuccess": "<strong>{title}</strong> angefragt.",
"components.RequestModal.requestadmin": "Deine Anfrage wird direkt genehmigt werden.",
"components.RequestModal.requestadmin": "Deine Anfrage wird direkt genehmigt.",
"components.RequestModal.requestfrom": "Derzeit steht eine Anfrage von {username} aus",
"components.RequestModal.requesting": "Wird angefragt …",
"components.RequestModal.requestseasons": "{seasonCount} {seasonCount, plural, one {Staffel} other {Staffeln}} anfragen",
@@ -183,7 +183,7 @@
"components.Settings.jobname": "Aufgabenname",
"components.Settings.librariesRemaining": "Verbleibende Bibliotheken: {count}",
"components.Settings.manualscan": "Manueller Bibliotheksscan",
"components.Settings.manualscanDescription": "Normalerweise wird dies nur einmal alle 24 Stunden ausgeführt. Overseerr überprüft die kürzlich hinzugefügten Plex-Server aggressiver. Wenn Sie Plex zum ersten Mal konfigurieren, wird ein einmaliger vollständiger manueller Bibliotheksscan empfohlen!",
"components.Settings.manualscanDescription": "Normalerweise wird dies nur einmal alle 24 Stunden ausgeführt. Overseerr überprüft die kürzlich hinzugefügten Plex-Server aggressiver. Wenn du Plex zum ersten Mal konfigurierst, wird ein einmaliger vollständiger manueller Bibliotheksscan empfohlen!",
"components.Settings.menuAbout": "Über",
"components.Settings.menuGeneralSettings": "Allgemeine Einstellungen",
"components.Settings.menuJobs": "Aufgaben",
@@ -367,11 +367,11 @@
"components.Settings.defaultPermissions": "Standardbenutzerberechtigungen",
"components.UserList.importfromplexerror": "Beim Importieren von Benutzern aus Plex ist etwas schief gelaufen",
"components.UserList.importfromplex": "Benutzer aus Plex importieren",
"components.TvDetails.viewfullcrew": "Komplettes Team anzeigen",
"components.TvDetails.TvCrew.fullseriescrew": "Komplettes Serien Team",
"components.PersonDetails.crewmember": "Teammitglied",
"components.MovieDetails.viewfullcrew": "Komplettes Team anzeigen",
"components.MovieDetails.MovieCrew.fullcrew": "Komplettes Team",
"components.TvDetails.viewfullcrew": "Komplette Crew anzeigen",
"components.TvDetails.TvCrew.fullseriescrew": "Komplette Serien Crew",
"components.PersonDetails.crewmember": "Crewmitglied",
"components.MovieDetails.viewfullcrew": "Komplette Crew anzeigen",
"components.MovieDetails.MovieCrew.fullcrew": "Komplette Crew",
"components.UserList.importedfromplex": "{userCount, plural, =0 {Keine neuen Benutzer} one {# neuer Benutzer} other {# neue Benutzer}} aus Plex importiert",
"components.TvDetails.firstAirDate": "Erstausstrahlung",
"components.Settings.Notifications.ssldisabletip": "SSL sollte bei Standard-TLS-Verbindungen deaktiviert werden (Port 587)",
@@ -391,5 +391,66 @@
"i18n.requested": "Angefragt",
"i18n.retry": "Wiederholen",
"i18n.failed": "Fehlgeschlagen",
"components.RequestList.RequestItem.failedretry": "Beim Wiederholen der Anfrage ist etwas schief gelaufen"
"components.RequestList.RequestItem.failedretry": "Beim Wiederholen der Anfrage ist etwas schief gelaufen",
"components.Settings.Notifications.NotificationsSlack.settingupslackDescription": "Um Slack-Benachrichtigungen zu verwenden, musst du eine <WebhookLink>Incoming Webhook</WebhookLink>-Integration erstellen und die unten angegebene Webhook-URL verwenden.",
"components.Settings.Notifications.NotificationsSlack.webhookUrlPlaceholder": "Webhook URL",
"components.Settings.Notifications.NotificationsSlack.webhookUrl": "Webhook URL",
"components.Settings.Notifications.NotificationsSlack.testsent": "Testbenachrichtigung gesendet!",
"components.Settings.Notifications.NotificationsSlack.test": "Test",
"components.Settings.Notifications.NotificationsSlack.slacksettingssaved": "Slack-Benachrichtigungseinstellungen gespeichert!",
"components.Settings.Notifications.NotificationsSlack.slacksettingsfailed": "Die Einstellungen für Slack-Benachrichtigungen konnten nicht gespeichert werden.",
"components.Settings.Notifications.NotificationsSlack.settingupslack": "Einrichten von Slack-Benachrichtigungen",
"components.Settings.Notifications.NotificationsSlack.saving": "Speichern …",
"components.Settings.Notifications.NotificationsSlack.save": "Änderungen speichern",
"components.Settings.Notifications.NotificationsSlack.agentenabled": "Agent aktiviert",
"components.UserEdit.autoapproveSeries": "Automatische Genehmigung von Serien",
"components.UserEdit.autoapproveMovies": "Automatische Genehmigung von Filmen",
"components.UserEdit.autoapproveSeriesDescription": "Gewährt die automatische Genehmigung für Serienanfragen von diesem Benutzer.",
"components.UserEdit.autoapproveMoviesDescription": "Gewährt die automatische Genehmigung für Filmanfragen von diesem Benutzer.",
"components.Settings.Notifications.NotificationsSlack.validationWebhookUrlRequired": "Du musst eine Webhook-URL angeben",
"components.Settings.Notifications.validationChatIdRequired": "Du musst eine Chat-ID angeben.",
"components.Settings.Notifications.validationBotAPIRequired": "Du musst einen Bot-API-Schlüssel angeben.",
"components.Settings.Notifications.telegramsettingssaved": "Telegram-Benachrichtigungseinstellungen gespeichert!",
"components.Settings.Notifications.telegramsettingsfailed": "Die Einstellungen für die Telegram-Benachrichtigung konnten nicht gespeichert werden.",
"components.Settings.Notifications.senderName": "Absendername",
"components.Settings.Notifications.settinguptelegramDescription": "Um Telegram einzurichten, musst du <CreateBotLink>einen Bot erstellen</CreateBotLink> und den Bot-API-Schlüssel erhalten. Außerdem benötigst du die Chat-ID für den Chat, an den der Bot Benachrichtigungen senden soll. Du kannst dies tun, indem du <GetIdBotLink>@get_id_bot</GetIdBotLink> zum Chat oder Gruppenchat hinzufügst.",
"components.Settings.Notifications.settinguptelegram": "Einrichten von Telegram-Benachrichtigungen",
"components.Settings.Notifications.chatId": "Chat-ID",
"components.Settings.Notifications.botAPI": "Bot-API",
"components.StatusChacker.reloadOverseerr": "Overseerr neu laden",
"components.StatusChacker.newversionavailable": "Neue Version verfügbar",
"components.StatusChacker.newversionDescription": "Eine Aktualisierung ist jetzt verfügbar. Klicke auf die Schaltfläche unten, um die Anwendung neu zu laden.",
"components.Settings.SettingsAbout.documentation": "Dokumentation",
"components.Settings.Notifications.notificationtypes": "Benachrichtigungstypen",
"components.Settings.Notifications.NotificationsSlack.notificationtypes": "Benachrichtigungstypen",
"components.NotificationTypeSelector.mediarequestedDescription": "Sendet eine Benachrichtigung, wenn neue Medien angefordert werden. Bei bestimmten Agenten wird die Benachrichtigung nur an Administratoren oder Benutzer mit der Berechtigung „Anfragen verwalten“ gesendet.",
"components.NotificationTypeSelector.mediarequested": "Angeforderte Medien",
"components.NotificationTypeSelector.mediafailedDescription": "Sendet eine Benachrichtigung, wenn Medien nicht zu Diensten hinzugefügt werden (Radarr / Sonarr). Bei bestimmten Agenten wird die Benachrichtigung nur an Administratoren oder Benutzer mit der Berechtigung „Anfragen verwalten“ gesendet.",
"components.NotificationTypeSelector.mediafailed": "Medienhinzufügen fehlgeschlagen",
"components.NotificationTypeSelector.mediaapprovedDescription": "Sendet eine Benachrichtigung, wenn Medien genehmigt werden.",
"components.NotificationTypeSelector.mediaavailableDescription": "Sendet eine Benachrichtigung, wenn Medien verfügbar werden.",
"components.NotificationTypeSelector.mediaavailable": "Medien verfügbar",
"components.NotificationTypeSelector.mediaapproved": "Medien genehmigt",
"i18n.request": "Anfragen",
"components.Settings.Notifications.NotificationsPushover.settinguppushoverDescription": "Um Pushover einzurichten, müssen Sie <RegisterApplicationLink> registrieren eine Anwendung</RegisterApplicationLink> registrieren und das Zugriffstoken erhalten. Beim Einrichten der Anwendung können Sie eines der Symbole im <IconLink>öffentlichen Ordner</IconLink> auf Github verwenden. Sie benötigen auch das Pushover Benutzertoken, das Sie auf der Startseite finden können, wenn Sie sich anmelden.",
"components.Settings.Notifications.NotificationsPushover.validationUserTokenRequired": "Du musst ein Benutzertoken bereitstellen.",
"components.Settings.Notifications.NotificationsPushover.validationAccessTokenRequired": "Du musst ein Zugriffstoken bereitstellen.",
"components.Settings.Notifications.NotificationsPushover.userToken": "Benutzertoken",
"components.Settings.Notifications.NotificationsPushover.testsent": "Test-Benarichtigung gesendet!",
"components.Settings.Notifications.NotificationsPushover.test": "Test",
"components.Settings.Notifications.NotificationsPushover.settinguppushover": "Pushover-Benarichtigungseinstellungen",
"components.Settings.Notifications.NotificationsPushover.saving": "Speichern …",
"components.Settings.Notifications.NotificationsPushover.save": "Änderungen speichern",
"components.Settings.Notifications.NotificationsPushover.pushoversettingssaved": "Pushover-Benarichtigungseinstellungen erfolgreich gespeichert!",
"components.Settings.Notifications.NotificationsPushover.notificationtypes": "Benarichtungstypen",
"components.Settings.Notifications.NotificationsPushover.pushoversettingsfailed": "Pushover-Benarichtigungseinstellungen konnten nicht gespeichert werden.",
"components.Settings.Notifications.NotificationsPushover.agentenabled": "Agent aktiviert",
"components.Settings.Notifications.NotificationsPushover.accessToken": "Zugangstoken",
"components.RequestList.sortModified": "Zuletzt geändert",
"components.RequestList.sortAdded": "Anfragedatum",
"components.RequestList.showallrequests": "All Anfragen anzeigen",
"components.RequestList.noresults": "Keine Ergebnisse.",
"components.RequestList.filterPending": "Unerledigt",
"components.RequestList.filterApproved": "Genehmigt",
"components.RequestList.filterAll": "Alle"
}

View File

@@ -60,6 +60,14 @@
"components.MovieDetails.viewfullcrew": "View Full Crew",
"components.MovieDetails.viewrequest": "View Request",
"components.MovieDetails.watchtrailer": "Watch Trailer",
"components.NotificationTypeSelector.mediaapproved": "Media Approved",
"components.NotificationTypeSelector.mediaapprovedDescription": "Sends a notification when media is approved.",
"components.NotificationTypeSelector.mediaavailable": "Media Available",
"components.NotificationTypeSelector.mediaavailableDescription": "Sends a notification when media becomes available.",
"components.NotificationTypeSelector.mediafailed": "Media Failed",
"components.NotificationTypeSelector.mediafailedDescription": "Sends a notification when media fails to be added to services (Radarr/Sonarr). For certain agents, this will only send the notification to admins or users with the \"Manage Requests\" permission.",
"components.NotificationTypeSelector.mediarequested": "Media Requested",
"components.NotificationTypeSelector.mediarequestedDescription": "Sends a notification when new media is requested. For certain agents, this will only send the notification to admins or users with the \"Manage Requests\" permission.",
"components.PersonDetails.appearsin": "Appears in",
"components.PersonDetails.ascharacter": "as {character}",
"components.PersonDetails.crewmember": "Crew Member",
@@ -75,13 +83,20 @@
"components.RequestList.RequestItem.notavailable": "N/A",
"components.RequestList.RequestItem.requestedby": "Requested by {username}",
"components.RequestList.RequestItem.seasons": "Seasons",
"components.RequestList.filterAll": "All",
"components.RequestList.filterApproved": "Approved",
"components.RequestList.filterPending": "Pending",
"components.RequestList.mediaInfo": "Media Info",
"components.RequestList.modifiedBy": "Last Modified By",
"components.RequestList.next": "Next",
"components.RequestList.noresults": "No Results.",
"components.RequestList.previous": "Previous",
"components.RequestList.requestedAt": "Requested At",
"components.RequestList.requests": "Requests",
"components.RequestList.showallrequests": "Show All Requests",
"components.RequestList.showingresults": "Showing <strong>{from}</strong> to <strong>{to}</strong> of <strong>{total}</strong> results",
"components.RequestList.sortAdded": "Request Date",
"components.RequestList.sortModified": "Last Modified",
"components.RequestList.status": "Status",
"components.RequestModal.cancel": "Cancel Request",
"components.RequestModal.cancelling": "Cancelling…",
@@ -104,7 +119,22 @@
"components.RequestModal.selectseason": "Select season(s)",
"components.RequestModal.status": "Status",
"components.Search.searchresults": "Search Results",
"components.Settings.Notifications.NotificationsPushover.accessToken": "Access Token",
"components.Settings.Notifications.NotificationsPushover.agentenabled": "Agent Enabled",
"components.Settings.Notifications.NotificationsPushover.notificationtypes": "Notification Types",
"components.Settings.Notifications.NotificationsPushover.pushoversettingsfailed": "Pushover notification settings failed to save.",
"components.Settings.Notifications.NotificationsPushover.pushoversettingssaved": "Pushover notification settings saved!",
"components.Settings.Notifications.NotificationsPushover.save": "Save Changes",
"components.Settings.Notifications.NotificationsPushover.saving": "Saving...",
"components.Settings.Notifications.NotificationsPushover.settinguppushover": "Setting up Pushover Notifications",
"components.Settings.Notifications.NotificationsPushover.settinguppushoverDescription": "To setup Pushover you need to <RegisterApplicationLink>register an application</RegisterApplicationLink> and get the access token. When setting up the application you can use one of the icons in the <IconLink>public folder</IconLink> on github. You also need the pushover user token which can be found on the start page when you log in.",
"components.Settings.Notifications.NotificationsPushover.test": "Test",
"components.Settings.Notifications.NotificationsPushover.testsent": "Test notification sent!",
"components.Settings.Notifications.NotificationsPushover.userToken": "User Token",
"components.Settings.Notifications.NotificationsPushover.validationAccessTokenRequired": "You must provide an access token.",
"components.Settings.Notifications.NotificationsPushover.validationUserTokenRequired": "You must provide a user token.",
"components.Settings.Notifications.NotificationsSlack.agentenabled": "Agent Enabled",
"components.Settings.Notifications.NotificationsSlack.notificationtypes": "Notification Types",
"components.Settings.Notifications.NotificationsSlack.save": "Save Changes",
"components.Settings.Notifications.NotificationsSlack.saving": "Saving...",
"components.Settings.Notifications.NotificationsSlack.settingupslack": "Setting up Slack Notifications",
@@ -120,19 +150,29 @@
"components.Settings.Notifications.allowselfsigned": "Allow Self-Signed Certificates",
"components.Settings.Notifications.authPass": "Auth Pass",
"components.Settings.Notifications.authUser": "Auth User",
"components.Settings.Notifications.botAPI": "Bot API",
"components.Settings.Notifications.chatId": "Chat Id",
"components.Settings.Notifications.discordsettingsfailed": "Discord notification settings failed to save.",
"components.Settings.Notifications.discordsettingssaved": "Discord notification settings saved!",
"components.Settings.Notifications.emailsender": "Email Sender Address",
"components.Settings.Notifications.emailsettingsfailed": "Email notification settings failed to save.",
"components.Settings.Notifications.emailsettingssaved": "Email notification settings saved!",
"components.Settings.Notifications.enableSsl": "Enable SSL",
"components.Settings.Notifications.notificationtypes": "Notification Types",
"components.Settings.Notifications.save": "Save Changes",
"components.Settings.Notifications.saving": "Saving…",
"components.Settings.Notifications.senderName": "Sender Name",
"components.Settings.Notifications.settinguptelegram": "Setting up Telegram Notifications",
"components.Settings.Notifications.settinguptelegramDescription": "To setup Telegram you need to <CreateBotLink>create a bot</CreateBotLink> and get the bot API key. Additionally, you need the chat id for the chat you want the bot to send notifications to. You can do this by adding <GetIdBotLink>@get_id_bot</GetIdBotLink> to the chat or group chat.",
"components.Settings.Notifications.smtpHost": "SMTP Host",
"components.Settings.Notifications.smtpPort": "SMTP Port",
"components.Settings.Notifications.ssldisabletip": "SSL should be disabled on standard TLS connections (Port 587)",
"components.Settings.Notifications.telegramsettingsfailed": "Telegram notification settings failed to save.",
"components.Settings.Notifications.telegramsettingssaved": "Telegram notification settings saved!",
"components.Settings.Notifications.test": "Test",
"components.Settings.Notifications.testsent": "Test notification sent!",
"components.Settings.Notifications.validationBotAPIRequired": "You must provide a Bot API key.",
"components.Settings.Notifications.validationChatIdRequired": "You must provide a Chat id.",
"components.Settings.Notifications.validationFromRequired": "You must provide an email sender address",
"components.Settings.Notifications.validationSmtpHostRequired": "You must provide an SMTP host",
"components.Settings.Notifications.validationSmtpPortRequired": "You must provide an SMTP port",
@@ -186,6 +226,7 @@
"components.Settings.SettingsAbout.Releases.viewchangelog": "View Changelog",
"components.Settings.SettingsAbout.Releases.viewongithub": "View on GitHub",
"components.Settings.SettingsAbout.clickheretojoindiscord": "Click here to join our Discord server.",
"components.Settings.SettingsAbout.documentation": "Documentation",
"components.Settings.SettingsAbout.gettingsupport": "Getting Support",
"components.Settings.SettingsAbout.githubdiscussions": "GitHub Discussions",
"components.Settings.SettingsAbout.helppaycoffee": "Help pay for coffee",
@@ -303,6 +344,9 @@
"components.Setup.tip": "Tip",
"components.Setup.welcome": "Welcome to Overseerr",
"components.Slider.noresults": "No Results",
"components.StatusChacker.newversionDescription": "An update is now available. Click the button below to reload the application.",
"components.StatusChacker.newversionavailable": "New Version Available",
"components.StatusChacker.reloadOverseerr": "Reload Overseerr",
"components.TitleCard.movie": "Movie",
"components.TitleCard.tvshow": "Series",
"components.TvDetails.TvCast.fullseriescast": "Full Series Cast",
@@ -398,6 +442,7 @@
"i18n.partiallyavailable": "Partially Available",
"i18n.pending": "Pending",
"i18n.processing": "Processing…",
"i18n.request": "Request",
"i18n.requested": "Requested",
"i18n.retry": "Retry",
"i18n.tvshows": "Series",

View File

@@ -384,5 +384,51 @@
"components.CollectionDetails.overviewunavailable": "Resumen no disponible",
"components.CollectionDetails.overview": "Resumen",
"components.CollectionDetails.numberofmovies": "Número de películas: {count}",
"components.CollectionDetails.movies": "Películas"
"components.CollectionDetails.movies": "Películas",
"i18n.retry": "Reintentar",
"i18n.requested": "Solicitado",
"i18n.failed": "Fallido",
"components.UserEdit.autoapproveSeriesDescription": "Concede aprobación automática para las solicitudes de series realizadas por este usuario.",
"components.UserEdit.autoapproveSeries": "Aprobación de Series automática",
"components.UserEdit.autoapproveMoviesDescription": "Concede aprobación automática para las solicitudes de película realizadas por este usuario.",
"components.UserEdit.autoapproveMovies": "Aprobación automática de películas",
"components.TvDetails.watchtrailer": "Ver Trailer",
"components.Settings.Notifications.NotificationsSlack.webhookUrlPlaceholder": "URL de Webhook",
"components.Settings.Notifications.NotificationsSlack.webhookUrl": "URL de Webhook",
"components.Settings.Notifications.NotificationsSlack.validationWebhookUrlRequired": "Debes proporcionar una URL de webhook",
"components.Settings.Notifications.NotificationsSlack.testsent": "¡Notificación de prueba enviada!",
"components.Settings.Notifications.NotificationsSlack.test": "Probar",
"components.Settings.Notifications.NotificationsSlack.slacksettingssaved": "¡Ajustes de notificación de Slack guardados!",
"components.Settings.Notifications.NotificationsSlack.slacksettingsfailed": "Fallo al guardar ajustes de notificación de Slack.",
"components.Settings.Notifications.NotificationsSlack.settingupslackDescription": "Para utilizar las notificaciones de Slack, deberá crear una integración de Webhook <WebhookLink>Incoming Webhook</WebhookLink> y utilizar la dirección URL de webhook proporcionada a continuación.",
"components.Settings.Notifications.NotificationsSlack.settingupslack": "Configurando notificaciones de Slack",
"components.Settings.Notifications.NotificationsSlack.saving": "Guardando...",
"components.Settings.Notifications.NotificationsSlack.save": "Guardar cambios",
"components.Settings.Notifications.NotificationsSlack.agentenabled": "Agente habilitado",
"components.RequestList.RequestItem.failedretry": "Algo salió mal al reintentar la solicitud",
"components.MovieDetails.watchtrailer": "Ver Trailer",
"components.MovieDetails.view": "Ver",
"components.NotificationTypeSelector.mediarequestedDescription": "Envía una notificación cuando se solicitan nuevos medios. Para ciertos agentes, esto solo enviará la notificación a los administradores o usuarios con el permiso \"Administrar solicitudes\".",
"components.StatusChacker.reloadOverseerr": "Relanzar Overseerr",
"components.StatusChacker.newversionavailable": "Nueva versión disponible",
"components.StatusChacker.newversionDescription": "Hay una actualización disponible. Haga clic en el botón de abajo para volver a cargar la aplicación.",
"components.Settings.SettingsAbout.documentation": "Documentación",
"components.Settings.Notifications.validationChatIdRequired": "Debe proporcionar una ID de chat.",
"components.Settings.Notifications.validationBotAPIRequired": "Debe proporcionar una clave de API de bot.",
"components.Settings.Notifications.telegramsettingssaved": "¡Se han guardado los ajustes de notificación de Telegram!",
"components.Settings.Notifications.telegramsettingsfailed": "La configuración de notificaciones de Telegram no se pudo guardar.",
"components.Settings.Notifications.settinguptelegramDescription": "Para configurar Telegram necesitas <CreateBotLink> crear un bot </CreateBotLink> y obtener la clave API del bot. Además, necesita la identificación del chat para el chat al que desea que el bot envíe notificaciones. Puede hacerlo agregando <GetIdBotLink> @get_id_bot </GetIdBotLink> al chat o al chat grupal.",
"components.Settings.Notifications.settinguptelegram": "Configuración de notificaciones de Telegram",
"components.Settings.Notifications.senderName": "Nombre del remitente",
"components.Settings.Notifications.notificationtypes": "Tipos de notificación",
"components.Settings.Notifications.chatId": "ID de chat",
"components.Settings.Notifications.botAPI": "API de bot",
"components.Settings.Notifications.NotificationsSlack.notificationtypes": "Tipos de notificación",
"components.NotificationTypeSelector.mediarequested": "Contenido Solicitado",
"components.NotificationTypeSelector.mediafailedDescription": "Envía una notificación cuando los medios no se agregan a los servicios (Radarr / Sonarr). Para ciertos agentes, esto solo enviará la notificación a los administradores o usuarios con el permiso \"Administrar solicitudes\".",
"components.NotificationTypeSelector.mediafailed": "Contenido Fallido",
"components.NotificationTypeSelector.mediaavailableDescription": "Envía una notificación cuando los medios están disponibles.",
"components.NotificationTypeSelector.mediaavailable": "Contenido disponible",
"components.NotificationTypeSelector.mediaapprovedDescription": "Envía una notificación cuando los medios son aprobados.",
"components.NotificationTypeSelector.mediaapproved": "Contenido Aprobado"
}

View File

@@ -388,5 +388,69 @@
"components.CollectionDetails.overview": "Résumé",
"components.CollectionDetails.numberofmovies": "Nombre de films : {count}",
"components.CollectionDetails.movies": "Films",
"i18n.requested": "Demandé"
"i18n.requested": "Demandé",
"i18n.retry": "Réessayer",
"i18n.failed": "Échec",
"components.UserEdit.autoapproveSeriesDescription": "Accorde la validation automatique pour les demandes de séries effectuées par cet utilisateur.",
"components.UserEdit.autoapproveSeries": "Valider automatiquement les séries",
"components.UserEdit.autoapproveMoviesDescription": "Accorde la validation automatique des demandes de films effectuées par cet utilisateur.",
"components.UserEdit.autoapproveMovies": "Valider automatiquement les films",
"components.Settings.Notifications.NotificationsSlack.webhookUrlPlaceholder": "URL webhook",
"components.Settings.Notifications.NotificationsSlack.webhookUrl": "URL webhook",
"components.Settings.Notifications.NotificationsSlack.validationWebhookUrlRequired": "Vous devez fournir une URL Webhook",
"components.Settings.Notifications.NotificationsSlack.testsent": "Notification de test envoyée !",
"components.Settings.Notifications.NotificationsSlack.test": "Test",
"components.Settings.Notifications.NotificationsSlack.slacksettingssaved": "Les paramètres des notifications Slack ont été enregistrés !",
"components.Settings.Notifications.NotificationsSlack.slacksettingsfailed": "Impossible d'enregistrer les paramètres des notifications Slack.",
"components.Settings.Notifications.NotificationsSlack.settingupslackDescription": "Pour utiliser les notifications Slack, vous aurez besoin de créer une intégration de <WebhookLink>Webhook Entrant</WebhookLink> et utiliser l'URL fournie du Webhook ci-dessous.",
"components.Settings.Notifications.NotificationsSlack.settingupslack": "Paramétrage des notifications Slack",
"components.Settings.Notifications.NotificationsSlack.saving": "Enregistrement…",
"components.Settings.Notifications.NotificationsSlack.save": "Enregistrer les changements",
"components.Settings.Notifications.NotificationsSlack.agentenabled": "Agent activé",
"components.RequestList.RequestItem.failedretry": "Une erreur s'est produite lors du renvoi de la requête",
"components.Settings.Notifications.validationChatIdRequired": "Vous devez fournir un identifiant de conversation.",
"components.Settings.Notifications.botAPI": "API du bot",
"components.Settings.Notifications.validationBotAPIRequired": "Vous devez fournir une clé API de bot.",
"components.Settings.Notifications.telegramsettingssaved": "Paramètres de notification Telegram enregistrés !",
"components.Settings.Notifications.telegramsettingsfailed": "Les paramètres de notification Telegram n'ont pas pu être enregistrés.",
"components.Settings.Notifications.senderName": "Nom de l'expéditeur",
"components.Settings.Notifications.chatId": "ID conversation",
"components.Settings.Notifications.settinguptelegramDescription": "Pour configurer Telegram, vous devez <CreateBotLink>créer un bot</CreateBotLink> et obtenir la clé API du bot. De plus, vous avez besoin de l'identifiant de conversion pour la conversation à laquelle vous souhaitez que le bot envoie des notifications. Vous pouvez le faire en ajoutant <GetIdBotLink>@get_id_bot</GetIdBotLink> à la conversation ou à la conversation de groupe.",
"components.Settings.Notifications.settinguptelegram": "Configuration des notifications Telegram",
"components.StatusChacker.reloadOverseerr": "Recharger Overseerr",
"components.StatusChacker.newversionavailable": "Nouvelle version disponible",
"components.StatusChacker.newversionDescription": "Une mise à jour est maintenant disponible. Cliquez sur le bouton ci-dessous pour recharger l'application.",
"components.Settings.SettingsAbout.documentation": "Documentation",
"components.Settings.Notifications.notificationtypes": "Types de notification",
"components.Settings.Notifications.NotificationsSlack.notificationtypes": "Types de notification",
"components.NotificationTypeSelector.mediarequestedDescription": "Envoie une notification quand un nouveau média est demandé. Pour certains agents, ceci n'enverra la notification qu'aux administrateurs ou aux utilisateurs disposant de l'autorisation « Gérer les demandes ».",
"components.NotificationTypeSelector.mediarequested": "Média demandé",
"components.NotificationTypeSelector.mediafailedDescription": "Envoie une notification quand des médias ne sont pas ajoutés aux services (Radarr / Sonarr). Pour certains agents, ceci n'enverra la notification qu'aux administrateurs ou aux utilisateurs disposant de l'autorisation « Gérer les demandes ».",
"components.NotificationTypeSelector.mediafailed": "Échec dajout du média",
"components.NotificationTypeSelector.mediaavailableDescription": "Envoie une notification quand le média devient disponible.",
"components.NotificationTypeSelector.mediaavailable": "Média disponible",
"components.NotificationTypeSelector.mediaapprovedDescription": "Envoie une notification quand le média est validé.",
"components.NotificationTypeSelector.mediaapproved": "Média validé",
"i18n.request": "Demander",
"components.Settings.Notifications.NotificationsPushover.validationUserTokenRequired": "Vous devez fournir un jeton utilisateur.",
"components.Settings.Notifications.NotificationsPushover.validationAccessTokenRequired": "Vous devez fournir un jeton d'accès.",
"components.Settings.Notifications.NotificationsPushover.userToken": "Jeton utilisateur",
"components.Settings.Notifications.NotificationsPushover.testsent": "Notification de test envoyée !",
"components.Settings.Notifications.NotificationsPushover.test": "Test",
"components.Settings.Notifications.NotificationsPushover.settinguppushoverDescription": "Pour configurer Pushover, vous devez <RegisterApplicationLink>enregistrer une application</RegisterApplicationLink> et obtenir le jeton d'accès. Lors de la configuration de l'application, vous pouvez utiliser l'une des icônes du <IconLink>dossier public</IconLink> sur GitHub. Vous avez également besoin du jeton d'utilisateur pushover qui se trouve sur la page de démarrage quand vous vous connectez.",
"components.Settings.Notifications.NotificationsPushover.settinguppushover": "Configuration des notifications pushover",
"components.Settings.Notifications.NotificationsPushover.saving": "Enregistrement…",
"components.Settings.Notifications.NotificationsPushover.save": "Enregistrer les changements",
"components.Settings.Notifications.NotificationsPushover.pushoversettingssaved": "Paramètres de notification pushover enregistrés !",
"components.Settings.Notifications.NotificationsPushover.pushoversettingsfailed": "Les paramètres de notification pushover n'ont pas pu être enregistrés.",
"components.Settings.Notifications.NotificationsPushover.notificationtypes": "Types de notification",
"components.Settings.Notifications.NotificationsPushover.agentenabled": "Agent activé",
"components.Settings.Notifications.NotificationsPushover.accessToken": "Jeton d'accès",
"components.RequestList.sortModified": "Dernière modification",
"components.RequestList.sortAdded": "Date de la demande",
"components.RequestList.showallrequests": "Afficher toutes les demandes",
"components.RequestList.noresults": "Aucun résultat.",
"components.RequestList.filterPending": "En attente",
"components.RequestList.filterApproved": "Validées",
"components.RequestList.filterAll": "Toutes"
}

View File

@@ -388,5 +388,69 @@
"components.TvDetails.watchtrailer": "Guarda il trailer",
"components.MovieDetails.watchtrailer": "Guarda il trailer",
"components.MovieDetails.view": "Visualizza",
"i18n.requested": "Richiesto"
"i18n.requested": "Richiesto",
"components.RequestList.RequestItem.failedretry": "Qualcosa è andato storto nel riprovare la richiesta",
"i18n.retry": "Riprova",
"i18n.failed": "Fallito",
"components.UserEdit.autoapproveSeriesDescription": "Concede l'approvazione automatica per le richieste di serie effettuate da questo utente.",
"components.UserEdit.autoapproveSeries": "Approva automaticamente le serie",
"components.UserEdit.autoapproveMoviesDescription": "Concede l'autoapprovazione per le richieste di film effettuate da questo utente.",
"components.UserEdit.autoapproveMovies": "Approva automaticamente i film",
"components.Settings.Notifications.NotificationsSlack.webhookUrlPlaceholder": "URL webhook",
"components.Settings.Notifications.NotificationsSlack.webhookUrl": "URL webhook",
"components.Settings.Notifications.NotificationsSlack.validationWebhookUrlRequired": "È necessario fornire un URL per il webhook",
"components.Settings.Notifications.NotificationsSlack.test": "Prova",
"components.Settings.Notifications.NotificationsSlack.testsent": "Notifica di prova inviata!",
"components.Settings.Notifications.NotificationsSlack.slacksettingssaved": "Impostazioni di notifica Slack salvate!",
"components.Settings.Notifications.NotificationsSlack.slacksettingsfailed": "Impossibile salvare le impostazioni di notifica Slack.",
"components.Settings.Notifications.NotificationsSlack.settingupslackDescription": "Per utilizzare le notifiche con Slack, sarà necessario creare un'integrazione <WebhookLink>Incoming Webhook</WebhookLink> e utilizzare l'URL webhook fornito di seguito.",
"components.Settings.Notifications.NotificationsSlack.settingupslack": "Configurazione delle notifiche di Slack",
"components.Settings.Notifications.NotificationsSlack.saving": "Salvataggio…",
"components.Settings.Notifications.NotificationsSlack.save": "Salva le modifiche",
"components.Settings.Notifications.NotificationsSlack.agentenabled": "Agente abilitato",
"components.Settings.Notifications.validationChatIdRequired": "Devi fornire un ID discussione.",
"components.Settings.Notifications.validationBotAPIRequired": "Devi fornire una chiave API bot.",
"components.Settings.Notifications.telegramsettingssaved": "Impostazioni di notifica di Telegram salvate!",
"components.Settings.Notifications.telegramsettingsfailed": "Impossibile salvare le impostazioni di notifica di Telegram.",
"components.Settings.Notifications.senderName": "Nome del mittente",
"components.Settings.Notifications.settinguptelegramDescription": "Per configurare Telegram devi <CreateBotLink>creare un bot</CreateBotLink> e ottenere la chiave API del bot. Inoltre, è necessario l'ID discussione per la discussione a cui si desidera che il bot invii le notifiche. Puoi farlo aggiungendo <GetIdBotLink>@get_id_bot</GetIdBotLink> alla discussione o alla discussione di gruppo.",
"components.Settings.Notifications.settinguptelegram": "Configurazione delle notifiche Telegram",
"components.Settings.Notifications.chatId": "ID discussione",
"components.Settings.Notifications.botAPI": "API bot",
"components.StatusChacker.reloadOverseerr": "Ricarica Overseerr",
"components.StatusChacker.newversionavailable": "Nuova versione disponibile",
"components.StatusChacker.newversionDescription": "È ora disponibile un aggiornamento. Fai clic sul pulsante in basso per ricaricare l'applicazione.",
"components.Settings.SettingsAbout.documentation": "Documentazione",
"components.Settings.Notifications.notificationtypes": "Tipi di notifica",
"components.Settings.Notifications.NotificationsSlack.notificationtypes": "Tipi di notifica",
"components.NotificationTypeSelector.mediarequestedDescription": "Invia una notifica quando un nuovo media viene richiesto. Per alcuni agenti, la notifica verrà inviata solo agli amministratori o agli utenti con l'autorizzazione «Gestione richieste».",
"components.NotificationTypeSelector.mediarequested": "Media richiesto",
"components.NotificationTypeSelector.mediafailedDescription": "Invia una notifica quando il media non viene aggiunto ai servizi (Radarr / Sonarr). Per alcuni agenti, la notifica verrà inviata solo agli amministratori o agli utenti con l'autorizzazione «Gestione richieste».",
"components.NotificationTypeSelector.mediafailed": "Aggiunta media non riuscita",
"components.NotificationTypeSelector.mediaavailableDescription": "Invia una notifica quando il media diventa disponibile.",
"components.NotificationTypeSelector.mediaapprovedDescription": "Invia una notifica quando il media viene approvato.",
"components.NotificationTypeSelector.mediaapproved": "Media approvato",
"components.NotificationTypeSelector.mediaavailable": "Media disponibile",
"i18n.request": "Richiedi",
"components.Settings.Notifications.NotificationsPushover.test": "Test",
"components.Settings.Notifications.NotificationsPushover.validationUserTokenRequired": "Devi fornire un token utente.",
"components.Settings.Notifications.NotificationsPushover.validationAccessTokenRequired": "Devi fornire un token di accesso.",
"components.Settings.Notifications.NotificationsPushover.userToken": "Token utente",
"components.Settings.Notifications.NotificationsPushover.testsent": "Notifica di prova inviata!",
"components.Settings.Notifications.NotificationsPushover.settinguppushoverDescription": "Per configurare Pushover è necessario <RegisterApplicationLink>registrare un'applicazione</RegisterApplicationLink> e ottenere il token di accesso. Quando si configura l'applicazione, è possibile utilizzare una delle icone nella <IconLink>cartella pubblica</IconLink> su GitHub. È inoltre necessario il token utente pushover che può essere trovato nella pagina iniziale quando si accede.",
"components.Settings.Notifications.NotificationsPushover.settinguppushover": "Configurazione delle notifiche pushover",
"components.Settings.Notifications.NotificationsPushover.saving": "Salvataggio…",
"components.Settings.Notifications.NotificationsPushover.save": "Salva le modifiche",
"components.Settings.Notifications.NotificationsPushover.pushoversettingssaved": "Impostazioni di notifica pushover salvate!",
"components.Settings.Notifications.NotificationsPushover.pushoversettingsfailed": "Impossibile salvare le impostazioni di notifica pushover.",
"components.Settings.Notifications.NotificationsPushover.notificationtypes": "Tipi di notifica",
"components.Settings.Notifications.NotificationsPushover.agentenabled": "Agente abilitato",
"components.Settings.Notifications.NotificationsPushover.accessToken": "Token di accesso",
"components.RequestList.sortModified": "Ultima modifica",
"components.RequestList.sortAdded": "Data della richiesta",
"components.RequestList.showallrequests": "Mostra tutte le richieste",
"components.RequestList.noresults": "Nessun risultato.",
"components.RequestList.filterPending": "In sospeso",
"components.RequestList.filterApproved": "Approvate",
"components.RequestList.filterAll": "Tutte"
}

View File

@@ -388,5 +388,22 @@
"components.TvDetails.TvCrew.fullseriescrew": "フルシリーズクルー",
"components.Settings.Notifications.ssldisabletip": "標準TLS接続ポート587ではSSLを無効にすることをおすすめします。",
"components.Settings.Notifications.allowselfsigned": "自己署名証明書を許可する",
"components.PersonDetails.crewmember": "クルーメンバー"
"components.PersonDetails.crewmember": "クルーメンバー",
"i18n.retry": "リトライ",
"i18n.failed": "失敗",
"components.UserEdit.autoapproveSeriesDescription": "このユーザーが行ったシリーズリクエストの自動承認をします。",
"components.UserEdit.autoapproveSeries": "シリーズを自動承認",
"components.UserEdit.autoapproveMoviesDescription": "このユーザーがリクエストした動画の自動承認を許可します。",
"components.UserEdit.autoapproveMovies": "ムービーの自動承認",
"components.Settings.Notifications.NotificationsSlack.webhookUrlPlaceholder": "ウェブフックURL",
"components.Settings.Notifications.NotificationsSlack.webhookUrl": "ウェブフックURL",
"components.Settings.Notifications.NotificationsSlack.validationWebhookUrlRequired": "ウェブフックのURLを指定してください。",
"components.Settings.Notifications.NotificationsSlack.testsent": "テスト通知が送信されました。",
"components.Settings.Notifications.NotificationsSlack.test": "テスト",
"components.Settings.Notifications.NotificationsSlack.slacksettingssaved": "Slackの通知設定が保存されました",
"components.Settings.Notifications.NotificationsSlack.slacksettingsfailed": "Slackの通知設定の保存に失敗しました。",
"components.Settings.Notifications.NotificationsSlack.settingupslackDescription": "Slack通知を使用するには、<WebhookLink>受信ウェブフック</WebhookLink>を作成し、以下のWebhook URLを使用する必要があります。",
"components.Settings.Notifications.NotificationsSlack.settingupslack": "Slack通知の設定",
"components.Settings.Notifications.NotificationsSlack.saving": "保存中...",
"components.Settings.Notifications.NotificationsSlack.save": "変更を保存"
}

View File

@@ -16,7 +16,7 @@
"components.Layout.Sidebar.settings": "Instellingen",
"components.Layout.Sidebar.users": "Gebruikers",
"components.Layout.UserDropdown.signout": "Uitloggen",
"components.Layout.alphawarning": "Dit is ALPHA software. Bijna alles is waarschijnlijk kapot of onstabiel. Rapporteer issues bij de Overseerr GitHub!",
"components.Layout.alphawarning": "Dit is ALPHA software. Bijna alles is waarschijnlijk kapot of instabiel. Rapporteer issues bij de Overseerr GitHub!",
"components.Login.signinplex": "Log in om door te gaan",
"components.MovieDetails.approve": "Goedkeuren",
"components.MovieDetails.available": "Beschikbaar",
@@ -25,7 +25,7 @@
"components.MovieDetails.cast": "Acteurs",
"components.MovieDetails.decline": "Weigeren",
"components.MovieDetails.manageModalClearMedia": "Wis Alle Media Data",
"components.MovieDetails.manageModalClearMediaWarning": "Dit wist alle media data inclusief alle verzoeken voor dit item zonder herstelmogelijkheden. Als dit item in je Plex bibliotheek bestaat zal alle media info bij de volgende sync hersteld worden.",
"components.MovieDetails.manageModalClearMediaWarning": "Dit wist alle mediadata voor dit item, inclusief alle verzoeken, zonder mogelijkheid tot herstel. Als dit item in je Plex-bibliotheek staat, zal alle media-info bij de volgende synchronisatie hersteld worden.",
"components.MovieDetails.manageModalNoRequests": "Geen Verzoeken",
"components.MovieDetails.manageModalRequests": "Verzoeken",
"components.MovieDetails.manageModalTitle": "Beheer Film",
@@ -50,15 +50,15 @@
"components.PersonDetails.nobiography": "Geen biografie beschikbaar.",
"components.PlexLoginButton.loading": "Bezig met laden…",
"components.PlexLoginButton.loggingin": "Bezig met inloggen…",
"components.PlexLoginButton.loginwithplex": "Login met Plex",
"components.PlexLoginButton.loginwithplex": "Inloggen met Plex",
"components.RequestBlock.seasons": "Seizoenen",
"components.RequestCard.all": "Alle",
"components.RequestCard.requestedby": "Verzocht door {username}",
"components.RequestCard.seasons": "Seizoenen",
"components.RequestList.RequestItem.notavailable": "Nvt",
"components.RequestList.RequestItem.notavailable": "N.v.t.",
"components.RequestList.RequestItem.requestedby": "Verzocht door {username}",
"components.RequestList.RequestItem.seasons": "Seizoenen",
"components.RequestList.mediaInfo": "Media Info",
"components.RequestList.mediaInfo": "Media-info",
"components.RequestList.modifiedBy": "Laatst Aangepast Door",
"components.RequestList.next": "Volgende",
"components.RequestList.previous": "Vorige",
@@ -183,7 +183,7 @@
"components.Settings.jobname": "Taaknaam",
"components.Settings.librariesRemaining": "Resterende Bibliotheken: {count}",
"components.Settings.manualscan": "Handmatige Bibliotheek Scan",
"components.Settings.manualscanDescription": "Normaal gesproken wordt dit eens elke 6 uur gedraaid. Overseerr controleert de recentelijk toegoevoegde items van je Plex Server aggresiever. Als dit je eerst keer is om Plex te configuren raden wij aan om eenmaal een volledige bibliotheek scan handmatig te draaien!",
"components.Settings.manualscanDescription": "Normaal gesproken wordt dit eens elke 24 uur gedraaid. Overseerr controleert de recentelijk toegoevoegde items van je Plex Server aggresiever. Als dit je eerst keer is om Plex te configuren raden wij aan om eenmaal een volledige bibliotheek scan handmatig te draaien!",
"components.Settings.menuAbout": "Over",
"components.Settings.menuGeneralSettings": "Algemene Instellingen",
"components.Settings.menuJobs": "Taken",
@@ -339,5 +339,110 @@
"components.Settings.SettingsAbout.Releases.currentversion": "Huidige versie",
"components.Settings.Notifications.testsent": "Test notificatie verzonden!",
"components.Settings.Notifications.test": "Test",
"components.MovieDetails.studio": "Studio"
"components.MovieDetails.studio": "Studio",
"components.CollectionDetails.requesting": "Aanvragen…",
"components.CollectionDetails.overviewunavailable": "Overzicht niet beschikbaar",
"components.CollectionDetails.overview": "Overzicht",
"components.CollectionDetails.numberofmovies": "Aantal films: {count}",
"components.CollectionDetails.movies": "Films",
"components.NotificationTypeSelector.mediafailed": "Media mislukt",
"components.NotificationTypeSelector.mediaapprovedDescription": "Stuurt een notificatie wanneer media is goedgekeurd.",
"components.NotificationTypeSelector.mediaavailableDescription": "Stuurt een notificatie wanneer media beschikbaar is.",
"components.NotificationTypeSelector.mediaapproved": "Media goedgekeurd",
"components.MovieDetails.view": "Bekijken",
"components.CollectionDetails.request": "Aanvragen",
"i18n.retry": "Opnieuw proberen",
"i18n.requested": "Aangevraagd",
"i18n.failed": "Mislukt",
"i18n.deleting": "Bezig met verwijderen…",
"i18n.close": "Sluiten",
"components.UserList.userdeleteerror": "Er ging iets mis bij het verwijderen van de gebruiker",
"components.UserList.userdeleted": "Gebruiker verwijderd",
"components.UserList.importfromplexerror": "Er is iets misgegaan bij het importeren van gebruikers uit Plex",
"components.UserList.importfromplex": "Gebruikers importeren uit Plex",
"components.UserList.deleteuser": "Gebruiker verwijderen",
"components.UserList.deleteconfirm": "Weet je zeker dat je deze gebruiker wilt verwijderen? Alle bestaande aanvraaggegevens van deze gebruiker zullen worden verwijderd.",
"components.UserEdit.autoapproveSeriesDescription": "Keurt serieverzoeken van deze gebruiker automatisch goed.",
"components.UserEdit.autoapproveSeries": "Serie automatisch goedkeuren",
"components.UserEdit.autoapproveMoviesDescription": "Keurt filmverzoeken van deze gebruiker automatisch goed.",
"components.UserEdit.autoapproveMovies": "Films automatisch goedkeuren",
"components.TvDetails.watchtrailer": "Trailer bekijken",
"components.TvDetails.viewfullcrew": "Bekijk volledige crew",
"components.TvDetails.showtype": "Type serie",
"components.TvDetails.network": "Netwerk",
"components.TvDetails.firstAirDate": "Eerste uitzenddatum",
"components.TvDetails.anime": "Anime",
"components.StatusChacker.reloadOverseerr": "Overseerr herladen",
"components.StatusChacker.newversionavailable": "Nieuwe versie beschikbaar",
"components.StatusChacker.newversionDescription": "Er is een update beschikbaar. Klik op de onderstaande knop om de toepassing opnieuw te laden.",
"components.Settings.toastSettingsSuccess": "Instellingen opgeslagen.",
"components.Settings.toastSettingsFailure": "Er ging iets mis met het opslaan van de instellingen.",
"components.Settings.toastApiKeySuccess": "Nieuwe API Key gegenereerd!",
"components.Settings.toastApiKeyFailure": "Er ging iets mis bij het genereren van een nieuwe API Key.",
"components.Settings.defaultPermissions": "Standaard gebruikersrechten",
"components.Settings.SonarrModal.animerootfolder": "Hoofdmap anime",
"components.Settings.SonarrModal.animequalityprofile": "Kwaliteitsprofiel anime",
"components.Settings.SettingsAbout.timezone": "Tijdzone",
"components.Settings.SettingsAbout.supportoverseerr": "Overseerr steunen",
"components.Settings.SettingsAbout.helppaycoffee": "Help een koffie te betalen",
"components.Settings.SettingsAbout.documentation": "Documentatie",
"components.Settings.SettingsAbout.Releases.viewongithub": "Bekijken op GitHub",
"components.Settings.SettingsAbout.Releases.viewchangelog": "Changelog bekijken",
"components.Settings.SettingsAbout.Releases.runningDevelopMessage": "De wijzigingen in je versie zijn hieronder niet beschikbaar. Kijk naar de <GithubLink>GitHub repository</GithubLink> voor de laatste updates.",
"components.Settings.SettingsAbout.Releases.runningDevelop": "Je gebruikt een ontwikkelversie van Overseerr!",
"components.Settings.Notifications.validationChatIdRequired": "Je moet een Chat-id opgeven.",
"components.Settings.Notifications.validationBotAPIRequired": "Je moet een Bot API-sleutel verstrekken.",
"components.Settings.Notifications.telegramsettingssaved": "Instellingen Telegramnotificaties opgeslagen!",
"components.Settings.Notifications.telegramsettingsfailed": "De instellingen voor Telegramnotificaties zijn niet opgeslagen.",
"components.Settings.Notifications.ssldisabletip": "SSL moet worden uitgeschakeld op standaard TLS-verbindingen (Poort 587)",
"components.Settings.Notifications.senderName": "Naam afzender",
"components.Settings.Notifications.notificationtypes": "Notificatietypes",
"components.Settings.Notifications.chatId": "Chat Id",
"components.Settings.Notifications.botAPI": "Bot API",
"components.Settings.Notifications.allowselfsigned": "Self-signed certificaten toestaan",
"components.Settings.Notifications.NotificationsSlack.webhookUrlPlaceholder": "Webhook URL",
"components.Settings.Notifications.NotificationsSlack.webhookUrl": "Webhook URL",
"components.Settings.Notifications.NotificationsSlack.validationWebhookUrlRequired": "Je moet een webhook URL ingeven",
"components.Settings.Notifications.NotificationsSlack.testsent": "Testnotificatie verstuurd!",
"components.Settings.Notifications.NotificationsSlack.test": "Test",
"components.Settings.Notifications.NotificationsSlack.saving": "Bezig met opslaan...",
"components.Settings.Notifications.NotificationsSlack.save": "Wijzigingen opslaan",
"components.Settings.Notifications.NotificationsSlack.notificationtypes": "Notificatietypes",
"components.Settings.Notifications.NotificationsSlack.agentenabled": "Agent ingeschakeld",
"components.RequestList.RequestItem.failedretry": "Er is iets misgegaan met het opnieuw proberen van het verzoek",
"components.PersonDetails.crewmember": "Crewlid",
"components.NotificationTypeSelector.mediarequested": "Media aangevraagd",
"components.NotificationTypeSelector.mediaavailable": "Media beschikbaar",
"components.CollectionDetails.requestswillbecreated": "De volgende titels zullen aangevraagd worden:",
"components.CollectionDetails.requestSuccess": "<strong>{title}</strong> succesvol aangevraagd!",
"components.MovieDetails.watchtrailer": "Trailer bekijken",
"components.MovieDetails.viewfullcrew": "Bekijk volledige crew",
"components.MovieDetails.MovieCrew.fullcrew": "Volledige crew",
"components.CollectionDetails.requestcollection": "Collectie aanvragen",
"components.UserList.importedfromplex": "{userCount, plural, =0 {Geen nieuwe gebruikers} one {# nieuwe gebruiker} other {# nieuwe gebruikers}} geïmporteerd vanuit Plex",
"components.Settings.SettingsAbout.Releases.versionChangelog": "Versie Changelog",
"components.Settings.SettingsAbout.Releases.releases": "Uitgaves",
"components.Settings.Notifications.settinguptelegramDescription": "Om Telegram in te stellen moet je <CreateBotLink>een bot aanmaken</CreateBotLink> en de bot API key gebruiken. Ook ben je het chat ID voor de chat waarheen je notificaties wil sturen nodig. Dit kun je doen door <GetIdBotLink>@get_id_bot</GetIdBotLink> toe te voegen aan de chat of group chat.",
"components.Settings.Notifications.settinguptelegram": "Telegram notificaties instellen",
"components.Settings.Notifications.NotificationsSlack.slacksettingssaved": "Slack notificatie instellingen zijn opgeslagen!",
"components.Settings.Notifications.NotificationsSlack.slacksettingsfailed": "Slack notificatie instellingen konden niet opgeslagen worden.",
"components.Settings.Notifications.NotificationsSlack.settingupslackDescription": "Om Slack notificaties te gebruiken moet je een <WebhookLink>Incoming Webhook</WebhookLink> integratie gebruiken en de onderstaande webhook URL gebruiken.",
"components.Settings.Notifications.NotificationsSlack.settingupslack": "Slack Notificaties Instellen",
"components.NotificationTypeSelector.mediarequestedDescription": "Stuurt een notificatie wanneer nieuwe media is verzocht. Voor bepaalde angenten wordt deze notificatie alleen verstuurd naar beheerders of gebruikers met de \"Beheer Verzoeken\" permissie.",
"components.NotificationTypeSelector.mediafailedDescription": "Stuurt een notificatie wanneer media niet toegevoegd kan worden aan diensten (Radarr/Sonarr). For bepaalde agenten stuurt dit alleen de notificatie naar beheerders of gebruikers met de \"Beheer Verzoeken\" permissie.",
"components.TvDetails.TvCrew.fullseriescrew": "Volledige crew van de serie",
"components.Settings.Notifications.NotificationsPushover.saving": "Bezig met opslaan...",
"components.Settings.Notifications.NotificationsPushover.save": "Sla Wijzingen Op",
"components.Settings.Notifications.NotificationsPushover.pushoversettingssaved": "Pushover notificatie instellingen zijn opgeslagen!",
"components.Settings.Notifications.NotificationsPushover.pushoversettingsfailed": "Pushover notificatie setting kon niet opgeslagen worden.",
"components.Settings.Notifications.NotificationsPushover.notificationtypes": "Notificatie Types",
"components.Settings.Notifications.NotificationsPushover.agentenabled": "Agent Ingeschakeld",
"components.Settings.Notifications.NotificationsPushover.accessToken": "Access Token",
"components.RequestList.sortModified": "Laatst Gewijzigd",
"components.RequestList.sortAdded": "Verzoek Datum",
"components.RequestList.showallrequests": "Toon Alle Verzoeken",
"components.RequestList.noresults": "Geen Resultaten.",
"components.RequestList.filterPending": "Wachtende",
"components.RequestList.filterApproved": "Goedgekeurd",
"components.RequestList.filterAll": "Alle"
}

View File

@@ -7,7 +7,7 @@
"components.RequestCard.seasons": "Temporadas",
"components.RequestCard.requestedby": "Solicitado por {username}",
"components.RequestBlock.seasons": "Temporadas",
"components.PlexLoginButton.loginwithplex": "Entrar com",
"components.PlexLoginButton.loginwithplex": "Entrar com Plex",
"components.PlexLoginButton.loggingin": "Fazendo login…",
"components.PlexLoginButton.loading": "Carregando…",
"components.PersonDetails.nobiography": "Biografia não disponível.",
@@ -17,14 +17,14 @@
"components.MovieDetails.userrating": "Avaliação do usuário",
"components.MovieDetails.unavailable": "Indisponível",
"components.MovieDetails.studio": "Estúdio",
"components.MovieDetails.status": "Detalhes",
"components.MovieDetails.status": "Estado",
"components.MovieDetails.similarsubtext": "Outros filmes semelhantes a {title}",
"components.MovieDetails.similar": "Títulos Semelhantes",
"components.MovieDetails.runtime": "{minutes} minutos",
"components.MovieDetails.revenue": "Receita",
"components.MovieDetails.request": "Solicitar",
"components.MovieDetails.releasedate": "Data de Lançamento",
"components.MovieDetails.recommendationssubtext": "Se você gostou {title}, você provavelmente irá gostar…",
"components.MovieDetails.releasedate": "Lançamento",
"components.MovieDetails.recommendationssubtext": "Se você gostou de {title}, você provavelmente irá gostar…",
"components.MovieDetails.recommendations": "Recomendações",
"components.MovieDetails.pending": "Pendente",
"components.MovieDetails.overviewunavailable": "Sinopse indisponível",
@@ -43,12 +43,12 @@
"components.MovieDetails.approve": "Aprovar",
"components.MovieDetails.MovieCast.fullcast": "Elenco Completo",
"components.Login.signinplex": "Faça login para continuar",
"components.Layout.alphawarning": "Esta é uma versão Alpha. Quase tudo é instável ou não funciona. Por favor reporte os problemas no GitHub do Overseerr!",
"components.Layout.alphawarning": "Essa é uma versão Alpha. Quase tudo é instável ou não funciona. Por favor reporte os problemas no GitHub do Overseerr!",
"components.Layout.UserDropdown.signout": "Sair",
"components.Layout.Sidebar.users": "Usuários",
"components.Layout.Sidebar.settings": "Configurações",
"components.Layout.Sidebar.requests": "Solicitações",
"components.Layout.Sidebar.dashboard": "Descobrir",
"components.Layout.Sidebar.dashboard": "Explorar",
"components.Layout.SearchInput.searchPlaceholder": "Pesquisar Filmes & Séries",
"components.Layout.LanguagePicker.changelanguage": "Alterar Idioma",
"components.Discover.upcomingmovies": "Em Breve",
@@ -255,25 +255,25 @@
"components.Settings.plexlibrariesDescription": "Bibliotecas que Overseerr irá buscar por títulos. Configure e salve as informações de conexão com Plex e click no botão abaixo se nenhuma biblioteca é listada.",
"components.Settings.SettingsAbout.timezone": "Fuso Horário",
"components.Settings.SettingsAbout.helppaycoffee": "Ajude a Pagar o Café",
"components.Settings.SettingsAbout.supportoverseerr": "Apoie Overseerr",
"components.Settings.SettingsAbout.supportoverseerr": "Apoie o Overseerr",
"components.Settings.SettingsAbout.Releases.viewongithub": "Ver no GitHub",
"components.Settings.SettingsAbout.Releases.viewchangelog": "Ver Mudanças",
"components.Settings.SettingsAbout.Releases.versionChangelog": "Mudanças Nessa Versão",
"components.Settings.SettingsAbout.Releases.runningDevelopMessage": "As mudanças em sua versão não serão exibidas abaixo. Por favor acesso o <GithubLink> repositório do GitHub</GithubLink> para saber o que mudou.",
"components.Settings.SettingsAbout.Releases.runningDevelopMessage": "As mudanças em sua versão não serão exibidas abaixo. Por favor acesse o <GithubLink> repositório do GitHub</GithubLink> para saber o que mudou.",
"components.Settings.SettingsAbout.Releases.runningDevelop": "Você está usando uma versão de desenvolvimento do Overseerr!",
"components.Settings.SettingsAbout.Releases.releases": "Versões",
"components.Settings.SettingsAbout.Releases.releasedataMissing": "Informações de versão faltando. O GitHub está indisponível?",
"components.Settings.SettingsAbout.Releases.latestversion": "Última Versão",
"components.Settings.SettingsAbout.Releases.currentversion": "Versão Atual",
"components.TvDetails.request": "Solicitar",
"components.TvDetails.recommendationssubtext": "Se você gostou {title}, você provavelmente irá gostar…",
"components.TvDetails.recommendationssubtext": "Se você gostou de {title}, você provavelmente irá gostar…",
"components.TvDetails.recommendations": "Recomendações",
"components.TvDetails.pending": "Pendente",
"components.TvDetails.overviewunavailable": "Sinopse indisponível",
"components.TvDetails.overview": "Sinopse",
"components.TvDetails.originallanguage": "Língua original",
"components.TvDetails.network": "Estúdio",
"components.TvDetails.manageModalTitle": "Gerenciar Séries",
"components.TvDetails.manageModalTitle": "Gerenciar Série",
"components.TvDetails.manageModalRequests": "Solicitações",
"components.TvDetails.manageModalNoRequests": "Nenhuma Solicitação",
"components.TvDetails.manageModalClearMedia": "Limpar Todos Dados de Mídia",
@@ -286,7 +286,7 @@
"components.TvDetails.approve": "Aprovar",
"components.TvDetails.anime": "Animes",
"components.TvDetails.TvCast.fullseriescast": "Elenco Completo da Série",
"components.TitleCard.tvshow": "Séries",
"components.TitleCard.tvshow": "Série",
"components.TitleCard.movie": "Filme",
"components.Slider.noresults": "Nenhum Resultado",
"components.Setup.welcome": "Bem-Vindo ao Overseerr",
@@ -359,8 +359,8 @@
"components.TvDetails.similar": "Séries Similares",
"components.TvDetails.showtype": "Categoria",
"components.TvDetails.manageModalClearMediaWarning": "Isso irá remover em definitivo todos dados de mídia, incluindo todas solicitações para esse item. Se este item existir in sua biblioteca do Plex, os dados de mídia serão recriados na próxima sincronia.",
"components.Settings.sonarrSettingsDescription": "Configure sua conexão com Sonarr abaixo. Você pode criar várias, mas apenas duas por vez como padrão (uma para padrão HD, e outra para 4K). Administradores podem alterar qual conexão será usada para novas solicitações.",
"components.Settings.radarrSettingsDescription": "Configure sua conexão com Radarr abaixo. Você pode criar várias, mas apenas duas por vez como padrão (uma para padrão HD, e outra para 4K). Administradores podem alterar qual conexão será usada para novas solicitações.",
"components.Settings.sonarrSettingsDescription": "Configure abaixo sua conexão com Sonarr. Você pode criar várias conexões, mas apenas duas por vez como padrão (uma para padrão HD, e outra para 4K). Administradores podem alterar qual conexão será usada para novas solicitações.",
"components.Settings.radarrSettingsDescription": "Configure abaixo sua conexão com Radarr. Você pode criar várias conexões, mas apenas duas por vez como padrão (uma para padrão HD, e outra para 4K). Administradores podem alterar qual conexão será usada para novas solicitações.",
"components.Settings.Notifications.testsent": "Notificação de teste enviada!",
"components.Settings.Notifications.test": "Testar",
"components.RequestModal.requestseasons": "Solicitar {seasonCount} {seasonCount, plural, one {Temporada} other {Temporadas}}",
@@ -372,5 +372,64 @@
"components.UserList.importfromplexerror": "Algo deu errado ao importar usuários do Plex",
"components.UserList.importfromplex": "Importar Usuários do Plex",
"components.UserList.importedfromplex": "{userCount, plural, =0 {Nenhum novo usuário} one {# novo usuário} other {# novos usuários}} importado(s) do Plex",
"components.Settings.defaultPermissions": "Permissões Padrões de Usúarios"
"components.Settings.defaultPermissions": "Permissões Padrões de Usúarios",
"components.Settings.Notifications.NotificationsSlack.settingupslack": "Configurando Notificações Via Slack",
"components.Settings.Notifications.NotificationsSlack.saving": "Salvando...",
"components.Settings.Notifications.NotificationsSlack.save": "Salvar Mudanças",
"components.Settings.Notifications.NotificationsSlack.agentenabled": "Agente Habilitado",
"components.RequestList.RequestItem.failedretry": "Algo deu errado ao retentar fazer a solicitação",
"components.MovieDetails.watchtrailer": "Assistir Trailer",
"components.MovieDetails.view": "Ver",
"components.CollectionDetails.requestswillbecreated": "Serão feitas solitações para os seguintes títulos:",
"components.CollectionDetails.requesting": "Solicitando…",
"components.CollectionDetails.requestcollection": "Solicitar Coleção",
"components.CollectionDetails.requestSuccess": "<strong>{title}</strong> solictiado com sucesso!",
"components.CollectionDetails.request": "Solicitar",
"components.CollectionDetails.overviewunavailable": "Sinopse indisponível",
"components.CollectionDetails.overview": "Sinopse",
"components.CollectionDetails.numberofmovies": "Número de Filmes: {count}",
"components.CollectionDetails.movies": "Filmes",
"i18n.requested": "Solicitado",
"i18n.retry": "Tentar Novamente",
"i18n.failed": "Falhou",
"components.UserEdit.autoapproveSeriesDescription": "Concede aprovação automática de séries solicitadas por este usuário.",
"components.UserEdit.autoapproveSeries": "Aprovar Séries Automaticamente",
"components.UserEdit.autoapproveMoviesDescription": "Concede aprovação automática de filmes solicitados por este usuário.",
"components.UserEdit.autoapproveMovies": "Aprovar Filmes Automaticamente",
"components.TvDetails.watchtrailer": "Assisitir Trailer",
"components.TvDetails.firstAirDate": "Primeira Exibição",
"components.Settings.Notifications.validationChatIdRequired": "Você deve prover um ID de Chat.",
"components.Settings.Notifications.validationBotAPIRequired": "Você deve prover a chave de API do Bot.",
"components.Settings.Notifications.senderName": "Nome do Remetente",
"components.Settings.Notifications.telegramsettingssaved": "Configurações de notificação via Telegram salvas!",
"components.Settings.Notifications.telegramsettingsfailed": "Falha ao salvar configurações de notificação via Telegram.",
"components.Settings.Notifications.ssldisabletip": "SSL deve ser desabilitado em conexões TLS padrão (Porta 587)",
"components.Settings.Notifications.settinguptelegramDescription": "Para configurar notificações via Telegram, você precisa <CreateBotLink>criar um bot</CreateBotLink> e obter a chave de API do mesmo. Além disso, você irá precisar do ID de Chat de onde você deseja que o bot envie as notificações. Você pode obter o ID de Chat adicionando <GetIdBotLink>@get_id_bot</GetIdBotLink> ao chat ou grupo ao qual você deseja obter o ID.",
"components.Settings.Notifications.settinguptelegram": "Configurando notificações via Telegram",
"components.Settings.Notifications.chatId": "ID de Chat",
"components.Settings.Notifications.botAPI": "API do Bot",
"components.Settings.Notifications.allowselfsigned": "Permitir certificados auto-assinados",
"components.Settings.Notifications.NotificationsSlack.webhookUrlPlaceholder": "URL de Webhook",
"components.Settings.Notifications.NotificationsSlack.webhookUrl": "URL de Webhook",
"components.Settings.Notifications.NotificationsSlack.validationWebhookUrlRequired": "Você deve prover uma URL de webhook",
"components.Settings.Notifications.NotificationsSlack.testsent": "Notificação de teste enviada!",
"components.Settings.Notifications.NotificationsSlack.test": "Testar",
"components.Settings.Notifications.NotificationsSlack.slacksettingssaved": "Configurações de notificação via Slack salvas!",
"components.Settings.Notifications.NotificationsSlack.slacksettingsfailed": "Falha ao salvar configurações de notificação via Slack.",
"components.Settings.Notifications.NotificationsSlack.settingupslackDescription": "Para usar notificações via Slack você irá p recisar criar uma integração <WebhookLink>Webhook de entrada</WebhookLink> e usar no campo abaixo a URL gerada.",
"components.StatusChacker.reloadOverseerr": "Reiniciar Overseerr",
"components.StatusChacker.newversionDescription": "Uma atualização está disponível. Clique no botão abaixo para reiniciar a aplicação.",
"components.StatusChacker.newversionavailable": "Nova Versão Disponível",
"components.Settings.SettingsAbout.documentation": "Documentação",
"components.Settings.Notifications.notificationtypes": "Tipos de Notificação",
"components.Settings.Notifications.NotificationsSlack.notificationtypes": "Tipos de Notificação",
"components.NotificationTypeSelector.mediarequestedDescription": "Envia uma notificação quando nova mídia é solicitada. Para alguns agentes, somente serão enviadas notificações aos administradores ou usuários com perfil “Gerenciar solicitações”.",
"components.NotificationTypeSelector.mediaavailable": "Mídia Disponível",
"components.NotificationTypeSelector.mediaapproved": "Mídia Aprovada",
"components.NotificationTypeSelector.mediarequested": "Mídia Solicitada",
"components.NotificationTypeSelector.mediafailedDescription": "Envia uma notificação quando houver falha ao adicionar título aos serviços (Radarr/Sonarr). Para alguns agentes, somente serão enviadas notificações aos administradores ou usuários com perfil “Gerenciar solicitações”.",
"components.NotificationTypeSelector.mediafailed": "Solicitação Falhou",
"components.NotificationTypeSelector.mediaavailableDescription": "Envia uma notificação quando o título solicitado estiver disponível.",
"components.NotificationTypeSelector.mediaapprovedDescription": "Envia uma notificação quando a solicitação é aprovada.",
"i18n.request": "Solicitar"
}

View File

@@ -364,5 +364,28 @@
"components.Settings.SettingsAbout.Releases.currentversion": "Aktuell Version",
"components.Settings.Notifications.testsent": "Test-notifikation skickad!",
"components.Settings.Notifications.test": "Testa",
"components.UserList.importfromplex": "Importera användare från Plex"
"components.UserList.importfromplex": "Importera användare från Plex",
"components.UserList.importfromplexerror": "Någonting gick fel vid importen av användare från Plex",
"components.UserEdit.autoapproveSeries": "Automatiskt Godkännande av TV Serier",
"components.UserEdit.autoapproveMovies": "Automatiskt Godkännande av Filmer",
"components.TvDetails.watchtrailer": "Kolla Trailer",
"components.Settings.Notifications.allowselfsigned": "Tillåt Självsignerade Certifikat",
"components.Settings.Notifications.NotificationsSlack.webhookUrl": "Webhook URL",
"components.Settings.Notifications.NotificationsSlack.validationWebhookUrlRequired": "Du måste lägga in en webhook URL",
"components.Settings.Notifications.NotificationsSlack.testsent": "Testnotifikation skickad!",
"components.Settings.Notifications.NotificationsSlack.test": "Test",
"components.Settings.Notifications.NotificationsSlack.slacksettingssaved": "Notiferingsinställningar för Slack sparade!",
"components.Settings.Notifications.NotificationsSlack.slacksettingsfailed": "Notifieringsinställningar för Slack misslyckades att sparas.",
"components.Settings.Notifications.NotificationsSlack.settingupslack": "Ställa in Slack Notifikationer",
"components.Settings.Notifications.NotificationsSlack.saving": "Sparar...",
"components.Settings.Notifications.NotificationsSlack.save": "Spara Ändringar",
"components.Settings.Notifications.NotificationsSlack.agentenabled": "Aktiverad",
"components.MovieDetails.watchtrailer": "Kolla Trailer",
"components.CollectionDetails.requesting": "Skickar förfrågan…",
"components.CollectionDetails.requestcollection": "Begär Kollektion",
"components.CollectionDetails.request": "Begär",
"components.CollectionDetails.overviewunavailable": "Översikt ej tillgänglig",
"components.CollectionDetails.overview": "Översikt",
"components.CollectionDetails.numberofmovies": "Antal Filmer: {count}",
"components.CollectionDetails.movies": "Filmer"
}

View File

@@ -13,6 +13,7 @@ import { LanguageContext, AvailableLocales } from '../context/LanguageContext';
import Head from 'next/head';
import Toast from '../components/Toast';
import { InteractionProvider } from '../context/InteractionContext';
import StatusChecker from '../components/StatusChacker';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const loadLocaleData = (locale: string): Promise<any> => {
@@ -74,7 +75,10 @@ const CoreApp: Omit<NextAppComponentType, 'origGetInitialProps'> = ({
useEffect(() => {
loadLocaleData(currentLocale).then(setMessages);
setCookie(null, 'locale', currentLocale, { path: '/' });
setCookie(null, 'locale', currentLocale, {
path: '/',
maxAge: 60 * 60 * 24 * 365 * 10,
});
}, [currentLocale]);
if (router.pathname.match(/(login|setup)/)) {
@@ -104,6 +108,7 @@ const CoreApp: Omit<NextAppComponentType, 'origGetInitialProps'> = ({
<Head>
<title>Overseerr</title>
</Head>
<StatusChecker />
<UserContext initialUser={user}>{component}</UserContext>
</ToastProvider>
</InteractionProvider>

View File

@@ -1,9 +0,0 @@
import React from 'react';
import { NextPage } from 'next';
import Holiday from '../../components/Discover/Holiday';
const HolidayPage: NextPage = () => {
return <Holiday />;
};
export default HolidayPage;

View File

@@ -0,0 +1,17 @@
import { NextPage } from 'next';
import React from 'react';
import NotificationsPushover from '../../../components/Settings/Notifications/NotificationsPushover';
import SettingsLayout from '../../../components/Settings/SettingsLayout';
import SettingsNotifications from '../../../components/Settings/SettingsNotifications';
const NotificationsPage: NextPage = () => {
return (
<SettingsLayout>
<SettingsNotifications>
<NotificationsPushover />
</SettingsNotifications>
</SettingsLayout>
);
};
export default NotificationsPage;

View File

@@ -0,0 +1,17 @@
import { NextPage } from 'next';
import React from 'react';
import NotificationsTelegram from '../../../components/Settings/Notifications/NotificationsTelegram';
import SettingsLayout from '../../../components/Settings/SettingsLayout';
import SettingsNotifications from '../../../components/Settings/SettingsNotifications';
const NotificationsPage: NextPage = () => {
return (
<SettingsLayout>
<SettingsNotifications>
<NotificationsTelegram />
</SettingsNotifications>
</SettingsLayout>
);
};
export default NotificationsPage;

View File

@@ -56,3 +56,7 @@ body {
code {
@apply px-2 py-1 bg-gray-800 rounded-md;
}
input[type='search']::-webkit-search-cancel-button {
-webkit-appearance: none;
}

Some files were not shown because too many files have changed in this diff Show More