Browse Source

Remove old webui folder

dev
David Ludwig 4 years ago
parent
commit
ae51e0684c
47 changed files with 0 additions and 4340 deletions
  1. +0
    -2
      services/autoplex-webui/.env.example
  2. +0
    -3
      services/autoplex-webui/README.md
  3. +0
    -13
      services/autoplex-webui/index.html
  4. +0
    -41
      services/autoplex-webui/package.json
  5. +0
    -6
      services/autoplex-webui/postcss.config.js
  6. BIN
      services/autoplex-webui/public/favicon.ico
  7. +0
    -27
      services/autoplex-webui/src/app/App.vue
  8. +0
    -1
      services/autoplex-webui/src/app/assets/plex_logo.svg
  9. +0
    -46
      services/autoplex-webui/src/app/components/AppModals.vue
  10. +0
    -100
      services/autoplex-webui/src/app/components/CheckBox.vue
  11. +0
    -51
      services/autoplex-webui/src/app/components/MovieList.vue
  12. +0
    -64
      services/autoplex-webui/src/app/components/MoviePoster.vue
  13. +0
    -97
      services/autoplex-webui/src/app/components/ProgressRing.vue
  14. +0
    -126
      services/autoplex-webui/src/app/components/SideNav.vue
  15. +0
    -113
      services/autoplex-webui/src/app/components/TextBox.vue
  16. +0
    -220
      services/autoplex-webui/src/app/components/modals/MovieModal.vue
  17. +0
    -22
      services/autoplex-webui/src/app/components/modals/index.ts
  18. +0
    -20
      services/autoplex-webui/src/app/index.ts
  19. +0
    -190
      services/autoplex-webui/src/app/routes/index.ts
  20. +0
    -5
      services/autoplex-webui/src/app/shims-vue.d.ts
  21. +0
    -9
      services/autoplex-webui/src/app/shims-vuex.d.ts
  22. +0
    -132
      services/autoplex-webui/src/app/store/actions.ts
  23. +0
    -33
      services/autoplex-webui/src/app/store/generics.ts
  24. +0
    -47
      services/autoplex-webui/src/app/store/getters.ts
  25. +0
    -36
      services/autoplex-webui/src/app/store/index.ts
  26. +0
    -92
      services/autoplex-webui/src/app/store/mutations.ts
  27. +0
    -6
      services/autoplex-webui/src/app/store/schema.ts
  28. +0
    -21
      services/autoplex-webui/src/app/store/state.ts
  29. +0
    -42
      services/autoplex-webui/src/app/styles/index.css
  30. +0
    -51
      services/autoplex-webui/src/app/util.ts
  31. +0
    -43
      services/autoplex-webui/src/app/views/Dashboard.vue
  32. +0
    -6
      services/autoplex-webui/src/app/views/Error404.vue
  33. +0
    -99
      services/autoplex-webui/src/app/views/LinkDiscord.vue
  34. +0
    -77
      services/autoplex-webui/src/app/views/Login.vue
  35. +0
    -156
      services/autoplex-webui/src/app/views/Register.vue
  36. +0
    -102
      services/autoplex-webui/src/app/views/Search.vue
  37. +0
    -3
      services/autoplex-webui/src/app/views/admin/Admin.vue
  38. +0
    -3
      services/autoplex-webui/src/app/views/admin/AdminDashboard.vue
  39. +0
    -60
      services/autoplex-webui/src/common/api_schema.ts
  40. +0
    -90
      services/autoplex-webui/src/common/validation.ts
  41. +0
    -27
      services/autoplex-webui/src/server/index.ts
  42. +0
    -30
      services/autoplex-webui/tailwind.config.js
  43. +0
    -76
      services/autoplex-webui/tsconfig.json
  44. +0
    -12
      services/autoplex-webui/tsconfig.server.json
  45. +0
    -26
      services/autoplex-webui/tsconfig.vite.json
  46. +0
    -14
      services/autoplex-webui/vite.config.ts
  47. +0
    -1900
      services/autoplex-webui/yarn.lock

+ 0
- 2
services/autoplex-webui/.env.example View File

@ -1,2 +0,0 @@
# The base URL the site is hosted on
BASE_URL = "https://autoplex.dlii.tech"

+ 0
- 3
services/autoplex-webui/README.md View File

@ -1,3 +0,0 @@
# Autoplex Web UI
The web UI for the Autoplex service

+ 0
- 13
services/autoplex-webui/index.html View File

@ -1,13 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Autoplex</title>
</head>
<body class="bg-blue-50">
<div id="app" class="contents"></div>
<script type="module" src="/src/app/index.ts"></script>
</body>
</html>

+ 0
- 41
services/autoplex-webui/package.json View File

@ -1,41 +0,0 @@
{
"name": "@autoplex-service/autoplex-webui",
"version": "0.0.0",
"keywords": [],
"author": "David Ludwig",
"license": "ISC",
"main": "./dist/server/index.js",
"scripts": {
"clean": "rimraf ./dist",
"build": "yarn run build:backend && yarn run build:frontend",
"build:backend": "tsc -p ./tsconfig.server.json",
"build:frontend": "vue-tsc --noEmit -p ./tsconfig.vite.json && vite build",
"start": "NODE_ENV=production node .",
"start:dev": "vite"
},
"dependencies": {
"@fortawesome/fontawesome-free": "^5.15.3",
"fastify": "^3.14.1",
"fastify-static": "^4.0.1",
"jwt-decode": "^3.1.2",
"validate.js": "^0.13.1",
"vue": "^3.0.5",
"vue-router": "^4.0.6",
"vuedraggable": "^4.0.1",
"vuex": "^4.0.0",
"websocket": "^1.0.33"
},
"devDependencies": {
"@types/node": "^15.0.1",
"@vitejs/plugin-vue": "^1.2.1",
"@vue/compiler-sfc": "^3.0.5",
"autoprefixer": "^10.2.5",
"postcss": "^8.2.9",
"rimraf": "^3.0.2",
"tailwindcss": "^2.1.1",
"ts-node": "^9.1.1",
"typescript": "^4.1.3",
"vite": "^2.1.5",
"vue-tsc": "^0.0.15"
}
}

+ 0
- 6
services/autoplex-webui/postcss.config.js View File

@ -1,6 +0,0 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {}
},
}

BIN
services/autoplex-webui/public/favicon.ico View File

Before After

+ 0
- 27
services/autoplex-webui/src/app/App.vue View File

@ -1,27 +0,0 @@
<template>
<div class="h-full flex flex-col lg:flex-row">
<side-nav v-if="!$route.meta['disableNavBar']"/>
<div class="h-full p-4 flex flex-col flex-grow overflow-y-auto">
<router-view/>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { Action, useStore } from "./store";
import AppModal from "./components/AppModals.vue";
import SideNav from "./components/SideNav.vue";
export default defineComponent({
name: "App",
components: {
AppModal,
SideNav
},
setup() {
const store = useStore();
store.dispatch(Action.AuthLoad, undefined);
}
});
</script>

+ 0
- 1
services/autoplex-webui/src/app/assets/plex_logo.svg View File

@ -1 +0,0 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 320.03 103.61"><defs><style>.cls-1{fill:#fff;}.cls-2{fill:url(#radial-gradient);}.cls-3{fill:#e5a00d;}</style><radialGradient id="radial-gradient" cx="258.33" cy="51.76" r="42.95" gradientUnits="userSpaceOnUse"><stop offset="0.17" stop-color="#f9be03"/><stop offset="0.51" stop-color="#e8a50b"/><stop offset="1" stop-color="#cc7c19"/></radialGradient></defs><title>plex-logo</title><polygon id="X" class="cls-1" points="320.03 -0.09 289.96 -0.09 259.88 51.76 289.96 103.61 320.01 103.61 289.96 51.79 320.03 -0.09"/><g id="chevron"><polygon class="cls-2" points="226.7 -0.09 256.78 -0.09 289.96 51.76 256.78 103.61 226.7 103.61 259.88 51.76 226.7 -0.09"/><polygon class="cls-3" points="226.7 -0.09 256.78 -0.09 289.96 51.76 256.78 103.61 226.7 103.61 259.88 51.76 226.7 -0.09"/></g><path id="E" class="cls-1" d="M216.32,103.61H156.49V-.09h59.83v18h-37.8V40.69H213.7v18H178.52V85.45h37.8Z"/><path id="L" class="cls-1" d="M82.07,103.61V-.09h22V85.45h42.07v18.16Z"/><path id="P" class="cls-1" d="M71.66,32.25Q71.66,49,61.2,57.87T31.44,66.73H22v36.88H0V-.09H33.14Q52-.09,61.83,8T71.66,32.25ZM22,48.71h7.24q10.15,0,15.18-4c3.37-2.66,5-6.56,5-11.67s-1.41-9-4.22-11.42S38,17.93,32,17.93H22Z"/></svg>

+ 0
- 46
services/autoplex-webui/src/app/components/AppModals.vue View File

