Compare commits
59 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6486c3643 | ||
|
|
f9695068a8 | ||
|
|
2bfe0f2bf6 | ||
|
|
493d82b6b0 | ||
|
|
e2c2172ac6 | ||
|
|
ee5d0181fc | ||
|
|
67f2b57f00 | ||
|
|
88810bf0a4 | ||
|
|
0ab4c3c36f | ||
|
|
effc80977a | ||
|
|
fd90adba4d | ||
|
|
970aa612fe | ||
|
|
71169944dc | ||
|
|
b04eda6c8a | ||
|
|
5add44cfb0 | ||
|
|
a524b9c4c8 | ||
|
|
b5a3a7a89f | ||
|
|
67ac9e075f | ||
|
|
ce0687922a | ||
|
|
ba106c447d | ||
|
|
8cbf39a9d1 | ||
|
|
78b15cad18 | ||
|
|
d77f9eb44e | ||
|
|
8c0903393c | ||
|
|
22f2037ea6 | ||
|
|
3e613a3e93 | ||
|
|
5c847026aa | ||
|
|
2f017fe2cb | ||
|
|
7d95fd0f30 | ||
|
|
28b1d8cf4c | ||
|
|
9d6970db57 | ||
|
|
f906711ee8 | ||
|
|
93b57a76f1 | ||
|
|
12db7a065a | ||
|
|
058fb65495 | ||
|
|
721ed9a930 | ||
|
|
5295f74915 | ||
|
|
7d6c6cb2a1 | ||
|
|
d53d61014f | ||
|
|
7c40d9131d | ||
|
|
f124d732a2 | ||
|
|
8af6a1f566 | ||
|
|
d00e470b55 | ||
|
|
55f9e41f1b | ||
|
|
343f466788 | ||
|
|
fae4818957 | ||
|
|
d188f6ffad | ||
|
|
289864af1a | ||
|
|
d2620281bf | ||
|
|
30ed82e155 | ||
|
|
c034496f55 | ||
|
|
2dcda39d40 | ||
|
|
75a426437a | ||
|
|
1438b08cf0 | ||
|
|
0185bb1a70 | ||
|
|
c8d4d674f4 | ||
|
|
7434a26f76 | ||
|
|
46af705014 | ||
|
|
2b27a715b0 |
@@ -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
@@ -0,0 +1,5 @@
|
||||
root: ./docs
|
||||
|
||||
structure:
|
||||
readme: README.md
|
||||
summary: SUMMARY.md
|
||||
63
CHANGELOG.md
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
15
README.md
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
132
docs/extending-overseerr/reverse-proxy-examples.md
Normal 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;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
196
docs/getting-started/installation.md
Normal 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
|
||||
```
|
||||
34
docs/support/asking-for-support.md
Normal 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
@@ -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 -> 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 -> 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).
|
||||
@@ -1,4 +1,7 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
commitTag: process.env.COMMIT_TAG || 'local',
|
||||
},
|
||||
webpack(config) {
|
||||
config.module.rules.push({
|
||||
test: /\.svg$/,
|
||||
|
||||
@@ -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
|
||||
|
||||
38
package.json
@@ -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",
|
||||
|
||||
|
Before Width: | Height: | Size: 140 KiB After Width: | Height: | Size: 134 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 315 KiB After Width: | Height: | Size: 382 KiB |
|
Before Width: | Height: | Size: 249 KiB After Width: | Height: | Size: 407 KiB |
BIN
public/images/rotate5.jpg
Normal file
|
After Width: | Height: | Size: 384 KiB |
BIN
public/images/rotate6.jpg
Normal file
|
After Width: | Height: | Size: 421 KiB |
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
122
server/lib/notifications/agents/pushover.ts
Normal 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;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
127
server/lib/notifications/agents/telegram.ts
Normal 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;
|
||||
@@ -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[] = [];
|
||||
|
||||
|
||||
@@ -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: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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") `
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
),
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
),
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
),
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
),
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
6
src/assets/extlogos/pushover.svg
Normal 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 |
3
src/assets/extlogos/telegram.svg
Normal 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
@@ -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 |
@@ -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})`,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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})`,
|
||||
|
||||
@@ -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;
|
||||
106
src/components/NotificationTypeSelector/index.tsx
Normal 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;
|
||||
@@ -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">
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
@@ -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">
|
||||
|
||||
255
src/components/Settings/Notifications/NotificationsTelegram.tsx
Normal 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;
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()}
|
||||
|
||||
@@ -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
|
||||
) : (
|
||||
|
||||
70
src/components/StatusChacker/index.tsx
Normal 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;
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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})`,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -6,6 +6,7 @@ const globalMessages = defineMessages({
|
||||
processing: 'Processing',
|
||||
unavailable: 'Unavailable',
|
||||
requested: 'Requested',
|
||||
request: 'Request',
|
||||
failed: 'Failed',
|
||||
pending: 'Pending',
|
||||
declined: 'Declined',
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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 d’ajout 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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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": "変更を保存"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
17
src/pages/settings/notifications/pushover.tsx
Normal 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;
|
||||
17
src/pages/settings/notifications/telegram.tsx
Normal 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;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||