Merge branch 'develop'

This commit is contained in:
sct
2021-09-19 21:08:16 +09:00
166 changed files with 4327 additions and 1751 deletions

View File

@@ -503,6 +503,42 @@
"contributions": [ "contributions": [
"translation" "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>", "badgeTemplate": "<a href=\"#contributors-\"><img alt=\"All Contributors\" src=\"https://img.shields.io/badge/all_contributors-<%= contributors.length %>-orange.svg\"/></a>",

View File

@@ -21,7 +21,7 @@ docker-compose.yml
docs docs
LICENSE LICENSE
node_modules node_modules
public/os_logo_square.png public/os_logo_filled.png
public/preview.jpg public/preview.jpg
snap snap
stylelint.config.js stylelint.config.js

View File

@@ -12,7 +12,7 @@ jobs:
test: test:
name: Lint & Test Build name: Lint & Test Build
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
container: node:14.16-alpine container: node:14.17-alpine
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2.3.4 uses: actions/checkout@v2.3.4

View File

@@ -9,7 +9,7 @@ jobs:
test: test:
name: Lint & Test Build name: Lint & Test Build
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
container: node:14.16-alpine container: node:14.17-alpine
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2.3.4 uses: actions/checkout@v2.3.4

View File

@@ -20,7 +20,7 @@ jobs:
name: Lint & Test Build name: Lint & Test Build
needs: jobs needs: jobs
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
container: node:14.16-alpine container: node:14.17-alpine
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2.3.4 uses: actions/checkout@v2.3.4

View File

@@ -1,4 +1,4 @@
FROM node:14.16-alpine AS BUILD_IMAGE FROM node:14.17-alpine AS BUILD_IMAGE
WORKDIR /app WORKDIR /app
@@ -31,7 +31,7 @@ RUN touch config/DOCKER
RUN echo "{\"commitTag\": \"${COMMIT_TAG}\"}" > committag.json RUN echo "{\"commitTag\": \"${COMMIT_TAG}\"}" > committag.json
FROM node:14.16-alpine FROM node:14.17-alpine
WORKDIR /app WORKDIR /app

View File

@@ -1,4 +1,4 @@
FROM node:14.16-alpine FROM node:14.17-alpine
COPY . /app COPY . /app
WORKDIR /app WORKDIR /app

View File

@@ -1,5 +1,5 @@
<p align="center"> <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>
<p align="center"> <p align="center">
<img src="https://github.com/sct/overseerr/workflows/Overseerr%20Release/badge.svg?branch=master" alt="Overseerr Release" /> <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://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> <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 --> <!-- 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 --> <!-- ALL-CONTRIBUTORS-BADGE:END -->
</p> </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/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/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/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> </tr>
</table> </table>

View File

@@ -145,7 +145,8 @@ location ^~ /overseerr {
sub_filter '/android-' '/$app/android-'; sub_filter '/android-' '/$app/android-';
sub_filter '/apple-' '/$app/apple-'; sub_filter '/apple-' '/$app/apple-';
sub_filter '/favicon' '/$app/favicon'; 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'; sub_filter '/site.webmanifest' '/$app/site.webmanifest';
} }
``` ```

View File

@@ -16,7 +16,7 @@ After running Overseerr for the first time, configure it by visiting the web UI
```bash ```bash
docker run -d \ docker run -d \
--name overseerr \ --name overseerr \
-e LOG_LEVEL=info \ -e LOG_LEVEL=debug \
-e TZ=Asia/Tokyo \ -e TZ=Asia/Tokyo \
-p 5055:5055 \ -p 5055:5055 \
-v /path/to/appdata/config:/app/config \ -v /path/to/appdata/config:/app/config \
@@ -39,7 +39,7 @@ services:
image: sctx/overseerr:latest image: sctx/overseerr:latest
container_name: overseerr container_name: overseerr
environment: environment:
- LOG_LEVEL=info - LOG_LEVEL=debug
- TZ=Asia/Tokyo - TZ=Asia/Tokyo
ports: ports:
- 5055:5055 - 5055:5055
@@ -56,7 +56,7 @@ services:
docker run -d \ docker run -d \
--name overseerr \ --name overseerr \
--user=[ user | user:group | uid | uid:gid | user:gid | uid:group ] \ --user=[ user | user:group | uid | uid:gid | user:gid | uid:group ] \
-e LOG_LEVEL=info \ -e LOG_LEVEL=debug \
-e TZ=Asia/Tokyo \ -e TZ=Asia/Tokyo \
-p 5055:5055 \ -p 5055:5055 \
-v /path/to/appdata/config:/app/config \ -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 ## 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" %} {% 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 %} {% endhint %}
First, create a volume to store the configuration data for Overseerr using using either the Docker CLI:
```bash ```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" %} {% 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. 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.) **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 %} {% endhint %}
## Linux ## Linux

View File

@@ -35,6 +35,6 @@ Try to answer the following questions:
## How can I share my logs? ## 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. 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). 3. **Share the link/URL to your secret gist** in the [`#support` channel in our Discord server](https://discord.gg/overseerr).

View File

@@ -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. 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 ### SMTP Username & Password

1
next-env.d.ts vendored
View File

@@ -1,2 +1,3 @@
/// <reference types="next" /> /// <reference types="next" />
/// <reference types="next/types/global" /> /// <reference types="next/types/global" />
/// <reference types="next/image-types/global" />

View File

@@ -5,9 +5,6 @@ module.exports = {
images: { images: {
domains: ['image.tmdb.org'], domains: ['image.tmdb.org'],
}, },
future: {
webpack5: true,
},
webpack(config) { webpack(config) {
config.module.rules.push({ config.module.rules.push({
test: /\.svg$/, test: /\.svg$/,

View File

@@ -52,9 +52,15 @@ components:
email: email:
type: string type: string
example: 'hey@itsme.com' example: 'hey@itsme.com'
readOnly: true
username:
type: string
plexToken: plexToken:
type: string type: string
readOnly: true readOnly: true
plexUsername:
type: string
readOnly: true
userType: userType:
type: integer type: integer
example: 1 example: 1
@@ -77,13 +83,6 @@ components:
type: number type: number
example: 5 example: 5
readOnly: true readOnly: true
requests:
type: array
readOnly: true
items:
$ref: '#/components/schemas/MediaRequest'
settings:
$ref: '#/components/schemas/UserSettings'
required: required:
- id - id
- email - email
@@ -92,11 +91,11 @@ components:
UserSettings: UserSettings:
type: object type: object
properties: properties:
discordId: locale:
type: string type: string
region: region:
type: string type: string
language: originalLanguage:
type: string type: string
MainSettings: MainSettings:
type: object type: object
@@ -398,7 +397,6 @@ components:
activeLanguageProfileId: activeLanguageProfileId:
type: number type: number
example: 1 example: 1
nullable: true
activeAnimeProfileId: activeAnimeProfileId:
type: number type: number
nullable: true nullable: true
@@ -408,6 +406,7 @@ components:
activeAnimeProfileName: activeAnimeProfileName:
type: string type: string
example: 720p/1080p example: 720p/1080p
nullable: true
activeAnimeDirectory: activeAnimeDirectory:
type: string type: string
nullable: true nullable: true
@@ -769,6 +768,10 @@ components:
$ref: '#/components/schemas/ExternalIds' $ref: '#/components/schemas/ExternalIds'
mediaInfo: mediaInfo:
$ref: '#/components/schemas/MediaInfo' $ref: '#/components/schemas/MediaInfo'
watchProviders:
type: array
items:
$ref: '#/components/schemas/WatchProviders'
Episode: Episode:
type: object type: object
properties: properties:
@@ -943,6 +946,10 @@ components:
$ref: '#/components/schemas/Keyword' $ref: '#/components/schemas/Keyword'
mediaInfo: mediaInfo:
$ref: '#/components/schemas/MediaInfo' $ref: '#/components/schemas/MediaInfo'
watchProviders:
type: array
items:
$ref: '#/components/schemas/WatchProviders'
MediaRequest: MediaRequest:
type: object type: object
properties: properties:
@@ -953,7 +960,7 @@ components:
status: status:
type: number type: number
example: 0 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 readOnly: true
media: media:
$ref: '#/components/schemas/MediaInfo' $ref: '#/components/schemas/MediaInfo'
@@ -1587,9 +1594,8 @@ components:
UserSettingsNotifications: UserSettingsNotifications:
type: object type: object
properties: properties:
notificationAgents: notificationTypes:
type: number $ref: '#/components/schemas/NotificationAgentTypes'
example: 0
emailEnabled: emailEnabled:
type: boolean type: boolean
pgpKey: pgpKey:
@@ -1614,6 +1620,52 @@ components:
telegramSendSilently: telegramSendSilently:
type: boolean type: boolean
nullable: true 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: securitySchemes:
cookieAuth: cookieAuth:
type: apiKey type: apiKey
@@ -1842,8 +1894,8 @@ paths:
$ref: '#/components/schemas/PlexLibrary' $ref: '#/components/schemas/PlexLibrary'
/settings/plex/devices/servers: /settings/plex/devices/servers:
get: get:
summary: Gets the user's available plex servers summary: Gets the user's available Plex servers
description: Returns a list of available plex servers and their connectivity state description: Returns a list of available Plex servers and their connectivity state
tags: tags:
- settings - settings
responses: responses:
@@ -2980,7 +3032,15 @@ paths:
content: content:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/User' type: object
properties:
email:
type: string
example: 'hey@itsme.com'
username:
type: string
permissions:
type: number
responses: responses:
'201': '201':
description: The created user description: The created user
@@ -2991,7 +3051,7 @@ paths:
put: put:
summary: Update batch of users summary: Update batch of users
description: | 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. Requires the `MANAGE_USERS` permission.
tags: tags:
@@ -3018,7 +3078,6 @@ paths:
type: array type: array
items: items:
$ref: '#/components/schemas/User' $ref: '#/components/schemas/User'
/user/import-from-plex: /user/import-from-plex:
post: post:
summary: Import all users from Plex summary: Import all users from Plex
@@ -3067,7 +3126,7 @@ paths:
get: get:
summary: Get user by ID summary: Get user by ID
description: | 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: tags:
- users - users
parameters: parameters:
@@ -4180,6 +4239,9 @@ paths:
type: string type: string
languageProfileId: languageProfileId:
type: number type: number
userId:
type: number
nullable: true
required: required:
- mediaType - mediaType
- mediaId - mediaId

View File

@@ -17,11 +17,11 @@
}, },
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@headlessui/react": "^1.2.0", "@headlessui/react": "^1.3.0",
"@heroicons/react": "^1.0.1", "@heroicons/react": "^1.0.1",
"@supercharge/request-ip": "^1.1.2", "@supercharge/request-ip": "^1.1.2",
"@svgr/webpack": "^5.5.0", "@svgr/webpack": "^5.5.0",
"@tanem/react-nprogress": "^3.0.67", "@tanem/react-nprogress": "^3.0.70",
"ace-builds": "^1.4.12", "ace-builds": "^1.4.12",
"axios": "^0.21.1", "axios": "^0.21.1",
"bcrypt": "^5.0.1", "bcrypt": "^5.0.1",
@@ -33,18 +33,18 @@
"csurf": "^1.11.0", "csurf": "^1.11.0",
"email-templates": "^8.0.7", "email-templates": "^8.0.7",
"express": "^4.17.1", "express": "^4.17.1",
"express-openapi-validator": "^4.12.11", "express-openapi-validator": "^4.12.14",
"express-rate-limit": "^5.2.6", "express-rate-limit": "^5.2.6",
"express-session": "^1.17.2", "express-session": "^1.17.2",
"formik": "^2.2.9", "formik": "^2.2.9",
"gravatar-url": "3.1.0", "gravatar-url": "3.1.0",
"intl": "^1.2.5", "intl": "^1.2.5",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"next": "10.1.3", "next": "11.0.1",
"node-cache": "^5.1.2", "node-cache": "^5.1.2",
"node-schedule": "^2.0.0", "node-schedule": "^2.0.0",
"nodemailer": "^6.6.1", "nodemailer": "^6.6.2",
"openpgp": "^5.0.0-2", "openpgp": "^5.0.0-3",
"plex-api": "^5.3.1", "plex-api": "^5.3.1",
"pug": "^3.0.2", "pug": "^3.0.2",
"react": "17.0.2", "react": "17.0.2",
@@ -52,7 +52,7 @@
"react-animate-height": "^2.0.23", "react-animate-height": "^2.0.23",
"react-dom": "17.0.2", "react-dom": "17.0.2",
"react-intersection-observer": "^8.32.0", "react-intersection-observer": "^8.32.0",
"react-intl": "5.19.0", "react-intl": "5.20.3",
"react-markdown": "^6.0.2", "react-markdown": "^6.0.2",
"react-select": "^4.3.1", "react-select": "^4.3.1",
"react-spring": "^9.2.3", "react-spring": "^9.2.3",
@@ -61,12 +61,11 @@
"react-truncate-markup": "^5.1.0", "react-truncate-markup": "^5.1.0",
"react-use-clipboard": "1.0.7", "react-use-clipboard": "1.0.7",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"secure-random-password": "^0.2.2", "secure-random-password": "^0.2.3",
"sqlite3": "^5.0.2", "sqlite3": "^5.0.2",
"swagger-ui-express": "^4.1.6", "swagger-ui-express": "^4.1.6",
"swr": "^0.5.6", "swr": "^0.5.6",
"typeorm": "0.2.32", "typeorm": "0.2.32",
"uuid": "^8.3.2",
"web-push": "^3.4.4", "web-push": "^3.4.4",
"winston": "^3.3.3", "winston": "^3.3.3",
"winston-daily-rotate-file": "^4.5.5", "winston-daily-rotate-file": "^4.5.5",
@@ -75,7 +74,7 @@
"yup": "^0.32.9" "yup": "^0.32.9"
}, },
"devDependencies": { "devDependencies": {
"@babel/cli": "^7.14.3", "@babel/cli": "^7.14.5",
"@commitlint/cli": "^12.1.4", "@commitlint/cli": "^12.1.4",
"@commitlint/config-conventional": "^12.1.4", "@commitlint/config-conventional": "^12.1.4",
"@semantic-release/changelog": "^5.0.1", "@semantic-release/changelog": "^5.0.1",
@@ -91,35 +90,35 @@
"@types/csurf": "^1.11.1", "@types/csurf": "^1.11.1",
"@types/email-templates": "^8.0.3", "@types/email-templates": "^8.0.3",
"@types/express": "^4.17.12", "@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/express-session": "^1.17.3",
"@types/lodash": "^4.14.170", "@types/lodash": "^4.14.170",
"@types/node": "^15.6.1", "@types/node": "^15.6.1",
"@types/node-schedule": "^1.3.1", "@types/node-schedule": "^1.3.1",
"@types/nodemailer": "^6.4.2", "@types/nodemailer": "^6.4.2",
"@types/react": "^17.0.9", "@types/react": "^17.0.11",
"@types/react-dom": "^17.0.6", "@types/react-dom": "^17.0.8",
"@types/react-select": "^4.0.15", "@types/react-select": "^4.0.15",
"@types/react-toast-notifications": "^2.4.1", "@types/react-toast-notifications": "^2.4.1",
"@types/react-transition-group": "^4.4.1", "@types/react-transition-group": "^4.4.1",
"@types/secure-random-password": "^0.2.0", "@types/secure-random-password": "^0.2.0",
"@types/swagger-ui-express": "^4.1.2", "@types/swagger-ui-express": "^4.1.2",
"@types/uuid": "^8.3.0", "@types/web-push": "^3.3.1",
"@types/web-push": "^3.3.0",
"@types/xml2js": "^0.4.8", "@types/xml2js": "^0.4.8",
"@types/yamljs": "^0.2.31", "@types/yamljs": "^0.2.31",
"@types/yup": "^0.29.11", "@types/yup": "^0.29.11",
"@typescript-eslint/eslint-plugin": "^4.26.0", "@typescript-eslint/eslint-plugin": "^4.28.0",
"@typescript-eslint/parser": "^4.26.0", "@typescript-eslint/parser": "^4.28.0",
"autoprefixer": "^10.2.6", "autoprefixer": "^10.2.6",
"babel-plugin-react-intl": "^8.2.25", "babel-plugin-react-intl": "^8.2.25",
"babel-plugin-react-intl-auto": "^3.3.0", "babel-plugin-react-intl-auto": "^3.3.0",
"commitizen": "^4.2.4", "commitizen": "^4.2.4",
"copyfiles": "^2.4.1", "copyfiles": "^2.4.1",
"cz-conventional-changelog": "^3.3.0", "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-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-jsx-a11y": "^6.4.1",
"eslint-plugin-prettier": "^3.4.0", "eslint-plugin-prettier": "^3.4.0",
"eslint-plugin-react": "^7.24.0", "eslint-plugin-react": "^7.24.0",
@@ -128,13 +127,13 @@
"husky": "4.3.8", "husky": "4.3.8",
"lint-staged": "^11.0.0", "lint-staged": "^11.0.0",
"nodemon": "^2.0.7", "nodemon": "^2.0.7",
"postcss": "^8.3.0", "postcss": "^8.3.5",
"prettier": "^2.3.1", "prettier": "^2.3.1",
"semantic-release": "^17.4.3", "semantic-release": "^17.4.4",
"semantic-release-docker-buildx": "^1.0.1", "semantic-release-docker-buildx": "^1.0.1",
"tailwindcss": "^2.1.4", "tailwindcss": "^2.2.2",
"ts-node": "^9.1.1", "ts-node": "^10.0.0",
"typescript": "^4.3.2" "typescript": "^4.3.4"
}, },
"resolutions": { "resolutions": {
"sqlite3/node-gyp": "^5.1.0" "sqlite3/node-gyp": "^5.1.0"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 762 B

After

Width:  |  Height:  |  Size: 774 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

BIN
public/logo_full.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

1
public/logo_full.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.8 KiB

1
public/logo_stacked.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.8 KiB

View File

@@ -31,7 +31,7 @@
<body> <body>
<h1>You are offline</h1> <h1>You are offline</h1>
<button type="button"> Reload</button> <button type="button"> Reload</button>
<!-- Inline the page's JavaScript file. --> <!-- Inline the page's JavaScript file. -->
<script> <script>

1
public/os_icon.svg Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 455 KiB

After

Width:  |  Height:  |  Size: 504 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -142,7 +142,7 @@ class PlexAPI {
`/library/sections/${id}/all` `/library/sections/${id}/all`
); );
return response.MediaContainer.Metadata; return response.MediaContainer.Metadata ?? [];
} }
public async getMetadata( public async getMetadata(

View File

@@ -1,39 +1,28 @@
import cacheManager from '../lib/cache'; import cacheManager from '../lib/cache';
import ExternalAPI from './externalapi'; import ExternalAPI from './externalapi';
interface RTMovieOldSearchResult { interface RTSearchResult {
id: number; meterClass: 'certified_fresh' | 'fresh' | 'rotten';
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';
meterScore: number; meterScore: number;
url: string; url: string;
}
interface RTTvSearchResult extends RTSearchResult {
title: string;
startYear: number; startYear: number;
endYear: number; endYear: number;
} }
interface RTMovieSearchResult extends RTSearchResult {
interface RTMovieSearchResponse { name: string;
total: number; url: string;
movies: RTMovieOldSearchResult[]; year: number;
} }
interface RTMultiSearchResponse { interface RTMultiSearchResponse {
tvCount: number; tvCount: number;
tvSeries: RTTvSearchResult[]; tvSeries: RTTvSearchResult[];
movieCount: number;
movies: RTMovieSearchResult[];
} }
export interface RTRating { export interface RTRating {
@@ -88,19 +77,19 @@ class RottenTomatoes extends ExternalAPI {
year: number year: number
): Promise<RTRating | null> { ): Promise<RTRating | null> {
try { try {
const data = await this.get<RTMovieSearchResponse>('/v1.0/movies', { const data = await this.get<RTMultiSearchResponse>('/v2.0/search/', {
params: { q: name }, params: { q: name, limit: 10 },
}); });
// First, attempt to match exact name and year // First, attempt to match exact name and year
let movie = data.movies.find( 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 we don't find a movie, try to match partial name and year
if (!movie) { if (!movie) {
movie = data.movies.find( 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 // One last try, try exact name match only
if (!movie) { if (!movie) {
movie = data.movies.find((movie) => movie.title === name); movie = data.movies.find((movie) => movie.name === name);
} }
if (!movie) { if (!movie) {
@@ -119,12 +108,15 @@ class RottenTomatoes extends ExternalAPI {
} }
return { return {
title: movie.title, title: movie.name,
url: movie.links.alternate, url: movie.url,
criticsRating: movie.ratings.critics_rating, criticsRating:
criticsScore: movie.ratings.critics_score, movie.meterClass === 'certified_fresh'
audienceRating: movie.ratings.audience_rating, ? 'Certified Fresh'
audienceScore: movie.ratings.audience_score, : movie.meterClass === 'fresh'
? 'Fresh'
: 'Rotten',
criticsScore: movie.meterScore,
year: movie.year, year: movie.year,
}; };
} catch (e) { } catch (e) {

View File

@@ -170,7 +170,8 @@ class TheMovieDb extends ExternalAPI {
{ {
params: { params: {
language, language,
append_to_response: 'credits,external_ids,videos,release_dates', append_to_response:
'credits,external_ids,videos,release_dates,watch/providers',
}, },
}, },
43200 43200
@@ -196,7 +197,7 @@ class TheMovieDb extends ExternalAPI {
params: { params: {
language, language,
append_to_response: append_to_response:
'aggregate_credits,credits,external_ids,keywords,videos,content_ratings', 'aggregate_credits,credits,external_ids,keywords,videos,content_ratings,watch/providers',
}, },
}, },
43200 43200

View File

@@ -166,6 +166,10 @@ export interface TmdbMovieDetails {
}; };
external_ids: TmdbExternalIds; external_ids: TmdbExternalIds;
videos: TmdbVideoResult; videos: TmdbVideoResult;
'watch/providers'?: {
id: number;
results?: { [iso_3166_1: string]: TmdbWatchProviders };
};
} }
export interface TmdbVideo { export interface TmdbVideo {
@@ -269,6 +273,10 @@ export interface TmdbTvDetails {
results: TmdbKeyword[]; results: TmdbKeyword[];
}; };
videos: TmdbVideoResult; videos: TmdbVideoResult;
'watch/providers'?: {
id: number;
results?: { [iso_3166_1: string]: TmdbWatchProviders };
};
} }
export interface TmdbVideoResult { export interface TmdbVideoResult {
@@ -401,3 +409,16 @@ export interface TmdbNetwork {
logo_path?: string; logo_path?: string;
origin_country?: 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;
}

View File

@@ -1,4 +1,4 @@
import { isEqual } from 'lodash'; import { isEqual, truncate } from 'lodash';
import { import {
AfterInsert, AfterInsert,
AfterRemove, AfterRemove,
@@ -145,7 +145,11 @@ export class MediaRequest {
subject: `${movie.title}${ subject: `${movie.title}${
movie.release_date ? ` (${movie.release_date.slice(0, 4)})` : '' 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}`, image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`,
media, media,
request: this, request: this,
@@ -158,7 +162,11 @@ export class MediaRequest {
subject: `${tv.name}${ subject: `${tv.name}${
tv.first_air_date ? ` (${tv.first_air_date.slice(0, 4)})` : '' 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}`, image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${tv.poster_path}`,
media, media,
extra: [ extra: [
@@ -217,7 +225,11 @@ export class MediaRequest {
subject: `${movie.title}${ subject: `${movie.title}${
movie.release_date ? ` (${movie.release_date.slice(0, 4)})` : '' 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}`, image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`,
notifyUser: autoApproved ? undefined : this.requestedBy, notifyUser: autoApproved ? undefined : this.requestedBy,
media, media,
@@ -236,7 +248,11 @@ export class MediaRequest {
subject: `${tv.name}${ subject: `${tv.name}${
tv.first_air_date ? ` (${tv.first_air_date.slice(0, 4)})` : '' 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}`, image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${tv.poster_path}`,
notifyUser: autoApproved ? undefined : this.requestedBy, notifyUser: autoApproved ? undefined : this.requestedBy,
media, media,
@@ -495,7 +511,11 @@ export class MediaRequest {
subject: `${movie.title}${ subject: `${movie.title}${
movie.release_date ? ` (${movie.release_date.slice(0, 4)})` : '' movie.release_date ? ` (${movie.release_date.slice(0, 4)})` : ''
}`, }`,
message: movie.overview, message: truncate(movie.overview, {
length: 500,
separator: /\s/,
omission: '…',
}),
media, media,
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`, image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`,
request: this, request: this,
@@ -707,7 +727,11 @@ export class MediaRequest {
? ` (${series.first_air_date.slice(0, 4)})` ? ` (${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}`, image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${series.poster_path}`,
media, media,
extra: [ extra: [

View File

@@ -1,4 +1,5 @@
import bcrypt from 'bcrypt'; import bcrypt from 'bcrypt';
import { randomUUID } from 'crypto';
import path from 'path'; import path from 'path';
import { default as generatePassword } from 'secure-random-password'; import { default as generatePassword } from 'secure-random-password';
import { import {
@@ -15,7 +16,6 @@ import {
RelationCount, RelationCount,
UpdateDateColumn, UpdateDateColumn,
} from 'typeorm'; } from 'typeorm';
import { v4 as uuid } from 'uuid';
import { MediaRequestStatus, MediaType } from '../constants/media'; import { MediaRequestStatus, MediaType } from '../constants/media';
import { UserType } from '../constants/user'; import { UserType } from '../constants/user';
import { QuotaResponse } from '../interfaces/api/userInterfaces'; import { QuotaResponse } from '../interfaces/api/userInterfaces';
@@ -189,7 +189,7 @@ export class User {
} }
public async resetPassword(): Promise<void> { public async resetPassword(): Promise<void> {
const guid = uuid(); const guid = randomUUID();
this.resetPasswordGuid = guid; this.resetPasswordGuid = guid;
// 24 hours into the future // 24 hours into the future
@@ -212,7 +212,7 @@ export class User {
}, },
locals: { locals: {
resetPasswordLink, resetPasswordLink,
applicationUrl: resetPasswordLink, applicationUrl,
applicationTitle, applicationTitle,
}, },
}); });
@@ -307,7 +307,7 @@ export class User {
limit: movieQuotaLimit, limit: movieQuotaLimit,
used: movieQuotaUsed, used: movieQuotaUsed,
remaining: movieQuotaLimit remaining: movieQuotaLimit
? movieQuotaLimit - movieQuotaUsed ? Math.max(0, movieQuotaLimit - movieQuotaUsed)
: undefined, : undefined,
restricted: restricted:
movieQuotaLimit && movieQuotaLimit - movieQuotaUsed <= 0 movieQuotaLimit && movieQuotaLimit - movieQuotaUsed <= 0
@@ -318,7 +318,9 @@ export class User {
days: tvQuotaDays, days: tvQuotaDays,
limit: tvQuotaLimit, limit: tvQuotaLimit,
used: tvQuotaUsed, used: tvQuotaUsed,
remaining: tvQuotaLimit ? tvQuotaLimit - tvQuotaUsed : undefined, remaining: tvQuotaLimit
? Math.max(0, tvQuotaLimit - tvQuotaUsed)
: undefined,
restricted: restricted:
tvQuotaLimit && tvQuotaLimit - tvQuotaUsed <= 0 ? true : false, tvQuotaLimit && tvQuotaLimit - tvQuotaUsed <= 0 ? true : false,
}, },

View File

@@ -1,4 +1,4 @@
import crypto from 'crypto'; import { randomBytes } from 'crypto';
import * as openpgp from 'openpgp'; import * as openpgp from 'openpgp';
import { Transform, TransformCallback } from 'stream'; import { Transform, TransformCallback } from 'stream';
@@ -41,7 +41,7 @@ class PGPEncryptor extends Transform {
const validPublicKeys = await Promise.all( const validPublicKeys = await Promise.all(
this._encryptionKeys.map((armoredKey) => openpgp.readKey({ armoredKey })) 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 // Just return the message if there is no one to encrypt for
if (!validPublicKeys.length) { if (!validPublicKeys.length) {
@@ -51,7 +51,7 @@ class PGPEncryptor extends Transform {
// Only sign the message if private key and password exist // Only sign the message if private key and password exist
if (this._signingKey && this._password) { if (this._signingKey && this._password) {
privateKey = await openpgp.readKey({ privateKey = await openpgp.readPrivateKey({
armoredKey: this._signingKey, armoredKey: this._signingKey,
}); });
@@ -107,7 +107,7 @@ class PGPEncryptor extends Transform {
} }
// Generate a new boundary for the email content // 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 * Concatenate everything into single strings
* and add pgp headers to the email headers * and add pgp headers to the email headers
@@ -135,8 +135,8 @@ class PGPEncryptor extends Transform {
emailPartDelimiter + emailPartDelimiter +
messageParts.join(emailPartDelimiter), messageParts.join(emailPartDelimiter),
}), }),
publicKeys: validPublicKeys, encryptionKeys: validPublicKeys,
privateKeys: privateKey, signingKeys: privateKey,
}); });
const body = const body =

View File

@@ -190,6 +190,7 @@ class WebPushAgent
const allSubs = await userPushSubRepository const allSubs = await userPushSubRepository
.createQueryBuilder('pushSub') .createQueryBuilder('pushSub')
.leftJoinAndSelect('pushSub.user', 'user')
.where('pushSub.userId IN (:users)', { .where('pushSub.userId IN (:users)', {
users: manageUsers.map((user) => user.id), users: manageUsers.map((user) => user.id),
}) })

View File

@@ -1,5 +1,5 @@
import { randomUUID } from 'crypto';
import { getRepository } from 'typeorm'; import { getRepository } from 'typeorm';
import { v4 as uuid } from 'uuid';
import TheMovieDb from '../../api/themoviedb'; import TheMovieDb from '../../api/themoviedb';
import { MediaStatus, MediaType } from '../../constants/media'; import { MediaStatus, MediaType } from '../../constants/media';
import Media from '../../entity/Media'; import Media from '../../entity/Media';
@@ -512,7 +512,7 @@ class BaseScanner<T> {
*/ */
protected startRun(): string { protected startRun(): string {
const settings = getSettings(); const settings = getSettings();
const sessionId = uuid(); const sessionId = randomUUID();
this.sessionId = sessionId; this.sessionId = sessionId;
this.log('Scan starting', 'info', { sessionId }); this.log('Scan starting', 'info', { sessionId });

View File

@@ -7,9 +7,9 @@ import { User } from '../../../entity/User';
import { getSettings, Library } from '../../settings'; import { getSettings, Library } from '../../settings';
import BaseScanner, { import BaseScanner, {
MediaIds, MediaIds,
ProcessableSeason,
RunnableScanner, RunnableScanner,
StatusBase, StatusBase,
ProcessableSeason,
} from '../baseScanner'; } from '../baseScanner';
const imdbRegex = new RegExp(/imdb:\/\/(tt[0-9]+)/); const imdbRegex = new RegExp(/imdb:\/\/(tt[0-9]+)/);

View File

@@ -1,7 +1,7 @@
import { randomUUID } from 'crypto';
import fs from 'fs'; import fs from 'fs';
import { merge } from 'lodash'; import { merge } from 'lodash';
import path from 'path'; import path from 'path';
import { v4 as uuidv4 } from 'uuid';
import webpush from 'web-push'; import webpush from 'web-push';
import { Permission } from './permissions'; import { Permission } from './permissions';
@@ -234,7 +234,7 @@ class Settings {
constructor(initialSettings?: AllSettings) { constructor(initialSettings?: AllSettings) {
this.data = { this.data = {
clientId: uuidv4(), clientId: randomUUID(),
vapidPrivate: '', vapidPrivate: '',
vapidPublic: '', vapidPublic: '',
main: { main: {
@@ -428,7 +428,7 @@ class Settings {
get clientId(): string { get clientId(): string {
if (!this.data.clientId) { if (!this.data.clientId) {
this.data.clientId = uuidv4(); this.data.clientId = randomUUID();
this.save(); this.save();
} }
@@ -454,7 +454,7 @@ class Settings {
} }
private generateApiKey(): string { private generateApiKey(): string {
return Buffer.from(`${Date.now()}${uuidv4()})`).toString('base64'); return Buffer.from(`${Date.now()}${randomUUID()})`).toString('base64');
} }
private generateVapidKeys(force = false): void { private generateVapidKeys(force = false): void {

View File

@@ -3,18 +3,20 @@ import type {
TmdbMovieReleaseResult, TmdbMovieReleaseResult,
TmdbProductionCompany, TmdbProductionCompany,
} from '../api/themoviedb/interfaces'; } from '../api/themoviedb/interfaces';
import Media from '../entity/Media';
import { import {
ProductionCompany,
Genre,
Cast, Cast,
Crew, Crew,
ExternalIds,
Genre,
mapCast, mapCast,
mapCrew, mapCrew,
ExternalIds,
mapExternalIds, mapExternalIds,
mapVideos, mapVideos,
mapWatchProviders,
ProductionCompany,
WatchProviders,
} from './common'; } from './common';
import Media from '../entity/Media';
export interface Video { export interface Video {
url?: string; url?: string;
@@ -78,6 +80,7 @@ export interface MovieDetails {
mediaInfo?: Media; mediaInfo?: Media;
externalIds: ExternalIds; externalIds: ExternalIds;
plexUrl?: string; plexUrl?: string;
watchProviders?: WatchProviders[];
} }
export const mapProductionCompany = ( export const mapProductionCompany = (
@@ -136,4 +139,5 @@ export const mapMovieDetails = (
: undefined, : undefined,
externalIds: mapExternalIds(movie.external_ids), externalIds: mapExternalIds(movie.external_ids),
mediaInfo: media, mediaInfo: media,
watchProviders: mapWatchProviders(movie['watch/providers']?.results ?? {}),
}); });

View File

@@ -1,25 +1,27 @@
import {
Genre,
ProductionCompany,
Cast,
Crew,
mapAggregateCast,
mapCrew,
ExternalIds,
mapExternalIds,
Keyword,
mapVideos,
TvNetwork,
} from './common';
import type { import type {
TmdbTvEpisodeResult,
TmdbTvSeasonResult,
TmdbTvDetails,
TmdbSeasonWithEpisodes,
TmdbTvRatingResult,
TmdbNetwork, TmdbNetwork,
TmdbSeasonWithEpisodes,
TmdbTvDetails,
TmdbTvEpisodeResult,
TmdbTvRatingResult,
TmdbTvSeasonResult,
} from '../api/themoviedb/interfaces'; } from '../api/themoviedb/interfaces';
import type Media from '../entity/Media'; 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'; import { Video } from './Movie';
interface Episode { interface Episode {
@@ -102,6 +104,7 @@ export interface TvDetails {
externalIds: ExternalIds; externalIds: ExternalIds;
keywords: Keyword[]; keywords: Keyword[];
mediaInfo?: Media; mediaInfo?: Media;
watchProviders?: WatchProviders[];
} }
const mapEpisodeResult = (episode: TmdbTvEpisodeResult): Episode => ({ const mapEpisodeResult = (episode: TmdbTvEpisodeResult): Episode => ({
@@ -213,4 +216,5 @@ export const mapTvDetails = (
name: keyword.name, name: keyword.name,
})), })),
mediaInfo: media, mediaInfo: media,
watchProviders: mapWatchProviders(show['watch/providers']?.results ?? {}),
}); });

View File

@@ -1,12 +1,13 @@
import type { import type {
TmdbCreditCast,
TmdbAggregateCreditCast, TmdbAggregateCreditCast,
TmdbCreditCast,
TmdbCreditCrew, TmdbCreditCrew,
TmdbExternalIds, TmdbExternalIds,
TmdbVideo, TmdbVideo,
TmdbVideoResult, TmdbVideoResult,
TmdbWatchProviderDetails,
TmdbWatchProviders,
} from '../api/themoviedb/interfaces'; } from '../api/themoviedb/interfaces';
import { Video } from '../models/Movie'; import { Video } from '../models/Movie';
export interface ProductionCompany { export interface ProductionCompany {
@@ -70,6 +71,20 @@ export interface ExternalIds {
twitterId?: string; 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 => ({ export const mapCast = (person: TmdbCreditCast): Cast => ({
castId: person.cast_id, castId: person.cast_id,
character: person.character, character: person.character,
@@ -124,7 +139,33 @@ export const mapVideos = (videoResult: TmdbVideoResult): Video[] =>
url: siteUrlCreator(site, key), 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 => 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]); }[site]);

View File

@@ -350,6 +350,14 @@ requestRoutes.post('/', async (req, res, next) => {
status: 202, status: 202,
message: 'No seasons available to request', 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); await mediaRepository.save(media);

View File

@@ -1,3 +1,4 @@
import { truncate } from 'lodash';
import { import {
EntitySubscriberInterface, EntitySubscriberInterface,
EventSubscriber, EventSubscriber,
@@ -34,7 +35,11 @@ export class MediaSubscriber implements EntitySubscriberInterface {
subject: `${movie.title}${ subject: `${movie.title}${
movie.release_date ? ` (${movie.release_date.slice(0, 4)})` : '' movie.release_date ? ` (${movie.release_date.slice(0, 4)})` : ''
}`, }`,
message: movie.overview, message: truncate(movie.overview, {
length: 500,
separator: /\s/,
omission: '…',
}),
media: entity, media: entity,
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`, image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`,
request: request, request: request,
@@ -89,7 +94,11 @@ export class MediaSubscriber implements EntitySubscriberInterface {
subject: `${tv.name}${ subject: `${tv.name}${
tv.first_air_date ? ` (${tv.first_air_date.slice(0, 4)})` : '' 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, notifyUser: request.requestedBy,
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${tv.poster_path}`, image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${tv.poster_path}`,
media: entity, media: entity,

View File

@@ -5,7 +5,7 @@ head
meta(http-equiv='x-ua-compatible' content='ie=edge') meta(http-equiv='x-ua-compatible' content='ie=edge')
meta(name='viewport' content='width=device-width, initial-scale=1') meta(name='viewport' content='width=device-width, initial-scale=1')
meta(name='format-detection' content='telephone=no, date=no, address=no, email=no') 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&amp;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 //if mso
xml xml
o:officedocumentsettings o:officedocumentsettings
@@ -26,72 +26,37 @@ head
mso-line-height-rule: exactly; mso-line-height-rule: exactly;
} }
style. style.
@media (max-width: 600px) { .title:hover * {
.sm-w-full { text-decoration: underline;
}
@media only screen and (max-width:600px) {
table {
font-size: 20px !important;
width: 100% !important; width: 100% !important;
} }
} }
div(role='article' aria-roledescription='email' aria-label='' lang='en') div(style='display: block; background-color: #111827;')
table(style="\ table(style='margin: 0 auto; font-family: Inter, Arial, Sans-Serif; color: #fff; font-size: 16px; width: 26rem;')
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')
tr tr
td(align='center') td(style="text-align: center;")
table(style='width: 100%' width='100%' cellpadding='0' cellspacing='0' role='presentation') a(href=applicationUrl)
tr img(src=applicationUrl +'/logo_full.png' style='width: 26rem; padding: 1rem; image-rendering: crisp-edges; image-rendering: -webkit-optimize-contrast;')
td(align='center' style='\ tr
padding-top: 25px;\ td(style='text-align: center;')
padding-bottom: 25px;\ div(style='margin: 0rem 1rem 1rem; font-size: 1.25em;')
text-align: center;\ span
') | An account has been created for you at #{applicationTitle}.
a(href=applicationUrl style='\ tr
text-shadow: 0 1px 0 #ffffff;\ td(style='text-align: center;')
font-weight: 700;\ div(style='margin: 1rem 1rem 1rem; font-size: 1.25em;')
font-size: 24px;\ span
color: #a8aaaf;\ | Your new password is:
text-decoration: none;\ div(style='margin: 0rem 1rem 1rem; font-size: 1.25em;')
') span
| #{applicationTitle} | #{password}
tr if applicationUrl
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')
tr tr
td(align='center' style='font-size: 16px; padding: 45px') td
p(style='\ a(href=applicationUrl style='display: block; margin: 1.5rem 3rem 2.5rem 3rem; text-decoration: none; font-size: 1.0em; line-height: 2.25em;')
font-size: 13px;\ 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);')
line-height: 24px;\ | Open #{applicationTitle}
margin-top: 6px;\
margin-bottom: 20px;\
text-align: center;\
color: #a8aaaf;\
')
| #{applicationTitle}

View File

@@ -5,7 +5,7 @@ head
meta(http-equiv='x-ua-compatible' content='ie=edge') meta(http-equiv='x-ua-compatible' content='ie=edge')
meta(name='viewport' content='width=device-width, initial-scale=1') meta(name='viewport' content='width=device-width, initial-scale=1')
meta(name='format-detection' content='telephone=no, date=no, address=no, email=no') 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&amp;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 //if mso
xml xml
o:officedocumentsettings o:officedocumentsettings
@@ -26,92 +26,56 @@ head
mso-line-height-rule: exactly; mso-line-height-rule: exactly;
} }
style. style.
@media (max-width: 600px) { .title:hover * {
.sm-w-full { text-decoration: underline;
}
@media only screen and (max-width:600px) {
table {
font-size: 20px !important;
width: 100% !important; width: 100% !important;
} }
} }
div(role='article' aria-roledescription='email' aria-label='' lang='en') div(style='display: block; background-color: #111827;')
table(style="\ table(style='margin: 0 auto; font-family: Inter, Arial, Sans-Serif; color: #fff; font-size: 16px; width: 26rem;')
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')
tr tr
td(align='center') td(style="text-align: center;")
table(style='width: 100%' width='100%' cellpadding='0' cellspacing='0' role='presentation') a(href=applicationUrl)
tr img(src=applicationUrl +'/logo_full.png' style='width: 26rem; padding: 1rem; image-rendering: crisp-edges; image-rendering: -webkit-optimize-contrast;')
td(align='center' style='\ tr
padding-top: 25px;\ td(style='text-align: center;')
padding-bottom: 25px;\ div(style='margin: 0rem 1rem 1rem; font-size: 1.25em;')
text-align: center;\ span
') | #{body}
a(href=applicationUrl style='\ tr
text-shadow: 0 1px 0 #ffffff;\ td
font-weight: 700;\ 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')
font-size: 24px;\ table(style='color: #fff; width: 100%;')
color: #a8aaaf;\ tr
text-decoration: none;\ 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')
| #{applicationTitle} span
tr | #{mediaName}
td(style='width: 100%' width='100%') 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;')
table.sm-w-full(align='center' style='\ span(style='display: block;')
background-color: #ffffff;\ b(style='color: #9ca3af; font-weight: 700;')
margin-left: auto;\ | Requested By&nbsp;
margin-right: auto;\ | #{requestedBy}
width: 570px;\ each extra in mediaExtra
' width='570' bgcolor='#ffffff' cellpadding='0' cellspacing='0' role='presentation') span(style='display: block;')
tr b(style='color: #9ca3af; font-weight: 700;')
td(style='padding: 45px') | #{extra.name}&nbsp;
div(style='font-size: 16px') | #{extra.value}
| #{body} td(rowspan='2' style='width: 7rem;')
br a(style='display: block; width: 7rem; overflow: hidden; border-radius: .375rem;' href=actionUrl)
br div(style='overflow: hidden; box-sizing: border-box; margin: 0px;')
p(style='margin-top: 4px; text-align: center') 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%;')
b tr
| #{mediaName} td(style='font-size: .85em; color: #9ca3af; line-height: 1em; vertical-align: bottom; margin-right: 1rem')
each extra in mediaExtra span
br | #{timestamp}
| #{extra.name}:&nbsp; if actionUrl
| #{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')
tr tr
td(align='center' style='font-size: 16px; padding: 45px') td
p(style='\ a(href=actionUrl style='display: block; margin: 1.5rem 3rem 2.5rem 3rem; text-decoration: none; font-size: 1.0em; line-height: 2.25em;')
font-size: 13px;\ 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);')
line-height: 24px;\ | Open in #{applicationTitle}
margin-top: 6px;\
margin-bottom: 20px;\
text-align: center;\
color: #a8aaaf;\
')
| #{applicationTitle}

View File

@@ -5,7 +5,7 @@ head
meta(http-equiv='x-ua-compatible' content='ie=edge') meta(http-equiv='x-ua-compatible' content='ie=edge')
meta(name='viewport' content='width=device-width, initial-scale=1') meta(name='viewport' content='width=device-width, initial-scale=1')
meta(name='format-detection' content='telephone=no, date=no, address=no, email=no') 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&amp;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 //if mso
xml xml
o:officedocumentsettings o:officedocumentsettings
@@ -26,74 +26,34 @@ head
mso-line-height-rule: exactly; mso-line-height-rule: exactly;
} }
style. style.
@media (max-width: 600px) { .title:hover * {
.sm-w-full { text-decoration: underline;
}
@media only screen and (max-width:600px) {
table {
font-size: 20px !important;
width: 100% !important; width: 100% !important;
} }
} }
div(role='article' aria-roledescription='email' aria-label='' lang='en') div(style='display: block; background-color: #111827;')
table(style="\ table(style='margin: 0 auto; font-family: Inter, Arial, Sans-Serif; color: #fff; font-size: 16px; width: 26rem;')
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')
tr tr
td(align='center') td(style="text-align: center;")
table(style='width: 100%' width='100%' cellpadding='0' cellspacing='0' role='presentation') a(href=applicationUrl)
tr img(src=applicationUrl +'/logo_full.png' style='width: 26rem; padding: 1rem; image-rendering: crisp-edges; image-rendering: -webkit-optimize-contrast;')
td(align='center' style='\ tr
padding-top: 25px;\ td(style='text-align: center;')
padding-bottom: 25px;\ div(style='margin: 0rem 1rem 1rem; font-size: 1.25em;')
text-align: center;\ span
') | Your #{applicationTitle} account password was requested to be reset. Click below to reset your password.
a(href=applicationUrl style='\ if resetPasswordLink
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')
tr tr
td(align='center' style='font-size: 16px; padding: 45px') td
p(style='\ a(href=resetPasswordLink style='display: block; margin: 1.5rem 3rem 2.5rem 3rem; text-decoration: none; font-size: 1.0em; line-height: 2.25em;')
font-size: 13px;\ 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);')
line-height: 24px;\ | Reset Password
margin-top: 6px;\ tr
margin-bottom: 20px;\ td(style='text-align: center;')
text-align: center;\ div(style='margin: 1rem; font-size: .85em;')
color: #a8aaaf;\ span
') | If you did not request that your password be reset, you can safely ignore this email.
| #{applicationTitle}.

View File

@@ -5,7 +5,7 @@ head
meta(http-equiv='x-ua-compatible' content='ie=edge') meta(http-equiv='x-ua-compatible' content='ie=edge')
meta(name='viewport' content='width=device-width, initial-scale=1') meta(name='viewport' content='width=device-width, initial-scale=1')
meta(name='format-detection' content='telephone=no, date=no, address=no, email=no') 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&amp;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 //if mso
xml xml
o:officedocumentsettings o:officedocumentsettings
@@ -26,70 +26,29 @@ head
mso-line-height-rule: exactly; mso-line-height-rule: exactly;
} }
style. style.
@media (max-width: 600px) { .title:hover * {
.sm-w-full { text-decoration: underline;
}
@media only screen and (max-width:600px) {
table {
font-size: 20px !important;
width: 100% !important; width: 100% !important;
} }
} }
div(role='article' aria-roledescription='email' aria-label='' lang='en') div(style='display: block; background-color: #111827;')
table(style="\ table(style='margin: 0 auto; font-family: Inter, Arial, Sans-Serif; color: #fff; font-size: 16px; width: 26rem;')
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')
tr tr
td(align='center') td(style="text-align: center;")
table(style='width: 100%' width='100%' cellpadding='0' cellspacing='0' role='presentation') a(href=applicationUrl)
tr img(src=applicationUrl +'/logo_full.png' style='width: 26rem; padding: 1rem; image-rendering: crisp-edges; image-rendering: -webkit-optimize-contrast;')
td(align='center' style='\ tr
padding-top: 25px;\ td(style='text-align: center;')
padding-bottom: 25px;\ div(style='margin: 0rem 1rem 1rem; font-size: 1.25em;')
text-align: center;\ span
') | #{body}
a(href=applicationUrl style='\ if applicationUrl
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')
tr tr
td(align='center' style='font-size: 16px; padding: 45px') td
p(style='\ a(href=applicationUrl style='display: block; margin: 1.5rem 3rem 2.5rem 3rem; text-decoration: none; font-size: 1.0em; line-height: 2.25em;')
font-size: 13px;\ 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);')
line-height: 24px;\ | Open #{applicationTitle}
margin-top: 6px;\
margin-bottom: 20px;\
text-align: center;\
color: #a8aaaf;\
')
| #{applicationTitle}

View File

@@ -11,7 +11,7 @@ confinement: strict
parts: parts:
overseerr: overseerr:
plugin: nodejs plugin: nodejs
nodejs-version: '14.16.1' nodejs-version: '14.17.0'
nodejs-package-manager: 'yarn' nodejs-package-manager: 'yarn'
nodejs-yarn-version: v1.22.10 nodejs-yarn-version: v1.22.10
build-packages: build-packages:

View File

@@ -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

View File

@@ -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

View File

@@ -13,7 +13,7 @@ interface AlertProps {
const Alert: React.FC<AlertProps> = ({ title, children, type }) => { const Alert: React.FC<AlertProps> = ({ title, children, type }) => {
let design = { let design = {
bgColor: 'bg-yellow-600', bgColor: 'bg-yellow-600',
titleColor: 'text-yellow-200', titleColor: 'text-yellow-100',
textColor: 'text-yellow-300', textColor: 'text-yellow-300',
svg: <ExclamationIcon className="w-5 h-5" />, svg: <ExclamationIcon className="w-5 h-5" />,
}; };
@@ -22,7 +22,7 @@ const Alert: React.FC<AlertProps> = ({ title, children, type }) => {
case 'info': case 'info':
design = { design = {
bgColor: 'bg-indigo-600', bgColor: 'bg-indigo-600',
titleColor: 'text-indigo-200', titleColor: 'text-indigo-100',
textColor: 'text-indigo-300', textColor: 'text-indigo-300',
svg: <InformationCircleIcon className="w-5 h-5" />, svg: <InformationCircleIcon className="w-5 h-5" />,
}; };
@@ -30,7 +30,7 @@ const Alert: React.FC<AlertProps> = ({ title, children, type }) => {
case 'error': case 'error':
design = { design = {
bgColor: 'bg-red-600', bgColor: 'bg-red-600',
titleColor: 'text-red-200', titleColor: 'text-red-100',
textColor: 'text-red-300', textColor: 'text-red-300',
svg: <XCircleIcon className="w-5 h-5" />, svg: <XCircleIcon className="w-5 h-5" />,
}; };

View File

@@ -45,7 +45,7 @@ function Button<P extends ElementTypes = 'button'>(
ref?: React.Ref<Element<P>> ref?: React.Ref<Element<P>>
): JSX.Element { ): JSX.Element {
const buttonStyle = [ 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) { switch (buttonType) {
case 'primary': case 'primary':

View File

@@ -10,7 +10,7 @@ const ListItem: React.FC<ListItemProps> = ({ title, className, children }) => {
return ( return (
<div> <div>
<div className="max-w-6xl py-4 sm:grid sm:grid-cols-3 sm:gap-4"> <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"> <dd className="flex text-sm text-white sm:mt-0 sm:col-span-2">
<span className={`flex-grow ${className}`}>{children}</span> <span className={`flex-grow ${className}`}>{children}</span>
</dd> </dd>

View File

@@ -137,7 +137,7 @@ const Modal: React.FC<ModalProps> = ({
> >
{title && ( {title && (
<span <span
className="text-lg font-medium leading-6 text-white" className="text-lg font-bold leading-6 text-white"
id="modal-headline" id="modal-headline"
> >
{title} {title}

View File

@@ -46,7 +46,7 @@ const SettingsLink: React.FC<{
if (tabType === 'button') { if (tabType === 'button') {
linkClasses = 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'; activeLinkColor = 'bg-indigo-700';
inactiveLinkColor = 'bg-gray-800 hover:bg-gray-700 focus:bg-gray-700'; inactiveLinkColor = 'bg-gray-800 hover:bg-gray-700 focus:bg-gray-700';
} }
@@ -119,8 +119,8 @@ const SettingsTabs: React.FC<{
</select> </select>
</div> </div>
{tabType === 'button' ? ( {tabType === 'button' ? (
<div className="hidden overflow-x-scroll overflow-y-hidden sm:block hide-scrollbar"> <div className="hidden sm:block">
<nav className="flex space-x-4" aria-label="Tabs"> <nav className="flex flex-wrap -mx-2 -my-1" aria-label="Tabs">
{settingsRoutes.map((route, index) => ( {settingsRoutes.map((route, index) => (
<SettingsLink <SettingsLink
tabType={tabType} tabType={tabType}

View File

@@ -73,7 +73,7 @@ const SlideOver: React.FC<SlideOverProps> = ({
<div className="flex flex-col h-full overflow-y-scroll bg-gray-700 shadow-xl"> <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"> <header className="px-4 space-y-1 bg-indigo-600 slideover">
<div className="flex items-center justify-between space-x-3"> <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} {title}
</h2> </h2>
<div className="flex items-center h-7"> <div className="flex items-center h-7">

View File

@@ -13,7 +13,7 @@ const TH: React.FC<AllHTMLAttributes<HTMLTableHeaderCellElement>> = ({
...props ...props
}) => { }) => {
const style = [ 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) { if (className) {
@@ -39,7 +39,7 @@ const TD: React.FC<TDProps> = ({
className, className,
...props ...props
}) => { }) => {
const style = ['whitespace-nowrap text-sm leading-5 text-white']; const style = ['text-sm leading-5 text-white'];
switch (alignText) { switch (alignText) {
case 'left': case 'left':
@@ -54,7 +54,7 @@ const TD: React.FC<TDProps> = ({
} }
if (!noPadding) { if (!noPadding) {
style.push('px-6 py-4'); style.push('px-4 py-4');
} }
if (className) { if (className) {
@@ -73,7 +73,7 @@ const Table: React.FC = ({ children }) => {
<div className="flex flex-col"> <div className="flex flex-col">
<div className="my-2 -mx-4 overflow-x-auto md:mx-0 lg:mx-0"> <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="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> <table className="min-w-full">{children}</table>
</div> </div>
</div> </div>

View File

@@ -10,7 +10,7 @@ import useLocale from '../../../hooks/useLocale';
import Transition from '../../Transition'; import Transition from '../../Transition';
const messages = defineMessages({ const messages = defineMessages({
changelanguage: 'Change Language', displaylanguage: 'Display Language',
}); });
const LanguagePicker: React.FC = () => { const LanguagePicker: React.FC = () => {
@@ -50,9 +50,9 @@ const LanguagePicker: React.FC = () => {
<div> <div>
<label <label
htmlFor="language" 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> </label>
<select <select
id="language" id="language"

View File

@@ -13,7 +13,7 @@ const SearchInput: React.FC = () => {
const { searchValue, setSearchValue, setIsOpen, clear } = useSearchInput(); const { searchValue, setSearchValue, setIsOpen, clear } = useSearchInput();
return ( return (
<div className="flex flex-1"> <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"> <label htmlFor="search_field" className="sr-only">
Search Search
</label> </label>
@@ -24,7 +24,7 @@ const SearchInput: React.FC = () => {
<input <input
id="search_field" id="search_field"
style={{ paddingRight: searchValue.length > 0 ? '1.75rem' : '' }} 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)} placeholder={intl.formatMessage(messages.searchPlaceholder)}
type="search" type="search"
inputMode="search" inputMode="search"

View File

@@ -2,9 +2,9 @@ import {
ClockIcon, ClockIcon,
CogIcon, CogIcon,
SparklesIcon, SparklesIcon,
UsersIcon,
XIcon, XIcon,
} from '@heroicons/react/outline'; } from '@heroicons/react/outline';
import { UsersIcon } from '@heroicons/react/solid';
import Link from 'next/link'; import Link from 'next/link';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React, { ReactNode, useRef } from 'react'; import React, { ReactNode, useRef } from 'react';
@@ -39,34 +39,26 @@ const SidebarLinks: SidebarLinkProps[] = [
{ {
href: '/', href: '/',
messagesKey: 'dashboard', messagesKey: 'dashboard',
svgIcon: ( svgIcon: <SparklesIcon className="w-6 h-6 mr-3" />,
<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" />
),
activeRegExp: /^\/(discover\/?(movies|tv)?)?$/, activeRegExp: /^\/(discover\/?(movies|tv)?)?$/,
}, },
{ {
href: '/requests', href: '/requests',
messagesKey: 'requests', messagesKey: 'requests',
svgIcon: ( svgIcon: <ClockIcon className="w-6 h-6 mr-3" />,
<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" />
),
activeRegExp: /^\/requests/, activeRegExp: /^\/requests/,
}, },
{ {
href: '/users', href: '/users',
messagesKey: 'users', messagesKey: 'users',
svgIcon: ( svgIcon: <UsersIcon className="w-6 h-6 mr-3" />,
<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" />
),
activeRegExp: /^\/users/, activeRegExp: /^\/users/,
requiredPermission: Permission.MANAGE_USERS, requiredPermission: Permission.MANAGE_USERS,
}, },
{ {
href: '/settings', href: '/settings',
messagesKey: 'settings', messagesKey: 'settings',
svgIcon: ( svgIcon: <CogIcon className="w-6 h-6 mr-3" />,
<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" />
),
activeRegExp: /^\/settings/, activeRegExp: /^\/settings/,
requiredPermission: Permission.MANAGE_SETTINGS, requiredPermission: Permission.MANAGE_SETTINGS,
}, },
@@ -81,7 +73,7 @@ const Sidebar: React.FC<SidebarProps> = ({ open, setClosed }) => {
return ( return (
<> <>
<div className="md:hidden"> <div className="lg:hidden">
<Transition show={open}> <Transition show={open}>
<div className="fixed inset-0 z-40 flex"> <div className="fixed inset-0 z-40 flex">
<Transition <Transition
@@ -93,7 +85,7 @@ const Sidebar: React.FC<SidebarProps> = ({ open, setClosed }) => {
leaveTo="opacity-0" leaveTo="opacity-0"
> >
<div className="fixed inset-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> </div>
</Transition> </Transition>
<Transition <Transition
@@ -117,16 +109,16 @@ const Sidebar: React.FC<SidebarProps> = ({ open, setClosed }) => {
</div> </div>
<div <div
ref={navRef} 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"> <div className="flex items-center flex-shrink-0 px-2">
<span className="text-xl text-gray-50"> <span className="px-4 text-xl text-gray-50">
<a href="/"> <a href="/">
<img src="/logo.png" alt="Logo" /> <img src="/logo_full.svg" alt="Logo" />
</a> </a>
</span> </span>
</div> </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) => {SidebarLinks.filter((link) =>
link.requiredPermission link.requiredPermission
? hasPermission(link.requiredPermission) ? hasPermission(link.requiredPermission)
@@ -147,13 +139,13 @@ const Sidebar: React.FC<SidebarProps> = ({ open, setClosed }) => {
}} }}
role="button" role="button"
tabIndex={0} 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( router.pathname.match(
sidebarLink.activeRegExp 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> </nav>
{hasPermission(Permission.ADMIN) && ( {hasPermission(Permission.ADMIN) && (
<VersionStatus onClick={() => setClosed()} /> <div className="px-2">
<VersionStatus onClick={() => setClosed()} />
</div>
)} )}
</div> </div>
</div> </div>
@@ -180,18 +174,18 @@ const Sidebar: React.FC<SidebarProps> = ({ open, setClosed }) => {
</Transition> </Transition>
</div> </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 w-64 sidebar">
<div className="flex flex-col flex-1 h-0 bg-gray-800"> <div className="flex flex-col flex-1 h-0">
<div className="flex flex-col flex-1 pt-5 pb-4 overflow-y-auto"> <div className="flex flex-col flex-1 pt-8 pb-4 overflow-y-auto">
<div className="flex items-center flex-shrink-0 px-4"> <div className="flex items-center flex-shrink-0">
<span className="text-2xl text-gray-50"> <span className="px-4 text-2xl text-gray-50">
<a href="/"> <a href="/">
<img src="/logo.png" alt="Logo" /> <img src="/logo_full.svg" alt="Logo" />
</a> </a>
</span> </span>
</div> </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) => {SidebarLinks.filter((link) =>
link.requiredPermission link.requiredPermission
? hasPermission(link.requiredPermission) ? hasPermission(link.requiredPermission)
@@ -204,13 +198,13 @@ const Sidebar: React.FC<SidebarProps> = ({ open, setClosed }) => {
as={sidebarLink.as} as={sidebarLink.as}
> >
<a <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( router.pathname.match(
sidebarLink.activeRegExp 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> </nav>
{hasPermission(Permission.ADMIN) && <VersionStatus />} {hasPermission(Permission.ADMIN) && (
<div className="px-2">
<VersionStatus />
</div>
)}
</div> </div>
</div> </div>
</div> </div>

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