@ -1,46 +0,0 @@
<template>
<div class="modal min-h-screen" v-if="visible">
<component :is="component"></component>
</div>
</template>
<script lang="ts">
import { Mutation } from "../store";
import { defineComponent } from "vue";
import { mapMutations, mapState } from "vuex";
import { modals } from "./modals";
export default defineComponent({
name: "AppModal",
components: modals,
computed: {
...mapState({
component: "modalName",
visible: "modalVisible"
})
},
methods: {
modalClicked() {
console.log("Modal clicked");
},
// ...mapMutations({
// hide: Mutation.ModalHide
// })
},
mounted() {
document.addEventListener("keydown", (event: KeyboardEvent) => {
if (event.key != "Escape") {
return;
}
// this.hide();
});
}
});
</script>
<style lang="postcss">
.modal {
@apply fixed inset-0 flex flex-col;
background: rgba(0, 0, 0, 0.5);
}
</style>

+ 0
- 100
services/autoplex-webui/src/app/components/CheckBox.vue View File

@ -1,100 +0,0 @@
<template>
<label class="checkbox-label relative flex items-center">
<input class="hidden" type="checkbox" v-model="isChecked" :disabled="disabled">
<span></span>
<span v-if="label" class="ml-2">{{label}}</span>
</label>
</template>
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
data() {
return {
isChecked: this.modelValue
}
},
props: {
disabled: {
type: Boolean,
default: false
},
label: {
type: String,
required: true
},
modelValue: {
type: Boolean,
default: false
}
},
watch: {
isChecked(newState) {
this.$emit("update:modelValue", newState);
this.$emit("onChange", newState);
}
}
});
</script>
<style>
.checkbox-label input[type="checkbox"]:focus-visible + span {
@apply ring ring-offset-2 ring-indigo-500;
}
.checkbox-label input[type="checkbox"] + span {
@apply inline-block w-4 h-4 bg-white border rounded-sm border-gray-300;
}
.checkbox-label input[type="checkbox"]:checked + span {
@apply bg-indigo-500 border-indigo-500;
}
.checkbox-label input[type="checkbox"] + span::before {
content: '';
position: absolute;
border-color: white;
border-width: 0;
/* transform: translateY(0.3rem) rotate(45deg); */
}
.checkbox-label input[type="checkbox"]:checked + span::before {
border-width: 0 0px 2px 2px;
width: .5rem;
height: .25rem;
transform: translateX(calc(50% - 1px)) translateY(calc(0.3rem - 1px)) rotate(-45deg);
}
@keyframes shrink-bounce{
0%{
transform: scale(1);
}
33%{
transform: scale(.85);
}
100%{
transform: scale(1);
}
}
@keyframes checkbox-check{
0%{
width: 0;
height: 0;
border-color: #212121;
transform: translate3d(0,0,0) rotate(45deg);
}
33%{
width: .2em;
height: 0;
transform: translate3d(0,0,0) rotate(45deg);
}
100%{
width: .2em;
height: .5em;
border-color: #212121;
transform: translate3d(0,-.5em,0) rotate(45deg);
}
}
</style>

+ 0
- 51
services/autoplex-webui/src/app/components/MovieList.vue View File

@ -1,51 +0,0 @@
<template>
<ul class="w-full grid gap-4 grid-cols-1 md:grid-cols-2 lg:grid-cols-4 xl:grid-cols-6 2xl:grid-cols-8 lg:gap-8">
<li class="flex flex-row bg-white rounded-lg overflow-hidden shadow-md lg:flex-col transform hover:scale-105 transition-transform cursor-pointer" v-for="(movie, index) in movies"
@click="$emit('onClickMovie', movie, index)">
<movie-poster class="w-3/12 lg:w-full flex-shrink-0" :src="movie.posterPath ?? undefined" size="w185"/>
<div class="relative box-border p-4 flex flex-col justify-center w-full lg:h-full text-md xs:text-lg sm:text-xl md:text-base xl:text-sm" style="box-shadow: 0px -10px 10px rgba(0, 0, 0, 0.10);">
<div class="flex-grow flex flex-col justify-center lg:justify-start">
<h3 class="font-medium">{{ movie.title }}</h3>
<span v-if="movie.releaseDate" class="opacity-50">{{ movie.releaseDate.slice(0, 4) }}</span>
</div>
<div v-if="movie.plexLink" title="Added to Plex" class="flex">
<div class="inline-block bg-ui-plexGray rounded-full w-1/6 lg:w-1/3">
<img src="../assets/plex_logo.svg" alt="Added to Plex" style="margin: 15% 25%;"/>
</div>
</div>
<!-- <div class="flex flex-row items-end space-x-2">
<div class="w-full rounded-full bg-gray-200 overflow-hidden flex-grow-0">
<div class="w-1/2 bg-red-500 h-1"></div>
</div>
<div class="relative h-1 flex justify-center items-center text-sm">
<span class="h-0 overflow-hidden">100%</span>
<span class="absolute left-0 text-center w-full opacity-75">{{ 50 }}%</span>
</div>
</div> -->
</div>
</li>
</ul>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import { IApiMovie } from "../../common/api_schema";
import MoviePoster from "./MoviePoster.vue";
export default defineComponent({
components: {
MoviePoster
},
methods: {
onModalClosed() {
let parentPath = this.$route.path.split('/').slice(0, -1).join('/');
this.$router.push({ path: parentPath, query: this.$route.query });
}
},
props: {
movies: {
default: <IApiMovie[]>[]
}
}
});
</script>

+ 0
- 64
services/autoplex-webui/src/app/components/MoviePoster.vue View File

@ -1,64 +0,0 @@
<template>
<div class="poster">
<div class="poster-padding"></div>
<img v-if="src" class="poster-content w-full h-full object-cover" loading="lazy" ref="poster"
:src="`/api/tmdb/image/${size}${src}`" @load="onPosterLoad">
<div v-else class="poster-content bg-gray-300 text-gray-500">
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="image" class="svg-inline--fa fa-image fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M464 448H48c-26.51 0-48-21.49-48-48V112c0-26.51 21.49-48 48-48h416c26.51 0 48 21.49 48 48v288c0 26.51-21.49 48-48 48zM112 120c-30.928 0-56 25.072-56 56s25.072 56 56 56 56-25.072 56-56-25.072-56-56-56zM64 384h384V272l-87.515-87.515c-4.686-4.686-12.284-4.686-16.971 0L208 320l-55.515-55.515c-4.686-4.686-12.284-4.686-16.971 0L64 336v48z"></path></svg>
</div>
</div>
</template>
<script lang="ts">
import ProgressRing from './ProgressRing.vue'
import { defineComponent } from "vue";
export default defineComponent({
components: {
ProgressRing,
},
data() {
return {
isLoaded: false
}
},
methods: {
onPosterLoad() {
this.isLoaded = true;
this.$emit("onLoad", this.$refs["poster"]);
}
},
props: {
size: {
type: String,
default: "w342"
},
src: String
}
});
</script>
<style lang="css">
.poster {
@apply relative overflow-hidden;
}
.poster > .poster-padding {
padding-top: 143.885%;
}
.poster > .poster-decal {
@apply absolute z-10 top-0 w-full shadow-sm;
padding: 4%;
background: rgb(31, 35, 38);
transform: rotate(-45deg) translateX(-28%) translateY(-100%);
}
.poster > .poster-decal > img {
@apply w-1/3 mx-auto;
}
.poster > .poster-content {
@apply absolute inset-0 flex justify-center items-center z-0 shadow-sm;
}
.poster > .poster-content > svg {
width: 35%;
}
</style>

+ 0
- 97
services/autoplex-webui/src/app/components/ProgressRing.vue View File

@ -1,97 +0,0 @@
<template>
<div class="box-border relative text-white radial-progress">
<svg class="transform -rotate-90" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid" style="height: 100%; width: 100%">
<circle
class="radial-progress-ring__body"
:r="50"
:cx="50"
:cy="50"/>
<circle
class="radial-progress-ring__progress"
:stroke-width="ringWidth"
:r="50 - ringWidth/2"
:cx="50"
:cy="50"
ref="ring"
/>
<text text-anchor="middle" alignment-baseline="central" x="50" y="50" :font-size="fontSize" transform="rotate(90) translate(0,-100)">{{ progressFormatted }}</text>
</svg>
<!-- <div class="antialiased flex absolute inset-0 items-center justify-center text-xs font-light rounded-full text-white">{{ progressFormatted }}%</div> -->
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
export enum RadialProgressState {
Downloading,
Paused,
Halted,
Error
}
export default defineComponent({
computed: {
circumference(): number {
return 2*Math.PI*(50 - this.ringWidth/2);
},
progressFormatted(): number {
return Math.floor(100*(<number>this.progress));
}
},
methods: {
updateProgressBar() {
(<HTMLElement>this.$refs.ring).style.strokeDasharray = `${this.circumference} ${this.circumference}`;
(<HTMLElement>this.$refs.ring).style.strokeDashoffset = `${this.circumference - this.circumference*this.progress}`;
}
},
mounted() {
this.updateProgressBar();
},
props: {
fontSize: {
type: Number,
required: true
},
progress: {
type: Number,
required: true
},
ringWidth: {
type: Number,
default: 1
}
},
watch: {
progress() {
this.updateProgressBar();
}
}
});
</script>
<style lang="css">
.radial-progress text {
fill: white;
}
.radial-progress-ring__body {
fill: #1f2937;
}
.radial-progress-ring__progress {
fill: transparent;
stroke: #34D399;
transition: stroke-dashoffset 0.25s;
}
.bg-modal {
background: rgba(0, 0, 0, 0.35);
opacity: 0.0;
transition: opacity 0.25s;
}
.show-state {
opacity: 1.0;
}
.radial-progress:hover .bg-modal {
opacity: 1.0;
}
</style>

