mirror of
https://github.com/KevinMidboe/seasoned.git
synced 2026-05-13 17:45:43 +00:00
Compare commits
13 Commits
dependabot
...
feat/vite
| Author | SHA1 | Date | |
|---|---|---|---|
| 59bca43c62 | |||
| 3f28988ef8 | |||
| 205d7e1ed5 | |||
| 91c81cafce | |||
| 76eca1b3b7 | |||
| 2883760362 | |||
| c5c1cf1c8d | |||
| b30c068f9e | |||
| 4b68a4ad7c | |||
| 3e5267933c | |||
| e427d9db26 | |||
| bcf5b7d890 | |||
| 037857cd51 |
31
Caddyfile
31
Caddyfile
@@ -1,31 +0,0 @@
|
|||||||
{
|
|
||||||
# Disable automatic HTTPS
|
|
||||||
auto_https off
|
|
||||||
}
|
|
||||||
|
|
||||||
:8080 {
|
|
||||||
root * {$DIST_PATH:/usr/share/caddy}
|
|
||||||
|
|
||||||
file_server
|
|
||||||
|
|
||||||
encode gzip zstd
|
|
||||||
|
|
||||||
try_files {path} {path}/ /index.html
|
|
||||||
|
|
||||||
# Cache favicons aggressively
|
|
||||||
@favicons path /favicons/*
|
|
||||||
header @favicons Cache-Control "public, max-age=31536000, immutable"
|
|
||||||
|
|
||||||
# Cache static assets based on MIME type
|
|
||||||
@static {
|
|
||||||
header Content-Type application/javascript*
|
|
||||||
header Content-Type text/css*
|
|
||||||
header Content-Type image/*
|
|
||||||
header Content-Type font/*
|
|
||||||
header Content-Type application/font-*
|
|
||||||
header Content-Type application/woff*
|
|
||||||
header Content-Type application/json*
|
|
||||||
}
|
|
||||||
|
|
||||||
header @static Cache-Control "public, max-age=2592000, immutable"
|
|
||||||
}
|
|
||||||
25
Dockerfile
25
Dockerfile
@@ -4,14 +4,14 @@ FROM node:24.13.1 AS build
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
COPY package.json yarn.lock ./
|
COPY package.json yarn.lock .
|
||||||
RUN yarn install --frozen-lockfile
|
RUN yarn install --frozen-lockfile
|
||||||
|
|
||||||
# Copy source files that the build depends on
|
# Copy source files that the build depends on
|
||||||
COPY index.html .
|
COPY index.html .
|
||||||
COPY public/ public/
|
COPY public/ public/
|
||||||
COPY src/ src/
|
COPY src/ src/
|
||||||
COPY tsconfig.json vite.config.ts ./
|
COPY tsconfig.json vite.config.ts .
|
||||||
|
|
||||||
ARG SEASONED_API=http://localhost:31459
|
ARG SEASONED_API=http://localhost:31459
|
||||||
ENV VITE_SEASONED_API=$SEASONED_API
|
ENV VITE_SEASONED_API=$SEASONED_API
|
||||||
@@ -23,16 +23,21 @@ ENV VITE_ELASTIC_API_KEY=$ELASTIC_API_KEY
|
|||||||
|
|
||||||
RUN yarn build
|
RUN yarn build
|
||||||
|
|
||||||
FROM caddy:2.11-alpine
|
FROM nginx:1.29.5
|
||||||
|
|
||||||
COPY Caddyfile /etc/caddy/Caddyfile
|
|
||||||
|
|
||||||
# Copy static files
|
|
||||||
COPY public /usr/share/caddy
|
|
||||||
|
|
||||||
# Copy the static build from the previous stage
|
# Copy the static build from the previous stage
|
||||||
COPY --from=build /app/dist /usr/share/caddy
|
COPY index.html /usr/share/nginx/html
|
||||||
|
COPY public/ /usr/share/nginx/html
|
||||||
|
COPY --from=build /app/dist /usr/share/nginx/html
|
||||||
|
|
||||||
EXPOSE 8080
|
# Copy nginx config file
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf.template
|
||||||
|
|
||||||
|
# Manual entrypoint after nginx substring
|
||||||
|
COPY docker-entrypoint.sh /docker-entrypoint.d/05-docker-entrypoint.sh
|
||||||
|
|
||||||
|
RUN chmod +x /docker-entrypoint.d/05-docker-entrypoint.sh
|
||||||
|
|
||||||
|
EXPOSE 5000
|
||||||
|
|
||||||
LABEL org.opencontainers.image.source https://github.com/kevinmidboe/seasoned
|
LABEL org.opencontainers.image.source https://github.com/kevinmidboe/seasoned
|
||||||
|
|||||||
9
docker-entrypoint.sh
Normal file
9
docker-entrypoint.sh
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
export SEASONED_API=${SEASONED_API:-http://localhost:31459}
|
||||||
|
export SEASONED_DOMAIN=${SEASONED_DOMAIN:-localhost}
|
||||||
|
|
||||||
|
envsubst '$SEASONED_API,$SEASONED_DOMAIN' < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
exec "$@"
|
||||||
@@ -10,7 +10,9 @@ import prettierPlugin from "eslint-plugin-prettier";
|
|||||||
const CUSTOM_RULES = {
|
const CUSTOM_RULES = {
|
||||||
"vue/no-v-model-argument": "off",
|
"vue/no-v-model-argument": "off",
|
||||||
"no-underscore-dangle": "off",
|
"no-underscore-dangle": "off",
|
||||||
"vue/multi-word-component-names": "off"
|
"vue/multi-word-component-names": "off",
|
||||||
|
"no-shadow": "off",
|
||||||
|
"@typescript-eslint/no-shadow": ["error"]
|
||||||
};
|
};
|
||||||
|
|
||||||
const gitignorePath = path.resolve(".", ".gitignore");
|
const gitignorePath = path.resolve(".", ".gitignore");
|
||||||
@@ -33,7 +35,6 @@ const nodeConfig = defineConfig([plugins.node, ...configs.node.recommended]);
|
|||||||
const typescriptConfig = defineConfig([
|
const typescriptConfig = defineConfig([
|
||||||
plugins.typescriptEslint,
|
plugins.typescriptEslint,
|
||||||
...configs.base.typescript
|
...configs.base.typescript
|
||||||
// rules.typescript.typescriptEslintStrict
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Prettier config
|
// Prettier config
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
<!doctype html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
name="viewport"
|
|
||||||
content="width=device-width, viewport-fit=cover, initial-scale=1"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<link
|
<link
|
||||||
href="https://fonts.googleapis.com/css?family=Roboto:300,400,500&subset=cyrillic"
|
href="https://fonts.googleapis.com/css?family=Roboto:300,400,500&subset=cyrillic"
|
||||||
|
|||||||
30
nginx.conf
Normal file
30
nginx.conf
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
server {
|
||||||
|
listen 5000 default_server;
|
||||||
|
listen [::]:5000 default_server;
|
||||||
|
|
||||||
|
server_name _;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
|
||||||
|
gzip on;
|
||||||
|
gzip_types application/javascript;
|
||||||
|
gzip_min_length 1000;
|
||||||
|
gzip_static on;
|
||||||
|
|
||||||
|
location /favicons {
|
||||||
|
autoindex on;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /dist {
|
||||||
|
add_header Content-Type application/javascript;
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /api {
|
||||||
|
proxy_pass $SEASONED_API;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
index index.html;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,8 +7,9 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "NODE_ENV=development vite",
|
"dev": "NODE_ENV=development vite",
|
||||||
"build": "yarn vite build",
|
"build": "yarn vite build",
|
||||||
"lint": "eslint src; prettier -c src",
|
"clean": "rm -r dist 2> /dev/null; rm public/index.html 2> /dev/null; rm -r lib 2> /dev/null",
|
||||||
"clean": "rm -rf dist/ yarn-*.log 2>/dev/null",
|
"start": "echo 'Start using docker, consult README'",
|
||||||
|
"lint": "eslint src --ext .ts,.vue",
|
||||||
"docs": "documentation build src/api.ts -f html -o docs/api && documentation build src/api.ts -f md -o docs/api.md"
|
"docs": "documentation build src/api.ts -f html -o docs/api && documentation build src/api.ts -f md -o docs/api.md"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -58,14 +58,13 @@
|
|||||||
|
|
||||||
.content {
|
.content {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-column: 2 / 3;
|
|
||||||
width: calc(100% - var(--header-size));
|
width: calc(100% - var(--header-size));
|
||||||
|
grid-column: 2 / 3;
|
||||||
grid-row: 2;
|
grid-row: 2;
|
||||||
z-index: 5;
|
z-index: 5;
|
||||||
|
|
||||||
@include mobile {
|
@include mobile {
|
||||||
grid-column: 1 / 3;
|
grid-column: 1 / 3;
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
259
src/api.ts
259
src/api.ts
@@ -1,10 +1,5 @@
|
|||||||
/* eslint-disable n/no-unsupported-features/node-builtins */
|
/* eslint-disable n/no-unsupported-features/node-builtins */
|
||||||
import {
|
import { IList, IMediaCredits, IPersonCredits } from "./interfaces/IList";
|
||||||
IList,
|
|
||||||
IMediaCredits,
|
|
||||||
IPersonCredits,
|
|
||||||
MediaTypes
|
|
||||||
} from "./interfaces/IList";
|
|
||||||
import type {
|
import type {
|
||||||
IRequestStatusResponse,
|
IRequestStatusResponse,
|
||||||
IRequestSubmitResponse
|
IRequestSubmitResponse
|
||||||
@@ -15,17 +10,22 @@ const ELASTIC_URL = import.meta.env.VITE_ELASTIC_URL;
|
|||||||
const ELASTIC_API_KEY = import.meta.env.VITE_ELASTIC_API_KEY;
|
const ELASTIC_API_KEY = import.meta.env.VITE_ELASTIC_API_KEY;
|
||||||
|
|
||||||
// - - - TMDB - - -
|
// - - - TMDB - - -
|
||||||
interface GetMediaOpts {
|
|
||||||
checkExistance: boolean;
|
|
||||||
credits: boolean;
|
|
||||||
releaseDates?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const getMovie = async (id: number, opts: GetMediaOpts) => {
|
/**
|
||||||
|
* Fetches tmdb movie by id. Can optionally include cast credits in result object.
|
||||||
|
* @param {number} id
|
||||||
|
* @returns {object} Tmdb response
|
||||||
|
*/
|
||||||
|
const getMovie = (
|
||||||
|
id,
|
||||||
|
{
|
||||||
|
checkExistance,
|
||||||
|
credits,
|
||||||
|
releaseDates
|
||||||
|
}: { checkExistance: boolean; credits: boolean; releaseDates?: boolean }
|
||||||
|
) => {
|
||||||
const url = new URL("/api/v2/movie", API_HOSTNAME);
|
const url = new URL("/api/v2/movie", API_HOSTNAME);
|
||||||
url.pathname = `${url.pathname}/${id.toString()}`;
|
url.pathname = `${url.pathname}/${id.toString()}`;
|
||||||
|
|
||||||
const { checkExistance, credits, releaseDates } = opts;
|
|
||||||
if (checkExistance) {
|
if (checkExistance) {
|
||||||
url.searchParams.append("check_existance", "true");
|
url.searchParams.append("check_existance", "true");
|
||||||
}
|
}
|
||||||
@@ -44,12 +44,22 @@ const getMovie = async (id: number, opts: GetMediaOpts) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fetches tmdb show by id. Can optionally include cast credits in result object.
|
/**
|
||||||
const getShow = async (id: number, opts: GetMediaOpts) => {
|
* Fetches tmdb show by id. Can optionally include cast credits in result object.
|
||||||
|
* @param {number} id
|
||||||
|
* @param {boolean} [credits=false] Include credits
|
||||||
|
* @returns {object} Tmdb response
|
||||||
|
*/
|
||||||
|
const getShow = (
|
||||||
|
id,
|
||||||
|
{
|
||||||
|
checkExistance,
|
||||||
|
credits,
|
||||||
|
releaseDates
|
||||||
|
}: { checkExistance: boolean; credits: boolean; releaseDates?: boolean }
|
||||||
|
) => {
|
||||||
const url = new URL("/api/v2/show", API_HOSTNAME);
|
const url = new URL("/api/v2/show", API_HOSTNAME);
|
||||||
url.pathname = `${url.pathname}/${id.toString()}`;
|
url.pathname = `${url.pathname}/${id.toString()}`;
|
||||||
|
|
||||||
const { checkExistance, credits, releaseDates } = opts;
|
|
||||||
if (checkExistance) {
|
if (checkExistance) {
|
||||||
url.searchParams.append("check_existance", "true");
|
url.searchParams.append("check_existance", "true");
|
||||||
}
|
}
|
||||||
@@ -68,8 +78,13 @@ const getShow = async (id: number, opts: GetMediaOpts) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fetches tmdb person by id. Can optionally include cast credits in result object.
|
/**
|
||||||
const getPerson = async (id: number, credits = false) => {
|
* Fetches tmdb person by id. Can optionally include cast credits in result object.
|
||||||
|
* @param {number} id
|
||||||
|
* @param {boolean} [credits=false] Include credits
|
||||||
|
* @returns {object} Tmdb response
|
||||||
|
*/
|
||||||
|
const getPerson = (id, credits = false) => {
|
||||||
const url = new URL("/api/v2/person", API_HOSTNAME);
|
const url = new URL("/api/v2/person", API_HOSTNAME);
|
||||||
url.pathname = `${url.pathname}/${id.toString()}`;
|
url.pathname = `${url.pathname}/${id.toString()}`;
|
||||||
if (credits) {
|
if (credits) {
|
||||||
@@ -84,8 +99,12 @@ const getPerson = async (id: number, credits = false) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fetches tmdb movie credits by id.
|
/**
|
||||||
const getMovieCredits = async (id: number): Promise<IMediaCredits> => {
|
* Fetches tmdb movie credits by id.
|
||||||
|
* @param {number} id
|
||||||
|
* @returns {object} Tmdb response
|
||||||
|
*/
|
||||||
|
const getMovieCredits = (id: number): Promise<IMediaCredits> => {
|
||||||
const url = new URL("/api/v2/movie", API_HOSTNAME);
|
const url = new URL("/api/v2/movie", API_HOSTNAME);
|
||||||
url.pathname = `${url.pathname}/${id.toString()}/credits`;
|
url.pathname = `${url.pathname}/${id.toString()}/credits`;
|
||||||
|
|
||||||
@@ -97,8 +116,12 @@ const getMovieCredits = async (id: number): Promise<IMediaCredits> => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fetches tmdb show credits by id.
|
/**
|
||||||
const getShowCredits = async (id: number): Promise<IMediaCredits> => {
|
* Fetches tmdb show credits by id.
|
||||||
|
* @param {number} id
|
||||||
|
* @returns {object} Tmdb response
|
||||||
|
*/
|
||||||
|
const getShowCredits = (id: number): Promise<IMediaCredits> => {
|
||||||
const url = new URL("/api/v2/show", API_HOSTNAME);
|
const url = new URL("/api/v2/show", API_HOSTNAME);
|
||||||
url.pathname = `${url.pathname}/${id.toString()}/credits`;
|
url.pathname = `${url.pathname}/${id.toString()}/credits`;
|
||||||
|
|
||||||
@@ -110,8 +133,12 @@ const getShowCredits = async (id: number): Promise<IMediaCredits> => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fetches tmdb person credits by id.
|
/**
|
||||||
const getPersonCredits = async (id: number): Promise<IPersonCredits> => {
|
* Fetches tmdb person credits by id.
|
||||||
|
* @param {number} id
|
||||||
|
* @returns {object} Tmdb response
|
||||||
|
*/
|
||||||
|
const getPersonCredits = (id: number): Promise<IPersonCredits> => {
|
||||||
const url = new URL("/api/v2/person", API_HOSTNAME);
|
const url = new URL("/api/v2/person", API_HOSTNAME);
|
||||||
url.pathname = `${url.pathname}/${id.toString()}/credits`;
|
url.pathname = `${url.pathname}/${id.toString()}/credits`;
|
||||||
|
|
||||||
@@ -123,11 +150,13 @@ const getPersonCredits = async (id: number): Promise<IPersonCredits> => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fetches tmdb list by name.
|
/**
|
||||||
const getTmdbMovieListByName = async (
|
* Fetches tmdb list by name.
|
||||||
name: string,
|
* @param {string} name List the fetch
|
||||||
page = 1
|
* @param {number} [page=1]
|
||||||
): Promise<IList> => {
|
* @returns {object} Tmdb list response
|
||||||
|
*/
|
||||||
|
const getTmdbMovieListByName = (name: string, page = 1): Promise<IList> => {
|
||||||
const url = new URL(`/api/v2/movie/${name}`, API_HOSTNAME);
|
const url = new URL(`/api/v2/movie/${name}`, API_HOSTNAME);
|
||||||
url.searchParams.append("page", page.toString());
|
url.searchParams.append("page", page.toString());
|
||||||
|
|
||||||
@@ -135,8 +164,12 @@ const getTmdbMovieListByName = async (
|
|||||||
// .catch(error => { console.error(`api error getting list: ${name}, page: ${page}`); throw error }) // eslint-disable-line no-console
|
// .catch(error => { console.error(`api error getting list: ${name}, page: ${page}`); throw error }) // eslint-disable-line no-console
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fetches requested items.
|
/**
|
||||||
const getRequests = async (page = 1) => {
|
* Fetches requested items.
|
||||||
|
* @param {number} [page=1]
|
||||||
|
* @returns {object} Request response
|
||||||
|
*/
|
||||||
|
const getRequests = (page = 1) => {
|
||||||
const url = new URL("/api/v2/request", API_HOSTNAME);
|
const url = new URL("/api/v2/request", API_HOSTNAME);
|
||||||
url.searchParams.append("page", page.toString());
|
url.searchParams.append("page", page.toString());
|
||||||
|
|
||||||
@@ -144,25 +177,20 @@ const getRequests = async (page = 1) => {
|
|||||||
// .catch(error => { console.error(`api error getting list: ${name}, page: ${page}`); throw error }) // eslint-disable-line no-console
|
// .catch(error => { console.error(`api error getting list: ${name}, page: ${page}`); throw error }) // eslint-disable-line no-console
|
||||||
};
|
};
|
||||||
|
|
||||||
const getUserRequests = async (page = 1) => {
|
const getUserRequests = (page = 1) => {
|
||||||
const url = new URL("/api/v1/user/requests", API_HOSTNAME);
|
const url = new URL("/api/v1/user/requests", API_HOSTNAME);
|
||||||
url.searchParams.append("page", page.toString());
|
url.searchParams.append("page", page.toString());
|
||||||
|
|
||||||
const options: RequestInit = {
|
return fetch(url.href).then(resp => resp.json());
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
credentials: "include"
|
|
||||||
};
|
|
||||||
|
|
||||||
return fetch(url.href, options).then(resp => resp.json());
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fetches tmdb movies and shows by query.
|
/**
|
||||||
const searchTmdb = async (
|
* Fetches tmdb movies and shows by query.
|
||||||
query: string,
|
* @param {string} query
|
||||||
page = 1,
|
* @param {number} [page=1]
|
||||||
adult = false,
|
* @returns {object} Tmdb response
|
||||||
mediaType = null
|
*/
|
||||||
) => {
|
const searchTmdb = (query, page = 1, adult = false, mediaType = null) => {
|
||||||
const url = new URL("/api/v2/search", API_HOSTNAME);
|
const url = new URL("/api/v2/search", API_HOSTNAME);
|
||||||
if (mediaType != null && ["movie", "show", "person"].includes(mediaType)) {
|
if (mediaType != null && ["movie", "show", "person"].includes(mediaType)) {
|
||||||
url.pathname += `/${mediaType}`;
|
url.pathname += `/${mediaType}`;
|
||||||
@@ -182,15 +210,17 @@ const searchTmdb = async (
|
|||||||
|
|
||||||
// - - - Torrents - - -
|
// - - - Torrents - - -
|
||||||
|
|
||||||
// Search for torrents by query
|
/**
|
||||||
const searchTorrents = async (query: string) => {
|
* Search for torrents by query
|
||||||
|
* @param {string} query
|
||||||
|
* @param {boolean} credits Include credits
|
||||||
|
* @returns {object} Torrent response
|
||||||
|
*/
|
||||||
|
const searchTorrents = query => {
|
||||||
const url = new URL("/api/v1/pirate/search", API_HOSTNAME);
|
const url = new URL("/api/v1/pirate/search", API_HOSTNAME);
|
||||||
url.searchParams.append("query", query);
|
url.searchParams.append("query", query);
|
||||||
const options: RequestInit = {
|
|
||||||
credentials: "include"
|
|
||||||
};
|
|
||||||
|
|
||||||
return fetch(url.href, options)
|
return fetch(url.href)
|
||||||
.then(resp => resp.json())
|
.then(resp => resp.json())
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error(`api error searching torrents: ${query}`); // eslint-disable-line no-console
|
console.error(`api error searching torrents: ${query}`); // eslint-disable-line no-console
|
||||||
@@ -198,18 +228,19 @@ const searchTorrents = async (query: string) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add magnet to download queue.
|
/**
|
||||||
const addMagnet = async (
|
* Add magnet to download queue.
|
||||||
magnet: string,
|
* @param {string} magnet Magnet link
|
||||||
name: string,
|
* @param {boolean} name Name of torrent
|
||||||
tmdbId: number | null
|
* @param {boolean} tmdbId
|
||||||
) => {
|
* @returns {object} Success/Failure response
|
||||||
|
*/
|
||||||
|
const addMagnet = (magnet: string, name: string, tmdbId: number | null) => {
|
||||||
const url = new URL("/api/v1/pirate/add", API_HOSTNAME);
|
const url = new URL("/api/v1/pirate/add", API_HOSTNAME);
|
||||||
|
|
||||||
const options: RequestInit = {
|
const options = {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
credentials: "include",
|
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
magnet,
|
magnet,
|
||||||
name,
|
name,
|
||||||
@@ -227,11 +258,14 @@ const addMagnet = async (
|
|||||||
|
|
||||||
// - - - Plex/Request - - -
|
// - - - Plex/Request - - -
|
||||||
|
|
||||||
// Request a movie or show from id. If authorization token is included the user will be linked
|
/**
|
||||||
const request = async (
|
* Request a movie or show from id. If authorization token is included the user will be linked
|
||||||
id: number,
|
* to the requested item.
|
||||||
type: MediaTypes.Movie | MediaTypes.Show
|
* @param {number} id Movie or show id
|
||||||
): Promise<IRequestSubmitResponse> => {
|
* @param {string} type Movie or show type
|
||||||
|
* @returns {object} Success/Failure response
|
||||||
|
*/
|
||||||
|
const request = (id, type): Promise<IRequestSubmitResponse> => {
|
||||||
const url = new URL("/api/v2/request", API_HOSTNAME);
|
const url = new URL("/api/v2/request", API_HOSTNAME);
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
@@ -248,11 +282,13 @@ const request = async (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check request status by tmdb id and type
|
/**
|
||||||
const getRequestStatus = async (
|
* Check request status by tmdb id and type
|
||||||
id: number,
|
* @param {number} tmdb id
|
||||||
type = null
|
* @param {string} type
|
||||||
): Promise<IRequestStatusResponse> => {
|
* @returns {object} Success/Failure response
|
||||||
|
*/
|
||||||
|
const getRequestStatus = (id, type = null): Promise<IRequestStatusResponse> => {
|
||||||
const url = new URL("/api/v2/request", API_HOSTNAME);
|
const url = new URL("/api/v2/request", API_HOSTNAME);
|
||||||
url.pathname = `${url.pathname}/${id.toString()}`;
|
url.pathname = `${url.pathname}/${id.toString()}`;
|
||||||
url.searchParams.append("type", type);
|
url.searchParams.append("type", type);
|
||||||
@@ -262,8 +298,7 @@ const getRequestStatus = async (
|
|||||||
.catch(err => Promise.reject(err));
|
.catch(err => Promise.reject(err));
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
const watchLink = (title, year) => {
|
||||||
const watchLink = async (title, year) => {
|
|
||||||
const url = new URL("/api/v1/plex/watch-link", API_HOSTNAME);
|
const url = new URL("/api/v1/plex/watch-link", API_HOSTNAME);
|
||||||
url.searchParams.append("title", title);
|
url.searchParams.append("title", title);
|
||||||
url.searchParams.append("year", year);
|
url.searchParams.append("year", year);
|
||||||
@@ -278,16 +313,14 @@ const movieImages = id => {
|
|||||||
|
|
||||||
return fetch(url.href).then(resp => resp.json());
|
return fetch(url.href).then(resp => resp.json());
|
||||||
};
|
};
|
||||||
*/
|
|
||||||
|
|
||||||
// - - - Seasoned user endpoints - - -
|
// - - - Seasoned user endpoints - - -
|
||||||
|
|
||||||
const register = async (username: string, password: string) => {
|
const register = (username, password) => {
|
||||||
const url = new URL("/api/v1/user", API_HOSTNAME);
|
const url = new URL("/api/v1/user", API_HOSTNAME);
|
||||||
const options: RequestInit = {
|
const options = {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
credentials: "include",
|
|
||||||
body: JSON.stringify({ username, password })
|
body: JSON.stringify({ username, password })
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -309,17 +342,16 @@ const login = async (
|
|||||||
throwError = false
|
throwError = false
|
||||||
) => {
|
) => {
|
||||||
const url = new URL("/api/v1/user/login", API_HOSTNAME);
|
const url = new URL("/api/v1/user/login", API_HOSTNAME);
|
||||||
const options: RequestInit = {
|
const options = {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
credentials: "include",
|
|
||||||
body: JSON.stringify({ username, password })
|
body: JSON.stringify({ username, password })
|
||||||
};
|
};
|
||||||
|
|
||||||
return fetch(url.href, options).then(resp => {
|
return fetch(url.href, options).then(resp => {
|
||||||
if (resp.status === 200) return resp.json();
|
if (resp.status === 200) return resp.json();
|
||||||
|
|
||||||
if (throwError) return Promise.reject(resp.text().then(t => new Error(t)));
|
if (throwError) throw resp;
|
||||||
console.error("Error occured when trying to sign in.\nError:", resp); // eslint-disable-line no-console
|
console.error("Error occured when trying to sign in.\nError:", resp); // eslint-disable-line no-console
|
||||||
return Promise.reject(resp);
|
return Promise.reject(resp);
|
||||||
});
|
});
|
||||||
@@ -327,25 +359,21 @@ const login = async (
|
|||||||
|
|
||||||
const logout = async (throwError = false) => {
|
const logout = async (throwError = false) => {
|
||||||
const url = new URL("/api/v1/user/logout", API_HOSTNAME);
|
const url = new URL("/api/v1/user/logout", API_HOSTNAME);
|
||||||
const options: RequestInit = { method: "POST", credentials: "include" };
|
const options = { method: "POST" };
|
||||||
|
|
||||||
return fetch(url.href, options).then(resp => {
|
return fetch(url.href, options).then(resp => {
|
||||||
if (resp.status === 200) return resp.json();
|
if (resp.status === 200) return resp.json();
|
||||||
|
|
||||||
if (throwError) return Promise.reject(resp.text().then(t => new Error(t)));
|
if (throwError) throw resp;
|
||||||
console.error("Error occured when trying to log out.\nError:", resp); // eslint-disable-line no-console
|
console.error("Error occured when trying to log out.\nError:", resp); // eslint-disable-line no-console
|
||||||
return Promise.reject(resp);
|
return Promise.reject(resp);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSettings = async () => {
|
const getSettings = () => {
|
||||||
const url = new URL("/api/v1/user/settings", API_HOSTNAME);
|
const url = new URL("/api/v1/user/settings", API_HOSTNAME);
|
||||||
const options: RequestInit = {
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
credentials: "include"
|
|
||||||
};
|
|
||||||
|
|
||||||
return fetch(url.href, options)
|
return fetch(url.href)
|
||||||
.then(resp => resp.json())
|
.then(resp => resp.json())
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.log("api error getting user settings"); // eslint-disable-line no-console
|
console.log("api error getting user settings"); // eslint-disable-line no-console
|
||||||
@@ -353,13 +381,12 @@ const getSettings = async () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateSettings = async (settings: any) => {
|
const updateSettings = settings => {
|
||||||
const url = new URL("/api/v1/user/settings", API_HOSTNAME);
|
const url = new URL("/api/v1/user/settings", API_HOSTNAME);
|
||||||
|
|
||||||
const options: RequestInit = {
|
const options = {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
credentials: "include",
|
|
||||||
body: JSON.stringify(settings)
|
body: JSON.stringify(settings)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -373,14 +400,13 @@ const updateSettings = async (settings: any) => {
|
|||||||
|
|
||||||
// - - - Authenticate with plex - - -
|
// - - - Authenticate with plex - - -
|
||||||
|
|
||||||
const linkPlexAccount = async (username: string, password: string) => {
|
const linkPlexAccount = (username, password) => {
|
||||||
const url = new URL("/api/v1/user/link_plex", API_HOSTNAME);
|
const url = new URL("/api/v1/user/link_plex", API_HOSTNAME);
|
||||||
const body = { username, password };
|
const body = { username, password };
|
||||||
|
|
||||||
const options: RequestInit = {
|
const options = {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
credentials: "include",
|
|
||||||
body: JSON.stringify(body)
|
body: JSON.stringify(body)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -392,12 +418,12 @@ const linkPlexAccount = async (username: string, password: string) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const unlinkPlexAccount = async () => {
|
const unlinkPlexAccount = () => {
|
||||||
const url = new URL("/api/v1/user/unlink_plex", API_HOSTNAME);
|
const url = new URL("/api/v1/user/unlink_plex", API_HOSTNAME);
|
||||||
const options: RequestInit = {
|
|
||||||
|
const options = {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" }
|
||||||
credentials: "include"
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return fetch(url.href, options)
|
return fetch(url.href, options)
|
||||||
@@ -419,12 +445,7 @@ const fetchGraphData = async (
|
|||||||
url.searchParams.append("days", String(days));
|
url.searchParams.append("days", String(days));
|
||||||
url.searchParams.append("y_axis", chartType);
|
url.searchParams.append("y_axis", chartType);
|
||||||
|
|
||||||
const options: RequestInit = {
|
return fetch(url.href).then(resp => {
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
credentials: "include"
|
|
||||||
};
|
|
||||||
|
|
||||||
return fetch(url.href, options).then(resp => {
|
|
||||||
if (!resp.ok) {
|
if (!resp.ok) {
|
||||||
console.log("DAMN WE FAILED!", resp); // eslint-disable-line no-console
|
console.log("DAMN WE FAILED!", resp); // eslint-disable-line no-console
|
||||||
throw Error(resp.statusText);
|
throw Error(resp.statusText);
|
||||||
@@ -450,30 +471,15 @@ const getEmoji = async () => {
|
|||||||
// - - - ELASTIC SEARCH - - -
|
// - - - ELASTIC SEARCH - - -
|
||||||
// This elastic index contains titles mapped to ids. Lightning search
|
// This elastic index contains titles mapped to ids. Lightning search
|
||||||
// used for autocomplete
|
// used for autocomplete
|
||||||
interface TimeoutRequestInit extends RequestInit {
|
|
||||||
timeout: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchWithTimeout(url: string, options: TimeoutRequestInit) {
|
|
||||||
const { timeout = 2000 } = options;
|
|
||||||
|
|
||||||
const controller = new AbortController();
|
|
||||||
const timer = setTimeout(() => controller.abort(), timeout);
|
|
||||||
|
|
||||||
const response = await fetch(url, {
|
|
||||||
...options,
|
|
||||||
signal: controller.signal
|
|
||||||
});
|
|
||||||
clearTimeout(timer);
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search elastic indexes movies and shows by query. Doc includes Tmdb daily export of Movies and
|
* Search elastic indexes movies and shows by query. Doc includes Tmdb daily export of Movies and
|
||||||
* Tv Shows. See tmdb docs for more info: https://developers.themoviedb.org/3/getting-started/daily-file-exports
|
* Tv Shows. See tmdb docs for more info: https://developers.themoviedb.org/3/getting-started/daily-file-exports
|
||||||
|
* @param {string} query
|
||||||
|
* @returns {object} List of movies and shows matching query
|
||||||
*/
|
*/
|
||||||
const elasticSearchMoviesAndShows = async (query: string, count = 22) => {
|
|
||||||
|
const elasticSearchMoviesAndShows = (query, count = 22) => {
|
||||||
const url = new URL(`${ELASTIC_URL}/_search`);
|
const url = new URL(`${ELASTIC_URL}/_search`);
|
||||||
|
|
||||||
const body = {
|
const body = {
|
||||||
@@ -525,11 +531,10 @@ const elasticSearchMoviesAndShows = async (query: string, count = 22) => {
|
|||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
Authorization: `ApiKey ${ELASTIC_API_KEY}`
|
Authorization: `ApiKey ${ELASTIC_API_KEY}`
|
||||||
},
|
},
|
||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body)
|
||||||
timeout: 1000
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return fetchWithTimeout(url.href, options)
|
return fetch(url.href, options)
|
||||||
.then(resp => resp.json())
|
.then(resp => resp.json())
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.log(`api error searching elasticsearch: ${query}`); // eslint-disable-line no-console
|
console.log(`api error searching elasticsearch: ${query}`); // eslint-disable-line no-console
|
||||||
@@ -551,6 +556,8 @@ export {
|
|||||||
searchTorrents,
|
searchTorrents,
|
||||||
addMagnet,
|
addMagnet,
|
||||||
request,
|
request,
|
||||||
|
watchLink,
|
||||||
|
movieImages,
|
||||||
getRequestStatus,
|
getRequestStatus,
|
||||||
linkPlexAccount,
|
linkPlexAccount,
|
||||||
unlinkPlexAccount,
|
unlinkPlexAccount,
|
||||||
|
|||||||
@@ -136,7 +136,7 @@
|
|||||||
left: 10px;
|
left: 10px;
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 2px;
|
height: 2px;
|
||||||
background-color: white;
|
background: $white;
|
||||||
}
|
}
|
||||||
&:before {
|
&:before {
|
||||||
transform: rotate(45deg);
|
transform: rotate(45deg);
|
||||||
@@ -145,7 +145,7 @@
|
|||||||
transform: rotate(-45deg);
|
transform: rotate(-45deg);
|
||||||
}
|
}
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--highlight-color);
|
background: $green;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,11 +22,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</figure>
|
</figure>
|
||||||
|
|
||||||
<div
|
<div class="movie-item__info">
|
||||||
class="movie-item__info"
|
|
||||||
@click="openMoviePopup"
|
|
||||||
@keydown.enter="openMoviePopup"
|
|
||||||
>
|
|
||||||
<p v-if="listItem.title || listItem.name" class="movie-item__title">
|
<p v-if="listItem.title || listItem.name" class="movie-item__title">
|
||||||
{{ listItem.title || listItem.name }}
|
{{ listItem.title || listItem.name }}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
>
|
>
|
||||||
<IconMovie v-if="result.type == 'movie'" class="type-icon" />
|
<IconMovie v-if="result.type == 'movie'" class="type-icon" />
|
||||||
<IconShow v-if="result.type == 'show'" class="type-icon" />
|
<IconShow v-if="result.type == 'show'" class="type-icon" />
|
||||||
|
<IconPerson v-if="result.type == 'person'" class="type-icon" />
|
||||||
<span class="title">{{ result.title }}</span>
|
<span class="title">{{ result.title }}</span>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
@@ -23,6 +24,10 @@
|
|||||||
</transition>
|
</transition>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Searches Elasticsearch for results based on changes to `query`.
|
||||||
|
-->
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Ref } from "vue";
|
import type { Ref } from "vue";
|
||||||
import { ref, watch, defineProps } from "vue";
|
import { ref, watch, defineProps } from "vue";
|
||||||
@@ -33,7 +38,10 @@
|
|||||||
import { MediaTypes } from "../../interfaces/IList";
|
import { MediaTypes } from "../../interfaces/IList";
|
||||||
import type {
|
import type {
|
||||||
IAutocompleteResult,
|
IAutocompleteResult,
|
||||||
IAutocompleteSearchResults
|
IAutocompleteSearchResults,
|
||||||
|
Hit,
|
||||||
|
Option,
|
||||||
|
Source
|
||||||
} from "../../interfaces/IAutocompleteSearch";
|
} from "../../interfaces/IAutocompleteSearch";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -47,6 +55,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const numberOfResults = 10;
|
const numberOfResults = 10;
|
||||||
|
let timeoutId = null;
|
||||||
const props = defineProps<Props>();
|
const props = defineProps<Props>();
|
||||||
const emit = defineEmits<Emit>();
|
const emit = defineEmits<Emit>();
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
@@ -54,25 +63,9 @@
|
|||||||
const searchResults: Ref<Array<IAutocompleteResult>> = ref([]);
|
const searchResults: Ref<Array<IAutocompleteResult>> = ref([]);
|
||||||
const keyboardNavigationIndex: Ref<number> = ref(0);
|
const keyboardNavigationIndex: Ref<number> = ref(0);
|
||||||
|
|
||||||
let disableOnFailure = false;
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.query,
|
|
||||||
newQuery => {
|
|
||||||
if (newQuery?.length > 0 && !disableOnFailure)
|
|
||||||
fetchAutocompleteResults(); /* eslint-disable-line no-use-before-define */
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
function openPopup(result: IAutocompleteResult) {
|
|
||||||
if (!result.id || !result.type) return;
|
|
||||||
|
|
||||||
store.dispatch("popup/open", { ...result });
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeDuplicates(_searchResults: Array<IAutocompleteResult>) {
|
function removeDuplicates(_searchResults: Array<IAutocompleteResult>) {
|
||||||
const filteredResults = [];
|
const filteredResults = [];
|
||||||
_searchResults.forEach(result => {
|
_searchResults.forEach((result: IAutocompleteResult) => {
|
||||||
if (result === undefined) return;
|
if (result === undefined) return;
|
||||||
const numberOfDuplicates = filteredResults.filter(
|
const numberOfDuplicates = filteredResults.filter(
|
||||||
filterItem => filterItem.id === result.id
|
filterItem => filterItem.id === result.id
|
||||||
@@ -87,58 +80,83 @@
|
|||||||
return filteredResults;
|
return filteredResults;
|
||||||
}
|
}
|
||||||
|
|
||||||
function elasticTypeToMediaType(type: string): MediaTypes {
|
function convertMediaType(type: string | null): MediaTypes | null {
|
||||||
if (type === "movie") return MediaTypes.Movie;
|
if (type === "movie") return MediaTypes.Movie;
|
||||||
|
|
||||||
if (type === "tv_series") return MediaTypes.Show;
|
if (type === "tv_series") return MediaTypes.Show;
|
||||||
|
|
||||||
|
if (type === "person") return MediaTypes.Person;
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseElasticResponse(elasticResponse: IAutocompleteSearchResults) {
|
function parseElasticResponse(elasticResponse: IAutocompleteSearchResults) {
|
||||||
const data = elasticResponse.hits.hits;
|
const elasticResults = elasticResponse.hits.hits;
|
||||||
|
const suggestResults = elasticResponse.suggest["movie-suggest"][0].options;
|
||||||
|
|
||||||
|
let data: Array<Source> = elasticResults.map((el: Hit) => el._source);
|
||||||
|
data = data.concat(suggestResults.map((el: Option) => el._source));
|
||||||
|
|
||||||
|
// data = data.concat(elasticResponse['suggest']['person-suggest'][0]['options'])
|
||||||
|
// data = data.concat(elasticResponse['suggest']['show-suggest'][0]['options'])
|
||||||
|
data = data.sort((a, b) => (a.popularity < b.popularity ? 1 : -1));
|
||||||
|
|
||||||
const results: Array<IAutocompleteResult> = [];
|
const results: Array<IAutocompleteResult> = [];
|
||||||
|
|
||||||
data.forEach(item => {
|
data.forEach(item => {
|
||||||
if (!item._index) return;
|
|
||||||
|
|
||||||
results.push({
|
results.push({
|
||||||
title: item._source?.original_name || item._source.original_title,
|
title: item?.original_name || item?.original_title || item?.name,
|
||||||
id: item._source.id,
|
id: item.id,
|
||||||
adult: item._source.adult,
|
adult: item.adult,
|
||||||
type: elasticTypeToMediaType(item._source.type)
|
type: convertMediaType(item?.type)
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return removeDuplicates(results).map((el, index) => {
|
return removeDuplicates(results)
|
||||||
return { ...el, index };
|
.map((el, index) => {
|
||||||
});
|
return { ...el, index };
|
||||||
|
})
|
||||||
|
.slice(0, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchAutocompleteResults() {
|
function fetchAutocompleteResults() {
|
||||||
keyboardNavigationIndex.value = 0;
|
keyboardNavigationIndex.value = 0;
|
||||||
searchResults.value = [];
|
searchResults.value = [];
|
||||||
|
|
||||||
return elasticSearchMoviesAndShows(props.query, numberOfResults)
|
elasticSearchMoviesAndShows(props.query, numberOfResults)
|
||||||
.catch(error => {
|
|
||||||
// TODO display error
|
|
||||||
disableOnFailure = true;
|
|
||||||
throw error;
|
|
||||||
})
|
|
||||||
.then(elasticResponse => parseElasticResponse(elasticResponse))
|
.then(elasticResponse => parseElasticResponse(elasticResponse))
|
||||||
.then(_searchResults => {
|
.then(_searchResults => {
|
||||||
|
console.log(_searchResults);
|
||||||
emit("update:results", _searchResults);
|
emit("update:results", _searchResults);
|
||||||
searchResults.value = _searchResults;
|
searchResults.value = _searchResults;
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
// TODO display error
|
|
||||||
disableOnFailure = true;
|
|
||||||
throw error;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const debounce = (callback: () => void, wait: number) => {
|
||||||
|
window.clearTimeout(timeoutId);
|
||||||
|
timeoutId = window.setTimeout(() => {
|
||||||
|
callback();
|
||||||
|
}, wait);
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.query,
|
||||||
|
newQuery => {
|
||||||
|
if (newQuery?.length > 0) {
|
||||||
|
debounce(fetchAutocompleteResults, 150);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
function openPopup(result: IAutocompleteResult) {
|
||||||
|
if (!result.id || !result.type) return;
|
||||||
|
|
||||||
|
store.dispatch("popup/open", { ...result });
|
||||||
|
}
|
||||||
|
|
||||||
// on load functions
|
// on load functions
|
||||||
fetchAutocompleteResults();
|
fetchAutocompleteResults();
|
||||||
|
// end on load functions
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -62,7 +62,15 @@ the `query`.
|
|||||||
import AutocompleteDropdown from "./AutocompleteDropdown.vue";
|
import AutocompleteDropdown from "./AutocompleteDropdown.vue";
|
||||||
import IconSearch from "../../icons/IconSearch.vue";
|
import IconSearch from "../../icons/IconSearch.vue";
|
||||||
import IconClose from "../../icons/IconClose.vue";
|
import IconClose from "../../icons/IconClose.vue";
|
||||||
import { IAutocompleteResult } from "../../interfaces/IAutocompleteSearch";
|
import type { MediaTypes } from "../../interfaces/IList";
|
||||||
|
import { IAutocompleteResult } from "../../interfaces/IAutocompleteSearch";
|
||||||
|
|
||||||
|
interface ISearchResult {
|
||||||
|
title: string;
|
||||||
|
id: number;
|
||||||
|
adult: boolean;
|
||||||
|
type: MediaTypes;
|
||||||
|
}
|
||||||
|
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|||||||
@@ -65,7 +65,7 @@
|
|||||||
|
|
||||||
&.active > div > svg,
|
&.active > div > svg,
|
||||||
&.active > svg {
|
&.active > svg {
|
||||||
fill: var(--highlight-color);
|
fill: var(--color-green);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.disabled {
|
&.disabled {
|
||||||
|
|||||||
@@ -92,7 +92,6 @@
|
|||||||
border: none;
|
border: none;
|
||||||
background: none;
|
background: none;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 30px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
color: var(--highlight-color);
|
color: var(--color-green);
|
||||||
|
|
||||||
@include mobile {
|
@include mobile {
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
|
|||||||
@@ -167,7 +167,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted } from "vue";
|
import { ref, computed, onMounted } from "vue";
|
||||||
import { useStore } from "vuex";
|
import { useStore } from "vuex";
|
||||||
import type { Ref } from "vue";
|
|
||||||
|
|
||||||
// import img from "@/directives/v-image";
|
// import img from "@/directives/v-image";
|
||||||
import IconProfile from "../../icons/IconProfile.vue";
|
import IconProfile from "../../icons/IconProfile.vue";
|
||||||
@@ -184,7 +183,6 @@
|
|||||||
import ActionButton from "./ActionButton.vue";
|
import ActionButton from "./ActionButton.vue";
|
||||||
import Description from "./Description.vue";
|
import Description from "./Description.vue";
|
||||||
import LoadingPlaceholder from "../ui/LoadingPlaceholder.vue";
|
import LoadingPlaceholder from "../ui/LoadingPlaceholder.vue";
|
||||||
import type { IColors } from "../../interfaces/IColors.ts";
|
|
||||||
import type {
|
import type {
|
||||||
IMovie,
|
IMovie,
|
||||||
IShow,
|
IShow,
|
||||||
@@ -215,7 +213,6 @@
|
|||||||
|
|
||||||
const props = defineProps<Props>();
|
const props = defineProps<Props>();
|
||||||
const ASSET_URL = "https://image.tmdb.org/t/p/";
|
const ASSET_URL = "https://image.tmdb.org/t/p/";
|
||||||
const COLORS_URL = "https://colors.schleppe.cloud/colors";
|
|
||||||
const ASSET_SIZES = ["w500", "w780", "original"];
|
const ASSET_SIZES = ["w500", "w780", "original"];
|
||||||
|
|
||||||
const media: Ref<IMovie | IShow> = ref();
|
const media: Ref<IMovie | IShow> = ref();
|
||||||
@@ -236,8 +233,6 @@
|
|||||||
if (!media.value) return "/assets/placeholder.png";
|
if (!media.value) return "/assets/placeholder.png";
|
||||||
if (!media.value?.poster) return "/assets/no-image.svg";
|
if (!media.value?.poster) return "/assets/no-image.svg";
|
||||||
|
|
||||||
// compute & update highlight colors from poster image
|
|
||||||
colorsFromPoster(media.value.poster); // eslint-disable-line
|
|
||||||
return `${ASSET_URL}${ASSET_SIZES[0]}${media.value.poster}`;
|
return `${ASSET_URL}${ASSET_SIZES[0]}${media.value.poster}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -336,34 +331,6 @@
|
|||||||
const tmdbURL = `https://www.themoviedb.org/${tmdbType}/${props.id}`;
|
const tmdbURL = `https://www.themoviedb.org/${tmdbType}/${props.id}`;
|
||||||
window.location.href = tmdbURL;
|
window.location.href = tmdbURL;
|
||||||
}
|
}
|
||||||
function colorMain(colors: IColors) {
|
|
||||||
const parent = document.getElementsByClassName(
|
|
||||||
"movie-popup"
|
|
||||||
)[0] as HTMLElement;
|
|
||||||
parent.style.setProperty("--highlight-color", colors.s ?? colors.p);
|
|
||||||
parent.style.setProperty("--highlight-bg", colors.bg);
|
|
||||||
parent.style.setProperty("--highlight-secondary", colors.p);
|
|
||||||
parent.style.setProperty("--text-color", "#ffffff");
|
|
||||||
parent.style.setProperty("--text-color-90", "rgba(255, 255, 255, 0.9)");
|
|
||||||
parent.style.setProperty("--text-color-70", "rgba(255, 255, 255, 0.7)");
|
|
||||||
parent.style.setProperty("--text-color-50", "rgba(255, 255, 255, 0.5)");
|
|
||||||
parent.style.setProperty("--text-color-10", "rgba(255, 255, 255, 0.1)");
|
|
||||||
parent.style.setProperty("--text-color-5", "rgba(255, 255, 255, 0.05)");
|
|
||||||
}
|
|
||||||
|
|
||||||
async function colorsFromPoster(posterPath: string) {
|
|
||||||
const url = new URL(COLORS_URL);
|
|
||||||
url.searchParams.append("id", posterPath.replace("/", ""));
|
|
||||||
url.searchParams.append("size", "w342");
|
|
||||||
|
|
||||||
fetch(url.href)
|
|
||||||
.then(resp => {
|
|
||||||
if (resp.ok) return resp.json();
|
|
||||||
throw new Error(`invalid status: '${resp.status}' from server.`);
|
|
||||||
})
|
|
||||||
.then(colorMain)
|
|
||||||
.catch(error => console.log("unable to get colors, error:", error)); // eslint-disable-line no-console
|
|
||||||
}
|
|
||||||
|
|
||||||
// On created functions
|
// On created functions
|
||||||
fetchMedia();
|
fetchMedia();
|
||||||
@@ -424,7 +391,6 @@
|
|||||||
|
|
||||||
.movie__poster {
|
.movie__poster {
|
||||||
display: none;
|
display: none;
|
||||||
border-radius: 1.6rem;
|
|
||||||
|
|
||||||
@include desktop {
|
@include desktop {
|
||||||
background: var(--background-color);
|
background: var(--background-color);
|
||||||
@@ -435,7 +401,7 @@
|
|||||||
|
|
||||||
> img {
|
> img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-radius: inherit;
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -454,8 +420,8 @@
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
}
|
}
|
||||||
|
|
||||||
background-color: var(--highlight-bg, var(--background-color));
|
background-color: $background-color;
|
||||||
color: var(--text-color);
|
color: $text-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -464,9 +430,7 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: scale(0.97) translateZ(0);
|
transform: scale(0.97) translateZ(0);
|
||||||
transition:
|
transition: opacity 0.5s ease, transform 0.5s ease;
|
||||||
opacity 0.5s ease,
|
|
||||||
transform 0.5s ease;
|
|
||||||
|
|
||||||
&.is-loaded {
|
&.is-loaded {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
@@ -485,26 +449,21 @@
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
padding: 140px 30px 0 40px;
|
padding: 140px 30px 0 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
color: var(--highlight-color);
|
color: var(--color-green);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
line-height: 1.2;
|
line-height: 1.4;
|
||||||
font-size: 2.2rem;
|
font-size: 24px;
|
||||||
font-weight: 600;
|
|
||||||
letter-spacing: 1px;
|
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
|
||||||
@include tablet-min {
|
@include tablet-min {
|
||||||
font-size: 30px;
|
font-size: 30px;
|
||||||
font-size: 2.2rem;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
i {
|
i {
|
||||||
display: block;
|
display: block;
|
||||||
color: rgba(255, 255, 255, 0.8);
|
color: rgba(255, 255, 255, 0.8);
|
||||||
color: var(--highlight-secondary);
|
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -514,7 +473,7 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
order: 2;
|
order: 2;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
border-top: 1px solid var(--text-color-50);
|
border-top: 1px solid $text-color-5;
|
||||||
@include tablet-min {
|
@include tablet-min {
|
||||||
order: 1;
|
order: 1;
|
||||||
width: 45%;
|
width: 45%;
|
||||||
@@ -573,7 +532,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.torrents {
|
.torrents {
|
||||||
background-color: var(--highlight-bg, var(--background-color));
|
background-color: var(--background-color);
|
||||||
padding: 0 1rem;
|
padding: 0 1rem;
|
||||||
|
|
||||||
@include mobile {
|
@include mobile {
|
||||||
|
|||||||
@@ -137,7 +137,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@include mobile {
|
@include mobile {
|
||||||
padding: 0 0.8rem;
|
text-align: left;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -185,7 +185,6 @@
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background-color: var(--table-background-color);
|
background-color: var(--table-background-color);
|
||||||
background-color: var(--highlight-color);
|
|
||||||
// background-color: black;
|
// background-color: black;
|
||||||
// color: var(--color-green);
|
// color: var(--color-green);
|
||||||
letter-spacing: 0.8px;
|
letter-spacing: 0.8px;
|
||||||
@@ -233,9 +232,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// alternate background color per row
|
// alternate background color per row
|
||||||
tr {
|
|
||||||
background-color: var(--background-color);
|
|
||||||
}
|
|
||||||
tr:nth-child(even) {
|
tr:nth-child(even) {
|
||||||
background-color: var(--background-70);
|
background-color: var(--background-70);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,40 +1,36 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="search-results">
|
<torrent-search-results
|
||||||
<torrent-search-results
|
:query="query"
|
||||||
:query="query"
|
:tmdb-id="tmdbId"
|
||||||
:tmdb-id="tmdbId"
|
:class="{ truncated: truncated }"
|
||||||
:class="{ truncated: _truncated }"
|
><div
|
||||||
><div
|
v-if="truncated"
|
||||||
v-if="_truncated"
|
class="load-more"
|
||||||
class="load-more"
|
tabindex="0"
|
||||||
tabindex="0"
|
role="button"
|
||||||
role="button"
|
@click="truncated = false"
|
||||||
@click="truncated = false"
|
@keydown.enter="truncated = false"
|
||||||
@keydown.enter="truncated = false"
|
>
|
||||||
>
|
<icon-arrow-down />
|
||||||
<icon-arrow-down />
|
</div>
|
||||||
</div>
|
</torrent-search-results>
|
||||||
</torrent-search-results>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="edit-query-btn-container">
|
<div class="edit-query-btn-container">
|
||||||
<a :href="`/torrents?query=${encodeURIComponent(props.query)}`">
|
<seasonedButton @click="openInTorrentPage"
|
||||||
<button>
|
>View on torrent page</seasonedButton
|
||||||
<span class="text">View on torrent page</span
|
>
|
||||||
><span class="icon"><icon-arrow-down /></span>
|
|
||||||
</button>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
import { ref, defineProps, computed } from "vue";
|
import { ref, defineProps, computed } from "vue";
|
||||||
import TorrentSearchResults from "@/components/torrent/TorrentSearchResults.vue";
|
import TorrentSearchResults from "@/components/torrent/TorrentSearchResults.vue";
|
||||||
|
import SeasonedButton from "@/components/ui/SeasonedButton.vue";
|
||||||
import IconArrowDown from "@/icons/IconArrowDown.vue";
|
import IconArrowDown from "@/icons/IconArrowDown.vue";
|
||||||
import type { Ref } from "vue";
|
import type { Ref } from "vue";
|
||||||
import store from "../../store";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
query: string;
|
query: string;
|
||||||
@@ -42,13 +38,18 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<Props>();
|
const props = defineProps<Props>();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const truncated: Ref<boolean> = ref(true);
|
const truncated: Ref<boolean> = ref(true);
|
||||||
|
|
||||||
const _truncated = computed(() => {
|
function openInTorrentPage() {
|
||||||
const val = store.getters["torrentModule/resultCount"];
|
if (!props.query?.length) {
|
||||||
if (val > 10 && truncated.value) return true;
|
router.push("/torrents");
|
||||||
return false;
|
return;
|
||||||
});
|
}
|
||||||
|
|
||||||
|
router.push({ path: "/torrents", query: { query: props.query } });
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@@ -74,68 +75,14 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-results {
|
svg {
|
||||||
svg {
|
height: 30px;
|
||||||
height: 30px;
|
fill: var(--text-color);
|
||||||
fill: var(--text-color);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit-query-btn-container {
|
.edit-query-btn-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
padding-bottom: 2rem;
|
|
||||||
|
|
||||||
a button {
|
|
||||||
--height: 45px;
|
|
||||||
transition: all 0.8s ease !important;
|
|
||||||
position: relative;
|
|
||||||
font-size: 1rem;
|
|
||||||
line-height: 1.5;
|
|
||||||
letter-spacing: 0.2px;
|
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--highlight-bg, var(--background-color));
|
|
||||||
background-color: var(--text-color);
|
|
||||||
min-height: var(--height);
|
|
||||||
padding: 0rem 1.5rem;
|
|
||||||
margin: 0;
|
|
||||||
border: 2px solid var(--text-color);
|
|
||||||
border-radius: calc(var(--height) / 2);
|
|
||||||
cursor: pointer;
|
|
||||||
outline: none;
|
|
||||||
overflow-x: hidden;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--highlight-bg, var(--background-color));
|
|
||||||
color: var(--text-color);
|
|
||||||
padding: 0 2rem;
|
|
||||||
|
|
||||||
span.text {
|
|
||||||
margin-left: -0.5rem;
|
|
||||||
margin-right: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
span.icon {
|
|
||||||
right: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
span.icon {
|
|
||||||
--size: 1rem;
|
|
||||||
display: block;
|
|
||||||
transform: rotate(-90deg);
|
|
||||||
transform-origin: top left;
|
|
||||||
stroke: var(--text-color);
|
|
||||||
fill: var(--text-color);
|
|
||||||
height: var(--size);
|
|
||||||
width: var(--size);
|
|
||||||
margin-top: -4px;
|
|
||||||
position: absolute;
|
|
||||||
right: 1rem;
|
|
||||||
right: -1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
1895
src/components/torrents.json
Normal file
1895
src/components/torrents.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<svg
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||||
version="1.1"
|
|
||||||
height="100%"
|
|
||||||
width="100%"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 32 32"
|
|
||||||
>
|
|
||||||
<path
|
<path
|
||||||
d="M28.725 8.058l-12.725 12.721-12.725-12.721-1.887 1.887 13.667 13.667c0.258 0.258 0.6 0.392 0.942 0.392s0.683-0.129 0.942-0.392l13.667-13.667-1.879-1.887z"
|
d="M28.725 8.058l-12.725 12.721-12.725-12.721-1.887 1.887 13.667 13.667c0.258 0.258 0.6 0.392 0.942 0.392s0.683-0.129 0.942-0.392l13.667-13.667-1.879-1.887z"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export interface Hits {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface Hit {
|
export interface Hit {
|
||||||
_index: string;
|
_index: Index;
|
||||||
_type: Type;
|
_type: Type;
|
||||||
_id: string;
|
_id: string;
|
||||||
_score: number;
|
_score: number;
|
||||||
@@ -58,6 +58,11 @@ export interface Option {
|
|||||||
_source: Source;
|
_source: Source;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum Index {
|
||||||
|
Movies = "movies",
|
||||||
|
Shows = "shows"
|
||||||
|
}
|
||||||
|
|
||||||
export interface Source {
|
export interface Source {
|
||||||
tags: Tag[];
|
tags: Tag[];
|
||||||
ecs: Ecs;
|
ecs: Ecs;
|
||||||
@@ -74,7 +79,7 @@ export interface Source {
|
|||||||
original_title: string;
|
original_title: string;
|
||||||
original_name?: string;
|
original_name?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
type: string;
|
type?: MediaTypes;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Agent {
|
export interface Agent {
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
export interface IColors {
|
|
||||||
bg: string;
|
|
||||||
p: string;
|
|
||||||
s?: string;
|
|
||||||
}
|
|
||||||
117
src/modules/user.js
Normal file
117
src/modules/user.js
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import { refreshToken } from "@/api";
|
||||||
|
import { parseJwt } from "@/utils";
|
||||||
|
|
||||||
|
function getCookie(name) {
|
||||||
|
var arrayb = document.cookie.split(";");
|
||||||
|
for (const item of arrayb) {
|
||||||
|
const query = `${name}=`;
|
||||||
|
|
||||||
|
if (!item.startsWith(query)) continue;
|
||||||
|
return item.substr(query.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCookie(name, value, options = {}) {
|
||||||
|
options = {
|
||||||
|
path: "/",
|
||||||
|
// add other defaults here if necessary
|
||||||
|
...options
|
||||||
|
};
|
||||||
|
|
||||||
|
if (options.expires instanceof Date) {
|
||||||
|
options.expires = options.expires.toUTCString();
|
||||||
|
}
|
||||||
|
|
||||||
|
let updatedCookie =
|
||||||
|
encodeURIComponent(name) + "=" + encodeURIComponent(value);
|
||||||
|
|
||||||
|
for (let optionKey in options) {
|
||||||
|
updatedCookie += "; " + optionKey;
|
||||||
|
let optionValue = options[optionKey];
|
||||||
|
if (optionValue !== true) {
|
||||||
|
updatedCookie += "=" + optionValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.cookie = updatedCookie;
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteCookie(name) {
|
||||||
|
setCookie(name, "", {
|
||||||
|
"max-age": Date.now()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
namespaced: true,
|
||||||
|
state: {
|
||||||
|
token: null,
|
||||||
|
admin: false,
|
||||||
|
settings: null,
|
||||||
|
username: null
|
||||||
|
},
|
||||||
|
getters: {
|
||||||
|
username: state => state.username,
|
||||||
|
settings: state => state.settings,
|
||||||
|
token: state => state.token,
|
||||||
|
// loggedIn: state => true,
|
||||||
|
loggedIn: state => state && state.username !== null,
|
||||||
|
admin: state => state.admin,
|
||||||
|
plexUserId: state => {
|
||||||
|
if (state && state.settings && state.settings.plexUserId)
|
||||||
|
return state.settings.plexUserId;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
SET_TOKEN: (state, token) => (state.token = token),
|
||||||
|
SET_USERNAME: (state, username) => (state.username = username),
|
||||||
|
SET_SETTINGS: (state, settings) => (state.settings = settings),
|
||||||
|
SET_ADMIN: (state, admin) => (state.admin = admin),
|
||||||
|
LOGOUT: state => {
|
||||||
|
state.token = null;
|
||||||
|
state.username = null;
|
||||||
|
state.settings = null;
|
||||||
|
state.admin = false;
|
||||||
|
// deleteCookie('authorization');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
initUserFromCookie: async ({ dispatch }) => {
|
||||||
|
const jwtToken = getCookie("authorization");
|
||||||
|
if (!jwtToken) return null;
|
||||||
|
|
||||||
|
const token = parseJwt(jwtToken);
|
||||||
|
return await dispatch("setupStateFromToken", token);
|
||||||
|
},
|
||||||
|
setupStateFromToken: ({ commit }, token) => {
|
||||||
|
try {
|
||||||
|
const { username, admin, settings } = token;
|
||||||
|
|
||||||
|
if (!username) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
commit("SET_TOKEN", token);
|
||||||
|
commit("SET_USERNAME", username);
|
||||||
|
commit("SET_SETTINGS", settings);
|
||||||
|
commit("SET_ADMIN", admin != undefined);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Unable to parse JWT, failed with error:", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setSettings: ({ commit }, settings) => {
|
||||||
|
if (!(settings instanceof Object)) {
|
||||||
|
throw "Parameter is not a object.";
|
||||||
|
}
|
||||||
|
|
||||||
|
commit("SET_SETTINGS", settings);
|
||||||
|
},
|
||||||
|
logout: ({ commit }) => commit("LOGOUT"),
|
||||||
|
login: async ({ dispatch }) => await dispatch("initUserFromCookie")
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,178 +0,0 @@
|
|||||||
/* eslint-disable no-param-reassign */
|
|
||||||
|
|
||||||
import { Module } from "vuex";
|
|
||||||
import { parseJwt } from "../utils";
|
|
||||||
|
|
||||||
/* --------------------------------------------------------------------------- */
|
|
||||||
/* ── Utility helpers (cookie handling) ────────────────────────────────── */
|
|
||||||
/* --------------------------------------------------------------------------- */
|
|
||||||
|
|
||||||
export interface CookieOptions {
|
|
||||||
path?: string;
|
|
||||||
expires?: number | string | boolean;
|
|
||||||
[option: string]: string | number | boolean | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Read a cookie value.
|
|
||||||
*/
|
|
||||||
export function getCookie(name: string): string | null {
|
|
||||||
const array = document.cookie.split(";");
|
|
||||||
let match = null;
|
|
||||||
|
|
||||||
array.forEach((item: string) => {
|
|
||||||
const query = `${name}=`;
|
|
||||||
if (!item.trim().startsWith(query)) return;
|
|
||||||
match = item.trim().substring(query.length);
|
|
||||||
});
|
|
||||||
|
|
||||||
return match;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Write a cookie.
|
|
||||||
*/
|
|
||||||
export function setCookie(
|
|
||||||
name: string,
|
|
||||||
value: string,
|
|
||||||
options: CookieOptions = {}
|
|
||||||
): void {
|
|
||||||
const opts: CookieOptions = {
|
|
||||||
path: "/",
|
|
||||||
...options
|
|
||||||
};
|
|
||||||
|
|
||||||
let cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
|
|
||||||
/* eslint-disable-next-line no-restricted-syntax */
|
|
||||||
for (const [key, val] of Object.entries(opts)) {
|
|
||||||
cookie += `; ${key}`;
|
|
||||||
if (val !== true) cookie += `=${val}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
document.cookie = cookie;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete a cookie.
|
|
||||||
*/
|
|
||||||
export function deleteCookie(name: string): void {
|
|
||||||
setCookie(name, "", { "max-age": 0 });
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --------------------------------------------------------------------------- */
|
|
||||||
/* ── State / Types ─────────────────────────────────────────────────── */
|
|
||||||
/* --------------------------------------------------------------------------- */
|
|
||||||
|
|
||||||
export interface Settings {
|
|
||||||
/** Example property – replace with your real shape. */
|
|
||||||
plexUserId?: string | null;
|
|
||||||
// add the rest of your settings fields here
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UserState {
|
|
||||||
token: string | null;
|
|
||||||
admin: boolean;
|
|
||||||
settings: Settings | null;
|
|
||||||
username: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RootState {
|
|
||||||
val?: string;
|
|
||||||
// your root state interface – leave empty if you don't use it
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --------------------------------------------------------------------------- */
|
|
||||||
/* ── Vuex module ──────────────────────────────────────────────────────── */
|
|
||||||
/* --------------------------------------------------------------------------- */
|
|
||||||
|
|
||||||
const userModule: Module<UserState, RootState> = {
|
|
||||||
namespaced: true,
|
|
||||||
|
|
||||||
/* ── State ───────────────────────────────────────────────────── */
|
|
||||||
state: {
|
|
||||||
token: null,
|
|
||||||
admin: false,
|
|
||||||
settings: null,
|
|
||||||
username: null
|
|
||||||
},
|
|
||||||
|
|
||||||
/* ── Getters ─────────────────────────────────────────────────── */
|
|
||||||
getters: {
|
|
||||||
username: (state): string | null => state.username,
|
|
||||||
settings: (state): Settings | null => state.settings,
|
|
||||||
token: (state): string | null => state.token,
|
|
||||||
loggedIn: (state): boolean => !!state && state.username !== null,
|
|
||||||
admin: (state): boolean => state.admin,
|
|
||||||
plexUserId: (state): string | null => state?.settings?.plexUserId ?? null
|
|
||||||
},
|
|
||||||
|
|
||||||
/* ── Mutations ─────────────────────────────────────────────────── */
|
|
||||||
mutations: {
|
|
||||||
SET_TOKEN(state, token: string | null): void {
|
|
||||||
state.token = token;
|
|
||||||
},
|
|
||||||
SET_USERNAME(state, username: string | null): void {
|
|
||||||
state.username = username;
|
|
||||||
},
|
|
||||||
SET_SETTINGS(state, settings: Settings | null): void {
|
|
||||||
state.settings = settings;
|
|
||||||
},
|
|
||||||
SET_ADMIN(state, admin: boolean): void {
|
|
||||||
state.admin = admin;
|
|
||||||
},
|
|
||||||
LOGOUT(state): void {
|
|
||||||
state.token = null;
|
|
||||||
state.username = null;
|
|
||||||
state.settings = null;
|
|
||||||
state.admin = false;
|
|
||||||
deleteCookie("authorization");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/* ── Actions ─────────────────────────────────────────────────── */
|
|
||||||
actions: {
|
|
||||||
async initUserFromCookie({ dispatch }): Promise<boolean | null> {
|
|
||||||
const jwtToken = getCookie("authorization");
|
|
||||||
if (!jwtToken) return null;
|
|
||||||
|
|
||||||
const token = parseJwt(jwtToken);
|
|
||||||
return dispatch("setupStateFromToken", token);
|
|
||||||
},
|
|
||||||
|
|
||||||
setupStateFromToken({ commit }, token: any): boolean {
|
|
||||||
try {
|
|
||||||
const { username, admin, settings } = token;
|
|
||||||
if (!username) return false;
|
|
||||||
|
|
||||||
commit("SET_TOKEN", token);
|
|
||||||
commit("SET_USERNAME", username);
|
|
||||||
commit("SET_SETTINGS", settings);
|
|
||||||
commit("SET_ADMIN", admin !== undefined);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.error("Unable to parse JWT, failed with error:", e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
setSettings({ commit }, settings: Settings): void {
|
|
||||||
if (!(settings instanceof Object)) {
|
|
||||||
throw new Error("Parameter is not an object.");
|
|
||||||
}
|
|
||||||
commit("SET_SETTINGS", settings);
|
|
||||||
},
|
|
||||||
|
|
||||||
logout({ commit }): void {
|
|
||||||
commit("LOGOUT");
|
|
||||||
},
|
|
||||||
|
|
||||||
login({ dispatch }): Promise<boolean | null> {
|
|
||||||
return dispatch("initUserFromCookie");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const user = userModule;
|
|
||||||
export default user;
|
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
height: unset;
|
height: 100%;
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|||||||
@@ -21,8 +21,6 @@
|
|||||||
--background-40: rgba(255, 255, 255, 0.4);
|
--background-40: rgba(255, 255, 255, 0.4);
|
||||||
--background-0: rgba(255, 255, 255, 0);
|
--background-0: rgba(255, 255, 255, 0);
|
||||||
|
|
||||||
--highlight-color: #01d277;
|
|
||||||
|
|
||||||
--background-nav-logo: #081c24;
|
--background-nav-logo: #081c24;
|
||||||
--color-green: #01d277;
|
--color-green: #01d277;
|
||||||
--color-green-90: rgba(1, 210, 119, 0.9);
|
--color-green-90: rgba(1, 210, 119, 0.9);
|
||||||
|
|||||||
@@ -89,8 +89,9 @@ export function setUrlQueryParameter(parameter: string, value: string): void {
|
|||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
params.append(parameter, value);
|
params.append(parameter, value);
|
||||||
|
|
||||||
const url = `${window.location.protocol}//${window.location.hostname}${window.location.port ? `:${window.location.port}` : ""
|
const url = `${window.location.protocol}//${window.location.hostname}${
|
||||||
}${window.location.pathname}${params.toString().length ? `?${params}` : ""}`;
|
window.location.port ? `:${window.location.port}` : ""
|
||||||
|
}${ndow.location.pathname}${params.toString().length ? `?${params}` : ""}`;
|
||||||
|
|
||||||
window.history.pushState({}, "search", url);
|
window.history.pushState({}, "search", url);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2141,9 +2141,9 @@ ignore@^7.0.5:
|
|||||||
integrity sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==
|
integrity sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==
|
||||||
|
|
||||||
immutable@^4.0.0:
|
immutable@^4.0.0:
|
||||||
version "4.3.8"
|
version "4.3.7"
|
||||||
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.8.tgz#02d183c7727fb2bb1d5d0380da0d779dce9296a7"
|
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.7.tgz#c70145fc90d89fb02021e65c84eb0226e4e5a381"
|
||||||
integrity sha512-d/Ld9aLbKpNwyl0KiM2CT1WYvkitQ1TSvmRtkcV8FKStiDoA7Slzgjmb/1G2yhKM1p0XeNOieaTbFZmU1d3Xuw==
|
integrity sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==
|
||||||
|
|
||||||
imurmurhash@^0.1.4:
|
imurmurhash@^0.1.4:
|
||||||
version "0.1.4"
|
version "0.1.4"
|
||||||
|
|||||||
Reference in New Issue
Block a user