Merge branch 'develop'
@@ -503,6 +503,42 @@
|
||||
"contributions": [
|
||||
"translation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "iceHtwoO",
|
||||
"name": "Alexander Neuhäuser",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/27020492?v=4",
|
||||
"profile": "https://github.com/iceHtwoO",
|
||||
"contributions": [
|
||||
"translation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "liviokanone",
|
||||
"name": "Livio",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/37431541?v=4",
|
||||
"profile": "http://www.unext.co.jp",
|
||||
"contributions": [
|
||||
"design"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "tangentThought",
|
||||
"name": "tangentThought",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/25516090?v=4",
|
||||
"profile": "https://github.com/tangentThought",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "nicospz",
|
||||
"name": "Nicolás Espinoza",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/31373060?v=4",
|
||||
"profile": "https://github.com/nicospz",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
}
|
||||
],
|
||||
"badgeTemplate": "<a href=\"#contributors-\"><img alt=\"All Contributors\" src=\"https://img.shields.io/badge/all_contributors-<%= contributors.length %>-orange.svg\"/></a>",
|
||||
|
||||
@@ -21,7 +21,7 @@ docker-compose.yml
|
||||
docs
|
||||
LICENSE
|
||||
node_modules
|
||||
public/os_logo_square.png
|
||||
public/os_logo_filled.png
|
||||
public/preview.jpg
|
||||
snap
|
||||
stylelint.config.js
|
||||
|
||||
2
.github/workflows/ci.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
test:
|
||||
name: Lint & Test Build
|
||||
runs-on: ubuntu-20.04
|
||||
container: node:14.16-alpine
|
||||
container: node:14.17-alpine
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2.3.4
|
||||
|
||||
2
.github/workflows/release.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
||||
test:
|
||||
name: Lint & Test Build
|
||||
runs-on: ubuntu-20.04
|
||||
container: node:14.16-alpine
|
||||
container: node:14.17-alpine
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2.3.4
|
||||
|
||||
2
.github/workflows/snap.yaml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
name: Lint & Test Build
|
||||
needs: jobs
|
||||
runs-on: ubuntu-20.04
|
||||
container: node:14.16-alpine
|
||||
container: node:14.17-alpine
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2.3.4
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:14.16-alpine AS BUILD_IMAGE
|
||||
FROM node:14.17-alpine AS BUILD_IMAGE
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
@@ -31,7 +31,7 @@ RUN touch config/DOCKER
|
||||
RUN echo "{\"commitTag\": \"${COMMIT_TAG}\"}" > committag.json
|
||||
|
||||
|
||||
FROM node:14.16-alpine
|
||||
FROM node:14.17-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:14.16-alpine
|
||||
FROM node:14.17-alpine
|
||||
|
||||
COPY . /app
|
||||
WORKDIR /app
|
||||
|
||||
10
README.md
@@ -1,5 +1,5 @@
|
||||
<p align="center">
|
||||
<img src="https://i.imgur.com/TMoEG7g.png" alt="Overseerr">
|
||||
<img src="./public/logo_full.svg" alt="Overseerr" style="margin: 20px 0;">
|
||||
</p>
|
||||
<p align="center">
|
||||
<img src="https://github.com/sct/overseerr/workflows/Overseerr%20Release/badge.svg?branch=master" alt="Overseerr Release" />
|
||||
@@ -12,7 +12,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>
|
||||
<a href="https://github.com/sct/overseerr/blob/develop/LICENSE"><img alt="GitHub" src="https://img.shields.io/github/license/sct/overseerr"></a>
|
||||
<!-- 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-54-orange.svg"/></a>
|
||||
<a href="#contributors-"><img alt="All Contributors" src="https://img.shields.io/badge/all_contributors-58-orange.svg"/></a>
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
||||
</p>
|
||||
|
||||
@@ -143,6 +143,12 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
||||
<td align="center"><a href="https://github.com/littlerooster"><img src="https://avatars.githubusercontent.com/u/83890654?v=4?s=100" width="100px;" alt=""/><br /><sub><b>littlerooster</b></sub></a><br /><a href="#translation-littlerooster" title="Translation">🌍</a></td>
|
||||
<td align="center"><a href="https://github.com/dphildebrandt"><img src="https://avatars.githubusercontent.com/u/154459?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Dustin Hildebrandt</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=dphildebrandt" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/Generator"><img src="https://avatars.githubusercontent.com/u/44146?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Bruno Guerreiro</b></sub></a><br /><a href="#translation-Generator" title="Translation">🌍</a></td>
|
||||
<td align="center"><a href="https://github.com/iceHtwoO"><img src="https://avatars.githubusercontent.com/u/27020492?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Alexander Neuhäuser</b></sub></a><br /><a href="#translation-iceHtwoO" title="Translation">🌍</a></td>
|
||||
<td align="center"><a href="http://www.unext.co.jp"><img src="https://avatars.githubusercontent.com/u/37431541?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Livio</b></sub></a><br /><a href="#design-liviokanone" title="Design">🎨</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/tangentThought"><img src="https://avatars.githubusercontent.com/u/25516090?v=4?s=100" width="100px;" alt=""/><br /><sub><b>tangentThought</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=tangentThought" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/nicospz"><img src="https://avatars.githubusercontent.com/u/31373060?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Nicolás Espinoza</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=nicospz" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
@@ -145,7 +145,8 @@ location ^~ /overseerr {
|
||||
sub_filter '/android-' '/$app/android-';
|
||||
sub_filter '/apple-' '/$app/apple-';
|
||||
sub_filter '/favicon' '/$app/favicon';
|
||||
sub_filter '/logo.png' '/$app/logo.png';
|
||||
sub_filter '/logo_full.svg' '/$app/logo_full.svg';
|
||||
sub_filter '/logo_stacked.svg' '/$app/logo_stacked.svg';
|
||||
sub_filter '/site.webmanifest' '/$app/site.webmanifest';
|
||||
}
|
||||
```
|
||||
|
||||
@@ -16,7 +16,7 @@ After running Overseerr for the first time, configure it by visiting the web UI
|
||||
```bash
|
||||
docker run -d \
|
||||
--name overseerr \
|
||||
-e LOG_LEVEL=info \
|
||||
-e LOG_LEVEL=debug \
|
||||
-e TZ=Asia/Tokyo \
|
||||
-p 5055:5055 \
|
||||
-v /path/to/appdata/config:/app/config \
|
||||
@@ -39,7 +39,7 @@ services:
|
||||
image: sctx/overseerr:latest
|
||||
container_name: overseerr
|
||||
environment:
|
||||
- LOG_LEVEL=info
|
||||
- LOG_LEVEL=debug
|
||||
- TZ=Asia/Tokyo
|
||||
ports:
|
||||
- 5055:5055
|
||||
@@ -56,7 +56,7 @@ services:
|
||||
docker run -d \
|
||||
--name overseerr \
|
||||
--user=[ user | user:group | uid | uid:gid | user:gid | uid:group ] \
|
||||
-e LOG_LEVEL=info \
|
||||
-e LOG_LEVEL=debug \
|
||||
-e TZ=Asia/Tokyo \
|
||||
-p 5055:5055 \
|
||||
-v /path/to/appdata/config:/app/config \
|
||||
@@ -99,20 +99,41 @@ Use a 3rd party updating mechanism such as [Watchtower](https://github.com/conta
|
||||
|
||||
## Windows
|
||||
|
||||
Please refer to the [Docker Desktop for Windows user manual](https://docs.docker.com/docker-for-windows/) for details on how to install Docker on Windows.
|
||||
Please refer to the [Docker Desktop for Windows user manual](https://docs.docker.com/docker-for-windows/) for details on how to install Docker on Windows. There is no need to install a Linux distro if using named volumes like in the example below.
|
||||
|
||||
{% hint style="danger" %}
|
||||
**WSL2 will need to be installed to prevent DB corruption!** Please see the [Docker Desktop WSL 2 backend documentation](https://docs.docker.com/docker-for-windows/wsl/) for instructions on how to enable WSL2. The command below will only work with WSL2 installed!
|
||||
**WSL2 will need to be installed to prevent DB corruption!** Please see the [Docker Desktop WSL 2 backend documentation](https://docs.docker.com/docker-for-windows/wsl/) for instructions on how to enable WSL2. The commands below will only work with WSL2 installed!
|
||||
{% endhint %}
|
||||
|
||||
First, create a volume to store the configuration data for Overseerr using using either the Docker CLI:
|
||||
|
||||
```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
|
||||
docker volume create overseerr-data
|
||||
```
|
||||
|
||||
or the Docker Desktop app:
|
||||
|
||||
1. Open the Docker Desktop app
|
||||
2. Head to the Volumes tab
|
||||
3. Click on the "New Volume" button near the top right
|
||||
4. Enter a name for the volume (example: `overseerr-data`) and hit "Create"
|
||||
|
||||
Then, create and start the Overseerr container:
|
||||
|
||||
```bash
|
||||
docker run -d -e LOG_LEVEL=debug -e TZ=Asia/Tokyo -p 5055:5055 -v "overseerr-data:/app/config" --restart unless-stopped sctx/overseerr
|
||||
```
|
||||
|
||||
If using a named volume like above, you can safely ignore the warning about the `/app/config` folder being incorrectly mounted on the setup page.
|
||||
|
||||
To access the files inside the volume created above, navigate to `\\wsl$\docker-desktop-data\version-pack-data\community\docker\volumes\overseerr-data\_data` using File Explorer.
|
||||
|
||||
{% hint style="info" %}
|
||||
Docker on Windows works differently than it does on Linux; it runs Docker inside of a stripped-down Linux VM. Volume mounts are exposed to Docker inside 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 Docker on Windows, you should put the `/app/config` directory mount inside the VM and not on the Windows host.** (This also applies to other containers with SQLite databases.)
|
||||
|
||||
Named volumes, like in the example commands above, are automatically mounted inside the VM.
|
||||
{% endhint %}
|
||||
|
||||
## Linux
|
||||
|
||||
@@ -35,6 +35,6 @@ Try to answer the following questions:
|
||||
|
||||
## How can I share my logs?
|
||||
|
||||
1. Locate the current log file at `<your Overseeerr config directory>/logs/overseerr.log`.
|
||||
1. Locate the current log file at `<your Overseerr config directory>/logs/overseerr.log`.
|
||||
2. Open the log file and **copy its contents** into a [**secret gist** on GitHub](https://gist.github.com/). If you upload your logs elsewhere, we may ask you to share them again via GitHub Gist.
|
||||
3. **Share the link/URL to your secret gist** in the [`#support` channel in our Discord server](https://discord.gg/overseerr).
|
||||
|
||||
@@ -24,11 +24,11 @@ Set this to the hostname or IP address of your SMTP host/server.
|
||||
|
||||
Set this to a supported port number for your SMTP host. `465` and `587` are commonly used.
|
||||
|
||||
### Enable SSL (optional)
|
||||
### Encryption Method
|
||||
|
||||
This setting should only be enabled for ports that use [implicit SSL/TLS](https://tools.ietf.org/html/rfc8314) (e.g., port `465` in most cases).
|
||||
In most cases, [Use Implicit TLS](https://tools.ietf.org/html/rfc8314) should be selected for port 465, and [Use STARTTLS if available](https://en.wikipedia.org/wiki/Opportunistic_TLS) for port 587. Please refer to your email provider's documentations for details on how to configure this setting.
|
||||
|
||||
For servers that support [opportunistic TLS/STARTTLS](https://en.wikipedia.org/wiki/Opportunistic_TLS) (typically via port `587`), this setting should **not** be enabled.
|
||||
The default value for this setting is **Use STARTTLS if available**.
|
||||
|
||||
### SMTP Username & Password
|
||||
|
||||
|
||||
1
next-env.d.ts
vendored
@@ -1,2 +1,3 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/types/global" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
@@ -5,9 +5,6 @@ module.exports = {
|
||||
images: {
|
||||
domains: ['image.tmdb.org'],
|
||||
},
|
||||
future: {
|
||||
webpack5: true,
|
||||
},
|
||||
webpack(config) {
|
||||
config.module.rules.push({
|
||||
test: /\.svg$/,
|
||||
|
||||
@@ -52,9 +52,15 @@ components:
|
||||
email:
|
||||
type: string
|
||||
example: 'hey@itsme.com'
|
||||
readOnly: true
|
||||
username:
|
||||
type: string
|
||||
plexToken:
|
||||
type: string
|
||||
readOnly: true
|
||||
plexUsername:
|
||||
type: string
|
||||
readOnly: true
|
||||
userType:
|
||||
type: integer
|
||||
example: 1
|
||||
@@ -77,13 +83,6 @@ components:
|
||||
type: number
|
||||
example: 5
|
||||
readOnly: true
|
||||
requests:
|
||||
type: array
|
||||
readOnly: true
|
||||
items:
|
||||
$ref: '#/components/schemas/MediaRequest'
|
||||
settings:
|
||||
$ref: '#/components/schemas/UserSettings'
|
||||
required:
|
||||
- id
|
||||
- email
|
||||
@@ -92,11 +91,11 @@ components:
|
||||
UserSettings:
|
||||
type: object
|
||||
properties:
|
||||
discordId:
|
||||
locale:
|
||||
type: string
|
||||
region:
|
||||
type: string
|
||||
language:
|
||||
originalLanguage:
|
||||
type: string
|
||||
MainSettings:
|
||||
type: object
|
||||
@@ -398,7 +397,6 @@ components:
|
||||
activeLanguageProfileId:
|
||||
type: number
|
||||
example: 1
|
||||
nullable: true
|
||||
activeAnimeProfileId:
|
||||
type: number
|
||||
nullable: true
|
||||
@@ -408,6 +406,7 @@ components:
|
||||
activeAnimeProfileName:
|
||||
type: string
|
||||
example: 720p/1080p
|
||||
nullable: true
|
||||
activeAnimeDirectory:
|
||||
type: string
|
||||
nullable: true
|
||||
@@ -769,6 +768,10 @@ components:
|
||||
$ref: '#/components/schemas/ExternalIds'
|
||||
mediaInfo:
|
||||
$ref: '#/components/schemas/MediaInfo'
|
||||
watchProviders:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/WatchProviders'
|
||||
Episode:
|
||||
type: object
|
||||
properties:
|
||||
@@ -943,6 +946,10 @@ components:
|
||||
$ref: '#/components/schemas/Keyword'
|
||||
mediaInfo:
|
||||
$ref: '#/components/schemas/MediaInfo'
|
||||
watchProviders:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/WatchProviders'
|
||||
MediaRequest:
|
||||
type: object
|
||||
properties:
|
||||
@@ -953,7 +960,7 @@ components:
|
||||
status:
|
||||
type: number
|
||||
example: 0
|
||||
description: Status of the request. 1 = PENDING APPROVAL, 2 = APPROVED, 3 = DECLINED, 4 = AVAILABLE
|
||||
description: Status of the request. 1 = PENDING APPROVAL, 2 = APPROVED, 3 = DECLINED
|
||||
readOnly: true
|
||||
media:
|
||||
$ref: '#/components/schemas/MediaInfo'
|
||||
@@ -1587,9 +1594,8 @@ components:
|
||||
UserSettingsNotifications:
|
||||
type: object
|
||||
properties:
|
||||
notificationAgents:
|
||||
type: number
|
||||
example: 0
|
||||
notificationTypes:
|
||||
$ref: '#/components/schemas/NotificationAgentTypes'
|
||||
emailEnabled:
|
||||
type: boolean
|
||||
pgpKey:
|
||||
@@ -1614,6 +1620,52 @@ components:
|
||||
telegramSendSilently:
|
||||
type: boolean
|
||||
nullable: true
|
||||
NotificationAgentTypes:
|
||||
type: object
|
||||
properties:
|
||||
discord:
|
||||
type: number
|
||||
email:
|
||||
type: number
|
||||
pushbullet:
|
||||
type: number
|
||||
pushover:
|
||||
type: number
|
||||
slack:
|
||||
type: number
|
||||
telegram:
|
||||
type: number
|
||||
webhook:
|
||||
type: number
|
||||
webpush:
|
||||
type: number
|
||||
WatchProviders:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
iso_3166_1:
|
||||
type: string
|
||||
link:
|
||||
type: string
|
||||
buy:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/WatchProviderDetails'
|
||||
flatrate:
|
||||
items:
|
||||
$ref: '#/components/schemas/WatchProviderDetails'
|
||||
WatchProviderDetails:
|
||||
type: object
|
||||
properties:
|
||||
displayPriority:
|
||||
type: number
|
||||
logoPath:
|
||||
type: string
|
||||
id:
|
||||
type: number
|
||||
name:
|
||||
type: string
|
||||
securitySchemes:
|
||||
cookieAuth:
|
||||
type: apiKey
|
||||
@@ -1842,8 +1894,8 @@ paths:
|
||||
$ref: '#/components/schemas/PlexLibrary'
|
||||
/settings/plex/devices/servers:
|
||||
get:
|
||||
summary: Gets the user's available plex servers
|
||||
description: Returns a list of available plex servers and their connectivity state
|
||||
summary: Gets the user's available Plex servers
|
||||
description: Returns a list of available Plex servers and their connectivity state
|
||||
tags:
|
||||
- settings
|
||||
responses:
|
||||
@@ -2980,7 +3032,15 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/User'
|
||||
type: object
|
||||
properties:
|
||||
email:
|
||||
type: string
|
||||
example: 'hey@itsme.com'
|
||||
username:
|
||||
type: string
|
||||
permissions:
|
||||
type: number
|
||||
responses:
|
||||
'201':
|
||||
description: The created user
|
||||
@@ -2991,7 +3051,7 @@ paths:
|
||||
put:
|
||||
summary: Update batch of users
|
||||
description: |
|
||||
Update users with given IDs with provided values in request `body.settings`. You cannot update users' plex tokens through this request.
|
||||
Update users with given IDs with provided values in request `body.settings`. You cannot update users' Plex tokens through this request.
|
||||
|
||||
Requires the `MANAGE_USERS` permission.
|
||||
tags:
|
||||
@@ -3018,7 +3078,6 @@ paths:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/User'
|
||||
|
||||
/user/import-from-plex:
|
||||
post:
|
||||
summary: Import all users from Plex
|
||||
@@ -3067,7 +3126,7 @@ paths:
|
||||
get:
|
||||
summary: Get user by ID
|
||||
description: |
|
||||
Retrieves user details in a JSON object.. Requires the `MANAGE_USERS` permission.
|
||||
Retrieves user details in a JSON object. Requires the `MANAGE_USERS` permission.
|
||||
tags:
|
||||
- users
|
||||
parameters:
|
||||
@@ -4180,6 +4239,9 @@ paths:
|
||||
type: string
|
||||
languageProfileId:
|
||||
type: number
|
||||
userId:
|
||||
type: number
|
||||
nullable: true
|
||||
required:
|
||||
- mediaType
|
||||
- mediaId
|
||||
|
||||
47
package.json
@@ -17,11 +17,11 @@
|
||||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^1.2.0",
|
||||
"@headlessui/react": "^1.3.0",
|
||||
"@heroicons/react": "^1.0.1",
|
||||
"@supercharge/request-ip": "^1.1.2",
|
||||
"@svgr/webpack": "^5.5.0",
|
||||
"@tanem/react-nprogress": "^3.0.67",
|
||||
"@tanem/react-nprogress": "^3.0.70",
|
||||
"ace-builds": "^1.4.12",
|
||||
"axios": "^0.21.1",
|
||||
"bcrypt": "^5.0.1",
|
||||
@@ -33,18 +33,18 @@
|
||||
"csurf": "^1.11.0",
|
||||
"email-templates": "^8.0.7",
|
||||
"express": "^4.17.1",
|
||||
"express-openapi-validator": "^4.12.11",
|
||||
"express-openapi-validator": "^4.12.14",
|
||||
"express-rate-limit": "^5.2.6",
|
||||
"express-session": "^1.17.2",
|
||||
"formik": "^2.2.9",
|
||||
"gravatar-url": "3.1.0",
|
||||
"intl": "^1.2.5",
|
||||
"lodash": "^4.17.21",
|
||||
"next": "10.1.3",
|
||||
"next": "11.0.1",
|
||||
"node-cache": "^5.1.2",
|
||||
"node-schedule": "^2.0.0",
|
||||
"nodemailer": "^6.6.1",
|
||||
"openpgp": "^5.0.0-2",
|
||||
"nodemailer": "^6.6.2",
|
||||
"openpgp": "^5.0.0-3",
|
||||
"plex-api": "^5.3.1",
|
||||
"pug": "^3.0.2",
|
||||
"react": "17.0.2",
|
||||
@@ -52,7 +52,7 @@
|
||||
"react-animate-height": "^2.0.23",
|
||||
"react-dom": "17.0.2",
|
||||
"react-intersection-observer": "^8.32.0",
|
||||
"react-intl": "5.19.0",
|
||||
"react-intl": "5.20.3",
|
||||
"react-markdown": "^6.0.2",
|
||||
"react-select": "^4.3.1",
|
||||
"react-spring": "^9.2.3",
|
||||
@@ -61,12 +61,11 @@
|
||||
"react-truncate-markup": "^5.1.0",
|
||||
"react-use-clipboard": "1.0.7",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"secure-random-password": "^0.2.2",
|
||||
"secure-random-password": "^0.2.3",
|
||||
"sqlite3": "^5.0.2",
|
||||
"swagger-ui-express": "^4.1.6",
|
||||
"swr": "^0.5.6",
|
||||
"typeorm": "0.2.32",
|
||||
"uuid": "^8.3.2",
|
||||
"web-push": "^3.4.4",
|
||||
"winston": "^3.3.3",
|
||||
"winston-daily-rotate-file": "^4.5.5",
|
||||
@@ -75,7 +74,7 @@
|
||||
"yup": "^0.32.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.14.3",
|
||||
"@babel/cli": "^7.14.5",
|
||||
"@commitlint/cli": "^12.1.4",
|
||||
"@commitlint/config-conventional": "^12.1.4",
|
||||
"@semantic-release/changelog": "^5.0.1",
|
||||
@@ -91,35 +90,35 @@
|
||||
"@types/csurf": "^1.11.1",
|
||||
"@types/email-templates": "^8.0.3",
|
||||
"@types/express": "^4.17.12",
|
||||
"@types/express-rate-limit": "^5.1.1",
|
||||
"@types/express-rate-limit": "^5.1.2",
|
||||
"@types/express-session": "^1.17.3",
|
||||
"@types/lodash": "^4.14.170",
|
||||
"@types/node": "^15.6.1",
|
||||
"@types/node-schedule": "^1.3.1",
|
||||
"@types/nodemailer": "^6.4.2",
|
||||
"@types/react": "^17.0.9",
|
||||
"@types/react-dom": "^17.0.6",
|
||||
"@types/react": "^17.0.11",
|
||||
"@types/react-dom": "^17.0.8",
|
||||
"@types/react-select": "^4.0.15",
|
||||
"@types/react-toast-notifications": "^2.4.1",
|
||||
"@types/react-transition-group": "^4.4.1",
|
||||
"@types/secure-random-password": "^0.2.0",
|
||||
"@types/swagger-ui-express": "^4.1.2",
|
||||
"@types/uuid": "^8.3.0",
|
||||
"@types/web-push": "^3.3.0",
|
||||
"@types/web-push": "^3.3.1",
|
||||
"@types/xml2js": "^0.4.8",
|
||||
"@types/yamljs": "^0.2.31",
|
||||
"@types/yup": "^0.29.11",
|
||||
"@typescript-eslint/eslint-plugin": "^4.26.0",
|
||||
"@typescript-eslint/parser": "^4.26.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.28.0",
|
||||
"@typescript-eslint/parser": "^4.28.0",
|
||||
"autoprefixer": "^10.2.6",
|
||||
"babel-plugin-react-intl": "^8.2.25",
|
||||
"babel-plugin-react-intl-auto": "^3.3.0",
|
||||
"commitizen": "^4.2.4",
|
||||
"copyfiles": "^2.4.1",
|
||||
"cz-conventional-changelog": "^3.3.0",
|
||||
"eslint": "^7.27.0",
|
||||
"eslint": "^7.29.0",
|
||||
"eslint-config-next": "^11.0.1",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-formatjs": "^2.15.5",
|
||||
"eslint-plugin-formatjs": "^2.16.1",
|
||||
"eslint-plugin-jsx-a11y": "^6.4.1",
|
||||
"eslint-plugin-prettier": "^3.4.0",
|
||||
"eslint-plugin-react": "^7.24.0",
|
||||
@@ -128,13 +127,13 @@
|
||||
"husky": "4.3.8",
|
||||
"lint-staged": "^11.0.0",
|
||||
"nodemon": "^2.0.7",
|
||||
"postcss": "^8.3.0",
|
||||
"postcss": "^8.3.5",
|
||||
"prettier": "^2.3.1",
|
||||
"semantic-release": "^17.4.3",
|
||||
"semantic-release": "^17.4.4",
|
||||
"semantic-release-docker-buildx": "^1.0.1",
|
||||
"tailwindcss": "^2.1.4",
|
||||
"ts-node": "^9.1.1",
|
||||
"typescript": "^4.3.2"
|
||||
"tailwindcss": "^2.2.2",
|
||||
"ts-node": "^10.0.0",
|
||||
"typescript": "^4.3.4"
|
||||
},
|
||||
"resolutions": {
|
||||
"sqlite3/node-gyp": "^5.1.0"
|
||||
|
||||
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 8.0 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 762 B After Width: | Height: | Size: 774 B |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 4.0 KiB |
BIN
public/logo.png
|
Before Width: | Height: | Size: 64 KiB |
BIN
public/logo_full.png
Normal file
|
After Width: | Height: | Size: 9.4 KiB |
1
public/logo_full.svg
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
1
public/logo_stacked.svg
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
@@ -31,7 +31,7 @@
|
||||
<body>
|
||||
<h1>You are offline</h1>
|
||||
|
||||
<button type="button">⤾ Reload</button>
|
||||
<button type="button">↻ Reload</button>
|
||||
|
||||
<!-- Inline the page's JavaScript file. -->
|
||||
<script>
|
||||
|
||||
1
public/os_icon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" fill="none" viewBox="0 0 96 96"><path fill="url(#paint0_linear)" fill-rule="evenodd" d="M48 96C74.5097 96 96 74.5097 96 48C96 21.4903 74.5097 0 48 0C21.4903 0 0 21.4903 0 48C0 74.5097 21.4903 96 48 96ZM80.0001 52C80.0001 67.464 67.4641 80 52.0001 80C36.5361 80 24.0001 67.464 24.0001 52C24.0001 49.1303 24.4318 46.3615 25.2338 43.7548C27.4288 48.6165 32.3194 52 38.0001 52C45.7321 52 52.0001 45.732 52.0001 38C52.0001 32.3192 48.6166 27.4287 43.755 25.2337C46.3616 24.4317 49.1304 24 52.0001 24C67.4641 24 80.0001 36.536 80.0001 52Z" clip-rule="evenodd"/><path fill="#131928" fill-rule="evenodd" d="M80.0002 52C80.0002 67.464 67.4642 80 52.0002 80C36.864 80 24.5329 67.9897 24.017 52.9791C24.0057 53.318 24 53.6583 24 54C24 70.5685 37.4315 84 54 84C70.5685 84 84 70.5685 84 54C84 37.4315 70.5685 24 54 24C53.6597 24 53.3207 24.0057 52.9831 24.0169C67.9919 24.5347 80.0002 36.865 80.0002 52Z" clip-rule="evenodd" opacity=".2"/><path fill="url(#paint1_linear)" fill-rule="evenodd" d="M48 12C28.1177 12 12 28.1177 12 48C12 50.2091 10.2091 52 8 52C5.79086 52 4 50.2091 4 48C4 23.6995 23.6995 4 48 4C50.2091 4 52 5.79086 52 8C52 10.2091 50.2091 12 48 12Z" clip-rule="evenodd"/><defs><linearGradient id="paint0_linear" x1="48" x2="117.5" y1="0" y2="69.5" gradientUnits="userSpaceOnUse"><stop stop-color="#C395FC"/><stop offset="1" stop-color="#4F65F5"/></linearGradient><linearGradient id="paint1_linear" x1="28" x2="28" y1="8" y2="48" gradientUnits="userSpaceOnUse"><stop stop-color="#fff" stop-opacity=".4"/><stop offset="1" stop-color="#fff" stop-opacity="0"/></linearGradient></defs></svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
BIN
public/os_logo_filled.png
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 455 KiB After Width: | Height: | Size: 504 KiB |
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.4 KiB |
@@ -142,7 +142,7 @@ class PlexAPI {
|
||||
`/library/sections/${id}/all`
|
||||
);
|
||||
|
||||
return response.MediaContainer.Metadata;
|
||||
return response.MediaContainer.Metadata ?? [];
|
||||
}
|
||||
|
||||
public async getMetadata(
|
||||
|
||||
@@ -1,39 +1,28 @@
|
||||
import cacheManager from '../lib/cache';
|
||||
import ExternalAPI from './externalapi';
|
||||
|
||||
interface RTMovieOldSearchResult {
|
||||
id: number;
|
||||
title: string;
|
||||
year: number;
|
||||
ratings: {
|
||||
critics_rating: 'Certified Fresh' | 'Fresh' | 'Rotten';
|
||||
critics_score: number;
|
||||
audience_rating: 'Upright' | 'Spilled';
|
||||
audience_score: number;
|
||||
};
|
||||
links: {
|
||||
self: string;
|
||||
alternate: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface RTTvSearchResult {
|
||||
title: string;
|
||||
meterClass: 'fresh' | 'rotten';
|
||||
interface RTSearchResult {
|
||||
meterClass: 'certified_fresh' | 'fresh' | 'rotten';
|
||||
meterScore: number;
|
||||
url: string;
|
||||
}
|
||||
|
||||
interface RTTvSearchResult extends RTSearchResult {
|
||||
title: string;
|
||||
startYear: number;
|
||||
endYear: number;
|
||||
}
|
||||
|
||||
interface RTMovieSearchResponse {
|
||||
total: number;
|
||||
movies: RTMovieOldSearchResult[];
|
||||
interface RTMovieSearchResult extends RTSearchResult {
|
||||
name: string;
|
||||
url: string;
|
||||
year: number;
|
||||
}
|
||||
|
||||
interface RTMultiSearchResponse {
|
||||
tvCount: number;
|
||||
tvSeries: RTTvSearchResult[];
|
||||
movieCount: number;
|
||||
movies: RTMovieSearchResult[];
|
||||
}
|
||||
|
||||
export interface RTRating {
|
||||
@@ -88,19 +77,19 @@ class RottenTomatoes extends ExternalAPI {
|
||||
year: number
|
||||
): Promise<RTRating | null> {
|
||||
try {
|
||||
const data = await this.get<RTMovieSearchResponse>('/v1.0/movies', {
|
||||
params: { q: name },
|
||||
const data = await this.get<RTMultiSearchResponse>('/v2.0/search/', {
|
||||
params: { q: name, limit: 10 },
|
||||
});
|
||||
|
||||
// First, attempt to match exact name and year
|
||||
let movie = data.movies.find(
|
||||
(movie) => movie.year === year && movie.title === name
|
||||
(movie) => movie.year === year && movie.name === name
|
||||
);
|
||||
|
||||
// If we don't find a movie, try to match partial name and year
|
||||
if (!movie) {
|
||||
movie = data.movies.find(
|
||||
(movie) => movie.year === year && movie.title.includes(name)
|
||||
(movie) => movie.year === year && movie.name.includes(name)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -111,7 +100,7 @@ class RottenTomatoes extends ExternalAPI {
|
||||
|
||||
// One last try, try exact name match only
|
||||
if (!movie) {
|
||||
movie = data.movies.find((movie) => movie.title === name);
|
||||
movie = data.movies.find((movie) => movie.name === name);
|
||||
}
|
||||
|
||||
if (!movie) {
|
||||
@@ -119,12 +108,15 @@ class RottenTomatoes extends ExternalAPI {
|
||||
}
|
||||
|
||||
return {
|
||||
title: movie.title,
|
||||
url: movie.links.alternate,
|
||||
criticsRating: movie.ratings.critics_rating,
|
||||
criticsScore: movie.ratings.critics_score,
|
||||
audienceRating: movie.ratings.audience_rating,
|
||||
audienceScore: movie.ratings.audience_score,
|
||||
title: movie.name,
|
||||
url: movie.url,
|
||||
criticsRating:
|
||||
movie.meterClass === 'certified_fresh'
|
||||
? 'Certified Fresh'
|
||||
: movie.meterClass === 'fresh'
|
||||
? 'Fresh'
|
||||
: 'Rotten',
|
||||
criticsScore: movie.meterScore,
|
||||
year: movie.year,
|
||||
};
|
||||
} catch (e) {
|
||||
|
||||
@@ -170,7 +170,8 @@ class TheMovieDb extends ExternalAPI {
|
||||
{
|
||||
params: {
|
||||
language,
|
||||
append_to_response: 'credits,external_ids,videos,release_dates',
|
||||
append_to_response:
|
||||
'credits,external_ids,videos,release_dates,watch/providers',
|
||||
},
|
||||
},
|
||||
43200
|
||||
@@ -196,7 +197,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
params: {
|
||||
language,
|
||||
append_to_response:
|
||||
'aggregate_credits,credits,external_ids,keywords,videos,content_ratings',
|
||||
'aggregate_credits,credits,external_ids,keywords,videos,content_ratings,watch/providers',
|
||||
},
|
||||
},
|
||||
43200
|
||||
|
||||
@@ -166,6 +166,10 @@ export interface TmdbMovieDetails {
|
||||
};
|
||||
external_ids: TmdbExternalIds;
|
||||
videos: TmdbVideoResult;
|
||||
'watch/providers'?: {
|
||||
id: number;
|
||||
results?: { [iso_3166_1: string]: TmdbWatchProviders };
|
||||
};
|
||||
}
|
||||
|
||||
export interface TmdbVideo {
|
||||
@@ -269,6 +273,10 @@ export interface TmdbTvDetails {
|
||||
results: TmdbKeyword[];
|
||||
};
|
||||
videos: TmdbVideoResult;
|
||||
'watch/providers'?: {
|
||||
id: number;
|
||||
results?: { [iso_3166_1: string]: TmdbWatchProviders };
|
||||
};
|
||||
}
|
||||
|
||||
export interface TmdbVideoResult {
|
||||
@@ -401,3 +409,16 @@ export interface TmdbNetwork {
|
||||
logo_path?: string;
|
||||
origin_country?: string;
|
||||
}
|
||||
|
||||
export interface TmdbWatchProviders {
|
||||
link?: string;
|
||||
buy?: TmdbWatchProviderDetails[];
|
||||
flatrate?: TmdbWatchProviderDetails[];
|
||||
}
|
||||
|
||||
export interface TmdbWatchProviderDetails {
|
||||
display_priority?: number;
|
||||
logo_path?: string;
|
||||
provider_id: number;
|
||||
provider_name: string;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isEqual } from 'lodash';
|
||||
import { isEqual, truncate } from 'lodash';
|
||||
import {
|
||||
AfterInsert,
|
||||
AfterRemove,
|
||||
@@ -145,7 +145,11 @@ export class MediaRequest {
|
||||
subject: `${movie.title}${
|
||||
movie.release_date ? ` (${movie.release_date.slice(0, 4)})` : ''
|
||||
}`,
|
||||
message: movie.overview,
|
||||
message: truncate(movie.overview, {
|
||||
length: 500,
|
||||
separator: /\s/,
|
||||
omission: '…',
|
||||
}),
|
||||
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`,
|
||||
media,
|
||||
request: this,
|
||||
@@ -158,7 +162,11 @@ export class MediaRequest {
|
||||
subject: `${tv.name}${
|
||||
tv.first_air_date ? ` (${tv.first_air_date.slice(0, 4)})` : ''
|
||||
}`,
|
||||
message: tv.overview,
|
||||
message: truncate(tv.overview, {
|
||||
length: 500,
|
||||
separator: /\s/,
|
||||
omission: '…',
|
||||
}),
|
||||
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${tv.poster_path}`,
|
||||
media,
|
||||
extra: [
|
||||
@@ -217,7 +225,11 @@ export class MediaRequest {
|
||||
subject: `${movie.title}${
|
||||
movie.release_date ? ` (${movie.release_date.slice(0, 4)})` : ''
|
||||
}`,
|
||||
message: movie.overview,
|
||||
message: truncate(movie.overview, {
|
||||
length: 500,
|
||||
separator: /\s/,
|
||||
omission: '…',
|
||||
}),
|
||||
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`,
|
||||
notifyUser: autoApproved ? undefined : this.requestedBy,
|
||||
media,
|
||||
@@ -236,7 +248,11 @@ export class MediaRequest {
|
||||
subject: `${tv.name}${
|
||||
tv.first_air_date ? ` (${tv.first_air_date.slice(0, 4)})` : ''
|
||||
}`,
|
||||
message: tv.overview,
|
||||
message: truncate(tv.overview, {
|
||||
length: 500,
|
||||
separator: /\s/,
|
||||
omission: '…',
|
||||
}),
|
||||
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${tv.poster_path}`,
|
||||
notifyUser: autoApproved ? undefined : this.requestedBy,
|
||||
media,
|
||||
@@ -495,7 +511,11 @@ export class MediaRequest {
|
||||
subject: `${movie.title}${
|
||||
movie.release_date ? ` (${movie.release_date.slice(0, 4)})` : ''
|
||||
}`,
|
||||
message: movie.overview,
|
||||
message: truncate(movie.overview, {
|
||||
length: 500,
|
||||
separator: /\s/,
|
||||
omission: '…',
|
||||
}),
|
||||
media,
|
||||
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`,
|
||||
request: this,
|
||||
@@ -707,7 +727,11 @@ export class MediaRequest {
|
||||
? ` (${series.first_air_date.slice(0, 4)})`
|
||||
: ''
|
||||
}`,
|
||||
message: series.overview,
|
||||
message: truncate(series.overview, {
|
||||
length: 500,
|
||||
separator: /\s/,
|
||||
omission: '…',
|
||||
}),
|
||||
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${series.poster_path}`,
|
||||
media,
|
||||
extra: [
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import bcrypt from 'bcrypt';
|
||||
import { randomUUID } from 'crypto';
|
||||
import path from 'path';
|
||||
import { default as generatePassword } from 'secure-random-password';
|
||||
import {
|
||||
@@ -15,7 +16,6 @@ import {
|
||||
RelationCount,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { MediaRequestStatus, MediaType } from '../constants/media';
|
||||
import { UserType } from '../constants/user';
|
||||
import { QuotaResponse } from '../interfaces/api/userInterfaces';
|
||||
@@ -189,7 +189,7 @@ export class User {
|
||||
}
|
||||
|
||||
public async resetPassword(): Promise<void> {
|
||||
const guid = uuid();
|
||||
const guid = randomUUID();
|
||||
this.resetPasswordGuid = guid;
|
||||
|
||||
// 24 hours into the future
|
||||
@@ -212,7 +212,7 @@ export class User {
|
||||
},
|
||||
locals: {
|
||||
resetPasswordLink,
|
||||
applicationUrl: resetPasswordLink,
|
||||
applicationUrl,
|
||||
applicationTitle,
|
||||
},
|
||||
});
|
||||
@@ -307,7 +307,7 @@ export class User {
|
||||
limit: movieQuotaLimit,
|
||||
used: movieQuotaUsed,
|
||||
remaining: movieQuotaLimit
|
||||
? movieQuotaLimit - movieQuotaUsed
|
||||
? Math.max(0, movieQuotaLimit - movieQuotaUsed)
|
||||
: undefined,
|
||||
restricted:
|
||||
movieQuotaLimit && movieQuotaLimit - movieQuotaUsed <= 0
|
||||
@@ -318,7 +318,9 @@ export class User {
|
||||
days: tvQuotaDays,
|
||||
limit: tvQuotaLimit,
|
||||
used: tvQuotaUsed,
|
||||
remaining: tvQuotaLimit ? tvQuotaLimit - tvQuotaUsed : undefined,
|
||||
remaining: tvQuotaLimit
|
||||
? Math.max(0, tvQuotaLimit - tvQuotaUsed)
|
||||
: undefined,
|
||||
restricted:
|
||||
tvQuotaLimit && tvQuotaLimit - tvQuotaUsed <= 0 ? true : false,
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import crypto from 'crypto';
|
||||
import { randomBytes } from 'crypto';
|
||||
import * as openpgp from 'openpgp';
|
||||
import { Transform, TransformCallback } from 'stream';
|
||||
|
||||
@@ -41,7 +41,7 @@ class PGPEncryptor extends Transform {
|
||||
const validPublicKeys = await Promise.all(
|
||||
this._encryptionKeys.map((armoredKey) => openpgp.readKey({ armoredKey }))
|
||||
);
|
||||
let privateKey: openpgp.Key | undefined;
|
||||
let privateKey: openpgp.PrivateKey | undefined;
|
||||
|
||||
// Just return the message if there is no one to encrypt for
|
||||
if (!validPublicKeys.length) {
|
||||
@@ -51,7 +51,7 @@ class PGPEncryptor extends Transform {
|
||||
|
||||
// Only sign the message if private key and password exist
|
||||
if (this._signingKey && this._password) {
|
||||
privateKey = await openpgp.readKey({
|
||||
privateKey = await openpgp.readPrivateKey({
|
||||
armoredKey: this._signingKey,
|
||||
});
|
||||
|
||||
@@ -107,7 +107,7 @@ class PGPEncryptor extends Transform {
|
||||
}
|
||||
|
||||
// Generate a new boundary for the email content
|
||||
const boundary = 'nm_' + crypto.randomBytes(14).toString('hex');
|
||||
const boundary = 'nm_' + randomBytes(14).toString('hex');
|
||||
/**
|
||||
* Concatenate everything into single strings
|
||||
* and add pgp headers to the email headers
|
||||
@@ -135,8 +135,8 @@ class PGPEncryptor extends Transform {
|
||||
emailPartDelimiter +
|
||||
messageParts.join(emailPartDelimiter),
|
||||
}),
|
||||
publicKeys: validPublicKeys,
|
||||
privateKeys: privateKey,
|
||||
encryptionKeys: validPublicKeys,
|
||||
signingKeys: privateKey,
|
||||
});
|
||||
|
||||
const body =
|
||||
|
||||
@@ -190,6 +190,7 @@ class WebPushAgent
|
||||
|
||||
const allSubs = await userPushSubRepository
|
||||
.createQueryBuilder('pushSub')
|
||||
.leftJoinAndSelect('pushSub.user', 'user')
|
||||
.where('pushSub.userId IN (:users)', {
|
||||
users: manageUsers.map((user) => user.id),
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { randomUUID } from 'crypto';
|
||||
import { getRepository } from 'typeorm';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import TheMovieDb from '../../api/themoviedb';
|
||||
import { MediaStatus, MediaType } from '../../constants/media';
|
||||
import Media from '../../entity/Media';
|
||||
@@ -512,7 +512,7 @@ class BaseScanner<T> {
|
||||
*/
|
||||
protected startRun(): string {
|
||||
const settings = getSettings();
|
||||
const sessionId = uuid();
|
||||
const sessionId = randomUUID();
|
||||
this.sessionId = sessionId;
|
||||
|
||||
this.log('Scan starting', 'info', { sessionId });
|
||||
|
||||
@@ -7,9 +7,9 @@ import { User } from '../../../entity/User';
|
||||
import { getSettings, Library } from '../../settings';
|
||||
import BaseScanner, {
|
||||
MediaIds,
|
||||
ProcessableSeason,
|
||||
RunnableScanner,
|
||||
StatusBase,
|
||||
ProcessableSeason,
|
||||
} from '../baseScanner';
|
||||
|
||||
const imdbRegex = new RegExp(/imdb:\/\/(tt[0-9]+)/);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { randomUUID } from 'crypto';
|
||||
import fs from 'fs';
|
||||
import { merge } from 'lodash';
|
||||
import path from 'path';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import webpush from 'web-push';
|
||||
import { Permission } from './permissions';
|
||||
|
||||
@@ -234,7 +234,7 @@ class Settings {
|
||||
|
||||
constructor(initialSettings?: AllSettings) {
|
||||
this.data = {
|
||||
clientId: uuidv4(),
|
||||
clientId: randomUUID(),
|
||||
vapidPrivate: '',
|
||||
vapidPublic: '',
|
||||
main: {
|
||||
@@ -428,7 +428,7 @@ class Settings {
|
||||
|
||||
get clientId(): string {
|
||||
if (!this.data.clientId) {
|
||||
this.data.clientId = uuidv4();
|
||||
this.data.clientId = randomUUID();
|
||||
this.save();
|
||||
}
|
||||
|
||||
@@ -454,7 +454,7 @@ class Settings {
|
||||
}
|
||||
|
||||
private generateApiKey(): string {
|
||||
return Buffer.from(`${Date.now()}${uuidv4()})`).toString('base64');
|
||||
return Buffer.from(`${Date.now()}${randomUUID()})`).toString('base64');
|
||||
}
|
||||
|
||||
private generateVapidKeys(force = false): void {
|
||||
|
||||
@@ -3,18 +3,20 @@ import type {
|
||||
TmdbMovieReleaseResult,
|
||||
TmdbProductionCompany,
|
||||
} from '../api/themoviedb/interfaces';
|
||||
import Media from '../entity/Media';
|
||||
import {
|
||||
ProductionCompany,
|
||||
Genre,
|
||||
Cast,
|
||||
Crew,
|
||||
ExternalIds,
|
||||
Genre,
|
||||
mapCast,
|
||||
mapCrew,
|
||||
ExternalIds,
|
||||
mapExternalIds,
|
||||
mapVideos,
|
||||
mapWatchProviders,
|
||||
ProductionCompany,
|
||||
WatchProviders,
|
||||
} from './common';
|
||||
import Media from '../entity/Media';
|
||||
|
||||
export interface Video {
|
||||
url?: string;
|
||||
@@ -78,6 +80,7 @@ export interface MovieDetails {
|
||||
mediaInfo?: Media;
|
||||
externalIds: ExternalIds;
|
||||
plexUrl?: string;
|
||||
watchProviders?: WatchProviders[];
|
||||
}
|
||||
|
||||
export const mapProductionCompany = (
|
||||
@@ -136,4 +139,5 @@ export const mapMovieDetails = (
|
||||
: undefined,
|
||||
externalIds: mapExternalIds(movie.external_ids),
|
||||
mediaInfo: media,
|
||||
watchProviders: mapWatchProviders(movie['watch/providers']?.results ?? {}),
|
||||
});
|
||||
|
||||
@@ -1,25 +1,27 @@
|
||||
import {
|
||||
Genre,
|
||||
ProductionCompany,
|
||||
Cast,
|
||||
Crew,
|
||||
mapAggregateCast,
|
||||
mapCrew,
|
||||
ExternalIds,
|
||||
mapExternalIds,
|
||||
Keyword,
|
||||
mapVideos,
|
||||
TvNetwork,
|
||||
} from './common';
|
||||
import type {
|
||||
TmdbTvEpisodeResult,
|
||||
TmdbTvSeasonResult,
|
||||
TmdbTvDetails,
|
||||
TmdbSeasonWithEpisodes,
|
||||
TmdbTvRatingResult,
|
||||
TmdbNetwork,
|
||||
TmdbSeasonWithEpisodes,
|
||||
TmdbTvDetails,
|
||||
TmdbTvEpisodeResult,
|
||||
TmdbTvRatingResult,
|
||||
TmdbTvSeasonResult,
|
||||
} from '../api/themoviedb/interfaces';
|
||||
import type Media from '../entity/Media';
|
||||
import {
|
||||
Cast,
|
||||
Crew,
|
||||
ExternalIds,
|
||||
Genre,
|
||||
Keyword,
|
||||
mapAggregateCast,
|
||||
mapCrew,
|
||||
mapExternalIds,
|
||||
mapVideos,
|
||||
mapWatchProviders,
|
||||
ProductionCompany,
|
||||
TvNetwork,
|
||||
WatchProviders,
|
||||
} from './common';
|
||||
import { Video } from './Movie';
|
||||
|
||||
interface Episode {
|
||||
@@ -102,6 +104,7 @@ export interface TvDetails {
|
||||
externalIds: ExternalIds;
|
||||
keywords: Keyword[];
|
||||
mediaInfo?: Media;
|
||||
watchProviders?: WatchProviders[];
|
||||
}
|
||||
|
||||
const mapEpisodeResult = (episode: TmdbTvEpisodeResult): Episode => ({
|
||||
@@ -213,4 +216,5 @@ export const mapTvDetails = (
|
||||
name: keyword.name,
|
||||
})),
|
||||
mediaInfo: media,
|
||||
watchProviders: mapWatchProviders(show['watch/providers']?.results ?? {}),
|
||||
});
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import type {
|
||||
TmdbCreditCast,
|
||||
TmdbAggregateCreditCast,
|
||||
TmdbCreditCast,
|
||||
TmdbCreditCrew,
|
||||
TmdbExternalIds,
|
||||
TmdbVideo,
|
||||
TmdbVideoResult,
|
||||
TmdbWatchProviderDetails,
|
||||
TmdbWatchProviders,
|
||||
} from '../api/themoviedb/interfaces';
|
||||
|
||||
import { Video } from '../models/Movie';
|
||||
|
||||
export interface ProductionCompany {
|
||||
@@ -70,6 +71,20 @@ export interface ExternalIds {
|
||||
twitterId?: string;
|
||||
}
|
||||
|
||||
export interface WatchProviders {
|
||||
iso_3166_1: string;
|
||||
link?: string;
|
||||
buy?: WatchProviderDetails[];
|
||||
flatrate?: WatchProviderDetails[];
|
||||
}
|
||||
|
||||
export interface WatchProviderDetails {
|
||||
displayPriority?: number;
|
||||
logoPath?: string;
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export const mapCast = (person: TmdbCreditCast): Cast => ({
|
||||
castId: person.cast_id,
|
||||
character: person.character,
|
||||
@@ -124,7 +139,33 @@ export const mapVideos = (videoResult: TmdbVideoResult): Video[] =>
|
||||
url: siteUrlCreator(site, key),
|
||||
}));
|
||||
|
||||
export const mapWatchProviders = (watchProvidersResult: {
|
||||
[iso_3166_1: string]: TmdbWatchProviders;
|
||||
}): WatchProviders[] =>
|
||||
Object.entries(watchProvidersResult).map(
|
||||
([iso_3166_1, provider]) =>
|
||||
({
|
||||
iso_3166_1,
|
||||
link: provider.link,
|
||||
buy: mapWatchProviderDetails(provider.buy ?? []),
|
||||
flatrate: mapWatchProviderDetails(provider.flatrate ?? []),
|
||||
} as WatchProviders)
|
||||
);
|
||||
|
||||
export const mapWatchProviderDetails = (
|
||||
watchProviderDetails: TmdbWatchProviderDetails[]
|
||||
): WatchProviderDetails[] =>
|
||||
watchProviderDetails.map(
|
||||
(provider) =>
|
||||
({
|
||||
displayPriority: provider.display_priority,
|
||||
logoPath: provider.logo_path,
|
||||
id: provider.provider_id,
|
||||
name: provider.provider_name,
|
||||
} as WatchProviderDetails)
|
||||
);
|
||||
|
||||
const siteUrlCreator = (site: Video['site'], key: string): string =>
|
||||
({
|
||||
YouTube: `https://www.youtube.com/watch?v=${key}/`,
|
||||
YouTube: `https://www.youtube.com/watch?v=${key}`,
|
||||
}[site]);
|
||||
|
||||
@@ -350,6 +350,14 @@ requestRoutes.post('/', async (req, res, next) => {
|
||||
status: 202,
|
||||
message: 'No seasons available to request',
|
||||
});
|
||||
} else if (
|
||||
quotas.tv.limit &&
|
||||
finalSeasons.length > (quotas.tv.remaining ?? 0)
|
||||
) {
|
||||
return next({
|
||||
status: 403,
|
||||
message: 'Series Quota Exceeded',
|
||||
});
|
||||
}
|
||||
|
||||
await mediaRepository.save(media);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { truncate } from 'lodash';
|
||||
import {
|
||||
EntitySubscriberInterface,
|
||||
EventSubscriber,
|
||||
@@ -34,7 +35,11 @@ export class MediaSubscriber implements EntitySubscriberInterface {
|
||||
subject: `${movie.title}${
|
||||
movie.release_date ? ` (${movie.release_date.slice(0, 4)})` : ''
|
||||
}`,
|
||||
message: movie.overview,
|
||||
message: truncate(movie.overview, {
|
||||
length: 500,
|
||||
separator: /\s/,
|
||||
omission: '…',
|
||||
}),
|
||||
media: entity,
|
||||
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`,
|
||||
request: request,
|
||||
@@ -89,7 +94,11 @@ export class MediaSubscriber implements EntitySubscriberInterface {
|
||||
subject: `${tv.name}${
|
||||
tv.first_air_date ? ` (${tv.first_air_date.slice(0, 4)})` : ''
|
||||
}`,
|
||||
message: tv.overview,
|
||||
message: truncate(tv.overview, {
|
||||
length: 500,
|
||||
separator: /\s/,
|
||||
omission: '…',
|
||||
}),
|
||||
notifyUser: request.requestedBy,
|
||||
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${tv.poster_path}`,
|
||||
media: entity,
|
||||
|
||||
@@ -5,7 +5,7 @@ head
|
||||
meta(http-equiv='x-ua-compatible' content='ie=edge')
|
||||
meta(name='viewport' content='width=device-width, initial-scale=1')
|
||||
meta(name='format-detection' content='telephone=no, date=no, address=no, email=no')
|
||||
link(href='https://fonts.googleapis.com/css?family=Nunito+Sans:400,700&display=swap' rel='stylesheet' media='screen')
|
||||
link(href='https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap' rel='stylesheet' media='screen')
|
||||
//if mso
|
||||
xml
|
||||
o:officedocumentsettings
|
||||
@@ -26,72 +26,37 @@ head
|
||||
mso-line-height-rule: exactly;
|
||||
}
|
||||
style.
|
||||
@media (max-width: 600px) {
|
||||
.sm-w-full {
|
||||
.title:hover * {
|
||||
text-decoration: underline;
|
||||
}
|
||||
@media only screen and (max-width:600px) {
|
||||
table {
|
||||
font-size: 20px !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
div(role='article' aria-roledescription='email' aria-label='' lang='en')
|
||||
table(style="\
|
||||
background-color: #f2f4f6;\
|
||||
font-family: 'Nunito Sans', -apple-system, 'Segoe UI', sans-serif;\
|
||||
width: 100%;\
|
||||
" width='100%' bgcolor='#f2f4f6' cellpadding='0' cellspacing='0' role='presentation')
|
||||
div(style='display: block; background-color: #111827;')
|
||||
table(style='margin: 0 auto; font-family: Inter, Arial, Sans-Serif; color: #fff; font-size: 16px; width: 26rem;')
|
||||
tr
|
||||
td(align='center')
|
||||
table(style='width: 100%' width='100%' cellpadding='0' cellspacing='0' role='presentation')
|
||||
tr
|
||||
td(align='center' style='\
|
||||
padding-top: 25px;\
|
||||
padding-bottom: 25px;\
|
||||
text-align: center;\
|
||||
')
|
||||
a(href=applicationUrl style='\
|
||||
text-shadow: 0 1px 0 #ffffff;\
|
||||
font-weight: 700;\
|
||||
font-size: 24px;\
|
||||
color: #a8aaaf;\
|
||||
text-decoration: none;\
|
||||
')
|
||||
| #{applicationTitle}
|
||||
tr
|
||||
td(style='width: 100%' width='100%')
|
||||
table.sm-w-full(align='center' style='\
|
||||
background-color: #ffffff;\
|
||||
margin-left: auto;\
|
||||
margin-right: auto;\
|
||||
width: 570px;\
|
||||
' width='570' bgcolor='#ffffff' cellpadding='0' cellspacing='0' role='presentation')
|
||||
tr
|
||||
td(style='padding: 45px')
|
||||
div(style='font-size: 16px; text-align: center; padding-bottom: 14px;')
|
||||
| Your new password is:
|
||||
div(style='font-size: 16px; text-align: center')
|
||||
| #{password}
|
||||
p(style='\
|
||||
font-size: 13px;\
|
||||
line-height: 24px;\
|
||||
margin-top: 6px;\
|
||||
margin-bottom: 20px;\
|
||||
color: #51545e;\
|
||||
')
|
||||
a(href=applicationUrl style='color: #3869d4') Open #{applicationTitle}
|
||||
tr
|
||||
td
|
||||
table.sm-w-full(align='center' style='\
|
||||
margin-left: auto;\
|
||||
margin-right: auto;\
|
||||
text-align: center;\
|
||||
width: 570px;\
|
||||
' width='570' cellpadding='0' cellspacing='0' role='presentation')
|
||||
td(style="text-align: center;")
|
||||
a(href=applicationUrl)
|
||||
img(src=applicationUrl +'/logo_full.png' style='width: 26rem; padding: 1rem; image-rendering: crisp-edges; image-rendering: -webkit-optimize-contrast;')
|
||||
tr
|
||||
td(style='text-align: center;')
|
||||
div(style='margin: 0rem 1rem 1rem; font-size: 1.25em;')
|
||||
span
|
||||
| An account has been created for you at #{applicationTitle}.
|
||||
tr
|
||||
td(style='text-align: center;')
|
||||
div(style='margin: 1rem 1rem 1rem; font-size: 1.25em;')
|
||||
span
|
||||
| Your new password is:
|
||||
div(style='margin: 0rem 1rem 1rem; font-size: 1.25em;')
|
||||
span
|
||||
| #{password}
|
||||
if applicationUrl
|
||||
tr
|
||||
td(align='center' style='font-size: 16px; padding: 45px')
|
||||
p(style='\
|
||||
font-size: 13px;\
|
||||
line-height: 24px;\
|
||||
margin-top: 6px;\
|
||||
margin-bottom: 20px;\
|
||||
text-align: center;\
|
||||
color: #a8aaaf;\
|
||||
')
|
||||
| #{applicationTitle}
|
||||
td
|
||||
a(href=applicationUrl style='display: block; margin: 1.5rem 3rem 2.5rem 3rem; text-decoration: none; font-size: 1.0em; line-height: 2.25em;')
|
||||
span(style='padding: 0.2rem; font-weight: 500; text-align: center; border-radius: 10px; background-color: rgb(99,102,241); color: #fff; display: block; border: 1px solid rgba(255, 255, 255, 0.2);')
|
||||
| Open #{applicationTitle}
|
||||
|
||||
@@ -5,7 +5,7 @@ head
|
||||
meta(http-equiv='x-ua-compatible' content='ie=edge')
|
||||
meta(name='viewport' content='width=device-width, initial-scale=1')
|
||||
meta(name='format-detection' content='telephone=no, date=no, address=no, email=no')
|
||||
link(href='https://fonts.googleapis.com/css?family=Nunito+Sans:400,700&display=swap' rel='stylesheet' media='screen')
|
||||
link(href='https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap' rel='stylesheet' media='screen')
|
||||
//if mso
|
||||
xml
|
||||
o:officedocumentsettings
|
||||
@@ -26,92 +26,56 @@ head
|
||||
mso-line-height-rule: exactly;
|
||||
}
|
||||
style.
|
||||
@media (max-width: 600px) {
|
||||
.sm-w-full {
|
||||
.title:hover * {
|
||||
text-decoration: underline;
|
||||
}
|
||||
@media only screen and (max-width:600px) {
|
||||
table {
|
||||
font-size: 20px !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
div(role='article' aria-roledescription='email' aria-label='' lang='en')
|
||||
table(style="\
|
||||
background-color: #f2f4f6;\
|
||||
font-family: 'Nunito Sans', -apple-system, 'Segoe UI', sans-serif;\
|
||||
width: 100%;\
|
||||
" width='100%' bgcolor='#f2f4f6' cellpadding='0' cellspacing='0' role='presentation')
|
||||
div(style='display: block; background-color: #111827;')
|
||||
table(style='margin: 0 auto; font-family: Inter, Arial, Sans-Serif; color: #fff; font-size: 16px; width: 26rem;')
|
||||
tr
|
||||
td(align='center')
|
||||
table(style='width: 100%' width='100%' cellpadding='0' cellspacing='0' role='presentation')
|
||||
tr
|
||||
td(align='center' style='\
|
||||
padding-top: 25px;\
|
||||
padding-bottom: 25px;\
|
||||
text-align: center;\
|
||||
')
|
||||
a(href=applicationUrl style='\
|
||||
text-shadow: 0 1px 0 #ffffff;\
|
||||
font-weight: 700;\
|
||||
font-size: 24px;\
|
||||
color: #a8aaaf;\
|
||||
text-decoration: none;\
|
||||
')
|
||||
| #{applicationTitle}
|
||||
tr
|
||||
td(style='width: 100%' width='100%')
|
||||
table.sm-w-full(align='center' style='\
|
||||
background-color: #ffffff;\
|
||||
margin-left: auto;\
|
||||
margin-right: auto;\
|
||||
width: 570px;\
|
||||
' width='570' bgcolor='#ffffff' cellpadding='0' cellspacing='0' role='presentation')
|
||||
tr
|
||||
td(style='padding: 45px')
|
||||
div(style='font-size: 16px')
|
||||
| #{body}
|
||||
br
|
||||
br
|
||||
p(style='margin-top: 4px; text-align: center')
|
||||
b
|
||||
| #{mediaName}
|
||||
each extra in mediaExtra
|
||||
br
|
||||
| #{extra.name}:
|
||||
| #{extra.value}
|
||||
table(align='center' cellpadding='0' cellspacing='0' role='presentation')
|
||||
tr
|
||||
td
|
||||
a(href=actionUrl style='color: #3869d4')
|
||||
img(src=imageUrl alt='')
|
||||
p(style='\
|
||||
font-size: 16px;\
|
||||
line-height: 24px;\
|
||||
margin-top: 6px;\
|
||||
margin-bottom: 20px;\
|
||||
color: #51545e;\
|
||||
')
|
||||
| Requested by #{requestedBy} at #{timestamp}
|
||||
p(style='\
|
||||
font-size: 13px;\
|
||||
line-height: 24px;\
|
||||
margin-top: 6px;\
|
||||
margin-bottom: 20px;\
|
||||
color: #51545e;\
|
||||
')
|
||||
a(href=actionUrl style='color: #3869d4') Open in #{applicationTitle}
|
||||
tr
|
||||
td
|
||||
table.sm-w-full(align='center' style='\
|
||||
margin-left: auto;\
|
||||
margin-right: auto;\
|
||||
text-align: center;\
|
||||
width: 570px;\
|
||||
' width='570' cellpadding='0' cellspacing='0' role='presentation')
|
||||
td(style="text-align: center;")
|
||||
a(href=applicationUrl)
|
||||
img(src=applicationUrl +'/logo_full.png' style='width: 26rem; padding: 1rem; image-rendering: crisp-edges; image-rendering: -webkit-optimize-contrast;')
|
||||
tr
|
||||
td(style='text-align: center;')
|
||||
div(style='margin: 0rem 1rem 1rem; font-size: 1.25em;')
|
||||
span
|
||||
| #{body}
|
||||
tr
|
||||
td
|
||||
div(style='box-sizing: border-box; margin: 0; width: 100%; color: #fff; border-radius: .75rem; padding: 1rem; border: 1px solid rgba(100, 100, 100, 1); background: linear-gradient(135deg, rgba(17, 24, 39, 0.47) 0%, rgb(17, 24, 39) 75%), url(' + imageUrl + ') center 25%/cover')
|
||||
table(style='color: #fff; width: 100%;')
|
||||
tr
|
||||
td(style='vertical-align: top;')
|
||||
a(href=actionUrl style='display: block; max-width: 20rem; color: #fff; font-weight: 700; text-decoration: none; margin: 0 1rem 0.25rem 0; font-size: 1.3em; line-height: 1.25em; margin-bottom: 5px;' class='title')
|
||||
span
|
||||
| #{mediaName}
|
||||
div(style='overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: #d1d5db; font-size: .975em; line-height: 1.45em; padding-top: .25rem; padding-bottom: .25rem;')
|
||||
span(style='display: block;')
|
||||
b(style='color: #9ca3af; font-weight: 700;')
|
||||
| Requested By
|
||||
| #{requestedBy}
|
||||
each extra in mediaExtra
|
||||
span(style='display: block;')
|
||||
b(style='color: #9ca3af; font-weight: 700;')
|
||||
| #{extra.name}
|
||||
| #{extra.value}
|
||||
td(rowspan='2' style='width: 7rem;')
|
||||
a(style='display: block; width: 7rem; overflow: hidden; border-radius: .375rem;' href=actionUrl)
|
||||
div(style='overflow: hidden; box-sizing: border-box; margin: 0px;')
|
||||
img(alt='' src=imageUrl style='box-sizing: border-box; padding: 0px; border: none; margin: auto; display: block; min-width: 100%; max-width: 100%; min-height: 100%; max-height: 100%;')
|
||||
tr
|
||||
td(style='font-size: .85em; color: #9ca3af; line-height: 1em; vertical-align: bottom; margin-right: 1rem')
|
||||
span
|
||||
| #{timestamp}
|
||||
if actionUrl
|
||||
tr
|
||||
td(align='center' style='font-size: 16px; padding: 45px')
|
||||
p(style='\
|
||||
font-size: 13px;\
|
||||
line-height: 24px;\
|
||||
margin-top: 6px;\
|
||||
margin-bottom: 20px;\
|
||||
text-align: center;\
|
||||
color: #a8aaaf;\
|
||||
')
|
||||
| #{applicationTitle}
|
||||
td
|
||||
a(href=actionUrl style='display: block; margin: 1.5rem 3rem 2.5rem 3rem; text-decoration: none; font-size: 1.0em; line-height: 2.25em;')
|
||||
span(style='padding: 0.2rem; font-weight: 500; text-align: center; border-radius: 10px; background-color: rgb(99,102,241); color: #fff; display: block; border: 1px solid rgba(255, 255, 255, 0.2);')
|
||||
| Open in #{applicationTitle}
|
||||
|
||||
@@ -5,7 +5,7 @@ head
|
||||
meta(http-equiv='x-ua-compatible' content='ie=edge')
|
||||
meta(name='viewport' content='width=device-width, initial-scale=1')
|
||||
meta(name='format-detection' content='telephone=no, date=no, address=no, email=no')
|
||||
link(href='https://fonts.googleapis.com/css?family=Nunito+Sans:400,700&display=swap' rel='stylesheet' media='screen')
|
||||
link(href='https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap' rel='stylesheet' media='screen')
|
||||
//if mso
|
||||
xml
|
||||
o:officedocumentsettings
|
||||
@@ -26,74 +26,34 @@ head
|
||||
mso-line-height-rule: exactly;
|
||||
}
|
||||
style.
|
||||
@media (max-width: 600px) {
|
||||
.sm-w-full {
|
||||
.title:hover * {
|
||||
text-decoration: underline;
|
||||
}
|
||||
@media only screen and (max-width:600px) {
|
||||
table {
|
||||
font-size: 20px !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
div(role='article' aria-roledescription='email' aria-label='' lang='en')
|
||||
table(style="\
|
||||
background-color: #f2f4f6;\
|
||||
font-family: 'Nunito Sans', -apple-system, 'Segoe UI', sans-serif;\
|
||||
width: 100%;\
|
||||
" width='100%' bgcolor='#f2f4f6' cellpadding='0' cellspacing='0' role='presentation')
|
||||
div(style='display: block; background-color: #111827;')
|
||||
table(style='margin: 0 auto; font-family: Inter, Arial, Sans-Serif; color: #fff; font-size: 16px; width: 26rem;')
|
||||
tr
|
||||
td(align='center')
|
||||
table(style='width: 100%' width='100%' cellpadding='0' cellspacing='0' role='presentation')
|
||||
tr
|
||||
td(align='center' style='\
|
||||
padding-top: 25px;\
|
||||
padding-bottom: 25px;\
|
||||
text-align: center;\
|
||||
')
|
||||
a(href=applicationUrl style='\
|
||||
text-shadow: 0 1px 0 #ffffff;\
|
||||
font-weight: 700;\
|
||||
font-size: 24px;\
|
||||
color: #a8aaaf;\
|
||||
text-decoration: none;\
|
||||
')
|
||||
| #{applicationTitle}
|
||||
tr
|
||||
td(style='width: 100%' width='100%')
|
||||
table.sm-w-full(align='center' style='\
|
||||
background-color: #ffffff;\
|
||||
margin-left: auto;\
|
||||
margin-right: auto;\
|
||||
width: 570px;\
|
||||
' width='570' bgcolor='#ffffff' cellpadding='0' cellspacing='0' role='presentation')
|
||||
tr
|
||||
td(style='padding: 45px')
|
||||
div(style='font-size: 16px; text-align: center; padding-bottom: 14px;')
|
||||
| A request to reset the password was made. Click
|
||||
a(href=applicationUrl style='color: #3869d4; padding: 0px 5px;') here
|
||||
| to set a new password.
|
||||
div(style='font-size: 16px; text-align: center; padding-bottom: 14px;')
|
||||
| If you did not request this recovery link you can safely ignore this email.
|
||||
p(style='\
|
||||
font-size: 13px;\
|
||||
line-height: 24px;\
|
||||
margin-top: 6px;\
|
||||
margin-bottom: 20px;\
|
||||
color: #51545e;\
|
||||
')
|
||||
a(href=applicationUrl style='color: #3869d4') Open #{applicationTitle}
|
||||
tr
|
||||
td
|
||||
table.sm-w-full(align='center' style='\
|
||||
margin-left: auto;\
|
||||
margin-right: auto;\
|
||||
text-align: center;\
|
||||
width: 570px;\
|
||||
' width='570' cellpadding='0' cellspacing='0' role='presentation')
|
||||
td(style="text-align: center;")
|
||||
a(href=applicationUrl)
|
||||
img(src=applicationUrl +'/logo_full.png' style='width: 26rem; padding: 1rem; image-rendering: crisp-edges; image-rendering: -webkit-optimize-contrast;')
|
||||
tr
|
||||
td(style='text-align: center;')
|
||||
div(style='margin: 0rem 1rem 1rem; font-size: 1.25em;')
|
||||
span
|
||||
| Your #{applicationTitle} account password was requested to be reset. Click below to reset your password.
|
||||
if resetPasswordLink
|
||||
tr
|
||||
td(align='center' style='font-size: 16px; padding: 45px')
|
||||
p(style='\
|
||||
font-size: 13px;\
|
||||
line-height: 24px;\
|
||||
margin-top: 6px;\
|
||||
margin-bottom: 20px;\
|
||||
text-align: center;\
|
||||
color: #a8aaaf;\
|
||||
')
|
||||
| #{applicationTitle}.
|
||||
td
|
||||
a(href=resetPasswordLink style='display: block; margin: 1.5rem 3rem 2.5rem 3rem; text-decoration: none; font-size: 1.0em; line-height: 2.25em;')
|
||||
span(style='padding: 0.2rem; font-weight: 500; text-align: center; border-radius: 10px; background-color: rgb(99,102,241); color: #fff; display: block; border: 1px solid rgba(255, 255, 255, 0.2);')
|
||||
| Reset Password
|
||||
tr
|
||||
td(style='text-align: center;')
|
||||
div(style='margin: 1rem; font-size: .85em;')
|
||||
span
|
||||
| If you did not request that your password be reset, you can safely ignore this email.
|
||||
|
||||
@@ -5,7 +5,7 @@ head
|
||||
meta(http-equiv='x-ua-compatible' content='ie=edge')
|
||||
meta(name='viewport' content='width=device-width, initial-scale=1')
|
||||
meta(name='format-detection' content='telephone=no, date=no, address=no, email=no')
|
||||
link(href='https://fonts.googleapis.com/css?family=Nunito+Sans:400,700&display=swap' rel='stylesheet' media='screen')
|
||||
link(href='https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap' rel='stylesheet' media='screen')
|
||||
//if mso
|
||||
xml
|
||||
o:officedocumentsettings
|
||||
@@ -26,70 +26,29 @@ head
|
||||
mso-line-height-rule: exactly;
|
||||
}
|
||||
style.
|
||||
@media (max-width: 600px) {
|
||||
.sm-w-full {
|
||||
.title:hover * {
|
||||
text-decoration: underline;
|
||||
}
|
||||
@media only screen and (max-width:600px) {
|
||||
table {
|
||||
font-size: 20px !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
div(role='article' aria-roledescription='email' aria-label='' lang='en')
|
||||
table(style="\
|
||||
background-color: #f2f4f6;\
|
||||
font-family: 'Nunito Sans', -apple-system, 'Segoe UI', sans-serif;\
|
||||
width: 100%;\
|
||||
" width='100%' bgcolor='#f2f4f6' cellpadding='0' cellspacing='0' role='presentation')
|
||||
div(style='display: block; background-color: #111827;')
|
||||
table(style='margin: 0 auto; font-family: Inter, Arial, Sans-Serif; color: #fff; font-size: 16px; width: 26rem;')
|
||||
tr
|
||||
td(align='center')
|
||||
table(style='width: 100%' width='100%' cellpadding='0' cellspacing='0' role='presentation')
|
||||
tr
|
||||
td(align='center' style='\
|
||||
padding-top: 25px;\
|
||||
padding-bottom: 25px;\
|
||||
text-align: center;\
|
||||
')
|
||||
a(href=applicationUrl style='\
|
||||
text-shadow: 0 1px 0 #ffffff;\
|
||||
font-weight: 700;\
|
||||
font-size: 24px;\
|
||||
color: #a8aaaf;\
|
||||
text-decoration: none;\
|
||||
')
|
||||
| #{applicationTitle}
|
||||
tr
|
||||
td(style='width: 100%' width='100%')
|
||||
table.sm-w-full(align='center' style='\
|
||||
background-color: #ffffff;\
|
||||
margin-left: auto;\
|
||||
margin-right: auto;\
|
||||
width: 570px;\
|
||||
' width='570' bgcolor='#ffffff' cellpadding='0' cellspacing='0' role='presentation')
|
||||
tr
|
||||
td(style='padding: 45px')
|
||||
div(style='font-size: 16px')
|
||||
| #{body}
|
||||
p(style='\
|
||||
font-size: 13px;\
|
||||
line-height: 24px;\
|
||||
margin-top: 6px;\
|
||||
margin-bottom: 20px;\
|
||||
color: #51545e;\
|
||||
')
|
||||
a(href=applicationUrl style='color: #3869d4') Open #{applicationTitle}
|
||||
tr
|
||||
td
|
||||
table.sm-w-full(align='center' style='\
|
||||
margin-left: auto;\
|
||||
margin-right: auto;\
|
||||
text-align: center;\
|
||||
width: 570px;\
|
||||
' width='570' cellpadding='0' cellspacing='0' role='presentation')
|
||||
td(style="text-align: center;")
|
||||
a(href=applicationUrl)
|
||||
img(src=applicationUrl +'/logo_full.png' style='width: 26rem; padding: 1rem; image-rendering: crisp-edges; image-rendering: -webkit-optimize-contrast;')
|
||||
tr
|
||||
td(style='text-align: center;')
|
||||
div(style='margin: 0rem 1rem 1rem; font-size: 1.25em;')
|
||||
span
|
||||
| #{body}
|
||||
if applicationUrl
|
||||
tr
|
||||
td(align='center' style='font-size: 16px; padding: 45px')
|
||||
p(style='\
|
||||
font-size: 13px;\
|
||||
line-height: 24px;\
|
||||
margin-top: 6px;\
|
||||
margin-bottom: 20px;\
|
||||
text-align: center;\
|
||||
color: #a8aaaf;\
|
||||
')
|
||||
| #{applicationTitle}
|
||||
td
|
||||
a(href=applicationUrl style='display: block; margin: 1.5rem 3rem 2.5rem 3rem; text-decoration: none; font-size: 1.0em; line-height: 2.25em;')
|
||||
span(style='padding: 0.2rem; font-weight: 500; text-align: center; border-radius: 10px; background-color: rgb(99,102,241); color: #fff; display: block; border: 1px solid rgba(255, 255, 255, 0.2);')
|
||||
| Open #{applicationTitle}
|
||||
|
||||
@@ -11,7 +11,7 @@ confinement: strict
|
||||
parts:
|
||||
overseerr:
|
||||
plugin: nodejs
|
||||
nodejs-version: '14.16.1'
|
||||
nodejs-version: '14.17.0'
|
||||
nodejs-package-manager: 'yarn'
|
||||
nodejs-yarn-version: v1.22.10
|
||||
build-packages:
|
||||
|
||||
@@ -1 +1 @@
|
||||
<svg viewBox="57 57 593.71002 593.71002" xmlns="http://www.w3.org/2000/svg"><g transform="translate(55.201 55.147)"><circle transform="rotate(132.42 225.16 243.54)" cx="216.31" cy="152.08" r="296.86" fill="currentColor"/><path d="m280.95 172.51 74.48-9.8-72.52 163.66c12.74-0.98 25.233-5.307 37.48-12.98 12.253-7.68 23.527-17.317 33.82-28.91 10.287-11.6 19.187-24.503 26.7-38.71 7.513-14.213 12.903-28.18 16.17-41.9 1.96-8.493 2.86-16.66 2.7-24.5-0.167-7.84-2.21-14.7-6.13-20.58s-9.883-10.617-17.89-14.21c-8-3.593-18.86-5.39-32.58-5.39-16.007 0-31.77 2.613-47.29 7.84-15.513 5.227-29.887 12.823-43.12 22.79-13.227 9.96-24.74 22.373-34.54 37.24-9.8 14.86-16.823 31.763-21.07 50.71-1.633 6.207-2.613 11.187-2.94 14.94-0.327 3.76-0.407 6.863-0.24 9.31 0.16 2.453 0.483 4.333 0.97 5.64 0.493 1.307 0.903 2.613 1.23 3.92-16.66 0-28.83-3.35-36.51-10.05-7.673-6.693-9.55-18.37-5.63-35.03 3.92-17.313 12.823-33.81 26.71-49.49 13.88-15.68 30.373-29.483 49.48-41.41 19.113-11.92 40.02-21.39 62.72-28.41 22.707-7.027 44.84-10.54 66.4-10.54 18.947 0 34.87 2.693 47.77 8.08 12.907 5.393 22.953 12.5 30.14 21.32s11.677 19.11 13.47 30.87c1.8 11.76 1.23 24.01-1.71 36.75-3.593 15.353-10.373 30.79-20.34 46.31-9.96 15.513-22.453 29.56-37.48 42.14-15.027 12.573-32.26 22.78-51.7 30.62-19.433 7.84-40.093 11.76-61.98 11.76h-2.45l-62.23 139.65h-70.56z"/></g></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="57 57 593.71 593.71"><g transform="translate(55.201 55.147)"><circle cx="216.31" cy="152.08" r="296.86" fill="currentColor" transform="rotate(132.42 225.16 243.54)"/><path d="m280.95 172.51 74.48-9.8-72.52 163.66c12.74-0.98 25.233-5.307 37.48-12.98 12.253-7.68 23.527-17.317 33.82-28.91 10.287-11.6 19.187-24.503 26.7-38.71 7.513-14.213 12.903-28.18 16.17-41.9 1.96-8.493 2.86-16.66 2.7-24.5-0.167-7.84-2.21-14.7-6.13-20.58s-9.883-10.617-17.89-14.21c-8-3.593-18.86-5.39-32.58-5.39-16.007 0-31.77 2.613-47.29 7.84-15.513 5.227-29.887 12.823-43.12 22.79-13.227 9.96-24.74 22.373-34.54 37.24-9.8 14.86-16.823 31.763-21.07 50.71-1.633 6.207-2.613 11.187-2.94 14.94-0.327 3.76-0.407 6.863-0.24 9.31 0.16 2.453 0.483 4.333 0.97 5.64 0.493 1.307 0.903 2.613 1.23 3.92-16.66 0-28.83-3.35-36.51-10.05-7.673-6.693-9.55-18.37-5.63-35.03 3.92-17.313 12.823-33.81 26.71-49.49 13.88-15.68 30.373-29.483 49.48-41.41 19.113-11.92 40.02-21.39 62.72-28.41 22.707-7.027 44.84-10.54 66.4-10.54 18.947 0 34.87 2.693 47.77 8.08 12.907 5.393 22.953 12.5 30.14 21.32s11.677 19.11 13.47 30.87c1.8 11.76 1.23 24.01-1.71 36.75-3.593 15.353-10.373 30.79-20.34 46.31-9.96 15.513-22.453 29.56-37.48 42.14-15.027 12.573-32.26 22.78-51.7 30.62-19.433 7.84-40.093 11.76-61.98 11.76h-2.45l-62.23 139.65h-70.56z"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
@@ -1 +1 @@
|
||||
<svg viewBox="0 0 320.03 103.61" xmlns="http://www.w3.org/2000/svg"><defs><style>.cls-1{fill:#fff}.cls-2{fill:url(#a)}.cls-3{fill:#e5a00d}</style><radialGradient id="a" cx="258.33" cy="51.76" r="42.95" gradientUnits="userSpaceOnUse"><stop stop-color="#f9be03" offset=".17"/><stop stop-color="#e8a50b" offset=".51"/><stop stop-color="#cc7c19" offset="1"/></radialGradient></defs><polygon class="cls-1" points="320.03 -0.09 289.96 -0.09 259.88 51.76 289.96 103.61 320.01 103.61 289.96 51.79"/><polygon class="cls-2" points="226.7 -0.09 256.78 -0.09 289.96 51.76 256.78 103.61 226.7 103.61 259.88 51.76"/><polygon class="cls-3" points="226.7 -0.09 256.78 -0.09 289.96 51.76 256.78 103.61 226.7 103.61 259.88 51.76"/><path class="cls-1" d="M216.32,103.61H156.49V-.09h59.83v18h-37.8V40.69H213.7v18H178.52V85.45h37.8Z"/><path class="cls-1" d="M82.07,103.61V-.09h22V85.45h42.07v18.16Z"/><path class="cls-1" d="M71.66,32.25Q71.66,49,61.2,57.87T31.44,66.73H22v36.88H0V-.09H33.14Q52-.09,61.83,8T71.66,32.25ZM22,48.71h7.24q10.15,0,15.18-4c3.37-2.66,5-6.56,5-11.67s-1.41-9-4.22-11.42S38,17.93,32,17.93H22Z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320.03 103.61"><defs><style>.cls-1{fill:#fff}.cls-2{fill:url(#a)}.cls-3{fill:#e5a00d}</style><radialGradient id="a" cx="258.33" cy="51.76" r="42.95" gradientUnits="userSpaceOnUse"><stop offset=".17" stop-color="#f9be03"/><stop offset=".51" stop-color="#e8a50b"/><stop offset="1" stop-color="#cc7c19"/></radialGradient></defs><polygon points="320.03 -.09 289.96 -.09 259.88 51.76 289.96 103.61 320.01 103.61 289.96 51.79" class="cls-1"/><polygon points="226.7 -.09 256.78 -.09 289.96 51.76 256.78 103.61 226.7 103.61 259.88 51.76" class="cls-2"/><polygon points="226.7 -.09 256.78 -.09 289.96 51.76 256.78 103.61 226.7 103.61 259.88 51.76" class="cls-3"/><path d="M216.32,103.61H156.49V-.09h59.83v18h-37.8V40.69H213.7v18H178.52V85.45h37.8Z" class="cls-1"/><path d="M82.07,103.61V-.09h22V85.45h42.07v18.16Z" class="cls-1"/><path d="M71.66,32.25Q71.66,49,61.2,57.87T31.44,66.73H22v36.88H0V-.09H33.14Q52-.09,61.83,8T71.66,32.25ZM22,48.71h7.24q10.15,0,15.18-4c3.37-2.66,5-6.56,5-11.67s-1.41-9-4.22-11.42S38,17.93,32,17.93H22Z" class="cls-1"/></svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
@@ -13,7 +13,7 @@ interface AlertProps {
|
||||
const Alert: React.FC<AlertProps> = ({ title, children, type }) => {
|
||||
let design = {
|
||||
bgColor: 'bg-yellow-600',
|
||||
titleColor: 'text-yellow-200',
|
||||
titleColor: 'text-yellow-100',
|
||||
textColor: 'text-yellow-300',
|
||||
svg: <ExclamationIcon className="w-5 h-5" />,
|
||||
};
|
||||
@@ -22,7 +22,7 @@ const Alert: React.FC<AlertProps> = ({ title, children, type }) => {
|
||||
case 'info':
|
||||
design = {
|
||||
bgColor: 'bg-indigo-600',
|
||||
titleColor: 'text-indigo-200',
|
||||
titleColor: 'text-indigo-100',
|
||||
textColor: 'text-indigo-300',
|
||||
svg: <InformationCircleIcon className="w-5 h-5" />,
|
||||
};
|
||||
@@ -30,7 +30,7 @@ const Alert: React.FC<AlertProps> = ({ title, children, type }) => {
|
||||
case 'error':
|
||||
design = {
|
||||
bgColor: 'bg-red-600',
|
||||
titleColor: 'text-red-200',
|
||||
titleColor: 'text-red-100',
|
||||
textColor: 'text-red-300',
|
||||
svg: <XCircleIcon className="w-5 h-5" />,
|
||||
};
|
||||
|
||||
@@ -45,7 +45,7 @@ function Button<P extends ElementTypes = 'button'>(
|
||||
ref?: React.Ref<Element<P>>
|
||||
): JSX.Element {
|
||||
const buttonStyle = [
|
||||
'inline-flex items-center justify-center border border-transparent leading-5 font-medium rounded-md focus:outline-none transition ease-in-out duration-150 cursor-pointer disabled:opacity-50',
|
||||
'inline-flex items-center justify-center border border-transparent leading-5 font-medium rounded-md focus:outline-none transition ease-in-out duration-150 cursor-pointer disabled:opacity-50 whitespace-nowrap',
|
||||
];
|
||||
switch (buttonType) {
|
||||
case 'primary':
|
||||
|
||||
@@ -10,7 +10,7 @@ const ListItem: React.FC<ListItemProps> = ({ title, className, children }) => {
|
||||
return (
|
||||
<div>
|
||||
<div className="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4">
|
||||
<dt className="block text-sm font-medium text-gray-400">{title}</dt>
|
||||
<dt className="block text-sm font-bold text-gray-400">{title}</dt>
|
||||
<dd className="flex text-sm text-white sm:mt-0 sm:col-span-2">
|
||||
<span className={`flex-grow ${className}`}>{children}</span>
|
||||
</dd>
|
||||
|
||||
@@ -137,7 +137,7 @@ const Modal: React.FC<ModalProps> = ({
|
||||
>
|
||||
{title && (
|
||||
<span
|
||||
className="text-lg font-medium leading-6 text-white"
|
||||
className="text-lg font-bold leading-6 text-white"
|
||||
id="modal-headline"
|
||||
>
|
||||
{title}
|
||||
|
||||
@@ -46,7 +46,7 @@ const SettingsLink: React.FC<{
|
||||
|
||||
if (tabType === 'button') {
|
||||
linkClasses =
|
||||
'px-3 py-2 ml-8 text-sm font-medium transition duration-300 rounded-md whitespace-nowrap first:ml-0';
|
||||
'px-3 py-2 text-sm font-medium transition duration-300 rounded-md whitespace-nowrap mx-2 my-1';
|
||||
activeLinkColor = 'bg-indigo-700';
|
||||
inactiveLinkColor = 'bg-gray-800 hover:bg-gray-700 focus:bg-gray-700';
|
||||
}
|
||||
@@ -119,8 +119,8 @@ const SettingsTabs: React.FC<{
|
||||
</select>
|
||||
</div>
|
||||
{tabType === 'button' ? (
|
||||
<div className="hidden overflow-x-scroll overflow-y-hidden sm:block hide-scrollbar">
|
||||
<nav className="flex space-x-4" aria-label="Tabs">
|
||||
<div className="hidden sm:block">
|
||||
<nav className="flex flex-wrap -mx-2 -my-1" aria-label="Tabs">
|
||||
{settingsRoutes.map((route, index) => (
|
||||
<SettingsLink
|
||||
tabType={tabType}
|
||||
|
||||
@@ -73,7 +73,7 @@ const SlideOver: React.FC<SlideOverProps> = ({
|
||||
<div className="flex flex-col h-full overflow-y-scroll bg-gray-700 shadow-xl">
|
||||
<header className="px-4 space-y-1 bg-indigo-600 slideover">
|
||||
<div className="flex items-center justify-between space-x-3">
|
||||
<h2 className="text-lg font-medium leading-7 text-white">
|
||||
<h2 className="text-lg font-bold leading-7 text-white">
|
||||
{title}
|
||||
</h2>
|
||||
<div className="flex items-center h-7">
|
||||
|
||||
@@ -13,7 +13,7 @@ const TH: React.FC<AllHTMLAttributes<HTMLTableHeaderCellElement>> = ({
|
||||
...props
|
||||
}) => {
|
||||
const style = [
|
||||
'px-6 py-3 bg-gray-500 text-left text-xs leading-4 font-medium text-gray-200 uppercase tracking-wider',
|
||||
'px-4 py-3 bg-gray-500 text-left text-xs leading-4 font-medium text-gray-200 uppercase tracking-wider truncate',
|
||||
];
|
||||
|
||||
if (className) {
|
||||
@@ -39,7 +39,7 @@ const TD: React.FC<TDProps> = ({
|
||||
className,
|
||||
...props
|
||||
}) => {
|
||||
const style = ['whitespace-nowrap text-sm leading-5 text-white'];
|
||||
const style = ['text-sm leading-5 text-white'];
|
||||
|
||||
switch (alignText) {
|
||||
case 'left':
|
||||
@@ -54,7 +54,7 @@ const TD: React.FC<TDProps> = ({
|
||||
}
|
||||
|
||||
if (!noPadding) {
|
||||
style.push('px-6 py-4');
|
||||
style.push('px-4 py-4');
|
||||
}
|
||||
|
||||
if (className) {
|
||||
@@ -73,7 +73,7 @@ const Table: React.FC = ({ children }) => {
|
||||
<div className="flex flex-col">
|
||||
<div className="my-2 -mx-4 overflow-x-auto md:mx-0 lg:mx-0">
|
||||
<div className="inline-block min-w-full py-2 align-middle">
|
||||
<div className="overflow-hidden shadow sm:rounded-lg">
|
||||
<div className="overflow-hidden rounded-lg shadow md:mx-0 lg:mx-0">
|
||||
<table className="min-w-full">{children}</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,7 @@ import useLocale from '../../../hooks/useLocale';
|
||||
import Transition from '../../Transition';
|
||||
|
||||
const messages = defineMessages({
|
||||
changelanguage: 'Change Language',
|
||||
displaylanguage: 'Display Language',
|
||||
});
|
||||
|
||||
const LanguagePicker: React.FC = () => {
|
||||
@@ -50,9 +50,9 @@ const LanguagePicker: React.FC = () => {
|
||||
<div>
|
||||
<label
|
||||
htmlFor="language"
|
||||
className="block pb-2 text-sm font-medium leading-5 text-gray-300"
|
||||
className="block pb-2 text-sm font-bold leading-5 text-gray-300"
|
||||
>
|
||||
{intl.formatMessage(messages.changelanguage)}
|
||||
{intl.formatMessage(messages.displaylanguage)}
|
||||
</label>
|
||||
<select
|
||||
id="language"
|
||||
|
||||
@@ -13,7 +13,7 @@ const SearchInput: React.FC = () => {
|
||||
const { searchValue, setSearchValue, setIsOpen, clear } = useSearchInput();
|
||||
return (
|
||||
<div className="flex flex-1">
|
||||
<div className="flex w-full md:ml-0">
|
||||
<div className="flex w-full">
|
||||
<label htmlFor="search_field" className="sr-only">
|
||||
Search
|
||||
</label>
|
||||
@@ -24,7 +24,7 @@ const SearchInput: React.FC = () => {
|
||||
<input
|
||||
id="search_field"
|
||||
style={{ paddingRight: searchValue.length > 0 ? '1.75rem' : '' }}
|
||||
className="block w-full py-2 pl-10 text-white placeholder-gray-300 bg-gray-900 border border-gray-600 rounded-full focus:border-gray-500 hover:border-gray-500 focus:outline-none focus:ring-0 focus:placeholder-gray-400 sm:text-base"
|
||||
className="block w-full py-2 pl-10 text-white placeholder-gray-300 bg-gray-900 border border-gray-600 rounded-full bg-opacity-80 focus:bg-opacity-100 focus:border-gray-500 hover:border-gray-500 focus:outline-none focus:ring-0 focus:placeholder-gray-400 sm:text-base"
|
||||
placeholder={intl.formatMessage(messages.searchPlaceholder)}
|
||||
type="search"
|
||||
inputMode="search"
|
||||
|
||||
@@ -2,9 +2,9 @@ import {
|
||||
ClockIcon,
|
||||
CogIcon,
|
||||
SparklesIcon,
|
||||
UsersIcon,
|
||||
XIcon,
|
||||
} from '@heroicons/react/outline';
|
||||
import { UsersIcon } from '@heroicons/react/solid';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import React, { ReactNode, useRef } from 'react';
|
||||
@@ -39,34 +39,26 @@ const SidebarLinks: SidebarLinkProps[] = [
|
||||
{
|
||||
href: '/',
|
||||
messagesKey: 'dashboard',
|
||||
svgIcon: (
|
||||
<SparklesIcon className="w-6 h-6 mr-3 text-gray-300 transition duration-150 ease-in-out group-hover:text-gray-100 group-focus:text-gray-300" />
|
||||
),
|
||||
svgIcon: <SparklesIcon className="w-6 h-6 mr-3" />,
|
||||
activeRegExp: /^\/(discover\/?(movies|tv)?)?$/,
|
||||
},
|
||||
{
|
||||
href: '/requests',
|
||||
messagesKey: 'requests',
|
||||
svgIcon: (
|
||||
<ClockIcon className="w-6 h-6 mr-3 text-gray-300 transition duration-150 ease-in-out group-hover:text-gray-100 group-focus:text-gray-300" />
|
||||
),
|
||||
svgIcon: <ClockIcon className="w-6 h-6 mr-3" />,
|
||||
activeRegExp: /^\/requests/,
|
||||
},
|
||||
{
|
||||
href: '/users',
|
||||
messagesKey: 'users',
|
||||
svgIcon: (
|
||||
<UsersIcon className="w-6 h-6 mr-3 text-gray-300 transition duration-150 ease-in-out group-hover:text-gray-100 group-focus:text-gray-300" />
|
||||
),
|
||||
svgIcon: <UsersIcon className="w-6 h-6 mr-3" />,
|
||||
activeRegExp: /^\/users/,
|
||||
requiredPermission: Permission.MANAGE_USERS,
|
||||
},
|
||||
{
|
||||
href: '/settings',
|
||||
messagesKey: 'settings',
|
||||
svgIcon: (
|
||||
<CogIcon className="w-6 h-6 mr-3 text-gray-300 transition duration-150 ease-in-out group-hover:text-gray-100 group-focus:text-gray-300" />
|
||||
),
|
||||
svgIcon: <CogIcon className="w-6 h-6 mr-3" />,
|
||||
activeRegExp: /^\/settings/,
|
||||
requiredPermission: Permission.MANAGE_SETTINGS,
|
||||
},
|
||||
@@ -81,7 +73,7 @@ const Sidebar: React.FC<SidebarProps> = ({ open, setClosed }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="md:hidden">
|
||||
<div className="lg:hidden">
|
||||
<Transition show={open}>
|
||||
<div className="fixed inset-0 z-40 flex">
|
||||
<Transition
|
||||
@@ -93,7 +85,7 @@ const Sidebar: React.FC<SidebarProps> = ({ open, setClosed }) => {
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0">
|
||||
<div className="absolute inset-0 bg-gray-600 opacity-75"></div>
|
||||
<div className="absolute inset-0 bg-gray-900 opacity-90"></div>
|
||||
</div>
|
||||
</Transition>
|
||||
<Transition
|
||||
@@ -117,16 +109,16 @@ const Sidebar: React.FC<SidebarProps> = ({ open, setClosed }) => {
|
||||
</div>
|
||||
<div
|
||||
ref={navRef}
|
||||
className="flex flex-col flex-1 h-0 pt-5 pb-8 overflow-y-auto sm:pb-4"
|
||||
className="flex flex-col flex-1 h-0 pt-8 pb-8 overflow-y-auto sm:pb-4"
|
||||
>
|
||||
<div className="flex items-center flex-shrink-0 px-4">
|
||||
<span className="text-xl text-gray-50">
|
||||
<div className="flex items-center flex-shrink-0 px-2">
|
||||
<span className="px-4 text-xl text-gray-50">
|
||||
<a href="/">
|
||||
<img src="/logo.png" alt="Logo" />
|
||||
<img src="/logo_full.svg" alt="Logo" />
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
<nav className="flex-1 px-2 mt-5 space-y-1">
|
||||
<nav className="flex-1 px-4 mt-16 space-y-4">
|
||||
{SidebarLinks.filter((link) =>
|
||||
link.requiredPermission
|
||||
? hasPermission(link.requiredPermission)
|
||||
@@ -147,13 +139,13 @@ const Sidebar: React.FC<SidebarProps> = ({ open, setClosed }) => {
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className={`flex items-center px-2 py-2 text-base leading-6 font-medium rounded-md text-white focus:outline-none focus:bg-gray-700 transition ease-in-out duration-150
|
||||
className={`flex items-center px-2 py-2 text-base leading-6 font-medium rounded-md text-white focus:outline-none transition ease-in-out duration-150
|
||||
${
|
||||
router.pathname.match(
|
||||
sidebarLink.activeRegExp
|
||||
)
|
||||
? 'bg-gray-900'
|
||||
: ''
|
||||
? 'bg-gradient-to-br from-indigo-600 to-purple-600 hover:from-indigo-500 hover:to-purple-500'
|
||||
: 'hover:bg-gray-700 focus:bg-gray-700'
|
||||
}
|
||||
`}
|
||||
>
|
||||
@@ -167,7 +159,9 @@ const Sidebar: React.FC<SidebarProps> = ({ open, setClosed }) => {
|
||||
})}
|
||||
</nav>
|
||||
{hasPermission(Permission.ADMIN) && (
|
||||
<VersionStatus onClick={() => setClosed()} />
|
||||
<div className="px-2">
|
||||
<VersionStatus onClick={() => setClosed()} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -180,18 +174,18 @@ const Sidebar: React.FC<SidebarProps> = ({ open, setClosed }) => {
|
||||
</Transition>
|
||||
</div>
|
||||
|
||||
<div className="fixed top-0 bottom-0 left-0 hidden md:flex md:flex-shrink-0">
|
||||
<div className="fixed top-0 bottom-0 left-0 z-30 hidden lg:flex lg:flex-shrink-0">
|
||||
<div className="flex flex-col w-64 sidebar">
|
||||
<div className="flex flex-col flex-1 h-0 bg-gray-800">
|
||||
<div className="flex flex-col flex-1 pt-5 pb-4 overflow-y-auto">
|
||||
<div className="flex items-center flex-shrink-0 px-4">
|
||||
<span className="text-2xl text-gray-50">
|
||||
<div className="flex flex-col flex-1 h-0">
|
||||
<div className="flex flex-col flex-1 pt-8 pb-4 overflow-y-auto">
|
||||
<div className="flex items-center flex-shrink-0">
|
||||
<span className="px-4 text-2xl text-gray-50">
|
||||
<a href="/">
|
||||
<img src="/logo.png" alt="Logo" />
|
||||
<img src="/logo_full.svg" alt="Logo" />
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
<nav className="flex-1 px-2 mt-5 space-y-1 bg-gray-800">
|
||||
<nav className="flex-1 px-4 mt-16 space-y-4">
|
||||
{SidebarLinks.filter((link) =>
|
||||
link.requiredPermission
|
||||
? hasPermission(link.requiredPermission)
|
||||
@@ -204,13 +198,13 @@ const Sidebar: React.FC<SidebarProps> = ({ open, setClosed }) => {
|
||||
as={sidebarLink.as}
|
||||
>
|
||||
<a
|
||||
className={`flex group items-center px-2 py-2 text-base leading-6 font-medium rounded-md text-white hover:text-gray-100 hover:bg-gray-700 focus:outline-none focus:bg-gray-700 transition ease-in-out duration-150
|
||||
className={`flex group items-center px-2 py-2 text-lg leading-6 font-medium rounded-md text-white focus:outline-none transition ease-in-out duration-150
|
||||
${
|
||||
router.pathname.match(
|
||||
sidebarLink.activeRegExp
|
||||
)
|
||||
? 'bg-gray-900'
|
||||
: ''
|
||||
? 'bg-gradient-to-br from-indigo-600 to-purple-600 hover:from-indigo-500 hover:to-purple-500'
|
||||
: 'hover:bg-gray-700 focus:bg-gray-700'
|
||||
}
|
||||
`}
|
||||
>
|
||||
@@ -221,7 +215,11 @@ const Sidebar: React.FC<SidebarProps> = ({ open, setClosed }) => {
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
{hasPermission(Permission.ADMIN) && <VersionStatus />}
|
||||
{hasPermission(Permission.ADMIN) && (
|
||||
<div className="px-2">
|
||||
<VersionStatus />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||