+ 0
- 126
services/autoplex-webui/src/app/components/SideNav.vue View File

@ -1,126 +0,0 @@
<template>
<transition name="fade">
<div v-if="isOpen" class="modal md:hidden p-8 overflow-auto items-center z-30" @click.self="close"></div>
</transition>
<div class="contents" ref="root">
<div class="lg:hidden bg-white shadow-md items-center flex z-40">
<button class="nav-toggle-button" :class="{ 'active': isOpen }"
@click="isOpen = !isOpen">
<i class="fas fa-bars"></i>
</button>
</div>
<nav class="fixed lg:relative inset-y-0 w-72 lg:w-64 xl:w-72 flex flex-col flex-shrink-0 bg-white shadow-lg z-50" :class="{'active': isOpen}">
<div class="brand">AUTOPLEX</div>
<div class="mt-4 text-center">
<div class="user"><i class="fas opacity-40" :class="{'fa-user': !isAdmin, 'fa-user-tie': isAdmin}"></i></div>
<div class="font-black text-lg opacity-80">{{ userName }}</div>
<div class="text-xs opacity-60">{{ isAdmin ? "Admin" : "Plex Member" }}</div>
</div>
<ul class="mt-4 text-gray-800 links">
<li v-if="isAdmin">
<router-link :to="{ name: 'Admin' }">
<span><i class="fas fa-cog"></i></span>
<div>Admin</div>
</router-link>
</li>
<li>
<router-link :to="{ name: 'Dashboard' }">
<span><i class="fas fa-chart-pie"></i></span>
<div>Dashboard</div>
</router-link>
</li>
<li>
<router-link :to="{ name: 'Search' }">
<span><i class="fas fa-search"></i></span>
<div>Search</div>
</router-link>
</li>
</ul>
<div class="mt-auto p-4">
<router-link class="block p-2 text-center rounded-full shadow-md text-white bg-indigo-500"
:to="{ name: 'Logout' }">
Logout
</router-link>
</div>
</nav>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import { mapGetters } from "vuex";
export default defineComponent({
computed: {
...mapGetters([
"userName",
"isAdmin"
])
},
data() {
return {
isOpen: false
}
},
mounted() {
document.addEventListener("click", (event: any) => {
if (this.isOpen) {
if (-1 == event.path.indexOf(this.$refs["root"])) {
this.isOpen = false;
}
}
});
},
watch: {
$route(to, from) {
if (this.isOpen) {
this.isOpen = false;
}
}
}
});
</script>
<style lang="postcss">
.nav-toggle-button {
@apply w-14 h-14 flex items-center justify-center text-2xl focus:ring-0 focus:outline-none ring-indigo-500;
transition: margin 0.15s cubic-bezier(0.4, 0.0, 1, 1);
}
.nav-toggle-button.active {
@apply ml-72;
transition: margin 0.25s cubic-bezier(0.0, 0.0, 0.2, 1);
}
nav {
transform: translateX(-100%);
transition: transform 0.15s cubic-bezier(0.4, 0.0, 1, 1);
}
@media all and (min-width: 1024px) {
nav {
transform: none !important;
}
}
nav.active {
transform: none;
transition: transform 0.25s cubic-bezier(0.0, 0.0, 0.2, 1);
}
nav > .brand {
@apply mt-4 h-12 flex justify-center items-center text-4xl font-thin;
}
nav .user {
@apply w-20 h-20 mx-auto rounded-full flex justify-center items-end bg-gray-200 text-6xl overflow-hidden shadow-sm;
}
nav .user > i {
line-height: 3.7rem;
}
nav > .links a {
@apply py-3 px-8 flex space-x-4 hover:bg-gray-100;
}
nav > .links .router-link-active {
@apply bg-indigo-500 text-white shadow-md hover:bg-indigo-500 cursor-default;
}
nav > .links span {
@apply w-5
}
</style>

+ 0
- 113
services/autoplex-webui/src/app/components/TextBox.vue View File

@ -1,113 +0,0 @@
<template>
<div>
<label class="block text-sm mb-1 text-gray-700" :class="{ 'opacity-50': disabled, 'text-red-600': error }">
{{ label }}
</label>
<div class="block relative">
<input :type="type" :placeholder="placeholder" v-model="value" ref="textbox" :disabled="disabled"
class="w-full outline-none p-2 rounded-lg bg-gray-100 border border-gray-100 text-gray-800 placeholder-gray-400 disabled:cursor-not-allowed disabled:opacity-50 disabled:bg-gray-50 disabled:border-gray-400"
:class="{ 'pr-10': (isValid || error), 'border-red-600 text-red-600': error }"
@blur="onBlur" @change="onChange" @input="setErrorMessage('')">
<span class="w-10 absolute flex items-center top-0 bottom-0 right-0 justify-center" :class="{ 'hidden': !error && !isValid }">
<i v-if="error" class="fas fa-exclamation-circle align-middle text-red-600"></i>
<i v-else-if="isValid" class="fas fa-check-circle align-middle text-green-500"></i>
</span>
</div>
<div class="w-full error-message text-xs text-red-600 text-center" ref="error">{{ error }}</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import { validateValue } from "../util";
export default defineComponent({
emits: ["onBlur", "onChange", "update:modelValue", "update:errorValue"],
data() {
return {
error: "",
isValid: <boolean|undefined> this.valid,
value: this.modelValue
}
},
methods: {
/**
* Invoked when the input has lost focus
*/
onBlur(event: Event) {
this.$emit("onBlur", event);
this.validate();
},
/**
* Invoked when the value of the input has changed
*/
onChange(event: Event) {
this.$emit("update:modelValue", this.value);
this.$emit("onChange", event);
},
/**
* Set the displayed error message
*/
setErrorMessage(value: string) {
this.error = value;
this.$emit("update:errorValue", value);
if (value) {
this.isValid = false;
}
},
/**
* Validate the current input
*/
async validate() {
this.isValid = false;
if (this.validator !== undefined) {
if (typeof this.validator === "function") {
this.error = (await this.validator(this.value)) ?? "";
} else {
this.error = validateValue(this.value, this.validator);
}
if (!this.error) {
this.isValid = true;
}
}
}
},
mounted() {
if (this.value && this.validator) {
this.validate();
}
},
props: {
disabled: {
type: Boolean,
default: false
},
errorValue: {
type: String,
default: ""
},
label: String,
modelValue: {
type: String,
default: ""
},
placeholder: {
type: String,
default: ""
},
required: [Boolean, String],
valid: {
type: Boolean,
default: undefined
},
type: {
type: String,
default: "text"
},
validator: [Function, Object]
}
});
</script>

+ 0
- 220
services/autoplex-webui/src/app/components/modals/MovieModal.vue View File

