mirror of
https://github.com/jorenn92/Maintainerr.git
synced 2026-02-07 00:45:47 +01:00
chore: Use yarn workspaces & add Turborepo
- Fix eslint in UI project and fix identified issues - Update prettier configs to new format - Remove prettier eslint plugin as VSCode is configured to format on save - Add GitHub workflow to check formatting - Remove unused dependencies - Remove yarn requirement in final image - Use node v20 types & upgrade TS - Add an example of a cross-project reference
This commit is contained in:
@@ -1,3 +1,7 @@
|
||||
node_modules
|
||||
.yarn
|
||||
**/node_modules/
|
||||
.git
|
||||
data/
|
||||
server/dist
|
||||
ui/.next
|
||||
Dockerfile
|
||||
**/*.md
|
||||
35
.github/workflows/quality.yml
vendored
Normal file
35
.github/workflows/quality.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
name: Code quality checks
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
formatting:
|
||||
name: Formatting
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- name: Activate corepack
|
||||
run: |
|
||||
corepack install
|
||||
corepack enable
|
||||
- name: Install dependencies
|
||||
run: yarn --immutable
|
||||
- name: Check formatting
|
||||
run: yarn turbo format:check
|
||||
2
.github/workflows/run_jest_tests.yml
vendored
2
.github/workflows/run_jest_tests.yml
vendored
@@ -31,4 +31,4 @@ jobs:
|
||||
run: yarn --immutable
|
||||
|
||||
- name: Run tests
|
||||
run: yarn test
|
||||
run: yarn turbo test
|
||||
|
||||
12
.gitignore
vendored
12
.gitignore
vendored
@@ -1,5 +1,11 @@
|
||||
data/
|
||||
docs-output/
|
||||
node_modules
|
||||
.yarn
|
||||
.idea
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
.idea
|
||||
.turbo
|
||||
7
.vscode/settings.json
vendored
7
.vscode/settings.json
vendored
@@ -29,11 +29,14 @@
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[jsonc]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[css]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"typescript.preferences.importModuleSpecifier": "relative",
|
||||
"files.associations": {
|
||||
"globals.css": "tailwindcss"
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
934
.yarn/releases/yarn-4.5.1.cjs
vendored
Normal file
934
.yarn/releases/yarn-4.5.1.cjs
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -1,3 +1,5 @@
|
||||
nodeLinker: node-modules
|
||||
# yarnPath: .yarn/releases/yarn-4.0.2.cjs
|
||||
enableGlobalCache: false
|
||||
|
||||
nodeLinker: node-modules
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.5.1.cjs
|
||||
|
||||
@@ -7,7 +7,7 @@ All help is welcome and greatly appreciated! If you would like to contribute to
|
||||
### Tools Required
|
||||
|
||||
- HTML/Typescript/Javascript editor
|
||||
- [VSCode](https://code.visualstudio.com/) is recommended. Upon opening the project, a few extensions will be automatically recommended for install.
|
||||
- [VSCode](https://code.visualstudio.com/) is recommended. Upon opening the project, a few extensions will be automatically recommended for install.
|
||||
- [NodeJS](https://nodejs.org/en/download/) (Node 20.x or higher)
|
||||
- [Git](https://git-scm.com/downloads)
|
||||
|
||||
@@ -43,35 +43,34 @@ All help is welcome and greatly appreciated! If you would like to contribute to
|
||||
- `docs`
|
||||
- `feature`
|
||||
- `fix`
|
||||
- `patch`
|
||||
- `patch`
|
||||
|
||||
4. Activate the correct Yarn version. (*Note: In order to run `corepack enable`, you will need to be running cmd or Powershell as an Administrator.*)
|
||||
4. Activate the correct Yarn version. (_Note: In order to run `corepack enable`, you will need to be running cmd or PowerShell as an Administrator._)
|
||||
|
||||
```bash
|
||||
```bash
|
||||
corepack install
|
||||
corepack enable
|
||||
```
|
||||
|
||||
5. Install dependencies
|
||||
|
||||
```bash
|
||||
yarn
|
||||
```bash
|
||||
yarn
|
||||
```
|
||||
|
||||
6. As of Maintainerr v2.0, the project looks to ensure you have read/write permissions on the `data` directory. This `data` directory does not exist when you first clone your fork. Before running the below commands, create a folder inside of your main Maintainerr directory named `data`, and ensure it has full permissions to the `Everyone` user.
|
||||
|
||||
```bash
|
||||
example -> C:\Users\You\Documents\GitRepos\Maintainerr\data
|
||||
```
|
||||
|
||||
7. Run the development commands (you will need two different cmd/Powershell terminals. One for each command.)
|
||||
|
||||
```bash
|
||||
yarn dev:server
|
||||
yarn dev:ui
|
||||
example -> C:\Users\You\Documents\GitRepos\Maintainerr\data
|
||||
```
|
||||
|
||||
- If the build fails with Powershell, try to use cmd instead.
|
||||
7. Run the development command
|
||||
|
||||
```bash
|
||||
yarn dev
|
||||
```
|
||||
|
||||
- If the build fails with PowerShell, try to use cmd instead.
|
||||
|
||||
8. Make your code changes/improvements and test that they work as intended.
|
||||
|
||||
@@ -94,8 +93,8 @@ All help is welcome and greatly appreciated! If you would like to contribute to
|
||||
- Always rebase your commit to the latest `main` branch. Do **not** merge `main` into your branch.
|
||||
- It is your responsibility to keep your branch up-to-date. Your work will **not** be merged unless it is rebased off the latest `main` branch.
|
||||
- You can create a "draft" pull request early to get feedback on your work.
|
||||
- Your code **must** be formatted correctly, or the tests will fail.
|
||||
- We use Prettier to format our code base. It should automatically run with a Git hook, but it is recommended to have the Prettier extension installed in your editor and format on save.
|
||||
- Your code **must** be formatted correctly.
|
||||
- We use Prettier to format our code base. It is recommended to have the Prettier extension installed in your editor and format on save.
|
||||
- If you have questions or need help, you can reach out via [Discussions](https://github.com/jorenn92/Maintainerr/discussions) or our [Discord server](https://discord.gg/WP4ZW2QYwk).
|
||||
|
||||
### UI Text Style
|
||||
|
||||
108
Dockerfile
108
Dockerfile
@@ -1,33 +1,15 @@
|
||||
FROM node:20-alpine3.19 AS builder
|
||||
FROM node:20-alpine3.19 AS base
|
||||
LABEL Description="Contains the Maintainerr Docker image"
|
||||
|
||||
WORKDIR /opt/app/
|
||||
FROM base AS builder
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
ENV TARGETPLATFORM=${TARGETPLATFORM:-linux/amd64}
|
||||
WORKDIR /app
|
||||
|
||||
COPY server/ ./server/
|
||||
COPY ui/ ./ui/
|
||||
COPY package.json ./package.json
|
||||
COPY yarn.lock ./yarn.lock
|
||||
COPY .yarnrc.yml ./.yarnrc.yml
|
||||
|
||||
# Enable correct yarn version
|
||||
RUN corepack install && \
|
||||
corepack enable
|
||||
|
||||
RUN apk --update --no-cache add python3 make g++ curl
|
||||
|
||||
RUN yarn --immutable --network-timeout 99999999
|
||||
|
||||
RUN \
|
||||
case "${TARGETPLATFORM}" in ('linux/arm64' | 'linux/amd64') \
|
||||
yarn add sharp \
|
||||
;; \
|
||||
esac
|
||||
|
||||
RUN yarn build:server
|
||||
RUN yarn global add turbo@^2
|
||||
COPY . .
|
||||
|
||||
RUN yarn install --network-timeout 99999999
|
||||
RUN yarn cache clean
|
||||
|
||||
RUN <<EOF cat >> ./ui/.env
|
||||
NEXT_PUBLIC_BASE_PATH=/__PATH_PREFIX__
|
||||
@@ -35,36 +17,38 @@ EOF
|
||||
|
||||
RUN sed -i "s,basePath: '',basePath: '/__PATH_PREFIX__',g" ./ui/next.config.js
|
||||
|
||||
RUN yarn build:ui
|
||||
RUN yarn turbo build
|
||||
|
||||
# copy standalone UI
|
||||
RUN mv ./ui/.next/standalone/ui/ ./standalone-ui/ && \
|
||||
mv ./ui/.next/standalone/ ./standalone-ui/ && \
|
||||
mv ./ui/.next/static ./standalone-ui/.next/static && \
|
||||
mv ./ui/public ./standalone-ui/public && \
|
||||
rm -rf ./ui && \
|
||||
mv ./standalone-ui ./ui
|
||||
FROM base AS runner
|
||||
|
||||
WORKDIR /opt/app
|
||||
|
||||
# copy standalone UI
|
||||
COPY --from=builder --chmod=777 --chown=node:node /app/ui/.next/standalone/ui ./ui
|
||||
COPY --from=builder --chmod=777 --chown=node:node /app/ui/.next/static ./ui/.next/static
|
||||
COPY --from=builder --chmod=777 --chown=node:node /app/ui/public ./ui/public
|
||||
|
||||
# Copy standalone server
|
||||
RUN mv ./server/dist ./standalone-server && \
|
||||
rm -rf ./server && \
|
||||
mv ./standalone-server ./server
|
||||
COPY --from=builder --chmod=777 --chown=node:node /app/server/dist ./server
|
||||
COPY --from=builder --chmod=777 --chown=node:node /app/server/node_modules ./server/node_modules
|
||||
|
||||
RUN rm -rf node_modules .yarn
|
||||
COPY docker/supervisord.conf /etc/supervisord.conf
|
||||
COPY --chmod=777 --chown=node:node docker/start.sh /opt/app/start.sh
|
||||
|
||||
RUN yarn workspaces focus --production
|
||||
|
||||
RUN rm -rf .yarn && \
|
||||
rm -rf /opt/yarn-* && \
|
||||
chown -R node:node /opt/ && \
|
||||
chmod -R 755 /opt/ && \
|
||||
# Data dir
|
||||
mkdir -m 777 /opt/data && \
|
||||
# Create required directories
|
||||
RUN mkdir -m 777 /opt/data && \
|
||||
mkdir -m 777 /opt/data/logs && \
|
||||
chown -R node:node /opt/data
|
||||
|
||||
# Final build
|
||||
FROM node:20-alpine3.19
|
||||
# This is required for docker user directive to work
|
||||
RUN chmod 777 /opt/app/start.sh && \
|
||||
chmod 777 /opt/app/ui && \
|
||||
chmod 777 /opt/app/ui/public && \
|
||||
chmod 777 /opt/app/ui/.next/static && \
|
||||
mkdir -m 777 /opt/app/ui/.next/cache && \
|
||||
chown -R node:node /opt/app/ui/.next/cache
|
||||
|
||||
RUN apk --update --no-cache add curl supervisor
|
||||
|
||||
ARG NODE_ENV=production
|
||||
ENV NODE_ENV=${NODE_ENV}
|
||||
@@ -85,6 +69,8 @@ ENV UI_HOSTNAME=${UI_HOSTNAME}
|
||||
ARG GIT_SHA
|
||||
ENV GIT_SHA=$GIT_SHA
|
||||
|
||||
ENV DATA_DIR=/opt/data
|
||||
|
||||
# container version type. develop, stable, edge,.. a release=stable
|
||||
ARG VERSION_TAG=develop
|
||||
ENV VERSION_TAG=$VERSION_TAG
|
||||
@@ -92,39 +78,11 @@ ENV VERSION_TAG=$VERSION_TAG
|
||||
ARG BASE_PATH
|
||||
ENV BASE_PATH=${BASE_PATH}
|
||||
|
||||
# Set global yarn vars to a folder read/write able for all users
|
||||
ENV YARN_INSTALL_STATE_PATH=/tmp/.yarn/install-state.gz
|
||||
ENV YARN_GLOBAL_FOLDER=/tmp/.yarn/global
|
||||
ENV YARN_CACHE_FOLDER=/tmp/.yarn/cache
|
||||
|
||||
# Temporary workaround for https://github.com/libuv/libuv/pull/4141
|
||||
ENV UV_USE_IO_URING=0
|
||||
|
||||
COPY --from=builder --chown=node:node /opt /opt
|
||||
|
||||
WORKDIR /opt/app
|
||||
|
||||
COPY docker/supervisord.conf /etc/supervisord.conf
|
||||
COPY docker/start.sh /opt/app/start.sh
|
||||
|
||||
# Enable correct yarn version, add supervisor & chown root /opt dir
|
||||
RUN corepack install && \
|
||||
corepack enable && \
|
||||
apk add supervisor curl && \
|
||||
chown node:node /opt && \
|
||||
# This is required for docker user directive to work
|
||||
chmod 777 /opt && \
|
||||
chmod 777 /opt/app/start.sh && \
|
||||
chmod 777 -R /opt/app/ui && \
|
||||
mkdir -m 777 /.cache && \
|
||||
mkdir -pm 777 /opt/app/ui/.next/cache && \
|
||||
chown -R node:node /opt/app/ui/.next/cache
|
||||
|
||||
USER node
|
||||
|
||||
# Picked up for Node's .cache directory.
|
||||
ENV HOME=/
|
||||
|
||||
EXPOSE 6246
|
||||
|
||||
VOLUME [ "/opt/data" ]
|
||||
|
||||
@@ -4,7 +4,7 @@ logfile=/dev/null
|
||||
pidfile=/dev/null
|
||||
|
||||
[program:server]
|
||||
command=yarn node /opt/app/server/main.js
|
||||
command=node /opt/app/server/main.js
|
||||
autorestart=true
|
||||
startretries=100
|
||||
stdout_logfile=/dev/fd/1
|
||||
@@ -13,7 +13,7 @@ redirect_stderr=true
|
||||
|
||||
[program:ui]
|
||||
environment=PORT=%(ENV_UI_PORT)s,HOSTNAME=%(ENV_UI_HOSTNAME)s
|
||||
command=yarn node /opt/app/ui/server.js
|
||||
command=node /opt/app/ui/server.js
|
||||
autorestart=true
|
||||
startretries=100
|
||||
stdout_logfile=/dev/fd/1
|
||||
|
||||
130
package.json
130
package.json
@@ -2,133 +2,37 @@
|
||||
"name": "maintainerr",
|
||||
"version": "2.2.1",
|
||||
"private": true,
|
||||
"packageManager": "yarn@4.1.1",
|
||||
"packageManager": "yarn@4.5.1",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/jorenn92/Maintainerr.git"
|
||||
},
|
||||
"license": "MIT",
|
||||
"installConfig": {
|
||||
"hoistingLimits": "workspaces"
|
||||
},
|
||||
"scripts": {
|
||||
"prebuild:server": "cd server && rimraf dist",
|
||||
"dev:ui": "cd ui && next dev",
|
||||
"dev:server": "cd server && nest start --watch",
|
||||
"build:ui": "cd ui && next build",
|
||||
"build:server": "cd server && nest build",
|
||||
"start:ui": "cd ui && next start -p 80",
|
||||
"start:server": "cd server && node dist/main",
|
||||
"lint:ui": "cd ui && next lint",
|
||||
"lint:server": "cd server && eslint \"{src,test}/**/*.ts\" --fix",
|
||||
"test": "jest",
|
||||
"test:clear": "jest --clearCache",
|
||||
"test:watch": "jest --watch",
|
||||
"test:cov": "jest --coverage",
|
||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json",
|
||||
"migration:run": "ts-node node_modules/typeorm/cli.js migration:run -d ./datasource-config.ts",
|
||||
"migration:revert": "ts-node node_modules/typeorm/cli.js migration:revert -d ./datasource-config.ts",
|
||||
"migration:generate": "ts-node node_modules/typeorm/cli.js migration:generate --dataSource ./datasource-config.ts -p"
|
||||
"build": "turbo build",
|
||||
"dev": "turbo dev",
|
||||
"lint": "turbo lint",
|
||||
"format": "turbo format",
|
||||
"format:check": "turbo format:check",
|
||||
"knip": "knip"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/react": "2.2.0",
|
||||
"@heroicons/react": "^1.0.6",
|
||||
"@monaco-editor/react": "^4.6.0",
|
||||
"@nestjs/cli": "^10.4.7",
|
||||
"@nestjs/common": "^10.4.9",
|
||||
"@nestjs/core": "^10.4.7",
|
||||
"@nestjs/platform-express": "^10.4.12",
|
||||
"@nestjs/schedule": "^4.1.1",
|
||||
"@nestjs/typeorm": "^10.0.2",
|
||||
"@types/node": "^22.9.4",
|
||||
"axios": "^1.7.8",
|
||||
"bowser": "^2.11.0",
|
||||
"chalk": "^4.1.2",
|
||||
"cron-validator": "^1.3.1",
|
||||
"crypto": "^1.0.1",
|
||||
"http-server": "^14.1.1",
|
||||
"lodash": "^4.17.21",
|
||||
"nest-winston": "^1.10.0",
|
||||
"next": "15.0.3",
|
||||
"node-cache": "^5.1.2",
|
||||
"path": "^0.12.7",
|
||||
"plex-api": "^5.3.2",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.3.1",
|
||||
"react-select": "^5.8.0",
|
||||
"react-toast-notifications": "^2.5.1",
|
||||
"react-transition-group": "^4.4.5",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
"sqlite3": "^5.1.6",
|
||||
"typeorm": "^0.3.20",
|
||||
"typescript": "^5.3.3",
|
||||
"web-push": "^3.6.6",
|
||||
"winston": "^3.17.0",
|
||||
"winston-daily-rotate-file": "^5.0.0",
|
||||
"xml2js": "^0.6.2",
|
||||
"yaml": "^2.6.0"
|
||||
"workspaces": [
|
||||
"ui",
|
||||
"server"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@automock/jest": "^1.4.0",
|
||||
"@babel/core": "^7.26.0",
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
"@eslint/js": "^9.13.0",
|
||||
"@nestjs/cli": "^10.4.7",
|
||||
"@nestjs/schematics": "^10.2.3",
|
||||
"@nestjs/testing": "^10.4.12",
|
||||
"@semantic-release/changelog": "^6.0.3",
|
||||
"@semantic-release/commit-analyzer": "^13.0.0",
|
||||
"@semantic-release/exec": "^6.0.3",
|
||||
"@semantic-release/git": "^10.0.1",
|
||||
"@svgr/webpack": "^8.1.0",
|
||||
"@tailwindcss/aspect-ratio": "^0.4.2",
|
||||
"@tailwindcss/forms": "^0.5.9",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/lodash": "^4.17.13",
|
||||
"@types/react": "^18.2.73",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/react-transition-group": "^4.4.11",
|
||||
"@types/web-push": "^3.6.4",
|
||||
"@types/xml2js": "^0.4.14",
|
||||
"@typescript-eslint/eslint-plugin": "^8.17.0",
|
||||
"@typescript-eslint/parser": "^8.17.0",
|
||||
"autoprefixer": "10.4.20",
|
||||
"eslint": "^9.15.0",
|
||||
"eslint-config-next": "15.0.3",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"jest": "^29.7.0",
|
||||
"monaco-editor": "0.52.0",
|
||||
"postcss": "^8.4.49",
|
||||
"prettier": "^3.3.3",
|
||||
"prettier-plugin-tailwindcss": "^0.6.9",
|
||||
"@types/node": "^22.9.4",
|
||||
"knip": "^5.36.5",
|
||||
"semantic-release": "^24.2.0",
|
||||
"source-map-support": "^0.5.21",
|
||||
"tailwindcss": "^3.4.15",
|
||||
"ts-jest": "^29.2.5",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsconfig-paths": "^4.2.0"
|
||||
},
|
||||
"jest": {
|
||||
"moduleFileExtensions": [
|
||||
"js",
|
||||
"json",
|
||||
"ts"
|
||||
],
|
||||
"rootDir": "server/src",
|
||||
"testRegex": ".*\\.spec\\.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
},
|
||||
"collectCoverageFrom": [
|
||||
"**/*.(t|j)s"
|
||||
],
|
||||
"coverageDirectory": "./coverage",
|
||||
"testEnvironment": "node"
|
||||
"turbo": "^2.2.3",
|
||||
"typescript": "^5.6.3"
|
||||
},
|
||||
"release": {
|
||||
"plugins": [
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all"
|
||||
}
|
||||
@@ -1,55 +1,58 @@
|
||||
import typescriptEslintEslintPlugin from "@typescript-eslint/eslint-plugin";
|
||||
import globals from "globals";
|
||||
import tsParser from "@typescript-eslint/parser";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import js from "@eslint/js";
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
import typescriptEslintEslintPlugin from '@typescript-eslint/eslint-plugin';
|
||||
import globals from 'globals';
|
||||
import tsParser from '@typescript-eslint/parser';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import js from '@eslint/js';
|
||||
import { FlatCompat } from '@eslint/eslintrc';
|
||||
import eslintConfigPrettier from 'eslint-config-prettier';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
allConfig: js.configs.all
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
allConfig: js.configs.all,
|
||||
});
|
||||
|
||||
export default [{
|
||||
ignores: ["**/eslint.config.mjs"],
|
||||
}, ...compat.extends("plugin:@typescript-eslint/recommended", "plugin:prettier/recommended"), {
|
||||
export default [
|
||||
{
|
||||
ignores: ['**/eslint.config.mjs'],
|
||||
},
|
||||
...compat.extends('plugin:@typescript-eslint/recommended'),
|
||||
{
|
||||
plugins: {
|
||||
"@typescript-eslint": typescriptEslintEslintPlugin,
|
||||
'@typescript-eslint': typescriptEslintEslintPlugin,
|
||||
},
|
||||
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.jest,
|
||||
},
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.jest,
|
||||
},
|
||||
|
||||
parser: tsParser,
|
||||
ecmaVersion: 5,
|
||||
sourceType: "module",
|
||||
parser: tsParser,
|
||||
ecmaVersion: 5,
|
||||
sourceType: 'module',
|
||||
|
||||
parserOptions: {
|
||||
project: "tsconfig.json",
|
||||
tsconfigRootDir: ".",
|
||||
},
|
||||
parserOptions: {
|
||||
project: 'tsconfig.json',
|
||||
tsconfigRootDir: '.',
|
||||
},
|
||||
},
|
||||
|
||||
rules: {
|
||||
"@typescript-eslint/interface-name-prefix": "off",
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
caughtErrors: "none"
|
||||
}
|
||||
],
|
||||
"prettier/prettier": ["error", {
|
||||
endOfLine: "auto",
|
||||
}],
|
||||
'@typescript-eslint/interface-name-prefix': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{
|
||||
caughtErrors: 'none',
|
||||
},
|
||||
],
|
||||
},
|
||||
}];
|
||||
},
|
||||
eslintConfigPrettier,
|
||||
];
|
||||
|
||||
88
server/package.json
Normal file
88
server/package.json
Normal file
@@ -0,0 +1,88 @@
|
||||
{
|
||||
"name": "@maintainerr/server",
|
||||
"private": true,
|
||||
"exports": {
|
||||
"./*": "./src/*.ts"
|
||||
},
|
||||
"installConfig": {
|
||||
"hoistingLimits": "workspaces"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "nest start --watch",
|
||||
"build": "nest build",
|
||||
"start": "node dist/main",
|
||||
"lint": "eslint \"{src,test}/**/*.ts\" --fix",
|
||||
"format": "prettier --write --ignore-path .gitignore .",
|
||||
"format:check": "prettier --check --ignore-path .gitignore .",
|
||||
"test": "jest",
|
||||
"test:clear": "jest --clearCache",
|
||||
"test:watch": "jest --watch",
|
||||
"test:cov": "jest --coverage",
|
||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json",
|
||||
"migration:run": "ts-node node_modules/typeorm/cli.js migration:run -d ./src/datasource-config.ts",
|
||||
"migration:revert": "ts-node node_modules/typeorm/cli.js migration:revert -d ./src/datasource-config.ts",
|
||||
"migration:generate": "ts-node node_modules/typeorm/cli.js migration:generate --dataSource ./src/datasource-config.ts -p"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs/common": "^10.4.9",
|
||||
"@nestjs/core": "^10.4.7",
|
||||
"@nestjs/platform-express": "^10.4.12",
|
||||
"@nestjs/schedule": "^4.1.1",
|
||||
"@nestjs/typeorm": "^10.0.2",
|
||||
"axios": "^1.7.8",
|
||||
"chalk": "^4.1.2",
|
||||
"cron-validator": "^1.3.1",
|
||||
"lodash": "^4.17.21",
|
||||
"nest-winston": "^1.10.0",
|
||||
"node-cache": "^5.1.2",
|
||||
"plex-api": "^5.3.2",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
"sqlite3": "^5.1.6",
|
||||
"typeorm": "^0.3.20",
|
||||
"winston": "^3.17.0",
|
||||
"winston-daily-rotate-file": "^5.0.0",
|
||||
"xml2js": "^0.6.2",
|
||||
"yaml": "^2.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@automock/jest": "^1.4.0",
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
"@eslint/js": "^9.13.0",
|
||||
"@nestjs/cli": "^10.4.7",
|
||||
"@nestjs/schematics": "^10.2.3",
|
||||
"@nestjs/testing": "^10.4.12",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/lodash": "^4.17.13",
|
||||
"@types/node": "^20",
|
||||
"@types/xml2js": "^0.4.14",
|
||||
"@typescript-eslint/eslint-plugin": "^8.17.0",
|
||||
"@typescript-eslint/parser": "^8.17.0",
|
||||
"eslint": "^9.15.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"jest": "^29.7.0",
|
||||
"prettier": "^3.3.3",
|
||||
"ts-jest": "^29.2.5",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"typescript": "^5.6.3"
|
||||
},
|
||||
"jest": {
|
||||
"moduleFileExtensions": [
|
||||
"js",
|
||||
"json",
|
||||
"ts"
|
||||
],
|
||||
"rootDir": "src",
|
||||
"testRegex": ".*\\.spec\\.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
},
|
||||
"collectCoverageFrom": [
|
||||
"**/*.(t|j)s"
|
||||
],
|
||||
"coverageDirectory": "./coverage",
|
||||
"testEnvironment": "node"
|
||||
}
|
||||
}
|
||||
10
server/prettier.config.mjs
Normal file
10
server/prettier.config.mjs
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* @see https://prettier.io/docs/en/configuration.html
|
||||
* @type {import("prettier").Config}
|
||||
*/
|
||||
const config = {
|
||||
singleQuote: true,
|
||||
trailingComma: 'all',
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -1,12 +1,6 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ExternalApiService } from '../modules/api/external-api/external-api.service';
|
||||
|
||||
interface VersionResponse {
|
||||
status: 1 | 0;
|
||||
version: string;
|
||||
commitTag: string;
|
||||
updateAvailable: boolean;
|
||||
}
|
||||
import { VersionResponse } from './dto/version-response.dto';
|
||||
|
||||
@Injectable()
|
||||
export class AppService {
|
||||
|
||||
@@ -15,4 +15,4 @@ const ormConfig: TypeOrmModuleOptions = {
|
||||
autoLoadEntities: true,
|
||||
migrationsRun: true,
|
||||
};
|
||||
export = ormConfig;
|
||||
export default ormConfig;
|
||||
|
||||
6
server/src/app/dto/version-response.dto.ts
Normal file
6
server/src/app/dto/version-response.dto.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface VersionResponse {
|
||||
status: 1 | 0;
|
||||
version: string;
|
||||
commitTag: string;
|
||||
updateAvailable: boolean;
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
import { DataSource } from "typeorm";
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
const datasource = new DataSource({
|
||||
type: "sqlite",
|
||||
database: "./data/maintainerr.sqlite",
|
||||
entities: ["./server/dist/**/*.entities{.ts,.js}"],
|
||||
type: 'sqlite',
|
||||
database: './data/maintainerr.sqlite',
|
||||
entities: ['./dist/**/*.entities{.ts,.js}'],
|
||||
synchronize: false,
|
||||
migrationsTableName: "migrations",
|
||||
migrations: ["./server/dist/database/migrations/**/*{.js,.ts}"],
|
||||
migrationsTableName: 'migrations',
|
||||
migrations: ['./dist/database/migrations/**/*{.js,.ts}'],
|
||||
});
|
||||
|
||||
datasource
|
||||
@@ -7,6 +7,11 @@ import chalk from 'chalk';
|
||||
import path from 'path';
|
||||
import * as fs from 'fs';
|
||||
|
||||
const dataDir =
|
||||
process.env.NODE_ENV === 'production'
|
||||
? '/opt/data'
|
||||
: path.join(__dirname, '../../data');
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule, {
|
||||
// Winston logger config
|
||||
@@ -49,10 +54,7 @@ async function bootstrap() {
|
||||
transports: [
|
||||
new winston.transports.Console(),
|
||||
new DailyRotateFile({
|
||||
filename: path.join(
|
||||
__dirname,
|
||||
`../../data/logs/maintainerr-%DATE%.log`,
|
||||
),
|
||||
filename: path.join(dataDir, 'logs/maintainerr-%DATE%.log'),
|
||||
datePattern: 'YYYY-MM-DD',
|
||||
zippedArchive: true,
|
||||
maxSize: '20m',
|
||||
@@ -78,13 +80,10 @@ async function bootstrap() {
|
||||
function createDataDirectoryStructure() {
|
||||
try {
|
||||
// Check if data directory has read and write permissions
|
||||
fs.accessSync(
|
||||
path.join(__dirname, `../../data`),
|
||||
fs.constants.R_OK | fs.constants.W_OK,
|
||||
);
|
||||
fs.accessSync(dataDir, fs.constants.R_OK | fs.constants.W_OK);
|
||||
|
||||
// create logs dir
|
||||
const dir = path.join(__dirname, `../../data/logs`);
|
||||
const dir = path.join(dataDir, 'logs');
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, {
|
||||
recursive: true,
|
||||
@@ -93,7 +92,7 @@ function createDataDirectoryStructure() {
|
||||
}
|
||||
|
||||
// if db already exists, check r/w permissions
|
||||
const db = path.join(__dirname, `../../data/maintainerr.sqlite`);
|
||||
const db = path.join(dataDir, 'maintainerr.sqlite');
|
||||
if (fs.existsSync(db)) {
|
||||
fs.accessSync(db, fs.constants.R_OK | fs.constants.W_OK);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
RuleConstants,
|
||||
} from '../constants/rules.constants';
|
||||
import { EPlexDataType } from '../../api/plex-api/enums/plex-data-type-enum';
|
||||
import _ from 'lodash';
|
||||
import {
|
||||
TautulliApiService,
|
||||
TautulliHistoryRequestOptions,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
|
||||
import { RadarrSettingDto } from "../dto's/radarr-setting.dto";
|
||||
import { Collection } from '../../collections/entities/collection.entities';
|
||||
|
||||
@Entity()
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
|
||||
import { SonarrSettingDto } from "../dto's/sonarr-setting.dto";
|
||||
import { Collection } from '../../collections/entities/collection.entities';
|
||||
|
||||
@Entity()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { forwardRef, Inject, Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { IsNull, Not, Repository } from 'typeorm';
|
||||
import { Not, Repository } from 'typeorm';
|
||||
import { isValidCron } from 'cron-validator';
|
||||
import { BasicResponseDto } from '../api/external-api/dto/basic-response.dto';
|
||||
import { OverseerrApiService } from '../api/overseerr-api/overseerr-api.service';
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"target": "es2017",
|
||||
"target": "ES2021",
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"baseUrl": "./",
|
||||
|
||||
29
turbo.json
Normal file
29
turbo.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"$schema": "https://turbo.build/schema.json",
|
||||
"ui": "tui",
|
||||
"tasks": {
|
||||
"build": {
|
||||
"dependsOn": ["^build"],
|
||||
"inputs": ["$TURBO_DEFAULT$", ".env*"],
|
||||
"outputs": [".next/**", "!.next/cache/**", "dist/**"]
|
||||
},
|
||||
"lint": {
|
||||
"dependsOn": ["^lint"]
|
||||
},
|
||||
"dev": {
|
||||
"cache": false,
|
||||
"persistent": true
|
||||
},
|
||||
"test": {},
|
||||
"test:watch": {
|
||||
"cache": false,
|
||||
"persistent": true
|
||||
},
|
||||
"format": {
|
||||
"dependsOn": ["^format"]
|
||||
},
|
||||
"format:check": {
|
||||
"dependsOn": ["^format:check"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"extends": "next/core-web-vitals",
|
||||
"rules": {
|
||||
"@next/next/no-html-link-for-pages": "off",
|
||||
"react-hooks/exhaustive-deps": "off"
|
||||
}
|
||||
}
|
||||
28
ui/eslint.config.mjs
Normal file
28
ui/eslint.config.mjs
Normal file
@@ -0,0 +1,28 @@
|
||||
import { FlatCompat } from '@eslint/eslintrc'
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
import eslintConfigPrettier from 'eslint-config-prettier'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
})
|
||||
|
||||
/** @type {import('eslint').Linter.Config[]} */
|
||||
const configs = [
|
||||
...compat.extends('next/core-web-vitals', 'next/typescript'),
|
||||
{
|
||||
rules: {
|
||||
'@next/next/no-html-link-for-pages': 'off',
|
||||
'react-hooks/exhaustive-deps': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/ban-ts-comment': 'off',
|
||||
},
|
||||
},
|
||||
eslintConfigPrettier,
|
||||
]
|
||||
|
||||
export default configs
|
||||
55
ui/package.json
Normal file
55
ui/package.json
Normal file
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"name": "@maintainerr/ui",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start -p 80",
|
||||
"lint": "next lint",
|
||||
"format": "prettier --write --ignore-path .gitignore .",
|
||||
"format:check": "prettier --check --ignore-path .gitignore ."
|
||||
},
|
||||
"installConfig": {
|
||||
"hoistingLimits": "workspaces"
|
||||
},
|
||||
"dependencies": {
|
||||
"@heroicons/react": "^1.0.6",
|
||||
"@maintainerr/server": "workspace:^",
|
||||
"@monaco-editor/react": "^4.6.0",
|
||||
"axios": "^1.7.8",
|
||||
"bowser": "^2.11.0",
|
||||
"cron-validator": "^1.3.1",
|
||||
"lodash": "^4.17.21",
|
||||
"next": "15.0.3",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.3.1",
|
||||
"react-select": "^5.8.0",
|
||||
"react-toast-notifications": "^2.5.1",
|
||||
"react-transition-group": "^4.4.5",
|
||||
"yaml": "^2.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
"@eslint/js": "^9.13.0",
|
||||
"@tailwindcss/aspect-ratio": "^0.4.2",
|
||||
"@tailwindcss/forms": "^0.5.9",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"@types/lodash": "^4.17.13",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"@types/react-transition-group": "^4.4.11",
|
||||
"@typescript-eslint/eslint-plugin": "^8.17.0",
|
||||
"@typescript-eslint/parser": "^8.17.0",
|
||||
"autoprefixer": "10.4.20",
|
||||
"eslint": "^9.15.0",
|
||||
"eslint-config-next": "15.0.3",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"monaco-editor": "0.52.0",
|
||||
"postcss": "^8.4.49",
|
||||
"prettier": "^3.3.3",
|
||||
"prettier-plugin-tailwindcss": "^0.6.9",
|
||||
"tailwindcss": "^3.4.15",
|
||||
"typescript": "^5.6.3"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
9
ui/postcss.config.mjs
Normal file
9
ui/postcss.config.mjs
Normal file
@@ -0,0 +1,9 @@
|
||||
/** @type {import('postcss-load-config').Config} */
|
||||
const config = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
|
||||
export default config
|
||||
@@ -1,7 +0,0 @@
|
||||
module.exports = {
|
||||
arrowParens: 'always',
|
||||
singleQuote: true,
|
||||
tabWidth: 2,
|
||||
semi: false,
|
||||
tailwindConfig: './tailwind.config.js',
|
||||
}
|
||||
14
ui/prettier.config.mjs
Normal file
14
ui/prettier.config.mjs
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* @see https://prettier.io/docs/en/configuration.html
|
||||
* @type {import("prettier").Config}
|
||||
*/
|
||||
const config = {
|
||||
arrowParens: 'always',
|
||||
singleQuote: true,
|
||||
tabWidth: 2,
|
||||
semi: false,
|
||||
tailwindConfig: './tailwind.config.js',
|
||||
plugins: ['prettier-plugin-tailwindcss'],
|
||||
}
|
||||
|
||||
export default config
|
||||
@@ -174,44 +174,44 @@ const AddModal = (props: IAddModal) => {
|
||||
useEffect(() => {
|
||||
setLoading(true)
|
||||
|
||||
props.type === 2
|
||||
? selectedEpisodes !== -1
|
||||
? GetApiHandler(`/collections?typeId=4`).then((resp) => {
|
||||
// get collections for episodes
|
||||
setCollectionOptions([...origCollectionOptions, ...resp])
|
||||
setLoading(false)
|
||||
})
|
||||
: selectedSeasons !== -1
|
||||
? GetApiHandler(`/collections?typeId=3`).then((resp) => {
|
||||
// get collections for episodes and seasons
|
||||
GetApiHandler(`/collections?typeId=4`).then((resp2) => {
|
||||
setCollectionOptions([
|
||||
...origCollectionOptions,
|
||||
...resp,
|
||||
...resp2,
|
||||
])
|
||||
setLoading(false)
|
||||
})
|
||||
})
|
||||
: GetApiHandler(`/collections?typeId=2`).then((resp) => {
|
||||
// get collections for episodes, seasons and shows
|
||||
GetApiHandler(`/collections?typeId=3`).then((resp2) => {
|
||||
GetApiHandler(`/collections?typeId=4`).then((resp3) => {
|
||||
setCollectionOptions([
|
||||
...origCollectionOptions,
|
||||
...resp,
|
||||
...resp2,
|
||||
...resp3,
|
||||
])
|
||||
setLoading(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
: GetApiHandler(`/collections?typeId=1`).then((resp) => {
|
||||
// get collections for movies
|
||||
if (props.type === 2) {
|
||||
if (selectedEpisodes !== -1) {
|
||||
GetApiHandler(`/collections?typeId=4`).then((resp) => {
|
||||
// get collections for episodes
|
||||
setCollectionOptions([...origCollectionOptions, ...resp])
|
||||
setLoading(false)
|
||||
})
|
||||
} else if (selectedSeasons !== -1) {
|
||||
GetApiHandler(`/collections?typeId=3`).then((resp) => {
|
||||
// get collections for episodes and seasons
|
||||
GetApiHandler(`/collections?typeId=4`).then((resp2) => {
|
||||
setCollectionOptions([...origCollectionOptions, ...resp, ...resp2])
|
||||
setLoading(false)
|
||||
})
|
||||
})
|
||||
} else {
|
||||
GetApiHandler(`/collections?typeId=2`).then((resp) => {
|
||||
// get collections for episodes, seasons and shows
|
||||
GetApiHandler(`/collections?typeId=3`).then((resp2) => {
|
||||
GetApiHandler(`/collections?typeId=4`).then((resp3) => {
|
||||
setCollectionOptions([
|
||||
...origCollectionOptions,
|
||||
...resp,
|
||||
...resp2,
|
||||
...resp3,
|
||||
])
|
||||
setLoading(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
} else {
|
||||
GetApiHandler(`/collections?typeId=1`).then((resp) => {
|
||||
// get collections for movies
|
||||
setCollectionOptions([...origCollectionOptions, ...resp])
|
||||
setLoading(false)
|
||||
})
|
||||
}
|
||||
}, [selectedSeasons, selectedEpisodes])
|
||||
|
||||
return (
|
||||
|
||||
@@ -180,7 +180,7 @@ const CollectionInfo = (props: ICollectionInfo) => {
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div className="text-zinc-300 font-bold heading mt-5">
|
||||
<div className="heading mt-5 font-bold text-zinc-300">
|
||||
<h2>{'Logs'}</h2>
|
||||
</div>
|
||||
|
||||
@@ -188,7 +188,7 @@ const CollectionInfo = (props: ICollectionInfo) => {
|
||||
{/* full container */}
|
||||
<div className="mb-2 flex flex-grow flex-col sm:flex-grow-0 sm:flex-row sm:justify-end">
|
||||
{/* search */}
|
||||
<div className="mt-4 flex flex-grow mr-2 sm:w-1/2 w-full">
|
||||
<div className="mr-2 mt-4 flex w-full flex-grow sm:w-1/2">
|
||||
<span className="inline-flex cursor-default items-center rounded-l-md border border-r-0 border-gray-500 bg-zinc-800 px-3 text-sm text-gray-100">
|
||||
<SearchIcon className="h-6 w-6" />
|
||||
</span>
|
||||
@@ -203,7 +203,7 @@ const CollectionInfo = (props: ICollectionInfo) => {
|
||||
{/* sort/filter container */}
|
||||
<div className="mb-2 flex flex-1 flex-row justify-between sm:mb-0 sm:flex-none">
|
||||
{/* sort */}
|
||||
<div className="mt-4 mr-2 flex flex-grow sm:w-1/2">
|
||||
<div className="mr-2 mt-4 flex flex-grow sm:w-1/2">
|
||||
<span className="inline-flex cursor-default items-center rounded-l-md border border-r-0 border-gray-500 bg-zinc-800 px-3 text-sm text-gray-100">
|
||||
{currentSort === 'DESC' ? (
|
||||
<SortDescendingIcon className="h-6 w-6" />
|
||||
@@ -311,7 +311,7 @@ const CollectionInfo = (props: ICollectionInfo) => {
|
||||
{loadingExtraRef.current ? (
|
||||
<tr>
|
||||
<Table.TD colSpan={2} noPadding>
|
||||
<SmallLoadingSpinner className="w-8 m-auto mt-2 mb-2" />
|
||||
<SmallLoadingSpinner className="m-auto mb-2 mt-2 w-8" />
|
||||
</Table.TD>
|
||||
</tr>
|
||||
) : undefined}
|
||||
|
||||
@@ -43,7 +43,7 @@ const RemoveFromCollectionBtn = (props: IRemoveFromCollectionBtn) => {
|
||||
<Button
|
||||
buttonType="primary"
|
||||
buttonSize="md"
|
||||
className="mt-2 mb-1 h-6 w-full text-zinc-200 shadow-md"
|
||||
className="mb-1 mt-2 h-6 w-full text-zinc-200 shadow-md"
|
||||
onClick={() => setSure(true)}
|
||||
>
|
||||
{<TrashIcon className="m-auto ml-3 h-3" />}{' '}
|
||||
@@ -53,7 +53,7 @@ const RemoveFromCollectionBtn = (props: IRemoveFromCollectionBtn) => {
|
||||
<Button
|
||||
buttonType="primary"
|
||||
buttonSize="md"
|
||||
className="mt-2 mb-1 h-6 w-full text-zinc-200 shadow-md"
|
||||
className="mb-1 mt-2 h-6 w-full text-zinc-200 shadow-md"
|
||||
onClick={props.popup ? handlePopup : handle}
|
||||
>
|
||||
<p className="rules-button-text m-auto mr-2">{'Are you sure?'}</p>
|
||||
|
||||
@@ -29,7 +29,7 @@ const TestMediaItem = (props: ITestMediaItem) => {
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [ruleGroup, setRuleGroup] = useState<{
|
||||
dataType: EPlexDataType
|
||||
id: String
|
||||
id: string
|
||||
}>()
|
||||
const [selectedSeasons, setSelectedSeasons] = useState<number>(-1)
|
||||
const [selectedEpisodes, setSelectedEpisodes] = useState<number>(-1)
|
||||
@@ -146,7 +146,8 @@ const TestMediaItem = (props: ITestMediaItem) => {
|
||||
|
||||
useEffect(() => {
|
||||
GetApiHandler(`/rules/collection/${props.collectionId}`).then((resp) => {
|
||||
setRuleGroup(resp), setLoading(false)
|
||||
setRuleGroup(resp)
|
||||
setLoading(false)
|
||||
})
|
||||
}, [])
|
||||
|
||||
@@ -168,7 +169,7 @@ const TestMediaItem = (props: ITestMediaItem) => {
|
||||
}, [selectedMediaId])
|
||||
|
||||
return !loading && ruleGroup ? (
|
||||
<div className={'w-full h-full'}>
|
||||
<div className={'h-full w-full'}>
|
||||
<Modal
|
||||
loading={false}
|
||||
backgroundClickable={false}
|
||||
|
||||
@@ -155,14 +155,14 @@ const CollectionDetail: React.FC<ICollectionDetail> = (
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="m-auto mb-3 flex w-full">
|
||||
<h1 className="w-full whitespace-nowrap overflow-hidden overflow-ellipsis flex text-lg font-bold text-zinc-200 sm:m-0 xl:m-0 justify-center sm:justify-start">
|
||||
<h1 className="flex w-full justify-center overflow-hidden overflow-ellipsis whitespace-nowrap text-lg font-bold text-zinc-200 sm:m-0 sm:justify-start xl:m-0">
|
||||
{`${props.title}`}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex justify-center items-center h-full">
|
||||
<div className="mt-0 mb-4 w-fit sm:w-full">
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="mb-4 mt-0 w-fit sm:w-full">
|
||||
<TabbedLinks
|
||||
onChange={(t) => setSelectedTab(t)}
|
||||
routes={tabbedRoutes}
|
||||
@@ -173,11 +173,11 @@ const CollectionDetail: React.FC<ICollectionDetail> = (
|
||||
</div>
|
||||
<div className="flex justify-center sm:justify-start">
|
||||
<button
|
||||
className="edit-button mb-4 flex rounded h-9 text-zinc-200 shadow-md"
|
||||
className="edit-button mb-4 flex h-9 rounded text-zinc-200 shadow-md"
|
||||
onClick={() => setMediaTestModalOpen(true)}
|
||||
>
|
||||
{<PlayIcon className="m-auto h-5 ml-5" />}{' '}
|
||||
<p className="m-auto rules-button-text ml-1 mr-5">Test Media</p>
|
||||
{<PlayIcon className="m-auto ml-5 h-5" />}{' '}
|
||||
<p className="rules-button-text m-auto ml-1 mr-5">Test Media</p>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -40,10 +40,10 @@ const CollectionItem = (props: ICollectionItem) => {
|
||||
<div className="collection-backdrop"></div>
|
||||
</div>
|
||||
) : undefined}
|
||||
<div className="inset-0 z-0 h-fit p-3 ">
|
||||
<div className="inset-0 z-0 h-fit p-3">
|
||||
<div className="overflow-hidden overflow-ellipsis whitespace-nowrap text-base font-bold text-white sm:text-lg">
|
||||
<a
|
||||
className="hover:underline hover:cursor-pointer"
|
||||
className="hover:cursor-pointer hover:underline"
|
||||
// onClick={() => props.onClick(props.collection)}
|
||||
{...(props.onClick
|
||||
? { onClick: () => props.onClick!(props.collection) }
|
||||
@@ -54,7 +54,7 @@ const CollectionItem = (props: ICollectionItem) => {
|
||||
: props.collection.title}
|
||||
</a>
|
||||
</div>
|
||||
<div className="max-h-12 h-12 line-clamp-2 whitespace-normal text-base text-zinc-400 hover:overflow-y-scroll sm:text-lg sm:h-14 sm:max-h-14">
|
||||
<div className="line-clamp-2 h-12 max-h-12 whitespace-normal text-base text-zinc-400 hover:overflow-y-scroll sm:h-14 sm:max-h-14 sm:text-lg">
|
||||
{props.collection.manualCollection
|
||||
? `Handled by rule: '${props.collection.title}'`
|
||||
: props.collection.description}
|
||||
|
||||
@@ -16,8 +16,8 @@ const CollectionOverview = (props: ICollectionOverview) => {
|
||||
<div>
|
||||
<LibrarySwitcher onSwitch={props.onSwitchLibrary} />
|
||||
|
||||
<div className="m-auto mb-3 flex ">
|
||||
<div className="m-auto sm:m-0 ">
|
||||
<div className="m-auto mb-3 flex">
|
||||
<div className="m-auto sm:m-0">
|
||||
<ExecuteButton
|
||||
onClick={debounce(props.doActions, 5000, {
|
||||
leading: true,
|
||||
@@ -38,7 +38,7 @@ const CollectionOverview = (props: ICollectionOverview) => {
|
||||
{props.collections?.map((col) => (
|
||||
<li
|
||||
key={+col.id!}
|
||||
className="collection relative mb-5 flex h-fit flex-col overflow-hidden rounded-xl bg-zinc-800 bg-cover bg-center p-4 text-zinc-400 shadow ring-1 ring-zinc-700 sm:mb-0 sm:mr-5 xs:w-full transform-gpu"
|
||||
className="collection relative mb-5 flex h-fit transform-gpu flex-col overflow-hidden rounded-xl bg-zinc-800 bg-cover bg-center p-4 text-zinc-400 shadow ring-1 ring-zinc-700 xs:w-full sm:mb-0 sm:mr-5"
|
||||
>
|
||||
<CollectionItem
|
||||
key={col.id}
|
||||
|
||||
@@ -66,7 +66,7 @@ const Collection = () => {
|
||||
id != 9999
|
||||
? LibrariesCtx.libraries.find((el) => +el.key === id)
|
||||
: undefined
|
||||
lib ? setLibrary(lib) : setLibrary(undefined)
|
||||
setLibrary(lib)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -8,11 +8,11 @@ interface IAddButton {
|
||||
const AddButton = (props: IAddButton) => {
|
||||
return (
|
||||
<button
|
||||
className="add-button bg-amber-600 hover:bg-amber-500 flex m-auto h-9 rounded text-zinc-200 shadow-md"
|
||||
className="add-button m-auto flex h-9 rounded bg-amber-600 text-zinc-200 shadow-md hover:bg-amber-500"
|
||||
onClick={props.onClick}
|
||||
>
|
||||
{<PlusCircleIcon className="m-auto h-5 ml-4" />}
|
||||
<p className="m-auto rules-button-text ml-1 mr-4">{props.text}</p>
|
||||
{<PlusCircleIcon className="m-auto ml-4 h-5" />}
|
||||
<p className="rules-button-text m-auto ml-1 mr-4">{props.text}</p>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -41,12 +41,19 @@ const CommunityRuleTableRow = (props: ICommunityRuleTableRow) => {
|
||||
<td
|
||||
onClick={onClick}
|
||||
onDoubleClick={onDoubleClick}
|
||||
className="md:w-105 whitespace-wrap inline-block w-60 overflow-hidden overflow-ellipsis px-4 py-4 text-left text-sm leading-5 text-white max-h-44"
|
||||
className="md:w-105 whitespace-wrap inline-block max-h-44 w-60 overflow-hidden overflow-ellipsis px-4 py-4 text-left text-sm leading-5 text-white"
|
||||
>
|
||||
{props.rule.name}
|
||||
</td>
|
||||
<td className="px-4 py-4 text-center text-sm leading-5 text-white">
|
||||
<div className="content-left flex" title={props.thumbsActive ? '' : 'You Already submitted karma for this rule'}>
|
||||
<div
|
||||
className="content-left flex"
|
||||
title={
|
||||
props.thumbsActive
|
||||
? ''
|
||||
: 'You Already submitted karma for this rule'
|
||||
}
|
||||
>
|
||||
<ChevronUpIcon
|
||||
onClick={
|
||||
props.thumbsActive &&
|
||||
|
||||
@@ -219,7 +219,7 @@ const CommunityRuleModal = (props: ICommunityRuleModal) => {
|
||||
<SearchBar onSearch={handleSearch} />
|
||||
{originalRules.length > 0 ? (
|
||||
<div className="flex flex-col">
|
||||
<div className="my-2 -mx-4 overflow-x-auto md:mx-0 lg:mx-0">
|
||||
<div className="-mx-4 my-2 overflow-x-auto md:mx-0 lg:mx-0">
|
||||
<div className="inline-block min-w-full py-2 align-middle">
|
||||
<div className="overflow-hidden rounded-lg shadow md:mx-0 lg:mx-0">
|
||||
<table className="ml-2 mr-2 min-w-full table-fixed">
|
||||
@@ -298,7 +298,7 @@ const CommunityRuleModal = (props: ICommunityRuleModal) => {
|
||||
title={'Community Rule Description'}
|
||||
iconSvg={''}
|
||||
>
|
||||
<div className="block max-h-full w-full max-w-full overflow-auto bg-zinc-600 p-3 text-zinc-200">
|
||||
<div className="block max-h-full w-full max-w-full overflow-auto bg-zinc-600 p-3 text-zinc-200">
|
||||
{originalRules.find((r) => r.id === clickedRule)?.description}
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
@@ -9,7 +9,7 @@ interface IDocsButton {
|
||||
|
||||
const DocsButton = (props: IDocsButton) => {
|
||||
return (
|
||||
<span className="h-full w-full inline-flex">
|
||||
<span className="inline-flex h-full w-full">
|
||||
<Link
|
||||
legacyBehavior
|
||||
href={`https://docs.maintainerr.info/${props.page ? props.page : ''}`}
|
||||
|
||||
@@ -9,10 +9,10 @@ interface IEditButton {
|
||||
const EditButton = (props: IEditButton) => {
|
||||
return (
|
||||
<button
|
||||
className="bg-amber-600 hover:bg-amber-500 right-5 m-auto flex h-8 w-full rounded text-zinc-200 shadow-md"
|
||||
className="right-5 m-auto flex h-8 w-full rounded bg-amber-600 text-zinc-200 shadow-md hover:bg-amber-500"
|
||||
onClick={props.onClick}
|
||||
>
|
||||
<div className='m-auto ml-auto flex'>
|
||||
<div className="m-auto ml-auto flex">
|
||||
{props.svgIcon}
|
||||
<p className="button-text m-auto ml-1 text-zinc-200">{props.text}</p>
|
||||
</div>
|
||||
|
||||
@@ -22,16 +22,16 @@ const ExecuteButton = (props: IExecuteButton) => {
|
||||
|
||||
return (
|
||||
<button
|
||||
className="edit-button flex m-auto rounded h-9 text-zinc-200 shadow-md"
|
||||
className="edit-button m-auto flex h-9 rounded text-zinc-200 shadow-md"
|
||||
disabled={clicked}
|
||||
onClick={onClick}
|
||||
>
|
||||
{clicked ? (
|
||||
<SmallLoadingSpinner className="h-5 m-auto ml-2" />
|
||||
<SmallLoadingSpinner className="m-auto ml-2 h-5" />
|
||||
) : (
|
||||
<PlayIcon className="m-auto h-5 ml-4" />
|
||||
<PlayIcon className="m-auto ml-4 h-5" />
|
||||
)}{' '}
|
||||
<p className="m-auto rules-button-text ml-1 mr-4">{props.text}</p>
|
||||
<p className="rules-button-text m-auto ml-1 mr-4">{props.text}</p>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,11 +10,11 @@ const InfoButton = (props: IInfoButton) => {
|
||||
return (
|
||||
<button
|
||||
disabled={props.enabled !== undefined ? !props.enabled : false}
|
||||
className="bg-zinc-900 hover:bg-zinc-800 md:ml-2 flex mb-2 w-24 rounded h-9 text-zinc-200 shadow-md disabled:opacity-50"
|
||||
className="mb-2 flex h-9 w-24 rounded bg-zinc-900 text-zinc-200 shadow-md hover:bg-zinc-800 disabled:opacity-50 md:ml-2"
|
||||
onClick={props.onClick}
|
||||
>
|
||||
{<InformationCircleIcon className="m-auto h-5 ml-5" />}{' '}
|
||||
<p className="m-auto rules-button-text ml-1 mr-5">{props.text}</p>
|
||||
{<InformationCircleIcon className="m-auto ml-5 h-5" />}{' '}
|
||||
<p className="rules-button-text m-auto ml-1 mr-5">{props.text}</p>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import GetApiHandler from '../../../utils/ApiHandler'
|
||||
|
||||
interface ILibrarySwitcher {
|
||||
onSwitch: (libraryId: number) => void
|
||||
allPossible?: Boolean
|
||||
allPossible?: boolean
|
||||
}
|
||||
|
||||
const LibrarySwitcher = (props: ILibrarySwitcher) => {
|
||||
@@ -19,9 +19,9 @@ const LibrarySwitcher = (props: ILibrarySwitcher) => {
|
||||
GetApiHandler('/plex/libraries').then((resp) => {
|
||||
if (resp) {
|
||||
LibrariesCtx.addLibraries(resp)
|
||||
props.allPossible !== undefined && !props.allPossible
|
||||
? props.onSwitch(+resp[0].key)
|
||||
: undefined
|
||||
if (props.allPossible !== undefined && !props.allPossible) {
|
||||
props.onSwitch(+resp[0].key)
|
||||
}
|
||||
} else {
|
||||
LibrariesCtx.addLibraries([])
|
||||
}
|
||||
|
||||
@@ -181,7 +181,7 @@ const MediaCard: React.FC<IMediaCard> = ({
|
||||
|
||||
{/* on collection page and for manually added */}
|
||||
{collectionPage && isManual && !showDetail ? (
|
||||
<div className="absolute left-1/2 transform -translate-x-1/2 bottom-0 flex items-center justify-between p-2">
|
||||
<div className="absolute bottom-0 left-1/2 flex -translate-x-1/2 transform items-center justify-between p-2">
|
||||
<div
|
||||
className={`pointer-events-none z-40 rounded-full shadow ${
|
||||
mediaType === 'movie'
|
||||
@@ -252,7 +252,12 @@ const MediaCard: React.FC<IMediaCard> = ({
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="absolute inset-0 z-40 flex items-center justify-center rounded-xl bg-zinc-800 bg-opacity-75 text-zinc-200">
|
||||
<Spinner className="h-10 w-10" />
|
||||
<CachedImage
|
||||
priority
|
||||
src={Spinner}
|
||||
className="h-10 w-10"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
@@ -315,7 +320,7 @@ const MediaCard: React.FC<IMediaCard> = ({
|
||||
<Button
|
||||
buttonType="twin-primary-l"
|
||||
buttonSize="md"
|
||||
className="mb-1 mt-2 h-6 w-1/2 text-zinc-200 shadow-md"
|
||||
className="mb-1 mt-2 h-6 w-1/2 text-zinc-200 shadow-md"
|
||||
onClick={() => {
|
||||
setAddModal(true)
|
||||
}}
|
||||
|
||||
@@ -82,21 +82,20 @@ const Modal: React.FC<ModalProps> = ({
|
||||
}) => {
|
||||
const modalRef = useRef<HTMLDivElement>(null)
|
||||
useClickOutside(modalRef, () => {
|
||||
typeof onCancel === 'function' && backgroundClickable
|
||||
? onCancel()
|
||||
: undefined
|
||||
if (typeof onCancel === 'function' && backgroundClickable) {
|
||||
onCancel()
|
||||
}
|
||||
})
|
||||
useLockBodyScroll(true, disableScrollLock)
|
||||
|
||||
return ReactDOM.createPortal(
|
||||
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
|
||||
<div
|
||||
className="fixed top-0 bottom-0 left-0 right-0 z-50 flex h-full w-full items-center justify-center bg-zinc-800 bg-opacity-70"
|
||||
className="fixed bottom-0 left-0 right-0 top-0 z-50 flex h-full w-full items-center justify-center bg-zinc-800 bg-opacity-70"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Escape') {
|
||||
typeof onCancel === 'function' && backgroundClickable
|
||||
? onCancel()
|
||||
: undefined
|
||||
if (typeof onCancel === 'function' && backgroundClickable) {
|
||||
onCancel()
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -123,7 +122,7 @@ const Modal: React.FC<ModalProps> = ({
|
||||
show={!loading}
|
||||
>
|
||||
<div
|
||||
className={`relative inline-block w-full transform overflow-auto bg-zinc-700 px-4 pt-5 pb-4 text-left align-bottom shadow-xl ring-1 ring-zinc-700 transition-all sm:my-8 ${maxWidthMap[size]} sm:rounded-lg sm:align-middle`}
|
||||
className={`relative inline-block w-full transform overflow-auto bg-zinc-700 px-4 pb-4 pt-5 text-left align-bottom shadow-xl ring-1 ring-zinc-700 transition-all sm:my-8 ${maxWidthMap[size]} sm:rounded-lg sm:align-middle`}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="modal-headline"
|
||||
@@ -155,7 +154,7 @@ const Modal: React.FC<ModalProps> = ({
|
||||
</div>
|
||||
)}
|
||||
{typeof onSpecial === 'function' && (
|
||||
<div className="flex justify-center sm:justify-end mt-4">
|
||||
<div className="mt-4 flex justify-center sm:justify-end">
|
||||
<Button
|
||||
buttonType={specialButtonType}
|
||||
onClick={onSpecial}
|
||||
|
||||
@@ -12,7 +12,9 @@ const Pagination = (props: IPagination) => {
|
||||
<span className="mb-2 text-sm text-zinc-200">
|
||||
Showing{' '}
|
||||
<span className="font-bold text-zinc-400">
|
||||
{(props.totalItems === 0) ? 0 : (props.currentPage - 1) * props.pageSize + 1}
|
||||
{props.totalItems === 0
|
||||
? 0
|
||||
: (props.currentPage - 1) * props.pageSize + 1}
|
||||
</span>{' '}
|
||||
to{' '}
|
||||
<span className="font-bold text-zinc-400">
|
||||
@@ -27,7 +29,7 @@ const Pagination = (props: IPagination) => {
|
||||
{props.currentPage === 1 ? undefined : (
|
||||
<button
|
||||
onClick={() => props.handleBackward()}
|
||||
className="rounded-l bg-zinc-900 py-2 px-4 text-sm font-medium text-white hover:bg-zinc-800"
|
||||
className="rounded-l bg-zinc-900 px-4 py-2 text-sm font-medium text-white hover:bg-zinc-800"
|
||||
>
|
||||
Prev{' '}
|
||||
</button>
|
||||
@@ -36,7 +38,7 @@ const Pagination = (props: IPagination) => {
|
||||
<button
|
||||
onClick={() => props.handleForward()}
|
||||
className={
|
||||
'rounded-r border-0 border-l border-gray-700 bg-zinc-900 py-2 px-4 text-sm font-medium text-white hover:bg-zinc-800 '
|
||||
'rounded-r border-0 border-l border-gray-700 bg-zinc-900 px-4 py-2 text-sm font-medium text-white hover:bg-zinc-800'
|
||||
}
|
||||
>
|
||||
Next
|
||||
|
||||
@@ -20,13 +20,13 @@ const SearchBar = (props: ISearchBar) => {
|
||||
props.onSearch(text)
|
||||
}, [text])
|
||||
|
||||
let inputHandler = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const inputHandler = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setText(e.target.value.toLowerCase())
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative flex w-full items-center text-white focus-within:text-zinc-200">
|
||||
<div className="pointer-events-none absolute left-4 flex items-center">
|
||||
<div className="pointer-events-none absolute left-4 flex items-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
|
||||
@@ -3,8 +3,7 @@ import AsyncSelect from 'react-select/async'
|
||||
import GetApiHandler from '../../../utils/ApiHandler'
|
||||
import { IPlexMetadata } from '../../Overview/Content'
|
||||
import { EPlexDataType } from '../../../utils/PlexDataType-enum'
|
||||
import { SingleValue } from 'react-select/dist/declarations/src'
|
||||
import { MediaType } from '../../../contexts/constants-context'
|
||||
import { SingleValue } from 'react-select'
|
||||
|
||||
export interface IMediaOptions {
|
||||
id: string
|
||||
|
||||
@@ -27,8 +27,8 @@ const SectionHeading = (props: ISectionHeading) => {
|
||||
onClick={addRule}
|
||||
title={`Add rule to section ${props.id}`}
|
||||
>
|
||||
{<DocumentAddIcon className="m-auto h-5 ml-5 text-zinc-200" />}
|
||||
<p className="m-auto ml-1 mr-5 text-zinc-100 button-text">Add</p>
|
||||
{<DocumentAddIcon className="m-auto ml-5 h-5 text-zinc-200" />}
|
||||
<p className="button-text m-auto ml-1 mr-5 text-zinc-100">Add</p>
|
||||
</button>
|
||||
) : undefined}
|
||||
</div>
|
||||
|
||||
@@ -23,11 +23,11 @@ export interface ITabbedLink {
|
||||
}
|
||||
|
||||
const TabbedLink = (props: ITabbedLink) => {
|
||||
let linkClasses =
|
||||
const linkClasses =
|
||||
(props.disabled ? 'pointer-events-none touch-none ' : 'cursor-pointer ') +
|
||||
'px-1 py-4 ml-8 text-md font-semibold leading-5 transition duration-300 leading-5 whitespace-nowrap first:ml-0'
|
||||
let activeLinkColor = ' border-b text-amber-500 border-amber-600'
|
||||
let inactiveLinkColor =
|
||||
const activeLinkColor = ' border-b text-amber-500 border-amber-600'
|
||||
const inactiveLinkColor =
|
||||
'border-transparent text-zinc-500 hover:border-b focus:border-b hover:text-zinc-300 hover:border-zinc-400 focus:text-zinc-300 focus:border-zinc-400'
|
||||
|
||||
return (
|
||||
|
||||
@@ -78,7 +78,7 @@ type TableProps = {
|
||||
const Table = ({ children }: TableProps) => {
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="my-2 -mx-4 overflow-x-auto md:mx-0 lg:mx-0">
|
||||
<div className="-mx-4 my-2 overflow-x-auto md:mx-0 lg:mx-0">
|
||||
<div className="inline-block min-w-full py-2 align-middle">
|
||||
<div className="overflow-hidden rounded-lg shadow md:mx-0 lg:mx-0">
|
||||
<table className="min-w-full">{children}</table>
|
||||
|
||||
@@ -10,7 +10,7 @@ interface ITestButton {
|
||||
}
|
||||
|
||||
interface TestStatus {
|
||||
clicked: Boolean
|
||||
clicked: boolean
|
||||
status: boolean
|
||||
}
|
||||
|
||||
@@ -32,12 +32,10 @@ const TestButton = (props: ITestButton) => {
|
||||
setLoading(true)
|
||||
await GetApiHandler(props.testUrl).then((resp: BasicResponse) => {
|
||||
setClicked({ clicked: true, status: resp.code == 1 ? true : false })
|
||||
props.onClick
|
||||
? props.onClick({
|
||||
status: resp.code === 1 ? true : false,
|
||||
version: resp.message,
|
||||
})
|
||||
: undefined
|
||||
props.onClick?.({
|
||||
status: resp.code === 1 ? true : false,
|
||||
version: resp.message,
|
||||
})
|
||||
setLoading(false)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
import React from 'react';
|
||||
import { CSSTransition as ReactCSSTransition } from 'react-transition-group';
|
||||
import { useRef, useEffect, useContext } from 'react';
|
||||
import React from 'react'
|
||||
import { CSSTransition as ReactCSSTransition } from 'react-transition-group'
|
||||
import { useRef, useEffect, useContext } from 'react'
|
||||
|
||||
interface CSSTransitionProps {
|
||||
show?: boolean;
|
||||
enter?: string;
|
||||
enterFrom?: string;
|
||||
enterTo?: string;
|
||||
leave?: string;
|
||||
leaveFrom?: string;
|
||||
leaveTo?: string;
|
||||
appear?: boolean;
|
||||
children?: React.ReactNode;
|
||||
show?: boolean
|
||||
enter?: string
|
||||
enterFrom?: string
|
||||
enterTo?: string
|
||||
leave?: string
|
||||
leaveFrom?: string
|
||||
leaveTo?: string
|
||||
appear?: boolean
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
const TransitionContext = React.createContext<{
|
||||
parent: { show?: boolean; isInitialRender?: boolean; appear?: boolean };
|
||||
parent: { show?: boolean; isInitialRender?: boolean; appear?: boolean }
|
||||
}>({
|
||||
parent: {},
|
||||
});
|
||||
})
|
||||
|
||||
function useIsInitialRender() {
|
||||
const isInitialRender = useRef(true);
|
||||
const isInitialRender = useRef(true)
|
||||
useEffect(() => {
|
||||
isInitialRender.current = false;
|
||||
}, []);
|
||||
return isInitialRender.current;
|
||||
isInitialRender.current = false
|
||||
}, [])
|
||||
return isInitialRender.current
|
||||
}
|
||||
|
||||
const CSSTransition: React.FC<CSSTransitionProps> = ({
|
||||
@@ -39,20 +39,24 @@ const CSSTransition: React.FC<CSSTransitionProps> = ({
|
||||
appear,
|
||||
children,
|
||||
}) => {
|
||||
const enterClasses = enter.split(' ').filter((s) => s.length);
|
||||
const enterFromClasses = enterFrom.split(' ').filter((s) => s.length);
|
||||
const enterToClasses = enterTo.split(' ').filter((s) => s.length);
|
||||
const leaveClasses = leave.split(' ').filter((s) => s.length);
|
||||
const leaveFromClasses = leaveFrom.split(' ').filter((s) => s.length);
|
||||
const leaveToClasses = leaveTo.split(' ').filter((s) => s.length);
|
||||
const enterClasses = enter.split(' ').filter((s) => s.length)
|
||||
const enterFromClasses = enterFrom.split(' ').filter((s) => s.length)
|
||||
const enterToClasses = enterTo.split(' ').filter((s) => s.length)
|
||||
const leaveClasses = leave.split(' ').filter((s) => s.length)
|
||||
const leaveFromClasses = leaveFrom.split(' ').filter((s) => s.length)
|
||||
const leaveToClasses = leaveTo.split(' ').filter((s) => s.length)
|
||||
|
||||
const addClasses = (node: HTMLElement, classes: string[]) => {
|
||||
classes.length && node.classList.add(...classes);
|
||||
};
|
||||
if (classes.length) {
|
||||
node.classList.add(...classes)
|
||||
}
|
||||
}
|
||||
|
||||
const removeClasses = (node: HTMLElement, classes: string[]) => {
|
||||
classes.length && node.classList.remove(...classes);
|
||||
};
|
||||
if (classes.length) {
|
||||
node.classList.remove(...classes)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ReactCSSTransition
|
||||
@@ -60,42 +64,42 @@ const CSSTransition: React.FC<CSSTransitionProps> = ({
|
||||
unmountOnExit
|
||||
in={show}
|
||||
addEndListener={(node, done) => {
|
||||
node.addEventListener('transitionend', done, false);
|
||||
node.addEventListener('transitionend', done, false)
|
||||
}}
|
||||
onEnter={(node: HTMLElement) => {
|
||||
addClasses(node, [...enterClasses, ...enterFromClasses]);
|
||||
addClasses(node, [...enterClasses, ...enterFromClasses])
|
||||
}}
|
||||
onEntering={(node: HTMLElement) => {
|
||||
removeClasses(node, enterFromClasses);
|
||||
addClasses(node, enterToClasses);
|
||||
removeClasses(node, enterFromClasses)
|
||||
addClasses(node, enterToClasses)
|
||||
}}
|
||||
onEntered={(node: HTMLElement) => {
|
||||
removeClasses(node, [...enterToClasses, ...enterClasses]);
|
||||
removeClasses(node, [...enterToClasses, ...enterClasses])
|
||||
}}
|
||||
onExit={(node) => {
|
||||
addClasses(node, [...leaveClasses, ...leaveFromClasses]);
|
||||
addClasses(node, [...leaveClasses, ...leaveFromClasses])
|
||||
}}
|
||||
onExiting={(node) => {
|
||||
removeClasses(node, leaveFromClasses);
|
||||
addClasses(node, leaveToClasses);
|
||||
removeClasses(node, leaveFromClasses)
|
||||
addClasses(node, leaveToClasses)
|
||||
}}
|
||||
onExited={(node) => {
|
||||
removeClasses(node, [...leaveToClasses, ...leaveClasses]);
|
||||
removeClasses(node, [...leaveToClasses, ...leaveClasses])
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ReactCSSTransition>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
const Transition: React.FC<CSSTransitionProps> = ({
|
||||
show,
|
||||
appear,
|
||||
...rest
|
||||
}) => {
|
||||
const { parent } = useContext(TransitionContext);
|
||||
const isInitialRender = useIsInitialRender();
|
||||
const isChild = show === undefined;
|
||||
const { parent } = useContext(TransitionContext)
|
||||
const isInitialRender = useIsInitialRender()
|
||||
const isChild = show === undefined
|
||||
|
||||
if (isChild) {
|
||||
return (
|
||||
@@ -104,7 +108,7 @@ const Transition: React.FC<CSSTransitionProps> = ({
|
||||
show={parent.show}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -119,7 +123,7 @@ const Transition: React.FC<CSSTransitionProps> = ({
|
||||
>
|
||||
<CSSTransition appear={appear} show={show} {...rest} />
|
||||
</TransitionContext.Provider>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export default Transition;
|
||||
export default Transition
|
||||
|
||||
@@ -112,7 +112,7 @@ const NavBar: React.FC<NavBarProps> = ({ open, setClosed }) => {
|
||||
>
|
||||
<>
|
||||
<div className="sidebar relative flex w-full max-w-xs flex-1 flex-col bg-zinc-800">
|
||||
<div className="sidebar-close-button absolute top-0 right-0 -mr-14 p-1">
|
||||
<div className="sidebar-close-button absolute right-0 top-0 -mr-14 p-1">
|
||||
<button
|
||||
className="flex h-12 w-12 items-center justify-center rounded-full text-white focus:bg-zinc-600 focus:outline-none"
|
||||
aria-label="Close sidebar"
|
||||
@@ -123,7 +123,7 @@ const NavBar: React.FC<NavBarProps> = ({ open, setClosed }) => {
|
||||
</div>
|
||||
<div
|
||||
ref={navRef}
|
||||
className="flex h-0 flex-1 flex-col overflow-y-auto pt-4 pb-8 sm:pb-4"
|
||||
className="flex h-0 flex-1 flex-col overflow-y-auto pb-8 pt-4 sm:pb-4"
|
||||
>
|
||||
<div className="flex flex-shrink-0 items-center px-2">
|
||||
<span className="px-4 text-xl text-zinc-50">
|
||||
@@ -157,8 +157,7 @@ const NavBar: React.FC<NavBarProps> = ({ open, setClosed }) => {
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className={`flex items-center rounded-md px-2 py-2 text-base font-medium leading-6 text-white transition duration-150 ease-in-out focus:outline-none
|
||||
${
|
||||
className={`flex items-center rounded-md px-2 py-2 text-base font-medium leading-6 text-white transition duration-150 ease-in-out focus:outline-none ${
|
||||
link.selected
|
||||
? 'bg-gradient-to-br from-amber-600 to-amber-800 hover:from-amber-500 hover:to-amber-700'
|
||||
: 'hover:bg-zinc-700'
|
||||
@@ -185,10 +184,10 @@ const NavBar: React.FC<NavBarProps> = ({ open, setClosed }) => {
|
||||
</Transition>
|
||||
</div>
|
||||
|
||||
<div className="fixed top-0 bottom-0 left-0 z-30 hidden lg:flex lg:flex-shrink-0">
|
||||
<div className="fixed bottom-0 left-0 top-0 z-30 hidden lg:flex lg:flex-shrink-0">
|
||||
<div className="sidebar flex w-64 flex-col">
|
||||
<div className="flex h-0 flex-1 flex-col">
|
||||
<div className="flex flex-1 flex-col overflow-y-auto pt-4 pb-4">
|
||||
<div className="flex flex-1 flex-col overflow-y-auto pb-4 pt-4">
|
||||
<div className="flex flex-shrink-0 items-center">
|
||||
<span className="px-4 text-2xl text-zinc-50">
|
||||
<Link href="/">
|
||||
@@ -223,9 +222,7 @@ const NavBar: React.FC<NavBarProps> = ({ open, setClosed }) => {
|
||||
navBarLink.selected
|
||||
? 'bg-gradient-to-br from-amber-600 to-amber-800 hover:from-amber-500 hover:to-amber-700'
|
||||
: 'hover:bg-zinc-700'
|
||||
}
|
||||
focus:bg-amber-800 focus:outline-none
|
||||
`}
|
||||
} focus:bg-amber-800 focus:outline-none`}
|
||||
>
|
||||
{navBarLink.svgIcon}
|
||||
{navBarLink.name}
|
||||
|
||||
@@ -51,7 +51,7 @@ const Layout: React.FC<{ children?: ReactNode }> = (props: {
|
||||
WebkitBackdropFilter: isScrolled ? 'blur(5px)' : undefined,
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-1 items-center justify-between pr-4 md:pr-4 md:pl-4 transparent-glass-bg">
|
||||
<div className="transparent-glass-bg flex flex-1 items-center justify-between pr-4 md:pl-4 md:pr-4">
|
||||
<button
|
||||
className={`px-4 text-white ${
|
||||
isScrolled ? 'opacity-90' : 'opacity-70'
|
||||
|
||||
@@ -46,8 +46,8 @@ const PlexLoginButton: React.FC<PlexLoginButtonProps> = ({
|
||||
{loading
|
||||
? 'Loading'
|
||||
: isProcessing
|
||||
? 'Authenticating..'
|
||||
: 'Authenticate with Plex'}
|
||||
? 'Authenticating..'
|
||||
: 'Authenticate with Plex'}
|
||||
</span>
|
||||
</button>
|
||||
</span>
|
||||
|
||||
@@ -11,7 +11,7 @@ const Overview = () => {
|
||||
// const [isLoading, setIsLoading] = useState<Boolean>(false)
|
||||
const loadingRef = useRef<boolean>(false)
|
||||
|
||||
const [loadingExtra, setLoadingExtra] = useState<Boolean>(false)
|
||||
const [loadingExtra, setLoadingExtra] = useState<boolean>(false)
|
||||
|
||||
const [data, setData] = useState<IPlexMetadata[]>([])
|
||||
const dataRef = useRef<IPlexMetadata[]>([])
|
||||
@@ -21,7 +21,7 @@ const Overview = () => {
|
||||
|
||||
const [selectedLibrary, setSelectedLibrary] = useState<number>()
|
||||
const selectedLibraryRef = useRef<number>()
|
||||
const [searchUsed, setsearchUsed] = useState<Boolean>(false)
|
||||
const [searchUsed, setsearchUsed] = useState<boolean>(false)
|
||||
|
||||
const pageData = useRef<number>(0)
|
||||
const SearchCtx = useContext(SearchContext)
|
||||
@@ -43,7 +43,7 @@ const Overview = () => {
|
||||
LibrariesCtx.libraries.length > 0
|
||||
) {
|
||||
switchLib(
|
||||
selectedLibrary ? selectedLibrary : +LibrariesCtx.libraries[0].key
|
||||
selectedLibrary ? selectedLibrary : +LibrariesCtx.libraries[0].key,
|
||||
)
|
||||
}
|
||||
}, 300)
|
||||
@@ -58,7 +58,7 @@ const Overview = () => {
|
||||
pageData.current = resp.length * 50
|
||||
setData(resp ? resp : [])
|
||||
setIsLoading(false)
|
||||
}
|
||||
},
|
||||
)
|
||||
setSelectedLibrary(+LibrariesCtx.libraries[0]?.key)
|
||||
} else {
|
||||
@@ -108,7 +108,7 @@ const Overview = () => {
|
||||
await GetApiHandler(
|
||||
`/plex/library/${selectedLibraryRef.current}/content/${
|
||||
pageData.current + 1
|
||||
}?amount=${fetchAmount}`
|
||||
}?amount=${fetchAmount}`,
|
||||
)
|
||||
|
||||
if (askedLib === selectedLibraryRef.current) {
|
||||
|
||||
@@ -69,7 +69,7 @@ const CommunityRuleUpload = (props: ICommunityRuleUpload) => {
|
||||
<label htmlFor="name" className="text-label">
|
||||
Name *
|
||||
</label>
|
||||
<div className="form-input ">
|
||||
<div className="form-input">
|
||||
<div className="form-input-field">
|
||||
<input
|
||||
className="!bg-zinc-800"
|
||||
|
||||
@@ -180,15 +180,15 @@ const RuleInput = (props: IRuleInput) => {
|
||||
? customValType === RuleType.DATE
|
||||
? customValType
|
||||
: customValType === RuleType.NUMBER
|
||||
? customValType
|
||||
: customValType === RuleType.TEXT &&
|
||||
secondVal === CustomParams.CUSTOM_DAYS
|
||||
? RuleType.NUMBER
|
||||
: customValType === RuleType.TEXT
|
||||
? customValType
|
||||
: customValType === RuleType.BOOL
|
||||
? customValType
|
||||
: +ruleType
|
||||
? customValType
|
||||
: customValType === RuleType.TEXT &&
|
||||
secondVal === CustomParams.CUSTOM_DAYS
|
||||
? RuleType.NUMBER
|
||||
: customValType === RuleType.TEXT
|
||||
? customValType
|
||||
: customValType === RuleType.BOOL
|
||||
? customValType
|
||||
: +ruleType
|
||||
: +ruleType,
|
||||
value: customVal,
|
||||
},
|
||||
@@ -247,7 +247,6 @@ const RuleInput = (props: IRuleInput) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [firstval])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -267,13 +266,14 @@ const RuleInput = (props: IRuleInput) => {
|
||||
} else if (secondVal === CustomParams.CUSTOM_BOOLEAN) {
|
||||
setCustomValActive(true)
|
||||
setCustomValType(RuleType.BOOL)
|
||||
customVal !== '0' ? setCustomVal('1') : undefined
|
||||
if (customVal !== '0') {
|
||||
setCustomVal('1')
|
||||
}
|
||||
} else {
|
||||
setCustomValActive(false)
|
||||
setCustomVal(undefined)
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [secondVal])
|
||||
|
||||
const getPropFromTuple = (
|
||||
@@ -299,8 +299,8 @@ const RuleInput = (props: IRuleInput) => {
|
||||
{props.tagId
|
||||
? `Rule #${props.tagId}`
|
||||
: props.id
|
||||
? `Rule #${props.id}`
|
||||
: `Rule #1`}
|
||||
? `Rule #${props.id}`
|
||||
: `Rule #1`}
|
||||
</div>
|
||||
|
||||
{props.id && props.id > 1 ? (
|
||||
|
||||
@@ -39,15 +39,15 @@ const calculateRuleAmount = (
|
||||
sections: number,
|
||||
): [number, number[]] => {
|
||||
const sectionAmounts = [] as number[]
|
||||
data
|
||||
? data.rules.forEach((el) =>
|
||||
el.section !== undefined
|
||||
? sectionAmounts[el.section]
|
||||
? sectionAmounts[el.section]++
|
||||
: (sectionAmounts[el.section] = 1)
|
||||
: (sectionAmounts[0] = 1),
|
||||
)
|
||||
: undefined
|
||||
if (data) {
|
||||
data.rules.forEach((el) =>
|
||||
el.section !== undefined
|
||||
? sectionAmounts[el.section]
|
||||
? sectionAmounts[el.section]++
|
||||
: (sectionAmounts[el.section] = 1)
|
||||
: (sectionAmounts[0] = 1),
|
||||
)
|
||||
}
|
||||
|
||||
return [
|
||||
sections,
|
||||
@@ -57,12 +57,17 @@ const calculateRuleAmount = (
|
||||
|
||||
const calculateRuleAmountArr = (ruleAmount: [number, number[]]) => {
|
||||
let s = 0,
|
||||
r = 0,
|
||||
lenS = ruleAmount[0]
|
||||
r = 0
|
||||
const lenS = ruleAmount[0]
|
||||
|
||||
const worker: [number[], [number[]]] = [[], [[]]]
|
||||
|
||||
while (++s <= lenS) worker[0].push(s), s > 1 ? worker[1].push([]) : undefined
|
||||
while (++s <= lenS) {
|
||||
worker[0].push(s)
|
||||
if (s > 1) {
|
||||
worker[1].push([])
|
||||
}
|
||||
}
|
||||
|
||||
for (const sec of worker[0]) {
|
||||
r = 0
|
||||
@@ -73,10 +78,10 @@ const calculateRuleAmountArr = (ruleAmount: [number, number[]]) => {
|
||||
}
|
||||
|
||||
const RuleCreator = (props: iRuleCreator) => {
|
||||
const initialSections = props.editData
|
||||
? (props.editData.rules[props.editData.rules.length - 1]?.section! + 1 ??
|
||||
undefined)
|
||||
: undefined
|
||||
const initialSections =
|
||||
props.editData && props.editData.rules.length > 0
|
||||
? props.editData.rules[props.editData.rules.length - 1].section! + 1
|
||||
: undefined
|
||||
const initialRuleAmount: [number, number[]] = initialSections
|
||||
? calculateRuleAmount(props.editData, initialSections)
|
||||
: [1, [1]]
|
||||
@@ -151,11 +156,13 @@ const RuleCreator = (props: iRuleCreator) => {
|
||||
added.current = [...added.current, ruleId]
|
||||
|
||||
rulesCreated.current.map((e) => {
|
||||
e.id >= ruleId ? (e.id = e.id + 1) : e.id
|
||||
if (e.id >= ruleId) {
|
||||
e.id = e.id + 1
|
||||
}
|
||||
return e
|
||||
})
|
||||
|
||||
let rules = [...ruleAmount[1]]
|
||||
const rules = [...ruleAmount[1]]
|
||||
rules[section - 1] = rules[section - 1] + 1
|
||||
|
||||
updateRuleAmount([ruleAmount[0], rules])
|
||||
@@ -250,7 +257,7 @@ const RuleCreator = (props: iRuleCreator) => {
|
||||
title={`Add a new section`}
|
||||
>
|
||||
{<ClipboardListIcon className="m-auto ml-5 h-5" />}
|
||||
<p className="button-text m-auto ml-1 mr-5 text-zinc-200">
|
||||
<p className="button-text m-auto ml-1 mr-5 text-zinc-200">
|
||||
New Section
|
||||
</p>
|
||||
</button>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
export interface IRuleJson {
|
||||
id: number
|
||||
ruleJson: string
|
||||
ruleGroupId: number
|
||||
isActive: boolean
|
||||
}
|
||||
id: number
|
||||
ruleJson: string
|
||||
ruleGroupId: number
|
||||
isActive: boolean
|
||||
}
|
||||
|
||||
const Rule = () => {
|
||||
return <></>
|
||||
}
|
||||
const Rule = () => {
|
||||
return <></>
|
||||
}
|
||||
|
||||
export default Rule;
|
||||
export default Rule
|
||||
|
||||
@@ -54,7 +54,7 @@ interface ICreateApiObject {
|
||||
visibleOnHome: boolean
|
||||
deleteAfterDays: number
|
||||
manualCollection?: boolean
|
||||
manualCollectionName?: String
|
||||
manualCollectionName?: string
|
||||
keepLogsForMonths?: number
|
||||
}
|
||||
rules: IRule[]
|
||||
@@ -840,9 +840,9 @@ const AddModal = (props: AddModal) => {
|
||||
Specify the rules this group needs to enforce
|
||||
</p>
|
||||
</div>
|
||||
<div className="ml-auto ">
|
||||
<div className="ml-auto">
|
||||
<button
|
||||
className="ml-3 flex h-fit rounded bg-amber-900 p-1 text-zinc-900 shadow-md hover:bg-amber-800 md:h-10"
|
||||
className="ml-3 flex h-fit rounded bg-amber-900 p-1 text-zinc-900 shadow-md hover:bg-amber-800 md:h-10"
|
||||
onClick={toggleCommunityRuleModal}
|
||||
>
|
||||
{
|
||||
@@ -854,9 +854,9 @@ const AddModal = (props: AddModal) => {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex items-center justify-center sm:justify-end max-width-form-head">
|
||||
<div className="max-width-form-head mt-4 flex items-center justify-center sm:justify-end">
|
||||
<button
|
||||
className="ml-3 flex h-fit rounded bg-amber-600 p-1 text-zinc-900 shadow-md hover:bg-amber-500 md:h-10"
|
||||
className="ml-3 flex h-fit rounded bg-amber-600 p-1 text-zinc-900 shadow-md hover:bg-amber-500 md:h-10"
|
||||
onClick={toggleYamlImporter}
|
||||
>
|
||||
{
|
||||
@@ -868,7 +868,7 @@ const AddModal = (props: AddModal) => {
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="ml-3 flex h-fit rounded bg-amber-900 p-1 text-zinc-900 shadow-md hover:bg-amber-800 md:h-10"
|
||||
className="ml-3 flex h-fit rounded bg-amber-900 p-1 text-zinc-900 shadow-md hover:bg-amber-800 md:h-10"
|
||||
onClick={toggleYamlExporter}
|
||||
>
|
||||
{
|
||||
|
||||
@@ -49,7 +49,7 @@ const RuleGroup = (props: {
|
||||
return (
|
||||
<div className="relative mb-5 flex w-full flex-col overflow-hidden rounded-xl bg-zinc-800 bg-cover bg-center p-4 text-zinc-400 shadow ring-1 ring-zinc-700 sm:flex-row">
|
||||
<div className="relative z-10 flex w-full min-w-0 flex-col pr-4 sm:w-5/6 sm:flex-row">
|
||||
<div className="mb-3 flex flex-col sm:mb-0 sm:w-5/6 ">
|
||||
<div className="mb-3 flex flex-col sm:mb-0 sm:w-5/6">
|
||||
<div className="flex text-xs font-medium text-white">
|
||||
<div className="overflow-hidden overflow-ellipsis whitespace-nowrap text-base font-bold text-white sm:text-lg">
|
||||
{props.group.name}
|
||||
@@ -57,11 +57,13 @@ const RuleGroup = (props: {
|
||||
</div>
|
||||
|
||||
<div className="my-0.5 flex text-sm sm:my-1">
|
||||
<span className="mr-2 font-bold w-full overflow-hidden overflow-ellipsis">{props.group.description}</span>
|
||||
<span className="mr-2 w-full overflow-hidden overflow-ellipsis font-bold">
|
||||
{props.group.description}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full flex-col text-left sm:w-1/6 ">
|
||||
<div className="w-full flex-col text-left sm:w-1/6">
|
||||
<span className="text-sm font-medium">Status </span>
|
||||
{props.group.isActive ? (
|
||||
<span className="text-sm font-bold text-green-900">Active</span>
|
||||
@@ -71,7 +73,7 @@ const RuleGroup = (props: {
|
||||
<div className="m-auto mr-2 flex text-sm font-medium">
|
||||
{`Library ${
|
||||
LibrariesCtx.libraries.find(
|
||||
(el) => +el.key === +props.group.libraryId
|
||||
(el) => +el.key === +props.group.libraryId,
|
||||
)?.title
|
||||
}`}
|
||||
</div>
|
||||
@@ -88,7 +90,7 @@ const RuleGroup = (props: {
|
||||
</div>
|
||||
|
||||
<div className="m-auto w-full sm:w-1/6">
|
||||
<div className="mb-2 flex h-auto ">
|
||||
<div className="mb-2 flex h-auto">
|
||||
<EditButton
|
||||
onClick={onEdit}
|
||||
text="Edit"
|
||||
|
||||
@@ -37,7 +37,7 @@ const Rules: React.FC = () => {
|
||||
}, [selectedLibrary])
|
||||
|
||||
const showAddModal = () => {
|
||||
addModalActive ? setAddModal(false) : setAddModal(true)
|
||||
setAddModal(!addModalActive)
|
||||
}
|
||||
|
||||
const onSwitchLibrary = (libraryId: number) => {
|
||||
@@ -106,11 +106,11 @@ const Rules: React.FC = () => {
|
||||
<div className="w-full">
|
||||
<LibrarySwitcher onSwitch={onSwitchLibrary} />
|
||||
|
||||
<div className="m-auto mb-5 flex ">
|
||||
<div className="m-auto mb-5 flex">
|
||||
<div className="ml-auto sm:ml-0">
|
||||
<AddButton onClick={showAddModal} text="New Rule" />
|
||||
</div>
|
||||
<div className="ml-2 mr-auto sm:mr-0 ">
|
||||
<div className="ml-2 mr-auto sm:mr-0">
|
||||
<ExecuteButton
|
||||
onClick={debounce(sync, 5000, {
|
||||
leading: true,
|
||||
|
||||
@@ -5,7 +5,7 @@ const SettingsLander = () => {
|
||||
const router = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
document.title = "Maintainerr - Settings"
|
||||
document.title = 'Maintainerr - Settings'
|
||||
router.push('/settings/main')
|
||||
}, [])
|
||||
|
||||
|
||||
@@ -143,8 +143,8 @@ const MainSettings = () => {
|
||||
|
||||
<div className="actions mt-5 w-full">
|
||||
<div className="flex justify-end">
|
||||
<div className="w-full flex">
|
||||
<span className="mr-auto flex rounded-md shadow-sm">
|
||||
<div className="flex w-full">
|
||||
<span className="mr-auto flex rounded-md shadow-sm">
|
||||
<DocsButton />
|
||||
</span>
|
||||
<span className="ml-auto flex rounded-md shadow-sm">
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import { SaveIcon } from '@heroicons/react/solid'
|
||||
import {
|
||||
useContext,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useContext, useEffect, useRef, useState } from 'react'
|
||||
import SettingsContext from '../../../contexts/settings-context'
|
||||
import { PostApiHandler } from '../../../utils/ApiHandler'
|
||||
import Alert from '../../Common/Alert'
|
||||
@@ -28,7 +23,7 @@ const OverseerrSettings = () => {
|
||||
const [error, setError] = useState<boolean>()
|
||||
const [changed, setChanged] = useState<boolean>()
|
||||
const [testBanner, setTestbanner] = useState<{
|
||||
status: Boolean
|
||||
status: boolean
|
||||
version: string
|
||||
}>({ status: false, version: '0' })
|
||||
|
||||
@@ -79,9 +74,9 @@ const OverseerrSettings = () => {
|
||||
? hostnameRef.current.value
|
||||
: hostnameRef.current.value.includes('https://')
|
||||
? hostnameRef.current.value
|
||||
: portRef.current.value == '443'
|
||||
? 'https://' + hostnameRef.current.value
|
||||
: 'http://' + hostnameRef.current.value
|
||||
: portRef.current.value == '443'
|
||||
? 'https://' + hostnameRef.current.value
|
||||
: 'http://' + hostnameRef.current.value
|
||||
|
||||
const payload = {
|
||||
overseerr_url: addPortToUrl(hostnameVal, +portRef.current.value),
|
||||
@@ -173,7 +168,9 @@ const OverseerrSettings = () => {
|
||||
ref={portRef}
|
||||
value={portRef.current?.value}
|
||||
defaultValue={port}
|
||||
onChange={(e) => handleSettingsInputChange(e, portRef, setPort)}
|
||||
onChange={(e) =>
|
||||
handleSettingsInputChange(e, portRef, setPort)
|
||||
}
|
||||
></input>
|
||||
</div>
|
||||
</div>
|
||||
@@ -198,10 +195,10 @@ const OverseerrSettings = () => {
|
||||
|
||||
<div className="actions mt-5 w-full">
|
||||
<div className="flex w-full flex-wrap sm:flex-nowrap">
|
||||
<span className="m-auto rounded-md shadow-sm sm:mr-auto sm:ml-3">
|
||||
<span className="m-auto rounded-md shadow-sm sm:ml-3 sm:mr-auto">
|
||||
<DocsButton page="Configuration" />
|
||||
</span>
|
||||
<div className="m-auto flex sm:m-0 sm:justify-end mt-3 xs:mt-0">
|
||||
<div className="m-auto mt-3 flex xs:mt-0 sm:m-0 sm:justify-end">
|
||||
<TestButton
|
||||
onClick={appTest}
|
||||
testUrl="/settings/test/overseerr"
|
||||
|
||||
@@ -73,10 +73,10 @@ const PlexSettings = () => {
|
||||
const serverPresetRef = useRef<HTMLInputElement>(null)
|
||||
const [error, setError] = useState<boolean>()
|
||||
const [changed, setChanged] = useState<boolean>()
|
||||
const [tokenValid, setTokenValid] = useState<Boolean>(false)
|
||||
const [clearTokenClicked, setClearTokenClicked] = useState<Boolean>(false)
|
||||
const [tokenValid, setTokenValid] = useState<boolean>(false)
|
||||
const [clearTokenClicked, setClearTokenClicked] = useState<boolean>(false)
|
||||
const [testBanner, setTestbanner] = useState<{
|
||||
status: Boolean
|
||||
status: boolean
|
||||
version: string
|
||||
}>({ status: false, version: '0' })
|
||||
const [availableServers, setAvailableServers] = useState<PlexDevice[]>()
|
||||
@@ -92,7 +92,7 @@ const PlexSettings = () => {
|
||||
e: React.FormEvent<HTMLFormElement> | undefined,
|
||||
plex_token?: { plex_auth_token: string } | undefined,
|
||||
) => {
|
||||
e ? e.preventDefault() : undefined
|
||||
e?.preventDefault()
|
||||
if (
|
||||
hostnameRef.current?.value &&
|
||||
nameRef.current?.value &&
|
||||
@@ -500,11 +500,11 @@ const PlexSettings = () => {
|
||||
</div>
|
||||
|
||||
<div className="actions mt-5 w-full">
|
||||
<div className="flex flex-wrap sm:flex-nowrap w-full">
|
||||
<span className="m-auto sm:mr-auto sm:ml-3 rounded-md shadow-sm">
|
||||
<div className="flex w-full flex-wrap sm:flex-nowrap">
|
||||
<span className="m-auto rounded-md shadow-sm sm:ml-3 sm:mr-auto">
|
||||
<DocsButton page="Configuration" />
|
||||
</span>
|
||||
<div className="flex sm:justify-end m-auto sm:m-0 mt-3 xs:mt-0">
|
||||
<div className="m-auto mt-3 flex xs:mt-0 sm:m-0 sm:justify-end">
|
||||
<TestButton onClick={appTest} testUrl="/settings/test/plex" />
|
||||
|
||||
<span className="ml-3 inline-flex rounded-md shadow-sm">
|
||||
|
||||
@@ -134,7 +134,7 @@ const RadarrSettingsModal = (props: IRadarrSettingsModal) => {
|
||||
const radarrUrl = constructUrl(port)
|
||||
|
||||
if (hostname && port && apiKey && serverName) {
|
||||
let payload: RadarrSettingSaveRequest = {
|
||||
const payload: RadarrSettingSaveRequest = {
|
||||
url: `${radarrUrl}${baseUrl ? `/${baseUrl}` : ''}`,
|
||||
apiKey: apiKey,
|
||||
serverName: serverName,
|
||||
@@ -301,9 +301,7 @@ const RadarrSettingsModal = (props: IRadarrSettingsModal) => {
|
||||
<div className="form-row">
|
||||
<label htmlFor="baseUrl" className="text-label">
|
||||
Base URL
|
||||
<span className="label-tip">
|
||||
{`No Leading Slash`}
|
||||
</span>
|
||||
<span className="label-tip">{`No Leading Slash`}</span>
|
||||
</label>
|
||||
<div className="form-input">
|
||||
<div className="form-input-field">
|
||||
@@ -337,7 +335,7 @@ const RadarrSettingsModal = (props: IRadarrSettingsModal) => {
|
||||
|
||||
<div className="actions mt-5 w-full">
|
||||
<div className="flex w-full flex-wrap sm:flex-nowrap">
|
||||
<span className="m-auto rounded-md shadow-sm sm:mr-auto sm:ml-3">
|
||||
<span className="m-auto rounded-md shadow-sm sm:ml-3 sm:mr-auto">
|
||||
<DocsButton page="Configuration" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -119,20 +119,20 @@ const RadarrSettings = () => {
|
||||
{settings.map((setting) => (
|
||||
<li
|
||||
key={setting.id}
|
||||
className="rounded-xl bg-zinc-800 p-4 text-zinc-400 shadow ring-1 ring-zinc-700 h-full"
|
||||
className="h-full rounded-xl bg-zinc-800 p-4 text-zinc-400 shadow ring-1 ring-zinc-700"
|
||||
>
|
||||
<div className="flex gap-x-3 mb-2 items-center">
|
||||
<div className="mb-2 flex items-center gap-x-3">
|
||||
<div className="text-base font-medium text-white sm:text-lg">
|
||||
{setting.serverName}
|
||||
</div>
|
||||
{setting.isDefault && (
|
||||
<div className="bg-amber-600 px-2 py-0.5 rounded text-zinc-200 shadow-md text-xs">
|
||||
<div className="rounded bg-amber-600 px-2 py-0.5 text-xs text-zinc-200 shadow-md">
|
||||
Default
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="text-gray-300 space-x-2 mb-4 truncate">
|
||||
<p className="mb-4 space-x-2 truncate text-gray-300">
|
||||
<span className="font-semibold">Address</span>
|
||||
<a href={setting.url} className="hover:underline">
|
||||
{setting.url}
|
||||
@@ -148,7 +148,7 @@ const RadarrSettings = () => {
|
||||
}}
|
||||
>
|
||||
{<DocumentAddIcon className="m-auto" />}{' '}
|
||||
<p className="font-semibold m-auto">Edit</p>
|
||||
<p className="m-auto font-semibold">Edit</p>
|
||||
</Button>
|
||||
<DeleteButton
|
||||
onDeleteRequested={() => confirmedDelete(setting.id)}
|
||||
@@ -157,14 +157,14 @@ const RadarrSettings = () => {
|
||||
</li>
|
||||
))}
|
||||
|
||||
<li className="rounded-xl bg-zinc-800 p-4 text-zinc-400 shadow border-2 border-dashed border-gray-400 flex items-center justify-center h-full">
|
||||
<li className="flex h-full items-center justify-center rounded-xl border-2 border-dashed border-gray-400 bg-zinc-800 p-4 text-zinc-400 shadow">
|
||||
<button
|
||||
type="button"
|
||||
className="add-button bg-amber-600 hover:bg-amber-500 flex m-auto h-9 rounded text-zinc-200 shadow-md px-4"
|
||||
className="add-button m-auto flex h-9 rounded bg-amber-600 px-4 text-zinc-200 shadow-md hover:bg-amber-500"
|
||||
onClick={showAddModal}
|
||||
>
|
||||
{<PlusCircleIcon className="m-auto h-5" />}
|
||||
<p className="m-auto font-semibold ml-1">Add server</p>
|
||||
<p className="m-auto ml-1 font-semibold">Add server</p>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -190,7 +190,7 @@ const RadarrSettings = () => {
|
||||
>
|
||||
<p className="mb-4">
|
||||
This server is currently being used by the following rules:
|
||||
<ul className="list-disc list-inside">
|
||||
<ul className="list-inside list-disc">
|
||||
{collectionsInUseWarning.map((x) => (
|
||||
<li key={x.id}>{x.title}</li>
|
||||
))}
|
||||
@@ -228,7 +228,7 @@ const DeleteButton = ({
|
||||
}}
|
||||
>
|
||||
{<TrashIcon className="m-auto" />}{' '}
|
||||
<p className="font-semibold m-auto">
|
||||
<p className="m-auto font-semibold">
|
||||
{showSureDelete ? <>Are you sure?</> : <>Delete</>}
|
||||
</p>
|
||||
</Button>
|
||||
|
||||
@@ -134,7 +134,7 @@ const SonarrSettingsModal = (props: ISonarrSettingsModal) => {
|
||||
const sonarrUrl = constructUrl(port)
|
||||
|
||||
if (hostname && port && apiKey && serverName) {
|
||||
let payload: SonarrSettingSaveRequest = {
|
||||
const payload: SonarrSettingSaveRequest = {
|
||||
url: `${sonarrUrl}${baseUrl ? `/${baseUrl}` : ''}`,
|
||||
apiKey: apiKey,
|
||||
serverName: serverName,
|
||||
@@ -301,9 +301,7 @@ const SonarrSettingsModal = (props: ISonarrSettingsModal) => {
|
||||
<div className="form-row">
|
||||
<label htmlFor="baseUrl" className="text-label">
|
||||
Base URL
|
||||
<span className="label-tip">
|
||||
{`No Leading Slash`}
|
||||
</span>
|
||||
<span className="label-tip">{`No Leading Slash`}</span>
|
||||
</label>
|
||||
<div className="form-input">
|
||||
<div className="form-input-field">
|
||||
@@ -337,7 +335,7 @@ const SonarrSettingsModal = (props: ISonarrSettingsModal) => {
|
||||
|
||||
<div className="actions mt-5 w-full">
|
||||
<div className="flex w-full flex-wrap sm:flex-nowrap">
|
||||
<span className="m-auto rounded-md shadow-sm sm:mr-auto sm:ml-3">
|
||||
<span className="m-auto rounded-md shadow-sm sm:ml-3 sm:mr-auto">
|
||||
<DocsButton page="Configuration" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -119,20 +119,20 @@ const SonarrSettings = () => {
|
||||
{settings.map((setting) => (
|
||||
<li
|
||||
key={setting.id}
|
||||
className="rounded-xl bg-zinc-800 p-4 text-zinc-400 shadow ring-1 ring-zinc-700 h-full"
|
||||
className="h-full rounded-xl bg-zinc-800 p-4 text-zinc-400 shadow ring-1 ring-zinc-700"
|
||||
>
|
||||
<div className="flex gap-x-3 mb-2 items-center">
|
||||
<div className="mb-2 flex items-center gap-x-3">
|
||||
<div className="text-base font-medium text-white sm:text-lg">
|
||||
{setting.serverName}
|
||||
</div>
|
||||
{setting.isDefault && (
|
||||
<div className="bg-amber-600 px-2 py-0.5 rounded text-zinc-200 shadow-md text-xs">
|
||||
<div className="rounded bg-amber-600 px-2 py-0.5 text-xs text-zinc-200 shadow-md">
|
||||
Default
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="text-gray-300 space-x-2 mb-4 truncate">
|
||||
<p className="mb-4 space-x-2 truncate text-gray-300">
|
||||
<span className="font-semibold">Address</span>
|
||||
<a href={setting.url} className="hover:underline">
|
||||
{setting.url}
|
||||
@@ -148,7 +148,7 @@ const SonarrSettings = () => {
|
||||
}}
|
||||
>
|
||||
{<DocumentAddIcon className="m-auto" />}{' '}
|
||||
<p className="font-semibold m-auto">Edit</p>
|
||||
<p className="m-auto font-semibold">Edit</p>
|
||||
</Button>
|
||||
<DeleteButton
|
||||
onDeleteRequested={() => confirmedDelete(setting.id)}
|
||||
@@ -157,14 +157,14 @@ const SonarrSettings = () => {
|
||||
</li>
|
||||
))}
|
||||
|
||||
<li className="rounded-xl bg-zinc-800 p-4 text-zinc-400 shadow border-2 border-dashed border-gray-400 flex items-center justify-center h-full">
|
||||
<li className="flex h-full items-center justify-center rounded-xl border-2 border-dashed border-gray-400 bg-zinc-800 p-4 text-zinc-400 shadow">
|
||||
<button
|
||||
type="button"
|
||||
className="add-button bg-amber-600 hover:bg-amber-500 flex m-auto h-9 rounded text-zinc-200 shadow-md px-4"
|
||||
className="add-button m-auto flex h-9 rounded bg-amber-600 px-4 text-zinc-200 shadow-md hover:bg-amber-500"
|
||||
onClick={showAddModal}
|
||||
>
|
||||
{<PlusCircleIcon className="m-auto h-5" />}
|
||||
<p className="m-auto font-semibold ml-1">Add server</p>
|
||||
<p className="m-auto ml-1 font-semibold">Add server</p>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -190,7 +190,7 @@ const SonarrSettings = () => {
|
||||
>
|
||||
<p className="mb-4">
|
||||
This server is currently being used by the following rules:
|
||||
<ul className="list-disc list-inside">
|
||||
<ul className="list-inside list-disc">
|
||||
{collectionsInUseWarning.map((x) => (
|
||||
<li key={x.id}>{x.title}</li>
|
||||
))}
|
||||
@@ -228,7 +228,7 @@ const DeleteButton = ({
|
||||
}}
|
||||
>
|
||||
{<TrashIcon className="m-auto" />}{' '}
|
||||
<p className="font-semibold m-auto">
|
||||
<p className="m-auto font-semibold">
|
||||
{showSureDelete ? <>Are you sure?</> : <>Delete</>}
|
||||
</p>
|
||||
</Button>
|
||||
|
||||
@@ -22,10 +22,7 @@ export interface ISettingsLink {
|
||||
const SettingsLink: React.FC<ISettingsLink> = (props: ISettingsLink) => {
|
||||
if (props.isMobile) {
|
||||
return (
|
||||
<option
|
||||
disabled={props.disabled}
|
||||
value={props.route}
|
||||
>
|
||||
<option disabled={props.disabled} value={props.route}>
|
||||
{props.children}
|
||||
</option>
|
||||
)
|
||||
|
||||
@@ -22,7 +22,7 @@ const TautulliSettings = () => {
|
||||
const [error, setError] = useState<boolean>()
|
||||
const [changed, setChanged] = useState<boolean>()
|
||||
const [testBanner, setTestbanner] = useState<{
|
||||
status: Boolean
|
||||
status: boolean
|
||||
version: string
|
||||
}>({ status: false, version: '0' })
|
||||
|
||||
@@ -164,9 +164,7 @@ const TautulliSettings = () => {
|
||||
<div className="form-row">
|
||||
<label htmlFor="baseUrl" className="text-label">
|
||||
Base URL
|
||||
<span className="label-tip">
|
||||
{`No Leading Slash`}
|
||||
</span>
|
||||
<span className="label-tip">{`No Leading Slash`}</span>
|
||||
</label>
|
||||
<div className="form-input">
|
||||
<div className="form-input-field">
|
||||
@@ -200,10 +198,10 @@ const TautulliSettings = () => {
|
||||
|
||||
<div className="actions mt-5 w-full">
|
||||
<div className="flex w-full flex-wrap sm:flex-nowrap">
|
||||
<span className="m-auto rounded-md shadow-sm sm:mr-auto sm:ml-3">
|
||||
<span className="m-auto rounded-md shadow-sm sm:ml-3 sm:mr-auto">
|
||||
<DocsButton page="Configuration" />
|
||||
</span>
|
||||
<div className="m-auto flex sm:m-0 sm:justify-end mt-3 xs:mt-0">
|
||||
<div className="m-auto mt-3 flex xs:mt-0 sm:m-0 sm:justify-end">
|
||||
<TestButton
|
||||
onClick={appTest}
|
||||
testUrl="/settings/test/tautulli"
|
||||
|
||||
@@ -11,7 +11,7 @@ interface CSSTransitionProps {
|
||||
leaveFrom?: string
|
||||
leaveTo?: string
|
||||
appear?: boolean
|
||||
children?: React.ReactNode;
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
const TransitionContext = React.createContext<{
|
||||
@@ -47,11 +47,15 @@ const CSSTransition: React.FC<CSSTransitionProps> = ({
|
||||
const leaveToClasses = leaveTo.split(' ').filter((s) => s.length)
|
||||
|
||||
const addClasses = (node: HTMLElement, classes: string[]) => {
|
||||
classes.length && node.classList.add(...classes)
|
||||
if (classes.length) {
|
||||
node.classList.add(...classes)
|
||||
}
|
||||
}
|
||||
|
||||
const removeClasses = (node: HTMLElement, classes: string[]) => {
|
||||
classes.length && node.classList.remove(...classes)
|
||||
if (classes.length) {
|
||||
node.classList.remove(...classes)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
CodeIcon,
|
||||
ServerIcon,
|
||||
} from '@heroicons/react/outline'
|
||||
import type { VersionResponse } from '@maintainerr/server/app/dto/version-response.dto'
|
||||
|
||||
enum messages {
|
||||
DEVELOP = 'Maintainerr Develop',
|
||||
@@ -14,13 +15,6 @@ enum messages {
|
||||
OUT_OF_DATE = 'Out of Date',
|
||||
}
|
||||
|
||||
interface VersionResponse {
|
||||
status: 1 | 0
|
||||
version: string
|
||||
commitTag: string
|
||||
updateAvailable: boolean
|
||||
}
|
||||
|
||||
interface VersionStatusProps {
|
||||
onClick?: () => void
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ interface IInteractionProvider {
|
||||
}
|
||||
|
||||
export const InteractionProvider: React.FC<IInteractionProvider> = (
|
||||
props: IInteractionProvider
|
||||
props: IInteractionProvider,
|
||||
) => {
|
||||
const isTouch = useInteraction()
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
} from 'react'
|
||||
|
||||
export interface ISearch {
|
||||
text: String
|
||||
text: string
|
||||
}
|
||||
|
||||
const SearchContext = createContext({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect } from 'react'
|
||||
|
||||
/**
|
||||
* useClickOutside
|
||||
@@ -11,20 +11,20 @@ import { useEffect } from 'react';
|
||||
*/
|
||||
const useClickOutside = (
|
||||
ref: React.RefObject<HTMLElement>,
|
||||
callback: (e: MouseEvent) => void
|
||||
callback: (e: MouseEvent) => void,
|
||||
): void => {
|
||||
useEffect(() => {
|
||||
const handleBodyClick = (e: MouseEvent) => {
|
||||
if (ref.current && !ref.current.contains(e.target as Node)) {
|
||||
callback(e);
|
||||
callback(e)
|
||||
}
|
||||
};
|
||||
document.body.addEventListener('click', handleBodyClick, { capture: true });
|
||||
}
|
||||
document.body.addEventListener('click', handleBodyClick, { capture: true })
|
||||
|
||||
return () => {
|
||||
document.body.removeEventListener('click', handleBodyClick);
|
||||
};
|
||||
}, [ref, callback]);
|
||||
};
|
||||
document.body.removeEventListener('click', handleBodyClick)
|
||||
}
|
||||
}, [ref, callback])
|
||||
}
|
||||
|
||||
export default useClickOutside;
|
||||
export default useClickOutside
|
||||
|
||||
@@ -1,78 +1,78 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
export const INTERACTION_TYPE = {
|
||||
MOUSE: 'mouse',
|
||||
PEN: 'pen',
|
||||
TOUCH: 'touch',
|
||||
};
|
||||
}
|
||||
|
||||
const UPDATE_INTERVAL = 1000; // Throttle updates to the type to prevent flip flopping
|
||||
const UPDATE_INTERVAL = 1000 // Throttle updates to the type to prevent flip flopping
|
||||
|
||||
const useInteraction = (): boolean => {
|
||||
const [isTouch, setIsTouch] = useState(false);
|
||||
const [isTouch, setIsTouch] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const hasTapEvent = 'ontouchstart' in window;
|
||||
setIsTouch(hasTapEvent);
|
||||
const hasTapEvent = 'ontouchstart' in window
|
||||
setIsTouch(hasTapEvent)
|
||||
|
||||
let localTouch = hasTapEvent;
|
||||
let lastTouchUpdate = Date.now();
|
||||
let localTouch = hasTapEvent
|
||||
let lastTouchUpdate = Date.now()
|
||||
|
||||
const shouldUpdate = (): boolean =>
|
||||
lastTouchUpdate + UPDATE_INTERVAL < Date.now();
|
||||
lastTouchUpdate + UPDATE_INTERVAL < Date.now()
|
||||
|
||||
const onMouseMove = (): void => {
|
||||
if (localTouch && shouldUpdate()) {
|
||||
setTimeout(() => {
|
||||
if (shouldUpdate()) {
|
||||
setIsTouch(false);
|
||||
localTouch = false;
|
||||
setIsTouch(false)
|
||||
localTouch = false
|
||||
}
|
||||
}, UPDATE_INTERVAL);
|
||||
}, UPDATE_INTERVAL)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const onTouchStart = (): void => {
|
||||
lastTouchUpdate = Date.now();
|
||||
lastTouchUpdate = Date.now()
|
||||
|
||||
if (!localTouch) {
|
||||
setIsTouch(true);
|
||||
localTouch = true;
|
||||
setIsTouch(true)
|
||||
localTouch = true
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const onPointerMove = (e: PointerEvent): void => {
|
||||
const { pointerType } = e;
|
||||
const { pointerType } = e
|
||||
|
||||
switch (pointerType) {
|
||||
case INTERACTION_TYPE.TOUCH:
|
||||
case INTERACTION_TYPE.PEN:
|
||||
return onTouchStart();
|
||||
return onTouchStart()
|
||||
default:
|
||||
return onMouseMove();
|
||||
return onMouseMove()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (hasTapEvent) {
|
||||
window.addEventListener('mousemove', onMouseMove);
|
||||
window.addEventListener('touchstart', onTouchStart);
|
||||
window.addEventListener('mousemove', onMouseMove)
|
||||
window.addEventListener('touchstart', onTouchStart)
|
||||
} else {
|
||||
window.addEventListener('pointerdown', onPointerMove);
|
||||
window.addEventListener('pointermove', onPointerMove);
|
||||
window.addEventListener('pointerdown', onPointerMove)
|
||||
window.addEventListener('pointermove', onPointerMove)
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (hasTapEvent) {
|
||||
window.removeEventListener('mousemove', onMouseMove);
|
||||
window.removeEventListener('touchstart', onTouchStart);
|
||||
window.removeEventListener('mousemove', onMouseMove)
|
||||
window.removeEventListener('touchstart', onTouchStart)
|
||||
} else {
|
||||
window.removeEventListener('pointerdown', onPointerMove);
|
||||
window.removeEventListener('pointermove', onPointerMove);
|
||||
window.removeEventListener('pointerdown', onPointerMove)
|
||||
window.removeEventListener('pointermove', onPointerMove)
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
}
|
||||
}, [])
|
||||
|
||||
return isTouch;
|
||||
};
|
||||
return isTouch
|
||||
}
|
||||
|
||||
export default useInteraction;
|
||||
export default useInteraction
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect } from 'react'
|
||||
|
||||
/**
|
||||
* Hook to lock the body scroll whenever a component is mounted or
|
||||
@@ -12,17 +12,17 @@ import { useEffect } from 'react';
|
||||
*/
|
||||
export const useLockBodyScroll = (
|
||||
isLocked: boolean,
|
||||
disabled?: boolean
|
||||
disabled?: boolean,
|
||||
): void => {
|
||||
useEffect(() => {
|
||||
const originalStyle = window.getComputedStyle(document.body).overflow;
|
||||
const originalStyle = window.getComputedStyle(document.body).overflow
|
||||
if (isLocked && !disabled) {
|
||||
document.body.style.overflow = 'hidden';
|
||||
document.body.style.overflow = 'hidden'
|
||||
}
|
||||
return () => {
|
||||
if (!disabled) {
|
||||
document.body.style.overflow = originalStyle;
|
||||
document.body.style.overflow = originalStyle
|
||||
}
|
||||
};
|
||||
}, [isLocked, disabled]);
|
||||
};
|
||||
}
|
||||
}, [isLocked, disabled])
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
const CatchAll = () => {}
|
||||
|
||||
export default CatchAll
|
||||
export default CatchAll
|
||||
|
||||
@@ -5,4 +5,4 @@ const collections: NextPage = () => {
|
||||
return <Collection />
|
||||
}
|
||||
|
||||
export default collections
|
||||
export default collections
|
||||
|
||||
@@ -8,7 +8,7 @@ const Home: NextPage = () => {
|
||||
useEffect(() => {
|
||||
router.push('/overview')
|
||||
}, [router])
|
||||
|
||||
|
||||
return <></>
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import React from 'react';
|
||||
import LoadingSpinner from '../../../../components/Common/LoadingSpinner';
|
||||
|
||||
import React from 'react'
|
||||
import LoadingSpinner from '../../../../components/Common/LoadingSpinner'
|
||||
|
||||
const PlexLoading: React.FC = () => {
|
||||
return (
|
||||
<div>
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export default PlexLoading;
|
||||
export default PlexLoading
|
||||
|
||||
@@ -5,4 +5,4 @@ const overview: NextPage = () => {
|
||||
return <Overview />
|
||||
}
|
||||
|
||||
export default overview
|
||||
export default overview
|
||||
|
||||
@@ -5,4 +5,4 @@ const rules: NextPage = () => {
|
||||
return <Rules />
|
||||
}
|
||||
|
||||
export default rules
|
||||
export default rules
|
||||
|
||||
@@ -31,7 +31,7 @@ class PlexOAuth {
|
||||
public initializeHeaders(): void {
|
||||
if (!window) {
|
||||
throw new Error(
|
||||
'Window is not defined. Are you calling this in the browser?'
|
||||
'Window is not defined. Are you calling this in the browser?',
|
||||
)
|
||||
}
|
||||
const browser = getParser(window.navigator.userAgent)
|
||||
@@ -54,13 +54,13 @@ class PlexOAuth {
|
||||
public async getPin(): Promise<PlexPin> {
|
||||
if (!this.plexHeaders) {
|
||||
throw new Error(
|
||||
'You must initialize the plex headers clientside to login'
|
||||
'You must initialize the plex headers clientside to login',
|
||||
)
|
||||
}
|
||||
const response = await axios.post(
|
||||
'https://plex.tv/api/v2/pins?strong=true',
|
||||
undefined,
|
||||
{ headers: this.plexHeaders }
|
||||
{ headers: this.plexHeaders },
|
||||
)
|
||||
|
||||
this.pin = { id: response.data.id, code: response.data.code }
|
||||
@@ -98,7 +98,7 @@ class PlexOAuth {
|
||||
|
||||
if (this.popup) {
|
||||
this.popup.location.href = `https://app.plex.tv/auth/#!?${this.encodeData(
|
||||
params
|
||||
params,
|
||||
)}`
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ class PlexOAuth {
|
||||
private async pinPoll(): Promise<string> {
|
||||
const executePoll = async (
|
||||
resolve: (authToken: string) => void,
|
||||
reject: (e: Error) => void
|
||||
reject: (e: Error) => void,
|
||||
) => {
|
||||
try {
|
||||
if (!this.pin) {
|
||||
@@ -117,7 +117,7 @@ class PlexOAuth {
|
||||
|
||||
const response = await axios.get(
|
||||
`https://plex.tv/api/v2/pins/${this.pin.id}`,
|
||||
{ headers: this.plexHeaders }
|
||||
{ headers: this.plexHeaders },
|
||||
)
|
||||
|
||||
if (response.data?.authToken) {
|
||||
@@ -154,7 +154,7 @@ class PlexOAuth {
|
||||
}): Window | void {
|
||||
if (!window) {
|
||||
throw new Error(
|
||||
'Window is undefined. Are you running this in the browser?'
|
||||
'Window is undefined. Are you running this in the browser?',
|
||||
)
|
||||
}
|
||||
// Fixes dual-screen position Most browsers Firefox
|
||||
@@ -165,13 +165,13 @@ class PlexOAuth {
|
||||
const width = window.innerWidth
|
||||
? window.innerWidth
|
||||
: document.documentElement.clientWidth
|
||||
? document.documentElement.clientWidth
|
||||
: screen.width
|
||||
? document.documentElement.clientWidth
|
||||
: screen.width
|
||||
const height = window.innerHeight
|
||||
? window.innerHeight
|
||||
: document.documentElement.clientHeight
|
||||
? document.documentElement.clientHeight
|
||||
: screen.height
|
||||
? document.documentElement.clientHeight
|
||||
: screen.height
|
||||
const left = width / 2 - w / 2 + dualScreenLeft
|
||||
const top = height / 2 - h / 2 + dualScreenTop
|
||||
|
||||
@@ -186,7 +186,7 @@ class PlexOAuth {
|
||||
', top=' +
|
||||
top +
|
||||
', left=' +
|
||||
left
|
||||
left,
|
||||
)
|
||||
if (newWindow) {
|
||||
newWindow.focus()
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
// these numbers equal the Plex API media types
|
||||
// Note: if changes are made, also change this in SERVER's EPlexDataType
|
||||
export enum EPlexDataType {
|
||||
MOVIES = 1,
|
||||
SHOWS = 2,
|
||||
SEASONS = 3,
|
||||
EPISODES = 4,
|
||||
}
|
||||
|
||||
MOVIES = 1,
|
||||
SHOWS = 2,
|
||||
SEASONS = 3,
|
||||
EPISODES = 4,
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ export function getPortFromUrl(url: string): string | undefined {
|
||||
export function getHostname(url: string): string | undefined {
|
||||
try {
|
||||
const urlObject = new URL(url)
|
||||
let baseUrl = urlObject.protocol + '//' + urlObject.hostname
|
||||
const baseUrl = urlObject.protocol + '//' + urlObject.hostname
|
||||
return baseUrl
|
||||
} catch (error) {
|
||||
console.error('Invalid URL:', error.message)
|
||||
|
||||
@@ -61,8 +61,15 @@
|
||||
border-radius: 5px;
|
||||
padding: 0.75rem;
|
||||
font-size: 1.1rem;
|
||||
font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
|
||||
Bitstream Vera Sans Mono, Courier New, monospace;
|
||||
font-family:
|
||||
Menlo,
|
||||
Monaco,
|
||||
Lucida Console,
|
||||
Liberation Mono,
|
||||
DejaVu Sans Mono,
|
||||
Bitstream Vera Sans Mono,
|
||||
Courier New,
|
||||
monospace;
|
||||
}
|
||||
|
||||
.grid {
|
||||
@@ -81,7 +88,9 @@
|
||||
text-decoration: none;
|
||||
border: 1px solid #eaeaea;
|
||||
border-radius: 10px;
|
||||
transition: color 0.15s ease, border-color 0.15s ease;
|
||||
transition:
|
||||
color 0.15s ease,
|
||||
border-color 0.15s ease;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
|
||||
@@ -135,15 +135,11 @@
|
||||
}
|
||||
|
||||
.edit-button {
|
||||
@apply /*
|
||||
|
||||
background-color: #3f3f46; */ bg-amber-900;
|
||||
@apply /* background-color: #3f3f46; */ bg-amber-900;
|
||||
}
|
||||
|
||||
.edit-button:hover {
|
||||
@apply /*
|
||||
|
||||
background: #3a3a42; */ bg-amber-800;
|
||||
@apply /* background: #3a3a42; */ bg-amber-800;
|
||||
}
|
||||
|
||||
.delete-button {
|
||||
@@ -177,7 +173,7 @@
|
||||
}
|
||||
|
||||
.slider-header {
|
||||
@apply relative mt-6 mb-4 flex;
|
||||
@apply relative mb-4 mt-6 flex;
|
||||
}
|
||||
|
||||
.slider-title {
|
||||
@@ -244,7 +240,7 @@
|
||||
}
|
||||
|
||||
.media-overview {
|
||||
@apply flex flex-col pt-8 pb-4 text-white lg:flex-row;
|
||||
@apply flex flex-col pb-4 pt-8 text-white lg:flex-row;
|
||||
}
|
||||
|
||||
.media-overview-left {
|
||||
@@ -302,7 +298,7 @@
|
||||
}
|
||||
|
||||
.error-message {
|
||||
@apply relative top-0 bottom-0 left-0 right-0 flex h-screen flex-col items-center justify-center text-center text-zinc-300;
|
||||
@apply relative bottom-0 left-0 right-0 top-0 flex h-screen flex-col items-center justify-center text-center text-zinc-300;
|
||||
}
|
||||
|
||||
.heading {
|
||||
@@ -338,7 +334,7 @@
|
||||
}
|
||||
|
||||
.section {
|
||||
@apply mt-6 mb-10 text-white;
|
||||
@apply mb-10 mt-6 text-white;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
@@ -346,7 +342,7 @@
|
||||
}
|
||||
|
||||
.form-input {
|
||||
@apply text-sm text-white sm:col-span-2 border-0 bg-inherit;
|
||||
@apply border-0 bg-inherit text-sm text-white sm:col-span-2;
|
||||
}
|
||||
|
||||
.form-input-field {
|
||||
@@ -548,7 +544,7 @@
|
||||
.react-select-container .react-select__input-container,
|
||||
.react-select-container .react-select__placeholder,
|
||||
.react-select-container .react-select__single-value {
|
||||
@apply text-white font-semibold;
|
||||
@apply font-semibold text-white;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
const defaultTheme = require('tailwindcss/defaultTheme')
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"target": "es2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
@@ -9,7 +9,7 @@
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"moduleResolution": "bundler",
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"useUnknownInCatchVariables": false,
|
||||
@@ -17,8 +17,13 @@
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
]
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user