mirror of
https://github.com/KevinMidboe/seasoned.git
synced 2026-04-24 16:53:37 +00:00
Compare commits
6 Commits
feat/vite
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ddc13ac1f | ||
| 081240c83e | |||
| eac12748db | |||
| 426b376d05 | |||
| 1238cf50cc | |||
| 8e586811ec |
21
.drone.yml
21
.drone.yml
@@ -25,7 +25,7 @@ steps:
|
||||
path: /cache
|
||||
|
||||
- name: Frontend install
|
||||
image: node:18.2.0
|
||||
image: node:24.13.1
|
||||
commands:
|
||||
- node -v
|
||||
- yarn --version
|
||||
@@ -42,8 +42,14 @@ steps:
|
||||
- name: cache
|
||||
path: /cache
|
||||
|
||||
- name: Lint project using eslint
|
||||
image: node:24.13.1
|
||||
commands:
|
||||
- yarn lint
|
||||
failure: ignore
|
||||
|
||||
- name: Frontend build
|
||||
image: node:18.2.0
|
||||
image: node:24.13.1
|
||||
commands:
|
||||
- yarn build
|
||||
environment:
|
||||
@@ -56,12 +62,6 @@ steps:
|
||||
SEASONED_DOMAIN:
|
||||
from_secret: SEASONED_DOMAIN
|
||||
|
||||
- name: Lint project using eslint
|
||||
image: node:18.2.0
|
||||
commands:
|
||||
- yarn lint
|
||||
failure: ignore
|
||||
|
||||
- name: Build and publish docker image
|
||||
image: plugins/docker
|
||||
settings:
|
||||
@@ -105,3 +105,8 @@ trigger:
|
||||
include:
|
||||
- push
|
||||
# - pull_request
|
||||
---
|
||||
kind: signature
|
||||
hmac: 6f10b2871d2bd6b5cd26ddf72796325991ba211ba1eb62b657baf993e9d549c8
|
||||
|
||||
...
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
SEASONED_API=
|
||||
ELASTIC=
|
||||
ELASTIC_INDEX=shows,movies
|
||||
SEASONED_DOMAIN=
|
||||
SEASONED_API=http://localhost:31459
|
||||
ELASTIC_URL=http://elastic.local:9200/tmdb-movies-shows
|
||||
ELASTIC_API_KEY=
|
||||
|
||||
31
.eslintrc
31
.eslintrc
@@ -1,31 +0,0 @@
|
||||
{
|
||||
"root": true,
|
||||
"parser": "vue-eslint-parser",
|
||||
"parserOptions": {
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"extends": [
|
||||
"@vue/eslint-config-airbnb",
|
||||
"plugin:vue/recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:prettier/recommended",
|
||||
],
|
||||
"rules": {
|
||||
"vue/no-v-model-argument": "off",
|
||||
"no-underscore-dangle": "off",
|
||||
"vue/multi-word-component-names": "off",
|
||||
"no-shadow": "off",
|
||||
"@typescript-eslint/no-shadow": ["error"],
|
||||
},
|
||||
"settings": {
|
||||
"import/resolver": {
|
||||
webpack: {
|
||||
config: "./webpack.config.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,6 +4,7 @@ src/config.json
|
||||
|
||||
# Build directory
|
||||
dist/
|
||||
lib/
|
||||
|
||||
# Node packages
|
||||
node_modules/
|
||||
|
||||
31
Caddyfile
Normal file
31
Caddyfile
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
# 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"
|
||||
}
|
||||
39
Dockerfile
39
Dockerfile
@@ -1,11 +1,38 @@
|
||||
FROM nginx:latest
|
||||
FROM node:24.13.1 AS build
|
||||
|
||||
COPY public /usr/share/nginx/html
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf.template
|
||||
COPY docker-entrypoint.sh /docker-entrypoint.d/05-docker-entrypoint.sh
|
||||
# Set the working directory for the build stage
|
||||
WORKDIR /app
|
||||
|
||||
RUN chmod +x /docker-entrypoint.d/05-docker-entrypoint.sh
|
||||
# Install dependencies
|
||||
COPY package.json yarn.lock ./
|
||||
RUN yarn install --frozen-lockfile
|
||||
|
||||
EXPOSE 5000
|
||||
# Copy source files that the build depends on
|
||||
COPY index.html .
|
||||
COPY public/ public/
|
||||
COPY src/ src/
|
||||
COPY tsconfig.json vite.config.ts ./
|
||||
|
||||
ARG SEASONED_API=http://localhost:31459
|
||||
ENV VITE_SEASONED_API=$SEASONED_API
|
||||
|
||||
ARG ELASTIC_URL=http://elastic.local:9200/tmdb-movies-shows
|
||||
ENV VITE_ELASTIC_URL=$ELASTIC_URL
|
||||
ARG ELASTIC_API_KEY=
|
||||
ENV VITE_ELASTIC_API_KEY=$ELASTIC_API_KEY
|
||||
|
||||
RUN yarn build
|
||||
|
||||
FROM caddy:2.11-alpine
|
||||
|
||||
COPY Caddyfile /etc/caddy/Caddyfile
|
||||
|
||||
# Copy static files
|
||||
COPY public /usr/share/caddy
|
||||
|
||||
# Copy the static build from the previous stage
|
||||
COPY --from=build /app/dist /usr/share/caddy
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
LABEL org.opencontainers.image.source https://github.com/kevinmidboe/seasoned
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
#!/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 "$@"
|
||||
66
eslint.config.mjs
Normal file
66
eslint.config.mjs
Normal file
@@ -0,0 +1,66 @@
|
||||
import path from "node:path";
|
||||
|
||||
import { includeIgnoreFile } from "@eslint/compat";
|
||||
import js from "@eslint/js";
|
||||
import { defineConfig } from "eslint/config";
|
||||
import { configs, plugins } from "eslint-config-airbnb-extended";
|
||||
import { rules as prettierConfigRules } from "eslint-config-prettier";
|
||||
import prettierPlugin from "eslint-plugin-prettier";
|
||||
|
||||
const CUSTOM_RULES = {
|
||||
"vue/no-v-model-argument": "off",
|
||||
"no-underscore-dangle": "off",
|
||||
"vue/multi-word-component-names": "off"
|
||||
};
|
||||
|
||||
const gitignorePath = path.resolve(".", ".gitignore");
|
||||
|
||||
// ESLint recommended config
|
||||
const jsConfig = defineConfig([
|
||||
{
|
||||
name: "js/config",
|
||||
...js.configs.recommended
|
||||
},
|
||||
plugins.stylistic,
|
||||
plugins.importX,
|
||||
...configs.base.recommended // Airbnb base recommended config
|
||||
]);
|
||||
|
||||
// Node & Airbnb recommended config
|
||||
const nodeConfig = defineConfig([plugins.node, ...configs.node.recommended]);
|
||||
|
||||
// Typescript & Airbnb base TS config
|
||||
const typescriptConfig = defineConfig([
|
||||
plugins.typescriptEslint,
|
||||
...configs.base.typescript
|
||||
// rules.typescript.typescriptEslintStrict
|
||||
]);
|
||||
|
||||
// Prettier config
|
||||
const prettierConfig = defineConfig([
|
||||
{
|
||||
name: "prettier/plugin/config",
|
||||
plugins: {
|
||||
prettier: prettierPlugin
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "prettier/config",
|
||||
rules: {
|
||||
...prettierConfigRules,
|
||||
"prettier/prettier": "error"
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
export default defineConfig([
|
||||
// Ignore files and folders listed in .gitignore
|
||||
includeIgnoreFile(gitignorePath),
|
||||
...jsConfig,
|
||||
...nodeConfig,
|
||||
...typescriptConfig,
|
||||
...prettierConfig,
|
||||
{
|
||||
rules: CUSTOM_RULES
|
||||
}
|
||||
]);
|
||||
@@ -1,8 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, viewport-fit=cover, initial-scale=1"
|
||||
/>
|
||||
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css?family=Roboto:300,400,500&subset=cyrillic"
|
||||
@@ -24,7 +27,9 @@
|
||||
<meta name="theme-color" content="#081c24" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="entry"></div>
|
||||
<div id="app"></div>
|
||||
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
|
||||
<script
|
||||
30
nginx.conf
30
nginx.conf
@@ -1,30 +0,0 @@
|
||||
server {
|
||||
listen 5000 default_server;
|
||||
listen [::]:5000 default_server;
|
||||
|
||||
server_name $SEASONED_DOMAIN;
|
||||
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;
|
||||
}
|
||||
}
|
||||
63
package.json
63
package.json
@@ -5,57 +5,30 @@
|
||||
"author": "Kevin Midboe",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "NODE_ENV=development webpack server",
|
||||
"build": "yarn build:ts && yarn build:webpack",
|
||||
"build:ts": "tsc --project tsconfig.json",
|
||||
"build:webpack": "NODE_ENV=production webpack-cli build --progress",
|
||||
"postbuild": "cp public/dist/index.html public/index.html",
|
||||
"clean": "rm -r public/dist 2> /dev/null; rm public/index.html 2> /dev/null; rm -r lib 2> /dev/null",
|
||||
"start": "echo 'Start using docker, consult README'",
|
||||
"lint": "eslint src --ext .ts,.vue",
|
||||
"dev": "NODE_ENV=development vite",
|
||||
"build": "yarn vite build",
|
||||
"lint": "eslint src; prettier -c src",
|
||||
"clean": "rm -rf dist/ yarn-*.log 2>/dev/null",
|
||||
"docs": "documentation build src/api.ts -f html -o docs/api && documentation build src/api.ts -f md -o docs/api.md"
|
||||
},
|
||||
"dependencies": {
|
||||
"chart.js": "3.9.1",
|
||||
"connect-history-api-fallback": "2.0.0",
|
||||
"dotenv": "^16.0.1",
|
||||
"express": "4.18.1",
|
||||
"vue": "3.2.37",
|
||||
"vue-router": "4.1.3",
|
||||
"vuex": "4.0.2"
|
||||
"vue": "3.5.28",
|
||||
"vue-router": "5.0.3",
|
||||
"vuex": "4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.18.10",
|
||||
"@babel/plugin-transform-runtime": "7.18.10",
|
||||
"@babel/preset-env": "7.18.10",
|
||||
"@babel/runtime": "7.18.9",
|
||||
"@types/express": "4.17.13",
|
||||
"@types/node": "18.6.1",
|
||||
"@typescript-eslint/eslint-plugin": "5.33.0",
|
||||
"@typescript-eslint/parser": "5.33.0",
|
||||
"@vue/cli": "5.0.8",
|
||||
"@vue/cli-service": "5.0.8",
|
||||
"@vue/eslint-config-airbnb": "6.0.0",
|
||||
"babel-loader": "8.2.5",
|
||||
"css-loader": "6.7.1",
|
||||
"documentation": "13.2.5",
|
||||
"eslint": "8.21.0",
|
||||
"eslint-config-prettier": "8.5.0",
|
||||
"eslint-plugin-import": "2.26.0",
|
||||
"eslint-plugin-prettier": "4.2.1",
|
||||
"eslint-plugin-vue": "9.3.0",
|
||||
"eslint-plugin-vuejs-accessibility": "1.2.0",
|
||||
"file-loader": "6.2.0",
|
||||
"html-webpack-plugin": "5.5.0",
|
||||
"prettier": "2.7.1",
|
||||
"@eslint/compat": "^2.0.2",
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@types/node": "^25.3.0",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"eslint": "^10.0.1",
|
||||
"eslint-config-airbnb-extended": "^3.0.1",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.5.5",
|
||||
"prettier": "^3.8.1",
|
||||
"sass": "1.54.3",
|
||||
"sass-loader": "13.0.2",
|
||||
"terser-webpack-plugin": "5.3.3",
|
||||
"ts-loader": "9.3.1",
|
||||
"typescript": "4.7.4",
|
||||
"vue-loader": "17.0.0",
|
||||
"webpack": "5.74.0",
|
||||
"webpack-cli": "4.10.0",
|
||||
"webpack-dev-server": "4.9.3"
|
||||
"typescript": "5.9.3",
|
||||
"vite": "^6.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
12
src/App.vue
12
src/App.vue
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<div id="content">
|
||||
<!-- Header and hamburger navigation -->
|
||||
<NavigationHeader class="header" />
|
||||
|
||||
@@ -28,13 +28,13 @@
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "src/scss/main";
|
||||
@import "src/scss/media-queries";
|
||||
@import "scss/main";
|
||||
@import "scss/media-queries";
|
||||
|
||||
#app {
|
||||
#content {
|
||||
display: grid;
|
||||
grid-template-rows: var(--header-size);
|
||||
grid-template-columns: var(--header-size) 1fr;
|
||||
grid-template-columns: var(--header-size) 100%;
|
||||
|
||||
@include mobile {
|
||||
grid-template-columns: 1fr;
|
||||
@@ -59,11 +59,13 @@
|
||||
.content {
|
||||
display: grid;
|
||||
grid-column: 2 / 3;
|
||||
width: calc(100% - var(--header-size));
|
||||
grid-row: 2;
|
||||
z-index: 5;
|
||||
|
||||
@include mobile {
|
||||
grid-column: 1 / 3;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
278
src/api.ts
278
src/api.ts
@@ -1,29 +1,31 @@
|
||||
import { IList, IMediaCredits, IPersonCredits } from "./interfaces/IList";
|
||||
/* eslint-disable n/no-unsupported-features/node-builtins */
|
||||
import {
|
||||
IList,
|
||||
IMediaCredits,
|
||||
IPersonCredits,
|
||||
MediaTypes
|
||||
} from "./interfaces/IList";
|
||||
import type {
|
||||
IRequestStatusResponse,
|
||||
IRequestSubmitResponse
|
||||
} from "./interfaces/IRequestResponse";
|
||||
|
||||
const { ELASTIC, ELASTIC_INDEX, ELASTIC_APIKEY } = process.env;
|
||||
const API_HOSTNAME = window.location.origin;
|
||||
const API_HOSTNAME = import.meta.env.VITE_SEASONED_API;
|
||||
const ELASTIC_URL = import.meta.env.VITE_ELASTIC_URL;
|
||||
const ELASTIC_API_KEY = import.meta.env.VITE_ELASTIC_API_KEY;
|
||||
|
||||
// - - - TMDB - - -
|
||||
interface GetMediaOpts {
|
||||
checkExistance: boolean;
|
||||
credits: boolean;
|
||||
releaseDates?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 getMovie = async (id: number, opts: GetMediaOpts) => {
|
||||
const url = new URL("/api/v2/movie", API_HOSTNAME);
|
||||
url.pathname = `${url.pathname}/${id.toString()}`;
|
||||
|
||||
const { checkExistance, credits, releaseDates } = opts;
|
||||
if (checkExistance) {
|
||||
url.searchParams.append("check_existance", "true");
|
||||
}
|
||||
@@ -42,22 +44,12 @@ const getMovie = (
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 }
|
||||
) => {
|
||||
// Fetches tmdb show by id. Can optionally include cast credits in result object.
|
||||
const getShow = async (id: number, opts: GetMediaOpts) => {
|
||||
const url = new URL("/api/v2/show", API_HOSTNAME);
|
||||
url.pathname = `${url.pathname}/${id.toString()}`;
|
||||
|
||||
const { checkExistance, credits, releaseDates } = opts;
|
||||
if (checkExistance) {
|
||||
url.searchParams.append("check_existance", "true");
|
||||
}
|
||||
@@ -76,13 +68,8 @@ const getShow = (
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 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) => {
|
||||
// Fetches tmdb person by id. Can optionally include cast credits in result object.
|
||||
const getPerson = async (id: number, credits = false) => {
|
||||
const url = new URL("/api/v2/person", API_HOSTNAME);
|
||||
url.pathname = `${url.pathname}/${id.toString()}`;
|
||||
if (credits) {
|
||||
@@ -97,12 +84,8 @@ const getPerson = (id, credits = false) => {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches tmdb movie credits by id.
|
||||
* @param {number} id
|
||||
* @returns {object} Tmdb response
|
||||
*/
|
||||
const getMovieCredits = (id: number): Promise<IMediaCredits> => {
|
||||
// Fetches tmdb movie credits by id.
|
||||
const getMovieCredits = async (id: number): Promise<IMediaCredits> => {
|
||||
const url = new URL("/api/v2/movie", API_HOSTNAME);
|
||||
url.pathname = `${url.pathname}/${id.toString()}/credits`;
|
||||
|
||||
@@ -114,12 +97,8 @@ const getMovieCredits = (id: number): Promise<IMediaCredits> => {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches tmdb show credits by id.
|
||||
* @param {number} id
|
||||
* @returns {object} Tmdb response
|
||||
*/
|
||||
const getShowCredits = (id: number): Promise<IMediaCredits> => {
|
||||
// Fetches tmdb show credits by id.
|
||||
const getShowCredits = async (id: number): Promise<IMediaCredits> => {
|
||||
const url = new URL("/api/v2/show", API_HOSTNAME);
|
||||
url.pathname = `${url.pathname}/${id.toString()}/credits`;
|
||||
|
||||
@@ -131,12 +110,8 @@ const getShowCredits = (id: number): Promise<IMediaCredits> => {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches tmdb person credits by id.
|
||||
* @param {number} id
|
||||
* @returns {object} Tmdb response
|
||||
*/
|
||||
const getPersonCredits = (id: number): Promise<IPersonCredits> => {
|
||||
// Fetches tmdb person credits by id.
|
||||
const getPersonCredits = async (id: number): Promise<IPersonCredits> => {
|
||||
const url = new URL("/api/v2/person", API_HOSTNAME);
|
||||
url.pathname = `${url.pathname}/${id.toString()}/credits`;
|
||||
|
||||
@@ -148,13 +123,11 @@ const getPersonCredits = (id: number): Promise<IPersonCredits> => {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches tmdb list by name.
|
||||
* @param {string} name List the fetch
|
||||
* @param {number} [page=1]
|
||||
* @returns {object} Tmdb list response
|
||||
*/
|
||||
const getTmdbMovieListByName = (name: string, page = 1): Promise<IList> => {
|
||||
// Fetches tmdb list by name.
|
||||
const getTmdbMovieListByName = async (
|
||||
name: string,
|
||||
page = 1
|
||||
): Promise<IList> => {
|
||||
const url = new URL(`/api/v2/movie/${name}`, API_HOSTNAME);
|
||||
url.searchParams.append("page", page.toString());
|
||||
|
||||
@@ -162,12 +135,8 @@ const getTmdbMovieListByName = (name: string, page = 1): Promise<IList> => {
|
||||
// .catch(error => { console.error(`api error getting list: ${name}, page: ${page}`); throw error }) // eslint-disable-line no-console
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches requested items.
|
||||
* @param {number} [page=1]
|
||||
* @returns {object} Request response
|
||||
*/
|
||||
const getRequests = (page = 1) => {
|
||||
// Fetches requested items.
|
||||
const getRequests = async (page = 1) => {
|
||||
const url = new URL("/api/v2/request", API_HOSTNAME);
|
||||
url.searchParams.append("page", page.toString());
|
||||
|
||||
@@ -175,20 +144,25 @@ const getRequests = (page = 1) => {
|
||||
// .catch(error => { console.error(`api error getting list: ${name}, page: ${page}`); throw error }) // eslint-disable-line no-console
|
||||
};
|
||||
|
||||
const getUserRequests = (page = 1) => {
|
||||
const getUserRequests = async (page = 1) => {
|
||||
const url = new URL("/api/v1/user/requests", API_HOSTNAME);
|
||||
url.searchParams.append("page", page.toString());
|
||||
|
||||
return fetch(url.href).then(resp => resp.json());
|
||||
const options: RequestInit = {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
credentials: "include"
|
||||
};
|
||||
|
||||
return fetch(url.href, options).then(resp => resp.json());
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches tmdb movies and shows by query.
|
||||
* @param {string} query
|
||||
* @param {number} [page=1]
|
||||
* @returns {object} Tmdb response
|
||||
*/
|
||||
const searchTmdb = (query, page = 1, adult = false, mediaType = null) => {
|
||||
// Fetches tmdb movies and shows by query.
|
||||
const searchTmdb = async (
|
||||
query: string,
|
||||
page = 1,
|
||||
adult = false,
|
||||
mediaType = null
|
||||
) => {
|
||||
const url = new URL("/api/v2/search", API_HOSTNAME);
|
||||
if (mediaType != null && ["movie", "show", "person"].includes(mediaType)) {
|
||||
url.pathname += `/${mediaType}`;
|
||||
@@ -208,17 +182,15 @@ const searchTmdb = (query, page = 1, adult = false, mediaType = null) => {
|
||||
|
||||
// - - - Torrents - - -
|
||||
|
||||
/**
|
||||
* Search for torrents by query
|
||||
* @param {string} query
|
||||
* @param {boolean} credits Include credits
|
||||
* @returns {object} Torrent response
|
||||
*/
|
||||
const searchTorrents = query => {
|
||||
// Search for torrents by query
|
||||
const searchTorrents = async (query: string) => {
|
||||
const url = new URL("/api/v1/pirate/search", API_HOSTNAME);
|
||||
url.searchParams.append("query", query);
|
||||
const options: RequestInit = {
|
||||
credentials: "include"
|
||||
};
|
||||
|
||||
return fetch(url.href)
|
||||
return fetch(url.href, options)
|
||||
.then(resp => resp.json())
|
||||
.catch(error => {
|
||||
console.error(`api error searching torrents: ${query}`); // eslint-disable-line no-console
|
||||
@@ -226,19 +198,18 @@ const searchTorrents = query => {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Add magnet to download queue.
|
||||
* @param {string} magnet Magnet link
|
||||
* @param {boolean} name Name of torrent
|
||||
* @param {boolean} tmdbId
|
||||
* @returns {object} Success/Failure response
|
||||
*/
|
||||
const addMagnet = (magnet: string, name: string, tmdbId: number | null) => {
|
||||
// Add magnet to download queue.
|
||||
const addMagnet = async (
|
||||
magnet: string,
|
||||
name: string,
|
||||
tmdbId: number | null
|
||||
) => {
|
||||
const url = new URL("/api/v1/pirate/add", API_HOSTNAME);
|
||||
|
||||
const options = {
|
||||
const options: RequestInit = {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
credentials: "include",
|
||||
body: JSON.stringify({
|
||||
magnet,
|
||||
name,
|
||||
@@ -256,14 +227,11 @@ const addMagnet = (magnet: string, name: string, tmdbId: number | null) => {
|
||||
|
||||
// - - - Plex/Request - - -
|
||||
|
||||
/**
|
||||
* Request a movie or show from id. If authorization token is included the user will be linked
|
||||
* to the requested item.
|
||||
* @param {number} id Movie or show id
|
||||
* @param {string} type Movie or show type
|
||||
* @returns {object} Success/Failure response
|
||||
*/
|
||||
const request = (id, type): Promise<IRequestSubmitResponse> => {
|
||||
// Request a movie or show from id. If authorization token is included the user will be linked
|
||||
const request = async (
|
||||
id: number,
|
||||
type: MediaTypes.Movie | MediaTypes.Show
|
||||
): Promise<IRequestSubmitResponse> => {
|
||||
const url = new URL("/api/v2/request", API_HOSTNAME);
|
||||
|
||||
const options = {
|
||||
@@ -280,13 +248,11 @@ const request = (id, type): Promise<IRequestSubmitResponse> => {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Check request status by tmdb id and type
|
||||
* @param {number} tmdb id
|
||||
* @param {string} type
|
||||
* @returns {object} Success/Failure response
|
||||
*/
|
||||
const getRequestStatus = (id, type = null): Promise<IRequestStatusResponse> => {
|
||||
// Check request status by tmdb id and type
|
||||
const getRequestStatus = async (
|
||||
id: number,
|
||||
type = null
|
||||
): Promise<IRequestStatusResponse> => {
|
||||
const url = new URL("/api/v2/request", API_HOSTNAME);
|
||||
url.pathname = `${url.pathname}/${id.toString()}`;
|
||||
url.searchParams.append("type", type);
|
||||
@@ -296,7 +262,8 @@ const getRequestStatus = (id, type = null): Promise<IRequestStatusResponse> => {
|
||||
.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);
|
||||
url.searchParams.append("title", title);
|
||||
url.searchParams.append("year", year);
|
||||
@@ -311,14 +278,16 @@ const movieImages = id => {
|
||||
|
||||
return fetch(url.href).then(resp => resp.json());
|
||||
};
|
||||
*/
|
||||
|
||||
// - - - Seasoned user endpoints - - -
|
||||
|
||||
const register = (username, password) => {
|
||||
const register = async (username: string, password: string) => {
|
||||
const url = new URL("/api/v1/user", API_HOSTNAME);
|
||||
const options = {
|
||||
const options: RequestInit = {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
credentials: "include",
|
||||
body: JSON.stringify({ username, password })
|
||||
};
|
||||
|
||||
@@ -334,40 +303,49 @@ const register = (username, password) => {
|
||||
});
|
||||
};
|
||||
|
||||
const login = (username, password, throwError = false) => {
|
||||
const login = async (
|
||||
username: string,
|
||||
password: string,
|
||||
throwError = false
|
||||
) => {
|
||||
const url = new URL("/api/v1/user/login", API_HOSTNAME);
|
||||
const options = {
|
||||
const options: RequestInit = {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
credentials: "include",
|
||||
body: JSON.stringify({ username, password })
|
||||
};
|
||||
|
||||
return fetch(url.href, options).then(resp => {
|
||||
if (resp.status === 200) return resp.json();
|
||||
|
||||
if (throwError) throw resp;
|
||||
if (throwError) return Promise.reject(resp.text().then(t => new Error(t)));
|
||||
console.error("Error occured when trying to sign in.\nError:", resp); // eslint-disable-line no-console
|
||||
return Promise.reject(resp);
|
||||
});
|
||||
};
|
||||
|
||||
const logout = (throwError = false) => {
|
||||
const logout = async (throwError = false) => {
|
||||
const url = new URL("/api/v1/user/logout", API_HOSTNAME);
|
||||
const options = { method: "POST" };
|
||||
const options: RequestInit = { method: "POST", credentials: "include" };
|
||||
|
||||
return fetch(url.href, options).then(resp => {
|
||||
if (resp.status === 200) return resp.json();
|
||||
|
||||
if (throwError) throw resp;
|
||||
if (throwError) return Promise.reject(resp.text().then(t => new Error(t)));
|
||||
console.error("Error occured when trying to log out.\nError:", resp); // eslint-disable-line no-console
|
||||
return Promise.reject(resp);
|
||||
});
|
||||
};
|
||||
|
||||
const getSettings = () => {
|
||||
const getSettings = async () => {
|
||||
const url = new URL("/api/v1/user/settings", API_HOSTNAME);
|
||||
const options: RequestInit = {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
credentials: "include"
|
||||
};
|
||||
|
||||
return fetch(url.href)
|
||||
return fetch(url.href, options)
|
||||
.then(resp => resp.json())
|
||||
.catch(error => {
|
||||
console.log("api error getting user settings"); // eslint-disable-line no-console
|
||||
@@ -375,12 +353,13 @@ const getSettings = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const updateSettings = settings => {
|
||||
const updateSettings = async (settings: any) => {
|
||||
const url = new URL("/api/v1/user/settings", API_HOSTNAME);
|
||||
|
||||
const options = {
|
||||
const options: RequestInit = {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
credentials: "include",
|
||||
body: JSON.stringify(settings)
|
||||
};
|
||||
|
||||
@@ -394,13 +373,14 @@ const updateSettings = settings => {
|
||||
|
||||
// - - - Authenticate with plex - - -
|
||||
|
||||
const linkPlexAccount = (username, password) => {
|
||||
const linkPlexAccount = async (username: string, password: string) => {
|
||||
const url = new URL("/api/v1/user/link_plex", API_HOSTNAME);
|
||||
const body = { username, password };
|
||||
|
||||
const options = {
|
||||
const options: RequestInit = {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
credentials: "include",
|
||||
body: JSON.stringify(body)
|
||||
};
|
||||
|
||||
@@ -412,12 +392,12 @@ const linkPlexAccount = (username, password) => {
|
||||
});
|
||||
};
|
||||
|
||||
const unlinkPlexAccount = () => {
|
||||
const unlinkPlexAccount = async () => {
|
||||
const url = new URL("/api/v1/user/unlink_plex", API_HOSTNAME);
|
||||
|
||||
const options = {
|
||||
const options: RequestInit = {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" }
|
||||
headers: { "Content-Type": "application/json" },
|
||||
credentials: "include"
|
||||
};
|
||||
|
||||
return fetch(url.href, options)
|
||||
@@ -439,7 +419,12 @@ const fetchGraphData = async (
|
||||
url.searchParams.append("days", String(days));
|
||||
url.searchParams.append("y_axis", chartType);
|
||||
|
||||
return fetch(url.href).then(resp => {
|
||||
const options: RequestInit = {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
credentials: "include"
|
||||
};
|
||||
|
||||
return fetch(url.href, options).then(resp => {
|
||||
if (!resp.ok) {
|
||||
console.log("DAMN WE FAILED!", resp); // eslint-disable-line no-console
|
||||
throw Error(resp.statusText);
|
||||
@@ -465,15 +450,31 @@ const getEmoji = async () => {
|
||||
// - - - ELASTIC SEARCH - - -
|
||||
// This elastic index contains titles mapped to ids. Lightning search
|
||||
// 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
|
||||
* 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 = (query: string, count = 22) => {
|
||||
const url = new URL(`${ELASTIC_INDEX}/_search`, ELASTIC);
|
||||
const elasticSearchMoviesAndShows = async (query: string, count = 22) => {
|
||||
const url = new URL(`${ELASTIC_URL}/_search`);
|
||||
|
||||
const body = {
|
||||
sort: [{ popularity: { order: "desc" } }, "_score"],
|
||||
@@ -521,13 +522,14 @@ const elasticSearchMoviesAndShows = (query: string, count = 22) => {
|
||||
const options = {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `ApiKey ${ELASTIC_APIKEY}`,
|
||||
"Content-Type": "application/json"
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `ApiKey ${ELASTIC_API_KEY}`
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
body: JSON.stringify(body),
|
||||
timeout: 1000
|
||||
};
|
||||
|
||||
return fetch(url.href, options)
|
||||
return fetchWithTimeout(url.href, options)
|
||||
.then(resp => resp.json())
|
||||
.catch(error => {
|
||||
console.log(`api error searching elasticsearch: ${query}`); // eslint-disable-line no-console
|
||||
@@ -549,8 +551,6 @@ export {
|
||||
searchTorrents,
|
||||
addMagnet,
|
||||
request,
|
||||
watchLink,
|
||||
movieImages,
|
||||
getRequestStatus,
|
||||
linkPlexAccount,
|
||||
unlinkPlexAccount,
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import CastListItem from "src/components/CastListItem.vue";
|
||||
import { defineProps } from "vue";
|
||||
import CastListItem from "@/components/CastListItem.vue";
|
||||
import type {
|
||||
IMovie,
|
||||
IShow,
|
||||
|
||||
@@ -100,8 +100,8 @@
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "src/scss/variables";
|
||||
@import "src/scss/media-queries";
|
||||
@import "scss/variables";
|
||||
@import "scss/media-queries";
|
||||
|
||||
header {
|
||||
width: 100%;
|
||||
|
||||
@@ -47,9 +47,9 @@
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "src/scss/variables";
|
||||
@import "src/scss/media-queries";
|
||||
@import "src/scss/main";
|
||||
@import "scss/variables";
|
||||
@import "scss/media-queries";
|
||||
@import "scss/main";
|
||||
|
||||
header {
|
||||
width: 100%;
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
let _type: MediaTypes;
|
||||
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
params.forEach((value, key) => {
|
||||
params.forEach((_, key) => {
|
||||
if (
|
||||
key !== MediaTypes.Movie &&
|
||||
key !== MediaTypes.Show &&
|
||||
@@ -90,8 +90,8 @@
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "src/scss/variables";
|
||||
@import "src/scss/media-queries";
|
||||
@import "scss/variables";
|
||||
@import "scss/media-queries";
|
||||
|
||||
.movie-popup {
|
||||
position: fixed;
|
||||
@@ -136,7 +136,7 @@
|
||||
left: 10px;
|
||||
width: 20px;
|
||||
height: 2px;
|
||||
background: $white;
|
||||
background-color: white;
|
||||
}
|
||||
&:before {
|
||||
transform: rotate(45deg);
|
||||
@@ -145,7 +145,7 @@
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
&:hover {
|
||||
background: $green;
|
||||
background-color: var(--highlight-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,8 +34,8 @@
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "src/scss/media-queries";
|
||||
@import "src/scss/main";
|
||||
@import "scss/media-queries";
|
||||
@import "scss/main";
|
||||
|
||||
.no-results {
|
||||
width: 100%;
|
||||
|
||||
@@ -22,7 +22,11 @@
|
||||
</div>
|
||||
</figure>
|
||||
|
||||
<div class="movie-item__info">
|
||||
<div
|
||||
class="movie-item__info"
|
||||
@click="openMoviePopup"
|
||||
@keydown.enter="openMoviePopup"
|
||||
>
|
||||
<p v-if="listItem.title || listItem.name" class="movie-item__title">
|
||||
{{ listItem.title || listItem.name }}
|
||||
</p>
|
||||
@@ -111,9 +115,9 @@
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "src/scss/variables";
|
||||
@import "src/scss/media-queries";
|
||||
@import "src/scss/main";
|
||||
@import "scss/variables";
|
||||
@import "scss/media-queries";
|
||||
@import "scss/main";
|
||||
|
||||
.movie-item {
|
||||
padding: 15px;
|
||||
|
||||
@@ -172,7 +172,7 @@
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "src/scss/media-queries";
|
||||
@import "scss/media-queries";
|
||||
|
||||
.resultSection {
|
||||
background-color: var(--background-color);
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
>
|
||||
<IconMovie v-if="result.type == 'movie'" 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>
|
||||
</li>
|
||||
|
||||
@@ -24,25 +23,17 @@
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<!--
|
||||
Searches Elasticsearch for results based on changes to `query`.
|
||||
-->
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
import IconMovie from "@/icons/IconMovie.vue";
|
||||
import IconShow from "@/icons/IconShow.vue";
|
||||
import IconPerson from "@/icons/IconPerson.vue";
|
||||
import type { Ref } from "vue";
|
||||
import { ref, watch, defineProps } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
import IconMovie from "../../icons/IconMovie.vue";
|
||||
import IconShow from "../../icons/IconShow.vue";
|
||||
import { elasticSearchMoviesAndShows } from "../../api";
|
||||
import { MediaTypes } from "../../interfaces/IList";
|
||||
import type {
|
||||
IAutocompleteResult,
|
||||
IAutocompleteSearchResults,
|
||||
Hit,
|
||||
Option,
|
||||
Source
|
||||
IAutocompleteSearchResults
|
||||
} from "../../interfaces/IAutocompleteSearch";
|
||||
|
||||
interface Props {
|
||||
@@ -56,7 +47,6 @@ Searches Elasticsearch for results based on changes to `query`.
|
||||
}
|
||||
|
||||
const numberOfResults = 10;
|
||||
let timeoutId = null;
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits<Emit>();
|
||||
const store = useStore();
|
||||
@@ -64,9 +54,25 @@ Searches Elasticsearch for results based on changes to `query`.
|
||||
const searchResults: Ref<Array<IAutocompleteResult>> = ref([]);
|
||||
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>) {
|
||||
const filteredResults = [];
|
||||
_searchResults.forEach((result: IAutocompleteResult) => {
|
||||
_searchResults.forEach(result => {
|
||||
if (result === undefined) return;
|
||||
const numberOfDuplicates = filteredResults.filter(
|
||||
filterItem => filterItem.id === result.id
|
||||
@@ -81,89 +87,64 @@ Searches Elasticsearch for results based on changes to `query`.
|
||||
return filteredResults;
|
||||
}
|
||||
|
||||
function convertMediaType(type: string | null): MediaTypes | null {
|
||||
function elasticTypeToMediaType(type: string): MediaTypes {
|
||||
if (type === "movie") return MediaTypes.Movie;
|
||||
|
||||
if (type === "tv_series") return MediaTypes.Show;
|
||||
|
||||
if (type === "person") return MediaTypes.Person;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function parseElasticResponse(elasticResponse: IAutocompleteSearchResults) {
|
||||
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 data = elasticResponse.hits.hits;
|
||||
|
||||
const results: Array<IAutocompleteResult> = [];
|
||||
|
||||
data.forEach(item => {
|
||||
if (!item._index) return;
|
||||
|
||||
results.push({
|
||||
title: item?.original_name || item?.original_title || item?.name,
|
||||
id: item.id,
|
||||
adult: item.adult,
|
||||
type: convertMediaType(item?.type)
|
||||
title: item._source?.original_name || item._source.original_title,
|
||||
id: item._source.id,
|
||||
adult: item._source.adult,
|
||||
type: elasticTypeToMediaType(item._source.type)
|
||||
});
|
||||
});
|
||||
|
||||
return removeDuplicates(results)
|
||||
.map((el, index) => {
|
||||
return { ...el, index };
|
||||
})
|
||||
.slice(0, 10);
|
||||
return removeDuplicates(results).map((el, index) => {
|
||||
return { ...el, index };
|
||||
});
|
||||
}
|
||||
|
||||
function fetchAutocompleteResults() {
|
||||
async function fetchAutocompleteResults() {
|
||||
keyboardNavigationIndex.value = 0;
|
||||
searchResults.value = [];
|
||||
|
||||
elasticSearchMoviesAndShows(props.query, numberOfResults)
|
||||
return elasticSearchMoviesAndShows(props.query, numberOfResults)
|
||||
.catch(error => {
|
||||
// TODO display error
|
||||
disableOnFailure = true;
|
||||
throw error;
|
||||
})
|
||||
.then(elasticResponse => parseElasticResponse(elasticResponse))
|
||||
.then(_searchResults => {
|
||||
console.log(_searchResults);
|
||||
emit("update:results", _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
|
||||
fetchAutocompleteResults();
|
||||
// end on load functions
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "src/scss/variables";
|
||||
@import "src/scss/media-queries";
|
||||
@import "src/scss/main";
|
||||
@import "scss/variables";
|
||||
@import "scss/media-queries";
|
||||
@import "scss/main";
|
||||
$sizes: 22;
|
||||
|
||||
@for $i from 0 through $sizes {
|
||||
@@ -241,7 +222,9 @@ Searches Elasticsearch for results based on changes to `query`.
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
|
||||
transition: color 0.1s ease, fill 0.4s ease;
|
||||
transition:
|
||||
color 0.1s ease,
|
||||
fill 0.4s ease;
|
||||
|
||||
span {
|
||||
overflow-x: hidden;
|
||||
|
||||
@@ -61,8 +61,8 @@
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "src/scss/variables";
|
||||
@import "src/scss/media-queries";
|
||||
@import "scss/variables";
|
||||
@import "scss/media-queries";
|
||||
|
||||
.spacer {
|
||||
@include mobile-only {
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "src/scss/media-queries";
|
||||
@import "scss/media-queries";
|
||||
|
||||
.navigation-link {
|
||||
display: grid;
|
||||
@@ -47,8 +47,13 @@
|
||||
padding: 1rem 0.15rem;
|
||||
text-align: center;
|
||||
background-color: var(--background-color-secondary);
|
||||
transition: transform 0.3s ease, color 0.3s ease, stoke 0.3s ease,
|
||||
fill 0.3s ease, background-color 0.5s ease;
|
||||
transition:
|
||||
transform 0.3s ease,
|
||||
color 0.3s ease,
|
||||
stoke 0.3s ease,
|
||||
fill 0.3s ease,
|
||||
background-color 0.5s ease;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.05);
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "src/scss/media-queries";
|
||||
@import "scss/media-queries";
|
||||
|
||||
.navigation-icons {
|
||||
display: grid;
|
||||
|
||||
@@ -55,22 +55,14 @@ the `query`.
|
||||
-->
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Ref } from "vue";
|
||||
import { ref, computed } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
import { useRouter, useRoute } from "vue-router";
|
||||
import AutocompleteDropdown from "@/components/header/AutocompleteDropdown.vue";
|
||||
import IconSearch from "@/icons/IconSearch.vue";
|
||||
import IconClose from "@/icons/IconClose.vue";
|
||||
import type { Ref } from "vue";
|
||||
import type { MediaTypes } from "../../interfaces/IList";
|
||||
import { IAutocompleteResult } from "../../interfaces/IAutocompleteSearch";
|
||||
|
||||
interface ISearchResult {
|
||||
title: string;
|
||||
id: number;
|
||||
adult: boolean;
|
||||
type: MediaTypes;
|
||||
}
|
||||
import AutocompleteDropdown from "./AutocompleteDropdown.vue";
|
||||
import IconSearch from "../../icons/IconSearch.vue";
|
||||
import IconClose from "../../icons/IconClose.vue";
|
||||
import { IAutocompleteResult } from "../../interfaces/IAutocompleteSearch";
|
||||
|
||||
const store = useStore();
|
||||
const router = useRouter();
|
||||
@@ -98,13 +90,9 @@ import { IAutocompleteResult } from "../../interfaces/IAutocompleteSearch";
|
||||
query.value = decodeURIComponent(params.get("query"));
|
||||
}
|
||||
|
||||
const { ELASTIC, ELASTIC_APIKEY } = process.env;
|
||||
if (
|
||||
ELASTIC === undefined ||
|
||||
ELASTIC === "" ||
|
||||
ELASTIC_APIKEY === undefined ||
|
||||
ELASTIC_APIKEY === ""
|
||||
) {
|
||||
const ELASTIC_URL = import.meta.env.VITE_ELASTIC_URL;
|
||||
const ELASTIC_API_KEY = import.meta.env.VITE_ELASTIC_API_KEY;
|
||||
if (!ELASTIC_URL || !ELASTIC_API_KEY) {
|
||||
disabled.value = true;
|
||||
}
|
||||
|
||||
@@ -184,9 +172,9 @@ import { IAutocompleteResult } from "../../interfaces/IAutocompleteSearch";
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "src/scss/variables";
|
||||
@import "src/scss/media-queries";
|
||||
@import "src/scss/main";
|
||||
@import "scss/variables";
|
||||
@import "scss/media-queries";
|
||||
@import "scss/main";
|
||||
|
||||
.close-icon {
|
||||
position: absolute;
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "src/scss/media-queries";
|
||||
@import "scss/media-queries";
|
||||
|
||||
li.sidebar-list-element {
|
||||
display: flex;
|
||||
@@ -65,7 +65,7 @@
|
||||
|
||||
&.active > div > svg,
|
||||
&.active > svg {
|
||||
fill: var(--color-green);
|
||||
fill: var(--highlight-color);
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
const overflow: Ref<boolean> = ref(false);
|
||||
const descriptionElement: Ref<HTMLElement> = ref(null);
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
function removeElements(elems: NodeListOf<Element>) {
|
||||
elems.forEach(el => el.remove());
|
||||
}
|
||||
@@ -67,7 +66,7 @@
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "src/scss/media-queries";
|
||||
@import "scss/media-queries";
|
||||
|
||||
.movie-description {
|
||||
font-weight: 300;
|
||||
@@ -93,6 +92,7 @@
|
||||
border: none;
|
||||
background: none;
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "src/scss/media-queries";
|
||||
@import "scss/media-queries";
|
||||
|
||||
.movie-detail {
|
||||
margin-bottom: 20px;
|
||||
@@ -35,7 +35,7 @@
|
||||
font-weight: 400;
|
||||
text-transform: uppercase;
|
||||
font-size: 1.2rem;
|
||||
color: var(--color-green);
|
||||
color: var(--highlight-color);
|
||||
|
||||
@include mobile {
|
||||
font-size: 1.1rem;
|
||||
|
||||
@@ -167,23 +167,24 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
import type { Ref } from "vue";
|
||||
|
||||
// import img from "@/directives/v-image";
|
||||
import IconProfile from "@/icons/IconProfile.vue";
|
||||
import IconThumbsUp from "@/icons/IconThumbsUp.vue";
|
||||
import IconThumbsDown from "@/icons/IconThumbsDown.vue";
|
||||
import IconInfo from "@/icons/IconInfo.vue";
|
||||
import IconRequest from "@/icons/IconRequest.vue";
|
||||
import IconRequested from "@/icons/IconRequested.vue";
|
||||
import IconBinoculars from "@/icons/IconBinoculars.vue";
|
||||
import IconPlay from "@/icons/IconPlay.vue";
|
||||
import TorrentList from "@/components/torrent/TruncatedTorrentResults.vue";
|
||||
import CastList from "@/components/CastList.vue";
|
||||
import Detail from "@/components/popup/Detail.vue";
|
||||
import ActionButton from "@/components/popup/ActionButton.vue";
|
||||
import Description from "@/components/popup/Description.vue";
|
||||
import LoadingPlaceholder from "@/components/ui/LoadingPlaceholder.vue";
|
||||
import type { Ref } from "vue";
|
||||
import IconProfile from "../../icons/IconProfile.vue";
|
||||
import IconThumbsUp from "../../icons/IconThumbsUp.vue";
|
||||
import IconThumbsDown from "../../icons/IconThumbsDown.vue";
|
||||
import IconInfo from "../../icons/IconInfo.vue";
|
||||
import IconRequest from "../../icons/IconRequest.vue";
|
||||
import IconRequested from "../../icons/IconRequested.vue";
|
||||
import IconBinoculars from "../../icons/IconBinoculars.vue";
|
||||
import IconPlay from "../../icons/IconPlay.vue";
|
||||
import TorrentList from "../torrent/TruncatedTorrentResults.vue";
|
||||
import CastList from "../CastList.vue";
|
||||
import Detail from "./Detail.vue";
|
||||
import ActionButton from "./ActionButton.vue";
|
||||
import Description from "./Description.vue";
|
||||
import LoadingPlaceholder from "../ui/LoadingPlaceholder.vue";
|
||||
import type { IColors } from "../../interfaces/IColors.ts";
|
||||
import type {
|
||||
IMovie,
|
||||
IShow,
|
||||
@@ -214,6 +215,7 @@
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const ASSET_URL = "https://image.tmdb.org/t/p/";
|
||||
const COLORS_URL = "https://colors.schleppe.cloud/colors";
|
||||
const ASSET_SIZES = ["w500", "w780", "original"];
|
||||
|
||||
const media: Ref<IMovie | IShow> = ref();
|
||||
@@ -234,6 +236,8 @@
|
||||
if (!media.value) return "/assets/placeholder.png";
|
||||
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}`;
|
||||
});
|
||||
|
||||
@@ -332,6 +336,34 @@
|
||||
const tmdbURL = `https://www.themoviedb.org/${tmdbType}/${props.id}`;
|
||||
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
|
||||
fetchMedia();
|
||||
@@ -342,10 +374,10 @@
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "src/scss/loading-placeholder";
|
||||
@import "src/scss/variables";
|
||||
@import "src/scss/media-queries";
|
||||
@import "src/scss/main";
|
||||
@import "scss/loading-placeholder";
|
||||
@import "scss/variables";
|
||||
@import "scss/media-queries";
|
||||
@import "scss/main";
|
||||
|
||||
header {
|
||||
$duration: 0.2s;
|
||||
@@ -392,6 +424,7 @@
|
||||
|
||||
.movie__poster {
|
||||
display: none;
|
||||
border-radius: 1.6rem;
|
||||
|
||||
@include desktop {
|
||||
background: var(--background-color);
|
||||
@@ -402,7 +435,7 @@
|
||||
|
||||
> img {
|
||||
width: 100%;
|
||||
border-radius: 10px;
|
||||
border-radius: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -421,8 +454,8 @@
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
background-color: $background-color;
|
||||
color: $text-color;
|
||||
background-color: var(--highlight-bg, var(--background-color));
|
||||
color: var(--text-color);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -431,7 +464,9 @@
|
||||
width: 100%;
|
||||
opacity: 0;
|
||||
transform: scale(0.97) translateZ(0);
|
||||
transition: opacity 0.5s ease, transform 0.5s ease;
|
||||
transition:
|
||||
opacity 0.5s ease,
|
||||
transform 0.5s ease;
|
||||
|
||||
&.is-loaded {
|
||||
opacity: 1;
|
||||
@@ -450,21 +485,26 @@
|
||||
text-align: left;
|
||||
padding: 140px 30px 0 40px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: var(--color-green);
|
||||
color: var(--highlight-color);
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
font-size: 24px;
|
||||
line-height: 1.2;
|
||||
font-size: 2.2rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 1px;
|
||||
margin-bottom: 0;
|
||||
|
||||
@include tablet-min {
|
||||
font-size: 30px;
|
||||
font-size: 2.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
i {
|
||||
display: block;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
color: var(--highlight-secondary);
|
||||
margin-top: 1rem;
|
||||
}
|
||||
}
|
||||
@@ -474,7 +514,7 @@
|
||||
width: 100%;
|
||||
order: 2;
|
||||
padding: 20px;
|
||||
border-top: 1px solid $text-color-5;
|
||||
border-top: 1px solid var(--text-color-50);
|
||||
@include tablet-min {
|
||||
order: 1;
|
||||
width: 45%;
|
||||
@@ -533,7 +573,7 @@
|
||||
}
|
||||
|
||||
.torrents {
|
||||
background-color: var(--background-color);
|
||||
background-color: var(--highlight-bg, var(--background-color));
|
||||
padding: 0 1rem;
|
||||
|
||||
@include mobile {
|
||||
|
||||
@@ -165,10 +165,10 @@
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "src/scss/loading-placeholder";
|
||||
@import "src/scss/variables";
|
||||
@import "src/scss/media-queries";
|
||||
@import "src/scss/main";
|
||||
@import "scss/loading-placeholder";
|
||||
@import "scss/variables";
|
||||
@import "scss/media-queries";
|
||||
@import "scss/main";
|
||||
|
||||
section.person {
|
||||
overflow: hidden;
|
||||
|
||||
@@ -85,7 +85,7 @@
|
||||
try {
|
||||
validate();
|
||||
} catch (error) {
|
||||
console.log("not valid!"); // eslint-disable-line no-console
|
||||
console.log("not valid! error:", error); // eslint-disable-line no-console
|
||||
}
|
||||
|
||||
// const body: ResetPasswordPayload = {
|
||||
|
||||
@@ -102,9 +102,9 @@
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "src/scss/variables";
|
||||
@import "src/scss/media-queries";
|
||||
@import "src/scss/elements";
|
||||
@import "scss/variables";
|
||||
@import "scss/media-queries";
|
||||
@import "scss/elements";
|
||||
|
||||
h2 {
|
||||
font-size: 20px;
|
||||
@@ -115,13 +115,8 @@
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
background-color: $background-color;
|
||||
}
|
||||
|
||||
.no-results {
|
||||
display: flex;
|
||||
padding-bottom: 2rem;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
@@ -142,7 +137,7 @@
|
||||
}
|
||||
|
||||
@include mobile {
|
||||
text-align: left;
|
||||
padding: 0 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -155,9 +155,9 @@
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "src/scss/variables";
|
||||
@import "src/scss/media-queries";
|
||||
@import "src/scss/elements";
|
||||
@import "scss/variables";
|
||||
@import "scss/media-queries";
|
||||
@import "scss/elements";
|
||||
|
||||
table {
|
||||
border-spacing: 0;
|
||||
@@ -185,6 +185,7 @@
|
||||
text-transform: uppercase;
|
||||
cursor: pointer;
|
||||
background-color: var(--table-background-color);
|
||||
background-color: var(--highlight-color);
|
||||
// background-color: black;
|
||||
// color: var(--color-green);
|
||||
letter-spacing: 0.8px;
|
||||
@@ -232,6 +233,9 @@
|
||||
}
|
||||
|
||||
// alternate background color per row
|
||||
tr {
|
||||
background-color: var(--background-color);
|
||||
}
|
||||
tr:nth-child(even) {
|
||||
background-color: var(--background-70);
|
||||
}
|
||||
|
||||
@@ -1,36 +1,40 @@
|
||||
<template>
|
||||
<div>
|
||||
<torrent-search-results
|
||||
:query="query"
|
||||
:tmdb-id="tmdbId"
|
||||
:class="{ truncated: truncated }"
|
||||
><div
|
||||
v-if="truncated"
|
||||
class="load-more"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
@click="truncated = false"
|
||||
@keydown.enter="truncated = false"
|
||||
>
|
||||
<icon-arrow-down />
|
||||
</div>
|
||||
</torrent-search-results>
|
||||
<div class="search-results">
|
||||
<torrent-search-results
|
||||
:query="query"
|
||||
:tmdb-id="tmdbId"
|
||||
:class="{ truncated: _truncated }"
|
||||
><div
|
||||
v-if="_truncated"
|
||||
class="load-more"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
@click="truncated = false"
|
||||
@keydown.enter="truncated = false"
|
||||
>
|
||||
<icon-arrow-down />
|
||||
</div>
|
||||
</torrent-search-results>
|
||||
</div>
|
||||
|
||||
<div class="edit-query-btn-container">
|
||||
<seasonedButton @click="openInTorrentPage"
|
||||
>View on torrent page</seasonedButton
|
||||
>
|
||||
<a :href="`/torrents?query=${encodeURIComponent(props.query)}`">
|
||||
<button>
|
||||
<span class="text">View on torrent page</span
|
||||
><span class="icon"><icon-arrow-down /></span>
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { ref, defineProps, computed } from "vue";
|
||||
import TorrentSearchResults from "@/components/torrent/TorrentSearchResults.vue";
|
||||
import SeasonedButton from "@/components/ui/SeasonedButton.vue";
|
||||
import IconArrowDown from "@/icons/IconArrowDown.vue";
|
||||
import type { Ref } from "vue";
|
||||
import store from "../../store";
|
||||
|
||||
interface Props {
|
||||
query: string;
|
||||
@@ -38,18 +42,13 @@
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const router = useRouter();
|
||||
|
||||
const truncated: Ref<boolean> = ref(true);
|
||||
|
||||
function openInTorrentPage() {
|
||||
if (!props.query?.length) {
|
||||
router.push("/torrents");
|
||||
return;
|
||||
}
|
||||
|
||||
router.push({ path: "/torrents", query: { query: props.query } });
|
||||
}
|
||||
const _truncated = computed(() => {
|
||||
const val = store.getters["torrentModule/resultCount"];
|
||||
if (val > 10 && truncated.value) return true;
|
||||
return false;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -75,14 +74,68 @@
|
||||
);
|
||||
}
|
||||
|
||||
svg {
|
||||
height: 30px;
|
||||
fill: var(--text-color);
|
||||
.search-results {
|
||||
svg {
|
||||
height: 30px;
|
||||
fill: var(--text-color);
|
||||
}
|
||||
}
|
||||
|
||||
.edit-query-btn-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
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>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -23,7 +23,7 @@
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "src/scss/media-queries";
|
||||
@import "scss/media-queries";
|
||||
|
||||
.nav__hamburger {
|
||||
display: block;
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "src/scss/variables";
|
||||
@import "scss/variables";
|
||||
|
||||
.loader {
|
||||
display: flex;
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "src/scss/loading-placeholder";
|
||||
@import "scss/loading-placeholder";
|
||||
</style>
|
||||
|
||||
@@ -23,8 +23,8 @@
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "src/scss/variables";
|
||||
@import "src/scss/media-queries";
|
||||
@import "scss/variables";
|
||||
@import "scss/media-queries";
|
||||
|
||||
button {
|
||||
display: inline-block;
|
||||
@@ -42,7 +42,10 @@
|
||||
background: $background-color-secondary;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
transition: background 0.5s ease, color 0.5s ease, border-color 0.5s ease;
|
||||
transition:
|
||||
background 0.5s ease,
|
||||
color 0.5s ease,
|
||||
border-color 0.5s ease;
|
||||
|
||||
@include desktop {
|
||||
font-size: 0.8rem;
|
||||
|
||||
@@ -74,8 +74,8 @@
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import "src/scss/variables";
|
||||
@import "src/scss/media-queries";
|
||||
@import "scss/variables";
|
||||
@import "scss/media-queries";
|
||||
|
||||
.group {
|
||||
display: flex;
|
||||
|
||||
@@ -64,8 +64,8 @@
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "src/scss/variables";
|
||||
@import "src/scss/media-queries";
|
||||
@import "scss/variables";
|
||||
@import "scss/media-queries";
|
||||
|
||||
.fade-active {
|
||||
transition: opacity 0.4s;
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "src/scss/variables";
|
||||
@import "scss/variables";
|
||||
|
||||
$background: $background-ui;
|
||||
$background-selected: $background-color-secondary;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/*
|
||||
let setValue = function(el, binding) {
|
||||
let value = binding.value;
|
||||
let dateArray = value.split('-');
|
||||
@@ -13,3 +14,4 @@ module.exports = {
|
||||
setValue(el, binding);
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
let setValue = function(el, binding) {
|
||||
let img = new Image();
|
||||
/*
|
||||
const setValue = function(el, binding) {
|
||||
const img = new Image();
|
||||
img.src = binding.value;
|
||||
|
||||
img.onload = function() {
|
||||
@@ -10,10 +11,11 @@ let setValue = function(el, binding) {
|
||||
|
||||
module.exports = {
|
||||
isLiteral: true,
|
||||
bind(el, binding){
|
||||
bind(el, binding) {
|
||||
setValue(el, binding);
|
||||
},
|
||||
update(el, binding){
|
||||
update(el, binding) {
|
||||
setValue(el, binding);
|
||||
}
|
||||
}
|
||||
};
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
<template>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<svg
|
||||
version="1.1"
|
||||
height="100%"
|
||||
width="100%"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 32 32"
|
||||
>
|
||||
<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"
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable no-use-before-define */
|
||||
import { MediaTypes } from "./IList";
|
||||
|
||||
export interface IAutocompleteResult {
|
||||
@@ -30,7 +29,7 @@ export interface Hits {
|
||||
}
|
||||
|
||||
export interface Hit {
|
||||
_index: Index;
|
||||
_index: string;
|
||||
_type: Type;
|
||||
_id: string;
|
||||
_score: number;
|
||||
@@ -59,11 +58,6 @@ export interface Option {
|
||||
_source: Source;
|
||||
}
|
||||
|
||||
export enum Index {
|
||||
Movies = "movies",
|
||||
Shows = "shows"
|
||||
}
|
||||
|
||||
export interface Source {
|
||||
tags: Tag[];
|
||||
ecs: Ecs;
|
||||
@@ -80,7 +74,7 @@ export interface Source {
|
||||
original_title: string;
|
||||
original_name?: string;
|
||||
name?: string;
|
||||
type?: MediaTypes;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface Agent {
|
||||
|
||||
5
src/interfaces/IColors.ts
Normal file
5
src/interfaces/IColors.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface IColors {
|
||||
bg: string;
|
||||
p: string;
|
||||
s?: string;
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
/* eslint-disable no-use-before-define */
|
||||
|
||||
export enum GraphTypes {
|
||||
Plays = "plays",
|
||||
Duration = "duration"
|
||||
@@ -12,12 +10,12 @@ export enum GraphValueTypes {
|
||||
|
||||
export interface IGraphDataset {
|
||||
name: string;
|
||||
data: Array<number>;
|
||||
data: number[];
|
||||
}
|
||||
|
||||
export interface IGraphData {
|
||||
labels: Array<string>;
|
||||
series: Array<IGraphDataset>;
|
||||
labels: string[];
|
||||
series: IGraphDataset[];
|
||||
}
|
||||
|
||||
export interface IGraphResponse {
|
||||
|
||||
@@ -67,7 +67,7 @@ export interface IMovie {
|
||||
backdrop: string;
|
||||
release_date: string | Date;
|
||||
rating: number;
|
||||
genres: Array<MovieGenres>;
|
||||
genres: MovieGenres[];
|
||||
production_status: MovieProductionStatus;
|
||||
tagline: string;
|
||||
runtime: number;
|
||||
@@ -88,9 +88,9 @@ export interface IShow {
|
||||
seasons?: number;
|
||||
episodes?: number;
|
||||
popularity?: number;
|
||||
genres?: Array<ShowGenres>;
|
||||
genres?: ShowGenres[];
|
||||
production_status?: string;
|
||||
runtime?: Array<number>;
|
||||
runtime?: number[];
|
||||
exists_in_plex?: boolean;
|
||||
type: MediaTypes.Show;
|
||||
}
|
||||
@@ -135,19 +135,19 @@ export interface ICrew {
|
||||
}
|
||||
|
||||
export interface IMediaCredits {
|
||||
cast: Array<ICast>;
|
||||
crew: Array<ICrew>;
|
||||
cast: ICast[];
|
||||
crew: ICrew[];
|
||||
id: number;
|
||||
}
|
||||
|
||||
export interface IPersonCredits {
|
||||
cast: Array<IMovie | IShow>;
|
||||
crew: Array<ICrew>;
|
||||
cast: (IMovie | IShow)[];
|
||||
crew: ICrew[];
|
||||
id: number;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
export type ListResults = Array<IMovie | IShow | IPerson | IRequest>;
|
||||
export type ListResults = (IMovie | IShow | IPerson | IRequest)[];
|
||||
|
||||
export interface IList {
|
||||
results: ListResults;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export default interface INavigationIcon {
|
||||
title: string;
|
||||
route: string;
|
||||
icon: any; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
icon: any;
|
||||
requiresAuth?: boolean;
|
||||
useStroke?: boolean;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type ITorrent from "./ITorrent";
|
||||
|
||||
export default interface IStateTorrent {
|
||||
results: Array<ITorrent>;
|
||||
results: ITorrent[];
|
||||
resultCount: number | null;
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@ export default interface ITorrent {
|
||||
seed: string;
|
||||
leech: string;
|
||||
url: string | null;
|
||||
release_type: Array<string>;
|
||||
release_type: string[];
|
||||
}
|
||||
|
||||
@@ -3,8 +3,7 @@ import router from "./routes";
|
||||
import store from "./store";
|
||||
import Toast from "./plugins/Toast";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const App = require("./App.vue").default;
|
||||
import App from "./App.vue";
|
||||
|
||||
store.dispatch("darkmodeModule/findAndSetDarkmodeSupported");
|
||||
store.dispatch("user/initUserFromCookie");
|
||||
@@ -14,4 +13,5 @@ const app = createApp(App);
|
||||
app.use(router);
|
||||
app.use(store);
|
||||
app.use(Toast);
|
||||
app.mount("#entry");
|
||||
|
||||
app.mount("#app");
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
import IStateDarkmode from "../interfaces/IStateDarkmode";
|
||||
|
||||
const state: IStateDarkmode = {
|
||||
@@ -10,9 +11,7 @@ export default {
|
||||
namespaced: true,
|
||||
state,
|
||||
getters: {
|
||||
darkmodeSupported: (state: IStateDarkmode) => {
|
||||
return state.darkmodeSupported;
|
||||
}
|
||||
darkmodeSupported: (state: IStateDarkmode) => state.darkmodeSupported
|
||||
},
|
||||
mutations: {
|
||||
SET_DARKMODE_SUPPORT: (
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
import type IStateDocumentTitle from "../interfaces/IStateDocumentTitle";
|
||||
|
||||
const capitalize = (string: string) => {
|
||||
@@ -26,7 +27,7 @@ const state: IStateDocumentTitle = {
|
||||
title: undefined
|
||||
};
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-shadow, no-return-assign */
|
||||
/* eslint-disable @typescript-eslint/no-shadow */
|
||||
export default {
|
||||
namespaced: true,
|
||||
state,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
import type IStateHamburger from "../interfaces/IStateHamburger";
|
||||
|
||||
const state: IStateHamburger = {
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
import { MediaTypes } from "../interfaces/IList";
|
||||
import type { IStatePopup, IPopupQuery } from "../interfaces/IStatePopup";
|
||||
|
||||
/* eslint-disable-next-line import/no-cycle */
|
||||
/* eslint-disable-next-line import-x/no-cycle */
|
||||
import router from "../routes";
|
||||
|
||||
const removeIncludedQueryParams = (params, key) => {
|
||||
const removeIncludedQueryParams = (params: URLSearchParams, key: string) => {
|
||||
if (params.has(key)) params.delete(key);
|
||||
return params;
|
||||
};
|
||||
|
||||
function paramsToObject(entries) {
|
||||
function paramsToObject(entries: Iterator<[string, string]>) {
|
||||
const result = {};
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const [key, value] of entries) {
|
||||
@@ -65,7 +66,7 @@ export default {
|
||||
actions: {
|
||||
open: ({ commit }, { id, type }: { id: number; type: MediaTypes }) => {
|
||||
if (!Number.isNaN(id)) {
|
||||
id = Number(id); /* eslint-disable-line no-param-reassign */
|
||||
id = Number(id);
|
||||
}
|
||||
|
||||
commit("SET_OPEN", { id, type });
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
import type ITorrent from "../interfaces/ITorrent";
|
||||
import type IStateTorrent from "../interfaces/IStateTorrent";
|
||||
|
||||
@@ -11,16 +12,12 @@ export default {
|
||||
namespaced: true,
|
||||
state,
|
||||
getters: {
|
||||
results: (state: IStateTorrent) => {
|
||||
return state.results;
|
||||
},
|
||||
resultCount: (state: IStateTorrent) => {
|
||||
return state.resultCount;
|
||||
}
|
||||
results: (state: IStateTorrent) => state.results,
|
||||
resultCount: (state: IStateTorrent) => state.resultCount
|
||||
},
|
||||
|
||||
mutations: {
|
||||
SET_RESULTS: (state: IStateTorrent, results: Array<ITorrent>) => {
|
||||
SET_RESULTS: (state: IStateTorrent, results: ITorrent[]) => {
|
||||
state.results = results;
|
||||
},
|
||||
SET_RESULT_COUNT: (state: IStateTorrent, count: number) => {
|
||||
@@ -32,7 +29,7 @@ export default {
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
setResults({ commit }, results: Array<ITorrent>) {
|
||||
setResults({ commit }, results: ITorrent[]) {
|
||||
commit("SET_RESULTS", results);
|
||||
},
|
||||
setResultCount({ commit }, count: number) {
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
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")
|
||||
}
|
||||
};
|
||||
178
src/modules/user.ts
Normal file
178
src/modules/user.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
/* 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;
|
||||
@@ -28,8 +28,8 @@
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "src/scss/variables";
|
||||
@import "src/scss/media-queries";
|
||||
@import "scss/variables.scss";
|
||||
@import "scss/media-queries";
|
||||
|
||||
.button {
|
||||
font-size: 1.2rem;
|
||||
|
||||
@@ -141,7 +141,7 @@
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "src/scss/variables";
|
||||
@import "scss/variables";
|
||||
|
||||
.wrapper {
|
||||
padding: 2rem;
|
||||
|
||||
@@ -118,8 +118,8 @@
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "src/scss/variables";
|
||||
@import "src/scss/media-queries";
|
||||
@import "scss/variables";
|
||||
@import "scss/media-queries";
|
||||
|
||||
.button--group {
|
||||
display: flex;
|
||||
|
||||
@@ -131,7 +131,7 @@
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "src/scss/variables";
|
||||
@import "scss/variables";
|
||||
|
||||
section {
|
||||
padding: 1.3rem;
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "src/scss/media-queries";
|
||||
@import "scss/media-queries";
|
||||
|
||||
.filter {
|
||||
margin-top: 0.5rem;
|
||||
|
||||
@@ -48,8 +48,8 @@
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "src/scss/variables";
|
||||
@import "src/scss/media-queries";
|
||||
@import "scss/variables";
|
||||
@import "scss/media-queries";
|
||||
|
||||
.settings {
|
||||
padding: 3rem;
|
||||
|
||||
@@ -112,7 +112,7 @@
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "src/scss/variables";
|
||||
@import "scss/variables";
|
||||
|
||||
section {
|
||||
padding: 1.3rem;
|
||||
|
||||
@@ -94,7 +94,9 @@
|
||||
|
||||
background-color: white;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.17), 0 2px 4px 0 rgba(0, 0, 0, 0.08);
|
||||
box-shadow:
|
||||
0 4px 8px 0 rgba(0, 0, 0, 0.17),
|
||||
0 2px 4px 0 rgba(0, 0, 0, 0.08);
|
||||
padding: 0.5rem;
|
||||
margin: 1rem 2rem 1rem 0.71rem;
|
||||
// max-width: calc(100% - 3rem);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createRouter, createWebHistory } from "vue-router";
|
||||
import type { RouteRecordRaw, RouteLocationNormalized } from "vue-router";
|
||||
|
||||
/* eslint-disable-next-line import/no-cycle */
|
||||
/* eslint-disable-next-line import-x/no-cycle */
|
||||
import store from "./store";
|
||||
|
||||
declare global {
|
||||
@@ -10,7 +10,7 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
const routes: Array<RouteRecordRaw> = [
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
name: "home",
|
||||
path: "/",
|
||||
@@ -99,7 +99,6 @@ const loggedIn = () => store.getters["user/loggedIn"];
|
||||
const hasPlexAccount = () => store.getters["user/plexUserId"] !== null;
|
||||
const hamburgerIsOpen = () => store.getters["hamburger/isOpen"];
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
router.beforeEach(
|
||||
(to: RouteLocationNormalized, from: RouteLocationNormalized, next: any) => {
|
||||
store.dispatch("documentTitle/updateTitle", to.name);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@import "src/scss/variables";
|
||||
@import "src/scss/media-queries";
|
||||
@import "scss/variables";
|
||||
@import "scss/media-queries";
|
||||
|
||||
.filter {
|
||||
margin: 1rem;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import "src/scss/variables";
|
||||
@import "scss/variables";
|
||||
|
||||
// Loading placeholder styling
|
||||
@mixin nth-children($points...) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import "src/scss/variables";
|
||||
@import "scss/variables";
|
||||
|
||||
.noselect {
|
||||
-webkit-touch-callout: none; /* iOS Safari */
|
||||
@@ -14,7 +14,7 @@
|
||||
}
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
height: unset;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
|
||||
@@ -8,72 +8,72 @@ $desktop-l-width: 1600px;
|
||||
$mobile-width: 768px;
|
||||
|
||||
@mixin desktop {
|
||||
@media (min-width: #{$mobile-width + 1px}) {
|
||||
@content;
|
||||
}
|
||||
@media (min-width: #{$mobile-width + 1px}) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin mobile {
|
||||
@media (max-width: #{$mobile-width}) {
|
||||
@content;
|
||||
}
|
||||
@media (max-width: #{$mobile-width}) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
.desktop-only {
|
||||
@include mobile {
|
||||
display: none !important;
|
||||
}
|
||||
@include mobile {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-only {
|
||||
@include desktop {
|
||||
display: none !important;
|
||||
}
|
||||
@include desktop {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
// Media
|
||||
@mixin mobile-only {
|
||||
@media (max-width: #{$tablet-p-width - 1px}) {
|
||||
@content;
|
||||
}
|
||||
@media (max-width: #{$tablet-p-width - 1px}) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
@mixin mobile-ls-min {
|
||||
@media (min-width: #{$phone-xs-width}) {
|
||||
@content;
|
||||
}
|
||||
@media (min-width: #{$phone-xs-width}) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
@mixin tablet-only {
|
||||
@media (min-width: #{$tablet-p-width}) and (max-width: #{$desktop-width - 1px}) {
|
||||
@content;
|
||||
}
|
||||
@media (min-width: #{$tablet-p-width}) and (max-width: #{$desktop-width - 1px}) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
@mixin tablet-min {
|
||||
@media (min-width: #{$tablet-p-width}) {
|
||||
@content;
|
||||
}
|
||||
@media (min-width: #{$tablet-p-width}) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
@mixin tablet-portrait-only {
|
||||
@media (min-width: #{$tablet-p-width}) and (max-width: #{$tablet-l-width - 1px}) {
|
||||
@content;
|
||||
}
|
||||
@media (min-width: #{$tablet-p-width}) and (max-width: #{$tablet-l-width - 1px}) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
@mixin tablet-landscape-min {
|
||||
@media (min-width: #{$tablet-l-width}) {
|
||||
@content;
|
||||
}
|
||||
@media (min-width: #{$tablet-l-width}) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
@mixin desktop-min {
|
||||
@media (min-width: #{$desktop-width}) {
|
||||
@content;
|
||||
}
|
||||
@media (min-width: #{$desktop-width}) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
@mixin desktop-lg-min {
|
||||
@media (min-width: #{$desktop-l-width}) {
|
||||
@content;
|
||||
}
|
||||
@media (min-width: #{$desktop-l-width}) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
@mixin retina {
|
||||
@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
|
||||
@content;
|
||||
}
|
||||
@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Colors
|
||||
// @import "./media-queries";
|
||||
@import "src/scss/media-queries";
|
||||
@import "scss/media-queries";
|
||||
|
||||
:root {
|
||||
color-scheme: light;
|
||||
@@ -21,6 +21,8 @@
|
||||
--background-40: rgba(255, 255, 255, 0.4);
|
||||
--background-0: rgba(255, 255, 255, 0);
|
||||
|
||||
--highlight-color: #01d277;
|
||||
|
||||
--background-nav-logo: #081c24;
|
||||
--color-green: #01d277;
|
||||
--color-green-90: rgba(1, 210, 119, 0.9);
|
||||
|
||||
@@ -6,7 +6,7 @@ import torrentModule from "./modules/torrentModule";
|
||||
import user from "./modules/user";
|
||||
import hamburger from "./modules/hamburger";
|
||||
|
||||
/* eslint-disable-next-line import/no-cycle */
|
||||
/* eslint-disable-next-line import-x/no-cycle */
|
||||
import popup from "./modules/popup";
|
||||
|
||||
const store = createStore({
|
||||
|
||||
15
src/utils.ts
15
src/utils.ts
@@ -14,9 +14,7 @@ export const parseJwt = (token: string) => {
|
||||
const jsonPayload = decodeURIComponent(
|
||||
atob(base64)
|
||||
.split("")
|
||||
.map(c => {
|
||||
return `%${`00${c.charCodeAt(0).toString(16)}`.slice(-2)}`;
|
||||
})
|
||||
.map(c => `%${`00${c.charCodeAt(0).toString(16)}`.slice(-2)}`)
|
||||
.join("")
|
||||
);
|
||||
|
||||
@@ -62,10 +60,10 @@ export function focusOnNextElement(elementEvent: KeyboardEvent): void {
|
||||
}
|
||||
}
|
||||
|
||||
export function humanMinutes(minutes) {
|
||||
export function humanMinutes(minutes: number[] | number) {
|
||||
if (minutes instanceof Array) {
|
||||
/* eslint-disable-next-line prefer-destructuring, no-param-reassign */
|
||||
minutes = minutes[0];
|
||||
/* eslint-disable-next-line no-param-reassign */
|
||||
[minutes] = minutes;
|
||||
}
|
||||
|
||||
const hours = Math.floor(minutes / 60);
|
||||
@@ -91,9 +89,8 @@ export function setUrlQueryParameter(parameter: string, value: string): void {
|
||||
const params = new URLSearchParams();
|
||||
params.append(parameter, value);
|
||||
|
||||
const url = `${window.location.protocol}//${window.location.hostname}${
|
||||
window.location.port ? `:${window.location.port}` : ""
|
||||
}${window.location.pathname}${params.toString().length ? `?${params}` : ""}`;
|
||||
const url = `${window.location.protocol}//${window.location.hostname}${window.location.port ? `:${window.location.port}` : ""
|
||||
}${window.location.pathname}${params.toString().length ? `?${params}` : ""}`;
|
||||
|
||||
window.history.pushState({}, "search", url);
|
||||
}
|
||||
|
||||
9
src/vite-env.d.ts
vendored
Normal file
9
src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_SEASONED_API: string;
|
||||
readonly VITE_ELASTIC_URL: string;
|
||||
readonly VITE_ELASTIC_API_KEY: string;
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
@@ -12,7 +12,8 @@
|
||||
"outDir": "lib",
|
||||
"baseUrl": "/",
|
||||
"paths": {
|
||||
"@": ["src"]
|
||||
"@/*": ["./src/*"],
|
||||
"scss/*": ["./src/scss/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
|
||||
14
vite.config.ts
Normal file
14
vite.config.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import path from "path";
|
||||
import { defineConfig } from "vite";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
scss: path.resolve(__dirname, "./src/scss")
|
||||
}
|
||||
},
|
||||
plugins: [vue()]
|
||||
});
|
||||
@@ -1,157 +0,0 @@
|
||||
const path = require("path");
|
||||
const webpack = require("webpack");
|
||||
const sass = require("sass");
|
||||
const HTMLWebpackPlugin = require("html-webpack-plugin");
|
||||
const { VueLoaderPlugin } = require("vue-loader");
|
||||
const TerserPlugin = require("terser-webpack-plugin");
|
||||
const dotenv = require("dotenv").config({ path: "./.env" });
|
||||
const dotenvExample = require("dotenv").config({ path: "./.env.example" });
|
||||
|
||||
const sourcePath = path.resolve(__dirname, "src");
|
||||
const indexFile = path.join(sourcePath, "index.html");
|
||||
const javascriptEntry = path.join(sourcePath, "main.ts");
|
||||
const publicPath = path.resolve(__dirname, "public");
|
||||
const isProd = process.env.NODE_ENV === "production";
|
||||
const variables = dotenv.parsed || dotenvExample.parsed;
|
||||
|
||||
// Merge inn all process.env values that match dotenv keys
|
||||
Object.keys(process.env).forEach(key => {
|
||||
if (key in variables) {
|
||||
variables[key] = process.env[key];
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
mode: process.env.NODE_ENV,
|
||||
context: publicPath,
|
||||
entry: javascriptEntry,
|
||||
output: {
|
||||
path: `${publicPath}/dist/`,
|
||||
publicPath: "/dist/",
|
||||
filename: "[name].[contenthash].js",
|
||||
clean: true
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
loader: "babel-loader",
|
||||
options: {
|
||||
presets: ["@babel/preset-env"]
|
||||
},
|
||||
exclude: /node_modules/
|
||||
},
|
||||
{
|
||||
test: /\.vue$/,
|
||||
use: ["vue-loader"]
|
||||
},
|
||||
{
|
||||
test: /\.ts$/,
|
||||
loader: "ts-loader",
|
||||
exclude: /node_modules/,
|
||||
options: {
|
||||
appendTsSuffixTo: [/\.vue$/]
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.scss$/,
|
||||
use: [
|
||||
"vue-style-loader",
|
||||
// isProd ? MiniCssExtractPlugin.loader : "vue-style-loader",
|
||||
"css-loader",
|
||||
"sass-loader"
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpg|gif|svg)$/,
|
||||
loader: "file-loader",
|
||||
options: {
|
||||
name: "[name].[ext]?[hash]"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new VueLoaderPlugin(),
|
||||
new HTMLWebpackPlugin({
|
||||
template: indexFile,
|
||||
filename: "index.html",
|
||||
minify: isProd
|
||||
}),
|
||||
new webpack.DefinePlugin({
|
||||
"process.env": JSON.stringify(variables)
|
||||
})
|
||||
],
|
||||
resolve: {
|
||||
extensions: [".js", ".ts", ".vue", ".json", ".scss"],
|
||||
alias: {
|
||||
vue: "@vue/runtime-dom",
|
||||
"@": path.resolve(__dirname, "src"),
|
||||
src: path.resolve(__dirname, "src"),
|
||||
assets: `${publicPath}/assets`,
|
||||
components: path.resolve(__dirname, "src/components")
|
||||
}
|
||||
},
|
||||
devtool: "source-map",
|
||||
performance: {
|
||||
hints: false
|
||||
},
|
||||
optimization: {
|
||||
splitChunks: {
|
||||
chunks: "all",
|
||||
maxInitialRequests: Infinity,
|
||||
minSize: 0,
|
||||
cacheGroups: {
|
||||
vendor: {
|
||||
test: /[\\/]node_modules[\\/]/,
|
||||
name(module) {
|
||||
// get the name. E.g. node_modules/packageName/not/this/part.js
|
||||
// or node_modules/packageName
|
||||
const packageName = module.context.match(
|
||||
/[\\/]node_modules[\\/](.*?)([\\/]|$)/
|
||||
)[1];
|
||||
|
||||
// npm package names are URL-safe, but some servers don't like @ symbols
|
||||
return `npm.${packageName.replace("@", "")}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
minimizer: [
|
||||
new TerserPlugin({
|
||||
parallel: true,
|
||||
terserOptions: {
|
||||
sourceMap: true
|
||||
// https://github.com/webpack-contrib/terser-webpack-plugin#terseroptions
|
||||
}
|
||||
})
|
||||
]
|
||||
},
|
||||
devServer: {
|
||||
static: publicPath,
|
||||
historyApiFallback: {
|
||||
index: "/dist/index.html"
|
||||
},
|
||||
compress: true,
|
||||
hot: true,
|
||||
port: 8080
|
||||
}
|
||||
};
|
||||
|
||||
if (isProd) {
|
||||
module.exports.mode = "production";
|
||||
module.exports.devtool = false;
|
||||
module.exports.performance.hints = "warning";
|
||||
}
|
||||
|
||||
// enable proxy for anything that hits /Api
|
||||
// View README or update src/config.ts:SEASONED_API_URL
|
||||
const { SEASONED_API } = process.env;
|
||||
if (SEASONED_API) {
|
||||
module.exports.devServer.proxy = {
|
||||
"/api": {
|
||||
target: SEASONED_API,
|
||||
changeOrigin: true
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user