@ -1,220 +0,0 @@
<template>
<transition appear name="fade">
<div v-if="tmdbId !== undefined" class="modal p-8 overflow-auto items-center z-50" @click.self="close">
<transition name="slide" appear
@before-enter="lockScroll(true)" @enter-cancelled="lockScroll(false)"
@leave="lockScroll(false)" @leave-cancelled="lockScroll(true)">
<div v-if="movie !== undefined" class="relative w-full max-w-6xl bg-white md:bg-none overflow-hidden rounded-xl flex-col flex-shrink-0 shadow-xl">
<div class="bg-center bg-cover bg-no-repeat" :class="{ 'text-white': isDark }"
:style="movie.backdropPath ? `background: url('/api/tmdb/image/w1280${movie.backdropPath}')` : ''">
<div class="movie-modal-content z-10" :style="backdropOverlayStyle">
<div class="flex p-4 items-center">
<movie-poster v-if="movie.posterPath" :src="movie.posterPath"
class="w-3/12 flex-shrink-0 rounded-xl shadow-md md:shadow-xl" @onLoad="onPosterLoad"/>
<div class="p-4 flex flex-col justify-center">
<h2 class="text-lg md:text-4xl">
<span class="font-bold">{{ movie.title }}</span>
<span class="opacity-70" v-if="releaseYear"> ({{ releaseYear }})</span>
</h2>
<div class="flex font-light dot-separated">
<span v-if="releaseDate">{{ releaseDate }}</span>
<span v-if="runtime">{{ runtime }}</span>
</div>
<div v-if="movie?.overview" class="mt-4 hidden md:block">
<p class="">{{movie?.overview}}</p>
</div>
<div class="mt-4 hidden md:block">
<a class="inline-block py-2 px-6 rounded-full shadow-md hover:shadow-lg focus:outline-none ring-0 transition-transform transform hover:scale-105" :class="{ 'bg-white text-black': isDark, 'bg-black text-white': !isDark }"
v-if="movie.plexLink !== null" :href="movie.plexLink" target="_blank">Watch Now</a>
<button class="inline-block py-2 px-4 rounded-full shadow-md hover:shadow-lg disabled:opacity-50 disabled:cursor-default focus:outline-none ring-0 transition-transform transform hover:scale-105" :class="{ 'bg-white text-black': isDark, 'bg-black text-white': !isDark }"
v-else-if="movie.ticketId !== null">View Request Status</button>
<button v-else class="inline-block py-2 w-36 rounded-full shadow-md hover:shadow-lg disabled:opacity-50 disabled:cursor-default focus:outline-none ring-0 transition-transform transform hover:scale-105" :class="{ 'bg-white text-black': isDark, 'bg-black text-white': !isDark }"
:disabled="isRequesting" @click="request">{{isRequesting ? "Requesting..." : "Request"}}</button>
</div>
</div>
</div>
</div>
</div>
<div class="p-4 space-y-4 md:hidden z-20" style="box-shadow: 0px -10px 10px rgba(0, 0, 0, 0.10);" >
<div v-if="movie?.overview">
<h3 class="font-bold mb-2">Overview</h3>
<p>{{ movie.overview }}</p>
</div>
<div class="text-center">
<a class="inline-block py-2 px-8 rounded-full shadow-sm bg-red-500 hover:bg-black transition-colors text-white focus:outline-none ring-0"
v-if="movie.plexLink !== null" :href="movie.plexLink">Watch Now</a>
<button class="inline-block py-2 px-4 rounded-full bg-red-500 text-white disabled:opacity-50"
v-else-if="movie.ticketId !== null">View Request Status</button>
<button v-else class="inline-block py-2 px-8 rounded-full shadow-sm bg-red-500 hover:bg-black transition-colors text-white disabled:opacity-50 disabled:cursor-default focus:outline-none ring-0 disabled:bg-black"
:disabled="isRequesting" @click="request">{{isRequesting ? "Requesting..." : "Request"}}</button>
</div>
</div>
</div>
</transition>
</div>
</transition>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import { IApiDataResponse, IApiMovieDetails } from "@common/api_schema";
import { authFetch } from "../../routes";
import { getAverageRgb } from "../../util";
import { useStore, Mutation, Action } from "../../store";
import MoviePoster from "../MoviePoster.vue";
export default defineComponent({
components: {
MoviePoster
},
computed: {
backdropOverlayStyle(): string {
let { r, g, b } = this.rgb;
return `background-image: linear-gradient(to right, rgba(${r}, ${g}, ${b}, 1.0), rgba(${r}, ${g}, ${b}, 0.84)`;
},
isDark(): boolean {
let { r, g, b } = this.rgb;
return (r*0.299 + g*0.587 + b*0.114) <= 186;
// r /= 255;
// g /= 255;
// b /= 255;
// r = r <= 0.03928 ? r/12.92 : Math.pow((r+0.055)/1.055, 2.4);
// g = g <= 0.03928 ? r/12.92 : Math.pow((r+0.055)/1.055, 2.4);
// b = b <= 0.03928 ? r/12.92 : Math.pow((r+0.055)/1.055, 2.4);
// let L = 0.2126*r + 0.7152*g + 0.0722*b;
// return L <= 0.179;
},
releaseYear(): string {
if (!this.movie || !this.movie.releaseDate) {
return "";
}
return this.movie.releaseDate.slice(0, 4);
},
releaseDate(): string {
if (!this.movie || !this.movie.releaseDate) {
return "";
}
let [year, month, day] = this.movie.releaseDate.split('-');
return `${month}/${day}/${year}`;
},
runtime(): string {
if (!this.movie || !this.movie.runtime) {
return "";
}
let hours = Math.floor(this.movie.runtime / 60);
let minutes = Math.floor(this.movie.runtime % 60);
return (hours > 0 ? `${hours}h ` : "") + `${minutes}m`;
}
},
data() {
return {
rgb: {r: 0, g: 0, b: 0},
movie: <IApiMovieDetails|undefined>undefined,
isPosterLoaded: false,
isRequesting: false
}
},
methods: {
lockScroll(lock: boolean) {
useStore().commit(Mutation.LockScroll, lock);
},
close() {
this.movie = undefined;
this.$emit("onClose");
},
onKeyPress(event: KeyboardEvent) {
if (event.key != "Escape") {
return;
}
this.close();
},
onPosterLoad(img: HTMLImageElement) {
this.isPosterLoaded = true;
this.computeBackdropColor(img);
},
computeBackdropColor(img: HTMLImageElement) {
// This is being weird and this is the only way I can fix it...
let rgb: {[k: string]: number} = {r: 0, g: 0, b: 0};
try {
rgb = getAverageRgb(img);
} catch(e) {
return { r: 0, g: 0, b: 0 };
}
// Adjust contrast
let c = 60;
let f = 259*(c + 255) / (255*(259 - c));
for (let k in rgb) {
rgb[k] = Math.min(Math.max(f*(rgb[k] - 128) + 128, 0), 255);
}
// Adjust brightness
for (let k in rgb) {
rgb[k] *= 0.90;
}
this.rgb = <any>rgb;
},
async fetchMovieDetails() {
if (this.tmdbId === undefined) {
return;
}
let response = await (authFetch(`/api/movie/details/${this.tmdbId}`));
if (response.status != 200) {
this.close();
return;
}
let movie = <IApiDataResponse<IApiMovieDetails>> await response.json();
this.movie = movie.data;
},
async request() {
if (this.isRequesting || this.movie == null || this.movie.tmdbId == null) {
return;
}
this.isRequesting = true;
let response = await this.$store.dispatch(Action.RequestMovie, this.movie.tmdbId);
this.isRequesting = false;
if (response.status == "Forbidden") {
console.log("Failed to add movie: quota has been met");
return;
}
console.log(response);
this.movie.ticketId = response.data.ticketId;
}
},
mounted() {
document.addEventListener("keydown", this.onKeyPress);
this.fetchMovieDetails();
},
beforeUnmount() {
document.removeEventListener("keydown", this.onKeyPress);
},
props: {
tmdbId: {
type: [Number, String],
required: false
}
},
watch: {
tmdbId(newId: number|string|undefined, oldId: number|string|undefined) {
this.fetchMovieDetails();
}
}
});
</script>
<style lang="postcss">
.dot-separated > *:not(:first-child)::before {
@apply px-2;
content: '\2022'
}
.modal button {
@apply ring-0;
}
.modal button:focus-visible {
@apply ring-2 ring-indigo-500 ring-offset-2;
}
</style>

+ 0
- 22
services/autoplex-webui/src/app/components/modals/index.ts View File

@ -1,22 +0,0 @@
/**
* Modal names to export
*/
const enum Modal {
}
/**
* Modal export type
*/
type ModalTypes = { [K in keyof typeof Modal]: any }
/**
* Modals to export
*/
export const modals: ModalTypes = {
}
/**
* Export the list of modal names
*/
export default Modal;

+ 0
- 20
services/autoplex-webui/src/app/index.ts View File

@ -1,20 +0,0 @@
import { createApp } from 'vue'
import router from "./routes";
import store from "./store";
import App from './App.vue'
import "./styles/index.css";
let app = createApp(App);
app.use(router);
app.use(store);
app.mount("#app");
/**
* Dropdown menus
*/
document.addEventListener("click", (event) => {
let dropdownMenus = document.querySelectorAll(".dropdown.active").forEach((elem) => {
elem.classList.remove("active");
});
});

+ 0
- 190
services/autoplex-webui/src/app/routes/index.ts View File

@ -1,190 +0,0 @@
import { IApiDataResponse } from "@common/api_schema";
import { createRouter, createWebHistory, NavigationGuardNext, RouteLocationNormalized, RouteRecordRaw } from "vue-router";
import store, { Action } from "../store";
/**
* Check if the user is a guest; redirect otherwise
*/
function requiresGuest(to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext) {
if (store.getters.isAuthenticated) {
console.log("Checking guest...");
next({ name: "Home" });
return;
}
next();
}
/**
* Check if the user is authenticated; redirect otherwise
*/
function requiresAuth(to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext) {
if (!store.getters.isAuthenticated) {
next({ name: "Login" });
return;
}
next();
}
/**
* Check if the user is an authenticated admin, redirect otherwise
*/
function requiresAdmin(to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext) {
if (store.getters.isAuthenticated && !store.getters.isAdmin) {
next({ name: "Home" });
return;
}
next();
}
/**
* Fetch request providing authentication and logout upon unauthorized requests
*/
export async function authFetch(path: string, options: RequestInit = {}): Promise<Response> {
// Ensure the user is logged in
let token = store.getters.token;
if (token == null) {
router.push({ name: "Login" });
throw Error("Unauthorized");
}
// Perform the request
options.credentials = "include";
options.headers = Object.assign(options.headers ?? {}, {
"Authorization": `Bearer ${token}`
});
let response = await fetch(path, options);
// If an unauthorized gets thrown, forget the user and redirect to login
if (response.status === 401) {
store.dispatch(Action.AuthForget, undefined);
router.push({ name: "Login" });
throw Error("Unauthorized");
}
return response;
}
/**
* Perform an authorized API request returning a JSON object
*/
export async function authFetchApi<T>(path: string, options: RequestInit = {}) {
let response = await authFetch(path, options);
return <Promise<IApiDataResponse<T>>>response.json();
}
const routes: RouteRecordRaw[] = [
{
path: "/",
name: "Home",
redirect: to => ({ name: "Dashboard" })
},
{
path: "/dashboard",
name: "Dashboard",
component: () => import("../views/Dashboard.vue"),
beforeEnter: requiresAuth
},
{
path: "/search",
name: "Search",
component: () => import("../views/Search.vue"),
beforeEnter: requiresAuth,
children: [
{
path: ":tmdbId",
name: "Lookup",
component: () => import("../components/modals/MovieModal.vue"),
props: true
}
]
},
{
path: "/admin",
name: "Admin",
redirect: to => ({ name: "AdminDashboard" }),
component: () => import("../views/admin/Admin.vue"),
beforeEnter: requiresAdmin,
children: [
{
path: "dashboard",
name: "AdminDashboard",
component: () => import("../views/admin/AdminDashboard.vue"),
props: true
}
]
},
// Auth Routes ---------------------------------------------------------------------------------
{
path: "/login",
name: "Login",
component: () => import("../views/Login.vue"),
beforeEnter: requiresGuest,
meta: {
disableNavBar: true
}
},
{
path: "/register",
name: "Register",
component: () => import("../views/Register.vue"),
beforeEnter: requiresGuest,
meta: {
disableNavBar: true
}
},
{
path: "/logout",
name: "Logout",
component: {
beforeRouteEnter(to, from, next) {
store.dispatch(Action.AuthForget, undefined);
next({ name: "Login" });
}
}
},
{
path: "/link/discord/:token",
name: "LinkDiscord",
component: () => import("../views/LinkDiscord.vue"),
meta: {
disableNavBar: true
}
},
{
path: "/:pathMatch(.*)*",
component: () => import("../views/Error404.vue")
}
];
const router = createRouter({
history: createWebHistory(),
routes,
});
/**
* Guard authentication and guest routes
*/
router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.requiresAuth)) {
if (localStorage.getItem("jwt") == null) {
next({ name: "Login" });
return;
}
let user = JSON.parse(<any>localStorage.getItem("user"));
if (to.matched.some(record => record.meta.requiresAdmin)) {
if (!user.isAdmin) {
next({ name: "Home" });
return;
}
}
} else if (to.matched.some(record => record.meta.guest)) {
if (localStorage.getItem("jwt") != null) {
next({ name: "Home" });
return;
}
}
next();
});
export default router;

+ 0
- 5
services/autoplex-webui/src/app/shims-vue.d.ts View File

@ -1,5 +0,0 @@
declare module '*.vue' {
import { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}

+ 0
- 9
services/autoplex-webui/src/app/shims-vuex.d.ts View File

@ -1,9 +0,0 @@
import { ComponentCustomProperties } from 'vue'
import type { Store } from "./store";
declare module '@vue/runtime-core' {
// provide typings for `this.$store`
interface ComponentCustomProperties {
$store: Store
}
}

+ 0
- 132
services/autoplex-webui/src/app/store/actions.ts View File

@ -1,132 +0,0 @@
import { ActionTree } from "vuex";
import { Actions } from "./generics";
import { IState } from "./state";
import { Mutation, MutationsTypes } from "./mutations";
import router, { authFetch, authFetchApi } from "../routes";
import { GettersTypes } from "./getters";
import { IApiDataResponse } from "@common/api_schema";
// Payload types
type IAuthenticatePayload = { email: string, password: string, remember: boolean };
type IAuthenticateResult = { email?: string[], password?: string[] } | null;
type IAuthFetchPayload = { path: string, options?: RequestInit };
/**
* The available actions te perform
*/
export enum Action {
// Authentication
AuthAuthenticate = "AUTH_AUTHENTICATE",
AuthForget = "AUHT_FORGET",
AuthLoad = "AUTH_LOAD",
// RESTful
AuthFetch = "AUTH_FETCH",
RequestMovie = "REQUEST_MOVIE"
}
/**
* The action function signatures
*/
export type ActionsTypes = {
// Authentication
[Action.AuthAuthenticate]: (payload: IAuthenticatePayload) => Promise<IAuthenticateResult>,
[Action.AuthForget] : () => void,
[Action.AuthLoad] : () => boolean,
// RESTful
[Action.AuthFetch] : (payload: IAuthFetchPayload) => Promise<Response>,
[Action.RequestMovie]: (tmdbId: number | string) => Promise<IApiDataResponse<{ ticketId: number }>>
}
/**
* The action function implementations
*/
export const actions: Actions<IState, GettersTypes, MutationsTypes, ActionsTypes> & ActionTree<IState, IState> = {
// Authentication ------------------------------------------------------------------------------
/**
* Authenticate the credentials of a user and log them in
*/
[Action.AuthAuthenticate]({commit, dispatch}, {email, password, remember = false}) {
return new Promise((resolve, reject) => {
fetch(`/auth/login?use_cookies=${navigator.cookieEnabled}`, {
method: "post",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({email, password})
})
.then(async response => {
let body = await response.json();
if (response.status !== 200) {
if (response.status === 401) {
body.errors = { "email": [ "Email or password is incorrect" ] };
}
resolve(body.errors || {});
return;
}
commit(Mutation.UserLoad, body.token);
commit(Mutation.UserStore, remember);
resolve(null);
})
.catch(e => {
console.error("Error occurred during submission:", e);
reject(e);
});
});
},
/**
* Forget the login token and log the user out
*/
[Action.AuthForget]({commit}) {
commit(Mutation.UserForget, undefined);
},
/**
* Load the user from local storage
*/
[Action.AuthLoad]({getters, commit, dispatch}) {
let token = getters.storedToken;
if (!token) {
return false;
}
try {
commit(Mutation.UserLoad, token);
} catch(e) {
dispatch(Action.AuthForget, undefined);
return false;
}
return true;
},
// RESTful -------------------------------------------------------------------------------------
/**
* Fetch request providing authentication and logout upon unauthorized requests
*/
async [Action.AuthFetch]({commit, state}, {path, options = {}}) {
if (state.user == null) {
router.push({ name: "Login" });
throw Error("Unauthorized");
}
options.credentials = "include";
options.headers = Object.assign(options.headers ?? {}, {
"Authorization": `Bearer ${state.user.token}`
});
let response = await fetch(path, options);
if (response.status === 401) {
commit(Mutation.UserForget, undefined);
router.push({ name: "Login" });
throw Error("Unauthorized");
}
return response;
},
/**
* Request a movie
*/
async [Action.RequestMovie](_, tmdbId) {
return await authFetchApi(`/api/movie/request/create/tmdb/${tmdbId}`);
}
};

+ 0
- 33
services/autoplex-webui/src/app/store/generics.ts View File

@ -1,33 +0,0 @@
import { ActionContext, CommitOptions, DispatchOptions, GetterTree, MutationTree, Store as VuexStore } from "vuex";
type AugmentedActionContext<S, G extends GetterTree<S, S>, M extends MutationTree<S>, A extends ActionsType> = Omit<ActionContext<S, S>, "commit" | "dispatch" | "getters"> & {
commit<K extends keyof M>(key: K, payload: Parameters<M[K]>[1]): ReturnType<M[K]>,
dispatch<K extends keyof A, P extends Parameters<A[K]>[0]>(
key: K,
payload: P,
options?: DispatchOptions
): ReturnType<A[K]>,
getters: { [K in keyof G]: ReturnType<G[K]> }
};
type ActionsType = {
[key: string]: (payload?: any) => any
};
export type Actions<S, G extends GetterTree<S, S>, M extends MutationTree<S>, A extends ActionsType> = {
[K in keyof A]: (context: AugmentedActionContext<S, G, M, A>, payload: Parameters<A[K]>[0]) => ReturnType<A[K]>
}
export type GenericStore<S, G extends GetterTree<S, S>, M extends MutationTree<S>, A extends ActionsType> = Omit<VuexStore<S>, "commit" | "getters" | "dispatch"> & {
commit<K extends keyof M, P extends Parameters<M[K]>[1]>(
key: K,
payload: P,
options?: CommitOptions
): ReturnType<M[K]>,
getters: { [K in keyof G]: ReturnType<G[K]> },
dispatch<K extends keyof A, P extends Parameters<A[K]>[0]>(
key: K,
payload: P,
options?: DispatchOptions
): ReturnType<A[K]>
};

+ 0
- 47
services/autoplex-webui/src/app/store/getters.ts View File

@ -1,47 +0,0 @@
import { GetterTree } from "vuex";
import { IState } from "./state";
export type GettersTypes = {
isAdmin (state: IState): boolean,
isAuthenticated(state: IState): boolean,
storedToken () : string | null,
token (state: IState): string | null,
userName (state: IState): string | null
}
export const getters: GettersTypes & GetterTree<IState, IState> = {
/**
* Determine if the current user is authenticated
*/
isAuthenticated(state: IState) {
return state.user != null;
},
/**
* Determine if the user is an admin
*/
isAdmin(state: IState) {
return state.user?.isAdmin ?? false;
},
/**
* Retrieve the stored token
*/
storedToken() {
return sessionStorage.getItem("jwt") ?? localStorage.getItem("jwt");
},
/**
* Get the current token
*/
token(state: IState) {
return state.user?.token ?? null;
},
/**
* Get the user's name (assumes authenticated)
*/
userName(state: IState) {
return state.user?.name.split(" ")[0] ?? null;
}
};

+ 0
- 36
services/autoplex-webui/src/app/store/index.ts View File

@ -1,36 +0,0 @@
import { createLogger, createStore } from "vuex";
import { GenericStore } from "./generics";
import { getters, GettersTypes } from "./getters";
// Import store information
import { state, IState } from "./state";
import { mutations, MutationsTypes } from "./mutations";
import { actions, ActionsTypes } from "./actions";
// Export store keys
export { Mutation } from "./mutations";
export { Action } from "./actions";
/**
* The the store type
*/
export type Store = GenericStore<IState, GettersTypes, MutationsTypes, ActionsTypes>;
/**
* Create the store instance
*/
const store = createStore({
state, getters, mutations, actions, plugins: [createLogger()]
}) as Store;
/**
* Use the store
*/
export function useStore() {
return store;
}
/**
* Export the store instance by default
*/
export default store;

+ 0
- 92
services/autoplex-webui/src/app/store/mutations.ts View File

@ -1,92 +0,0 @@
import Modal from "@app/components/modals";
import jwtDecode from "jwt-decode";
import { MutationTree } from "vuex";
import { IState } from "./state";
/**
* All available mutation types
*/
export enum Mutation {
LockScroll = "LOCK_SCROLL",
UserForget = "USER_FORGET",
UserLoad = "USER_LOAD",
UserStore = "USER_STORE",
}
/**
* Type declarations for each mutation
*/
export type MutationsTypes<S = IState> = {
[Mutation.UserForget]: (state: S) => void,
[Mutation.UserLoad] : (state: S, token: string) => boolean,
[Mutation.UserStore] : (state: S, remember: boolean) => void,
[Mutation.LockScroll]: (state: S, lock: boolean) => void,
}
/**
* The auth mutations
*/
export const mutations: MutationsTypes & MutationTree<IState> = {
[Mutation.LockScroll](state: IState, lock: boolean) {
if (lock) {
// document.body.style.position = "fixed";
// document.body.style.top = `-${window.scrollY}px`;
document.body.style.paddingRight = `${window.innerWidth - document.body.clientWidth}px`;
document.body.style.overflow = "hidden";
} else {
// const scrollY = document.body.style.top;
// document.body.style.position = "";
// document.body.style.top = "";
document.body.style.paddingRight = "";
document.body.style.overflow = "";
// window.scrollTo(0, parseInt(scrollY || '0') * -1);
}
},
/**
* Log the user out and remove the token from storage
*/
[Mutation.UserForget](state: IState) {
state.user = null;
sessionStorage.removeItem("jwt");
localStorage.removeItem("jwt");
},
/**
* Try loading a user from the provided token
*/
[Mutation.UserLoad](state: IState, token: string) {
try {
let user: any = jwtDecode(token);
state.user = {
id : user.id,
name : user.name,
isAdmin: user.isAdmin,
token : token
}
} catch(e) {
return false;
}
return true;
},
/**
* Store the user to remember them
*/
[Mutation.UserStore](state: IState, remember: boolean) {
if (state.user == null) {
sessionStorage.removeItem("jwt");
localStorage.removeItem("jwt");
return;
}
if (remember) {
localStorage.setItem("jwt", state.user.token);
} else {
sessionStorage.setItem("jwt", state.user.token);
}
}
}

+ 0
- 6
services/autoplex-webui/src/app/store/schema.ts View File

@ -1,6 +0,0 @@
export interface IUser {
id : number,
name : string,
isAdmin: boolean,
token : string
}

+ 0
- 21
services/autoplex-webui/src/app/store/state.ts View File

@ -1,21 +0,0 @@
import { IUser } from "./schema";
import type Modals from "../components/modals";
import Modal from "../components/modals";
/**
* The state definition
*/
export interface IState {
user : IUser | null,
modalName : Modal | null,
modalVisible: boolean
}
/**
* The state implementation
*/
export const state: IState = {
user: null,
modalName: null,
modalVisible: false,
};

+ 0
- 42
services/autoplex-webui/src/app/styles/index.css View File

@ -1,42 +0,0 @@
@import '@fortawesome/fontawesome-free/css/all.css';
@tailwind base;
@tailwind components;
@tailwind utilities;
html, body {
@apply h-full;
}
.fade-enter-active, .fade-leave-active {
transition: opacity 0.15s;
}
.fade-enter-from, .fade-leave-to {
opacity: 0;
}
.slide-enter-active, .slide-leave-active {
transform: none;
opacity: 1.0;
transition: opacity 0.15s ease-in-out, transform 0.5s cubic-bezier(0.0, 0.0, 0.2, 1);
}
.slide-enter-from, .slide-leave-to {
opacity: 0;
transition: opacity 0.15s ease-in-out, transform 0.5s cubic-bezier(0.4, 0.0, 1, 1);
transform: translateY(10%);
}
.slide-left-enter-active, .slide-left-leave-active {
transform: none;
transition: transform 0.25s cubic-bezier(0.0, 0.0, 0.2, 1);
}
.slide-left-enter-from, .slide-left-leave-to {
transition: transform 0.25s cubic-bezier(0.4, 0.0, 1, 1);
transform: translateX(-100%);
}
.modal {
@apply fixed inset-0 flex flex-col h-screen;
background: rgba(0, 0, 0, 0.5);
}

+ 0
- 51
services/autoplex-webui/src/app/util.ts View File

@ -1,51 +0,0 @@
import { single as validate } from "validate.js";
export function validateValue(value: string, constraints: any) {
return <string>(validate(value || null, constraints) ?? [""])[0];
}
export function getAverageRgb(imgEl: HTMLImageElement) {
var blockSize = 5, // only visit every 5 pixels
defaultRGB = {r:0,g:0,b:0}, // for non-supporting envs
canvas = document.createElement('canvas'),
context = canvas.getContext && canvas.getContext('2d'),
data, width, height,
i = -4,
length,
rgb = {r:0,g:0,b:0},
count = 0;
if (!context) {
return defaultRGB;
}
height = canvas.height = imgEl.naturalHeight || imgEl.offsetHeight || imgEl.height;
width = canvas.width = imgEl.naturalWidth || imgEl.offsetWidth || imgEl.width;
context.drawImage(imgEl, 0, 0);
try {
data = context.getImageData(0, 0, width, height);
} catch(e) {
/* security error, img on diff domain */alert('x');
return defaultRGB;
}
length = data.data.length;
while ( (i += blockSize * 4) < length ) {
++count;
rgb.r += data.data[i];
rgb.g += data.data[i+1];
rgb.b += data.data[i+2];
}
// ~~ used to floor values
rgb.r = ~~(rgb.r/count);
rgb.g = ~~(rgb.g/count);
rgb.b = ~~(rgb.b/count);
return rgb;
}

+ 0
- 43
services/autoplex-webui/src/app/views/Dashboard.vue View File

@ -1,43 +0,0 @@
<template>
<div class="h-12 text-2xl flex items-center">
<h1 class="">Active Requests</h1>
</div>
<movie-list :movies="activeRequests" @onClickMovie="displayMovie"/>
<movie-modal :tmdb-id="activeTmdb" @onClose="activeTmdb = undefined"/>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import { authFetchApi } from "../routes";
import MovieList from "../components/MovieList.vue";
import MovieModal from "../components/modals/MovieModal.vue";
import { IApiMovie } from "../../common/api_schema";
export default defineComponent({
components: {
MovieList,
MovieModal
},
data() {
return {
activeRequests: <IApiMovie[]>[],
activeTmdb: <number|undefined> undefined
}
},
methods: {
displayMovie(movie: IApiMovie, index: number) {
this.activeTmdb = movie.tmdbId;
},
fetchRequests() {
return authFetchApi<IApiMovie[]>("/api/movie/request/tickets/active").then((response) => {
this.activeRequests = response.data;
}).catch(() => {
console.error("Failed to fetch active movie tickets");
});
}
},
mounted() {
this.fetchRequests();
}
});
</script>

+ 0
- 6
services/autoplex-webui/src/app/views/Error404.vue View File

@ -1,6 +0,0 @@
<template>
<div class="mx-auto my-auto">
<h1 class="text-9xl font-black opacity-30">404</h1>
</div>
</template>

+ 0
- 99
services/autoplex-webui/src/app/views/LinkDiscord.vue View File

@ -1,99 +0,0 @@
<template>
<div v-if="success" class="w-full sm:max-w-sm sm:mx-auto sm:my-auto p-6 space-y-4 bg-white rounded-lg shadow-md text-center">
<div class="text-center font-thin text-4xl pt-4">AUTOPLEX</div>
<div>
<i class="fas fa-check-circle align-middle text-green-500 text-4xl"></i>
</div>
<p>Discord account linked successfully! You may close this tab now</p>
</div>
<div v-else class="w-full sm:max-w-sm sm:mx-auto sm:my-auto p-6 space-y-4 bg-white rounded-lg shadow-md">
<div class="text-center font-thin text-4xl pt-4">AUTOPLEX</div>
<div>
<p class="opacity-70 text-sm text-center">Enter your credientials to complete linking your Discord account</p>
</div>
<form @submit.prevent="authenticate">
<div class="space-y-4">
<div>
<text-box label="Email" type="email" ref="email" placeholder="john@example.com" :disabled="isSubmitting"
v-model="fields.email"/>
</div>
<div>
<text-box label="Password" type="password" ref="password" placeholder="············" :disabled="isSubmitting"
v-model="fields.password"/>
</div>
<div>
<button :disabled="isSubmitting" class="block w-full rounded-full bg-indigo-500 text-white p-2 focus:outline-none">Link Discord Account</button>
</div>
</div>
</form>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import TextBox from "../components/TextBox.vue";
export default defineComponent({
components: {
TextBox
},
data() {
return {
isSubmitting: false,
success: false,
fields: {
email: "",
password: ""
},
}
},
methods: {
async authenticate() {
if (this.isSubmitting) {
return;
}
this.isSubmitting = true;
try {
// Send the request and wait far the response
let response = await fetch(`/auth/link/discord/${this.$route.params["token"]}`, {
method: "post",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(this.fields)
});
this.isSubmitting = false;
// Check the response for errors
let body = await response.json();
if (response.status !== 200) {
if (response.status === 401) {
body.errors = { "email": [ "Email or password is incorrect" ] };
}
for (let fieldName of ["email", "password"]) {
let field = <any>this.$refs[fieldName];
let message = <string>((<any>body.errors)[fieldName] ?? [""])[0];
field.setErrorMessage(message);
}
return;
}
} catch(e) {
console.error("Something went wrong logging in:", e);
this.isSubmitting = false;
}
this.success = true;
}
},
async beforeRouteEnter(to, from, next) {
let token = to.params["token"];
if (typeof token !== "string") {
next({ name: "Home" });
return;
}
// Verify that the token is valid. Redirect otherwise
let response = await fetch(`/auth/link/verify/discord/${token}`, { method: "post" });
if (response.status !== 200) {
next({ name: "Home" });
return;
}
next();
}
});
</script>

+ 0
- 77
services/autoplex-webui/src/app/views/Login.vue View File

@ -1,77 +0,0 @@
<template>
<div class="w-full sm:max-w-sm sm:mx-auto sm:my-auto p-6 space-y-4 bg-white rounded-lg shadow-md">
<div class="text-center font-thin text-4xl py-4">AUTOPLEX</div>
<div class="font-medium text-center text-xl">Sign in</div>
<form @submit.prevent="login">
<div class="space-y-4">
<div>
<text-box label="Email" type="email" ref="email" placeholder="john@example.com" :disabled="isSubmitting"
v-model="fields.email"/>
</div>
<div>
<text-box label="Password" type="password" ref="password" placeholder="············" :disabled="isSubmitting"
v-model="fields.password"/>
</div>
<div>
<check-box label="Remember Me" :disabled="isSubmitting" v-model="fields.remember"/>
</div>
<div>
<button :disabled="isSubmitting" class="block w-full rounded-full bg-indigo-500 text-white p-2 focus:outline-none">Sign In</button>
</div>
</div>
</form>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import { Action } from "../store";
import CheckBox from "../components/CheckBox.vue";
import TextBox from "../components/TextBox.vue";
export default defineComponent({
components: {
CheckBox,
TextBox
},
data() {
return {
isSubmitting: false,
fields: {
email: "",
password: "",
remember: false
},
}
},
methods: {
async login() {
if (this.isSubmitting) {
return;
}
this.isSubmitting = true;
try {
let errors = await this.$store.dispatch(Action.AuthAuthenticate, this.fields);
if (errors) {
for (let fieldName of ["email", "password"]) {
let field = <any>this.$refs[fieldName];
let message = <string>((<any>errors)[fieldName] ?? [""])[0];
field.setErrorMessage(message);
}
} else {
this.$router.push({ name: "Home" });
}
} catch(e) {
console.error("Something went wrong logging in:", e);
}
this.isSubmitting = false;
}
}
});
</script>
<style lang="postcss">
button:focus-visible {
@apply ring-2 ring-indigo-500 ring-offset-2;
}
</style>

+ 0
- 156
services/autoplex-webui/src/app/views/Register.vue View File

@ -1,156 +0,0 @@
<template>
<div class="w-full sm:max-w-sm sm:mx-auto sm:my-auto p-6 space-y-4 bg-white rounded-lg shadow-md">
<div class="text-center font-thin text-4xl py-4">AUTOPLEX</div>
<div class="font-medium text-center text-xl">Sign Up</div>
<form @submit.prevent="register">
<div class="space-y-4">
<div>
<text-box label="Access Token" type="text" disabled
v-model="fields.token" :validator="validateToken" ref="token"/>
</div>
<div>
<text-box label="Your Name" type="text" placeholder="John Doe" :disabled="isSubmitting"
v-model="fields.name" :validator="constraints.name" ref="name"/>
</div>
<div>
<text-box label="Email" type="email" placeholder="john@example.com" :disabled="isSubmitting"
v-model="fields.email" :validator="validateEmail" ref="email"/>
</div>
<div>
<text-box label="Password" type="password" placeholder="············" :disabled="isSubmitting"
v-model="fields.password" :validator="constraints.password" ref="password"
/>
</div>
<div>
<text-box label="Re-type Password" type="password" placeholder="············" :disabled="isSubmitting"
v-model="fields.retypePassword" :validator="validateRetypePassword" ref="retypePassword"/>
</div>
<div>
<button type="submit" class="block w-full rounded-full bg-indigo-500 text-white p-3 focus:outline-none" :disabled="isSubmitting">Register</button>
</div>
</div>
</form>
</div>
</template>
<script lang="ts">
import { defineComponent, reactive } from "vue";
import CheckBox from "../components/CheckBox.vue";
import TextBox from "../components/TextBox.vue";
import { constraints } from "../../common/validation";
import { validateValue } from "../util";
export default defineComponent({
components: {
CheckBox,
TextBox
},
data() {
return {
isSubmitting : false,
fields: reactive({
token : <string>this.$route.query["token"],
name : "",
email : "",
password : "",
retypePassword: "",
}),
constraints: {
name: constraints.register.name,
email: constraints.register.email,
password: constraints.register.password
}
}
},
methods: {
/**
* Submit the registration form
*/
async register() {
if (this.isSubmitting) {
return;
}
this.isSubmitting = true;
fetch(`/auth/register`, {
method: "post",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(this.fields)
})
.then(async response => {
this.isSubmitting = false;
if (response.status !== 200) {
let body = await response.json();
if (body.errors) {
for (let fieldName in this.fields) {
let field = <any>this.$refs[fieldName];
let message = <string>(body.errors[fieldName] ?? [""])[0];
field.setErrorMessage(message);
}
}
return;
}
this.$router.push({ name: "Login" });
})
.catch(e => {
console.error("Error occurred during submission:", e);
this.isSubmitting = false;
});
},
/**
* Validate the provided registration token
*/
async validateToken(token: string) {
let response = await fetch(`/auth/register/validate_token/${token}`, { method: "post" });
if (response.status !== 200) {
return "A valid token is required to register";
}
},
/**
* Validate the provided email address field
*/
async validateEmail(email: string) {
let error = validateValue(email, constraints.register.email);
if (error) {
return error;
}
// Check email availability
let response = await fetch(`/auth/register/available_email/${email}`, { method: "post" });
if (response.status !== 200) {
return "An account with that email address already exists";
}
},
/**
* Validate the provided retype-password field
*/
validateRetypePassword(value: string) {
if (value.trim().length == 0) {
return constraints.register.retypePassword.presence.message;
}
if (value !== this.fields.password) {
return constraints.register.retypePassword.equality.message;
}
}
},
/**
* Navigation Guard
*/
beforeRouteEnter(to, from, next) {
if (to.query["token"] === undefined) {
next({ name: "Login" });
} else {
next();
}
}
});
</script>
<style lang="postcss">
button:focus-visible {
@apply ring-2 ring-indigo-500 ring-offset-2;
}
</style>

+ 0
- 102
services/autoplex-webui/src/app/views/Search.vue View File

@ -1,102 +0,0 @@
<template>
<div class="space-y-4 lg:p-4">
<form @submit.prevent="() => search()">
<div class="flex rounded-xl overflow-hidden shadow-md">
<input type="text" v-model="searchValue" :placeholder="exampleTitles[Math.floor(exampleTitles.length*Math.random())]"
class="w-full outline-none p-2 bg-white border border-gray-100 text-gray-800 placeholder-gray-400 disabled:cursor-not-allowed disabled:opacity-50 disabled:bg-gray-50 disabled:border-gray-400">
<button class="py-3 px-6 bg-indigo-500 text-white" type="submit">Search</button>
</div>
</form>
<div v-if="$route.query['query']" class="text-center">{{ movies.length }} result{{ movies.length != 1 ? 's' : ""}} found</div>
<movie-list :movies="movies" @onClickMovie="displayMovie"/>
</div>
<router-view @onClose="onModalClosed"></router-view>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import { IApiMovie, IApiPaginatedResponse } from "@common/api_schema";
import { authFetchApi } from "../routes";
import MovieList from "../components/MovieList.vue";
export default defineComponent({
components: {
MovieList
},
data() {
return {
exampleTitles: ["John Wick", "The Incredibles 2", "Stand by Me", "Shawshank Redemption", "The Dark Knight", "Pulp Fiction", "Forrest Gump"],
isSubmitting: false,
movies: <IApiMovie[]>[],
searchValue: <string>this.$route.query["query"] || "",
page: -1,
totalPages: 0,
totalResults: 0
}
},
methods: {
displayMovie(movie: IApiMovie) {
this.$router.push({
name : "Lookup",
params: { tmdbId: movie.tmdbId },
query : this.$route.query
});
},
onModalClosed() {
this.$router.push({ name: "Search", query: this.$route.query });
},
async search(pushRoute: boolean = true) {
if (this.isSubmitting || this.searchValue.trim().length == 0) {
return;
}
if (document.activeElement && (<any>document.activeElement).blur) {
(<any>document.activeElement).blur();
}
if (pushRoute) {
this.$router.push({ name: "Search", query: { query: this.searchValue } });
}
this.isSubmitting = true;
try {
let response = await authFetchApi<IApiPaginatedResponse<IApiMovie>>(`/api/movie/search?query=${encodeURI(this.searchValue)}`);
this.movies = response.data.results;
this.page = response.data.page;
this.totalPages = response.data.totalPages;
this.totalResults = response.data.totalResults;
console.log("Got results", this.totalResults);
} catch(e) {
console.log("Error fetching movies:", e);
}
this.isSubmitting = false;
}
},
mounted() {
this.search(false);
},
});
</script>
<style scoped>
.overlay {
position: relative;
}
.overlay::before {
content: "";
position: absolute;
left: 0; top: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
filter: grayscale(100%);
z-index: 0;
}
.overlay > * {
z-index: 1;
}
.active .body {
display: block !important;
}
</style>

+ 0
- 3
services/autoplex-webui/src/app/views/admin/Admin.vue View File

@ -1,3 +0,0 @@
<template>
<router-view/>
</template>

+ 0
- 3
services/autoplex-webui/src/app/views/admin/AdminDashboard.vue View File

@ -1,3 +0,0 @@
<template>
<div>Admin dashboard</div>
</template>

+ 0
- 60
services/autoplex-webui/src/common/api_schema.ts View File

@ -1,60 +0,0 @@
/**
* Basic user information schema
*/
export interface IUser {
id : number,
name : string,
isAdmin: boolean
}
/**
* The JWT auth token structure
*/
export interface ITokenSchema extends IUser {
iat : number,
exp : number
}
/**
* The general API response structure
*/
export interface IApiResponse {
status: string
}
/**
* A generic data response from the API
*/
export interface IApiDataResponse<T> extends IApiResponse {
data: T
}
export interface IApiPaginatedResponse<T> {
page : number,
results : T[],
totalPages : number,
totalResults: number
};
/**
* A movie listing returned from the API
*/
export interface IApiMovie {
plexLink : string | null,
posterPath : string | null,
releaseDate: string | null,
ticketId : number | null,
title : string,
tmdbId : number
}
/**
* Movie details returned from the API
*/
export interface IApiMovieDetails extends IApiMovie {
backdropPath: string | null,
imdbId : string | null,
overview : string | null,
runtime : number | null,
requestedBy : IUser | null
}

+ 0
- 90
services/autoplex-webui/src/common/validation.ts View File

@ -1,90 +0,0 @@
export const constraints = {
api: {
movie: {
search: {
query: {
presence: {
allowEmpty: false,
message: "The query cannot be blank"
}
},
year: {
numericality: {
onlyInteger: true,
greaterThan: 0,
notGreaterThan: "Invalid year",
notValid: "Invalid year",
notInteger: "Invalid year"
}
}
}
}
},
login: {
email: {
presence: {
allowEmpty: false,
message: "An email address is required"
}
},
password: {
presence: {
allowEmpty: false,
message: "A password is required"
}
}
},
register: {
token: {
presence: {
message: "A valid token is required to register"
},
token: {
message: "A valid token is required to register"
}
},
name: {
presence: {
allowEmpty: false,
message: "Your name is required"
},
length: {
maximum: 50,
tooLong: "Your name cannot exceed 50 characters"
}
},
email: {
presence: {
allowEmpty: false,
message: "Your email is required"
},
length: {
maximum: 255,
tooLong: "An email address cannot exceed 255 characters"
},
email: {
message: "A valid email address is required"
}
},
password: {
presence: {
allowEmpty: false,
message: "A password is required"
},
length: {
minimum: 8,
tooShort: "Password should be at least 8 characters"
}
},
retypePassword: {
presence: {
allowEmpty: false,
message: "Re-type your password to confirm it"
},
equality: {
attribute: "password",
message: "Passwords must match"
}
}
}
};

+ 0
- 27
services/autoplex-webui/src/server/index.ts View File

@ -1,27 +0,0 @@
import fastify from "fastify";
import fastifyStatic from "fastify-static";
import { join } from "path";
/**
* Create the server instance
*/
let server = fastify();
/**
* Serve static assets
*/
server.register(fastifyStatic, {
root: join(__dirname, "../public")
});
/**
* If a file is not found, return the web UI
*/
server.setNotFoundHandler((_, reply) => {
return reply.sendFile("index.html");
});
/**
* Serve the web UI
*/
server.listen(3201, "0.0.0.0");

+ 0
- 30
services/autoplex-webui/tailwind.config.js View File

@ -1,30 +0,0 @@
const defaultTheme = require("tailwindcss/defaultTheme");
module.exports = {
purge: ["./index.html", "src/app/**/*.{vue,ts}"],
darkMode: false, // or 'media' or 'class'
theme: {
extend: {
colors: {
ui: {
plexGray: "rgb(31, 35, 38)"
}
}
},
screens: {
xs: "475px",
...defaultTheme.screens
}
},
variants: {
extend: {
cursor: ["disabled"],
backgroundColor: ["disabled"],
opacity: ["disabled"],
borderWidth: ["disabled"],
borderColor: ["disabled"],
transform: ["motion-safe"]
},
},
plugins: [],
}

+ 0
- 76
services/autoplex-webui/tsconfig.json View File

@ -1,76 +0,0 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
// "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
// "lib": [], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
"sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
"outDir": "./dist", /* Redirect output structure to the directory. */
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
"downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
// "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
// "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */
/* Module Resolution Options */
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
"baseUrl": "./src", /* Base directory to resolve non-absolute module names. */
// "paths": {} /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
"typeRoots": [ /* List of folders to include type definitions from. */
"src/typings"
],
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
"experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
"emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
/* Advanced Options */
"skipLibCheck": true, /* Skip type checking of declaration files. */
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
},
"include": [
"src"
]
}

+ 0
- 12
services/autoplex-webui/tsconfig.server.json View File

@ -1,12 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
"outDir": "dist", /* Redirect output structure to the directory. */
"sourceRoot": "src/server" /* Specify the location where debugger should locate TypeScript files instead of source locations. */
},
"exclude": [
"src/app"
]
}

+ 0
- 26
services/autoplex-webui/tsconfig.vite.json View File

@ -1,26 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"moduleResolution": "node",
"jsx": "preserve",
"resolveJsonModule": true,
"lib": ["esnext", "dom"],
"types": ["vite/client"],
"sourceRoot": "src/app",
"paths": {
"@app/*": ["app/*"],
"@common/*": ["common/*"]
}
},
"include": [
"src/app/**/*.ts",
"src/app**/*.d.ts",
"src/app/**/*.tsx",
"src/app/**/*.vue"
],
"exclude": [
"src/server"
]
}

+ 0
- 14
services/autoplex-webui/vite.config.ts View File

@ -1,14 +0,0 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
build: {
manifest: true,
outDir: "./dist/public"
},
server: {
port: 3201
}
})

+ 0
- 1900
services/autoplex-webui/yarn.lock
File diff suppressed because it is too large
View File


Loading…
Cancel
Save