128
index.html
58
package.json
@@ -1,45 +1,45 @@
|
||||
{
|
||||
"name": "seasoned-request",
|
||||
"description": "seasoned request app",
|
||||
"version": "1.0.0",
|
||||
"version": "1.22.17",
|
||||
"author": "Kevin Midboe",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "cross-env NODE_ENV=development webpack-dev-server --hot",
|
||||
"build": "cross-env NODE_ENV=production webpack --progress --hide-modules",
|
||||
"dev": "cross-env NODE_ENV=development webpack server",
|
||||
"build": "cross-env 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",
|
||||
"start": "node server.js",
|
||||
"docs": "documentation build src/api.js -f html -o docs/api && documentation build src/api.js -f md -o docs/api.md"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.18.1",
|
||||
"babel-plugin-transform-object-rest-spread": "^6.26.0",
|
||||
"chart.js": "^2.9.2",
|
||||
"connect-history-api-fallback": "^1.3.0",
|
||||
"express": "^4.16.1",
|
||||
"vue": "^2.5.2",
|
||||
"vue-axios": "^1.2.2",
|
||||
"vue-data-tablee": "^0.12.1",
|
||||
"vue-js-modal": "^1.3.16",
|
||||
"vue-router": "^3.0.1",
|
||||
"vuex": "^3.1.0"
|
||||
"connect-history-api-fallback": "1.6.0",
|
||||
"cross-env": "6.0.0",
|
||||
"express": "4.17.3",
|
||||
"vue": "^3.2.37",
|
||||
"vue-router": "4.1.2",
|
||||
"vuex": "3.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.4.5",
|
||||
"@babel/plugin-transform-runtime": "^7.4.4",
|
||||
"@babel/preset-env": "^7.4.5",
|
||||
"@babel/runtime": "^7.4.5",
|
||||
"babel-loader": "^8.0.6",
|
||||
"cross-env": "^3.0.0",
|
||||
"css-loader": "^3.4.2",
|
||||
"@babel/core": "7.17.2",
|
||||
"@babel/plugin-transform-runtime": "7.17.0",
|
||||
"@babel/preset-env": "7.16.11",
|
||||
"@babel/runtime": "7.17.2",
|
||||
"@types/node": "^18.6.1",
|
||||
"babel-loader": "8.2.3",
|
||||
"css-loader": "6.7.0",
|
||||
"documentation": "^11.0.0",
|
||||
"file-loader": "^0.9.0",
|
||||
"node-sass": "^4.5.0",
|
||||
"sass-loader": "^5.0.1",
|
||||
"schema-utils": "^2.4.1",
|
||||
"vue-loader": "^10.0.0",
|
||||
"vue-svg-inline-loader": "^1.3.1",
|
||||
"vue-template-compiler": "2.6.10",
|
||||
"webpack": "^2.2.0",
|
||||
"webpack-dev-server": "^2.2.0"
|
||||
"file-loader": "6.2.0",
|
||||
"html-webpack-plugin": "^5.5.0",
|
||||
"sass": "1.49.9",
|
||||
"sass-loader": "12.6.0",
|
||||
"terser-webpack-plugin": "5.3.1",
|
||||
"ts-loader": "^9.3.1",
|
||||
"typescript": "^4.7.4",
|
||||
"vue-loader": "17.0.0",
|
||||
"webpack": "5.70.0",
|
||||
"webpack-cli": "4.9.2",
|
||||
"webpack-dev-server": "4.7.4"
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 139 KiB After Width: | Height: | Size: 139 KiB |
BIN
public/assets/dune.jpg
Normal file
|
After Width: | Height: | Size: 641 KiB |
BIN
public/assets/mandalorian.jpg
Normal file
|
After Width: | Height: | Size: 331 KiB |
13
public/assets/no-image.svg
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
13
public/assets/no-image_small.svg
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 275 KiB After Width: | Height: | Size: 275 KiB |
|
Before Width: | Height: | Size: 423 KiB After Width: | Height: | Size: 423 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 889 B After Width: | Height: | Size: 889 B |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
27
server.js
@@ -1,23 +1,18 @@
|
||||
var express = require('express');
|
||||
var path = require('path');
|
||||
const compression = require('compression')
|
||||
var history = require('connect-history-api-fallback');
|
||||
const express = require("express");
|
||||
const path = require("path");
|
||||
const history = require("connect-history-api-fallback");
|
||||
|
||||
const publicPath = path.join(__dirname, "public");
|
||||
|
||||
app = express();
|
||||
app.use("/", express.static(publicPath));
|
||||
app.use(history({ index: "/" }));
|
||||
|
||||
app.use(compression())
|
||||
app.use('/dist', express.static(path.join(__dirname + "/dist")));
|
||||
app.use('/dist', express.static(path.join(__dirname + "/dist/")));
|
||||
app.use('/favicons', express.static(path.join(__dirname + "/favicons")));
|
||||
app.use(history({
|
||||
index: '/'
|
||||
}));
|
||||
|
||||
var port = process.env.PORT || 5000;
|
||||
|
||||
app.get('/', function(req, res) {
|
||||
res.sendFile(path.join(__dirname + '/index.html'));
|
||||
app.get("/", function (req, res) {
|
||||
res.sendFile(`${publicPath}/index.html`);
|
||||
});
|
||||
|
||||
const port = process.env.PORT || 5001;
|
||||
console.log("Server runnning at port:", port);
|
||||
|
||||
app.listen(port);
|
||||
|
||||
180
src/App.vue
@@ -1,159 +1,77 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
|
||||
<!-- Header and hamburger navigation -->
|
||||
<navigation></navigation>
|
||||
<NavigationHeader class="header"></NavigationHeader>
|
||||
|
||||
<!-- Header with search field -->
|
||||
|
||||
<!-- TODO move this to the navigation component -->
|
||||
<header class="header">
|
||||
<search-input v-model="query"></search-input>
|
||||
</header>
|
||||
|
||||
<!-- Movie popup that will show above existing rendered content -->
|
||||
<movie-popup v-if="moviePopupIsVisible" :id="popupID" :type="popupType"></movie-popup>
|
||||
|
||||
|
||||
<darkmode-toggle />
|
||||
<div class="navigation-icons-gutter desktop-only">
|
||||
<NavigationIcons />
|
||||
</div>
|
||||
|
||||
<!-- Display the component assigned to the given route (default: home) -->
|
||||
<router-view class="content" :key="$route.fullPath"></router-view>
|
||||
|
||||
<!-- Popup that will show above existing rendered content -->
|
||||
<popup />
|
||||
|
||||
<darkmode-toggle />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Vue from 'vue'
|
||||
import Navigation from '@/components/Navigation'
|
||||
import MoviePopup from '@/components/MoviePopup'
|
||||
import SearchInput from '@/components/SearchInput'
|
||||
import DarkmodeToggle from '@/components/ui/darkmodeToggle'
|
||||
import NavigationHeader from "@/components/header/NavigationHeader";
|
||||
import NavigationIcons from "@/components/header/NavigationIcons";
|
||||
import Popup from "@/components/Popup";
|
||||
import DarkmodeToggle from "@/components/ui/DarkmodeToggle";
|
||||
|
||||
export default {
|
||||
name: 'app',
|
||||
name: "app",
|
||||
components: {
|
||||
Navigation,
|
||||
MoviePopup,
|
||||
SearchInput,
|
||||
NavigationHeader,
|
||||
NavigationIcons,
|
||||
Popup,
|
||||
DarkmodeToggle
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
query: '',
|
||||
moviePopupIsVisible: false,
|
||||
popupID: 0,
|
||||
popupType: 'movie'
|
||||
}
|
||||
},
|
||||
created(){
|
||||
let that = this
|
||||
Vue.prototype.$popup = {
|
||||
get isOpen() {
|
||||
return that.moviePopupIsVisible
|
||||
},
|
||||
open: (id, type) => {
|
||||
this.popupID = id || this.popupID
|
||||
this.popupType = type || this.popupType
|
||||
this.moviePopupIsVisible = true
|
||||
console.log('opened')
|
||||
},
|
||||
close: () => {
|
||||
this.moviePopupIsVisible = false
|
||||
console.log('closed')
|
||||
}
|
||||
}
|
||||
console.log('MoviePopup registered at this.$popup and has state: ', this.$popup.isOpen)
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "./src/scss/media-queries";
|
||||
@import "./src/scss/variables";
|
||||
.content {
|
||||
@include tablet-min{
|
||||
width: calc(100% - 95px);
|
||||
margin-top: $header-size;
|
||||
margin-left: 95px;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
// @import "./src/scss/main";
|
||||
@import "./src/scss/variables";
|
||||
@import "./src/scss/media-queries";
|
||||
@import "src/scss/main";
|
||||
@import "src/scss/media-queries";
|
||||
|
||||
*{
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html {
|
||||
height: 100%;
|
||||
}
|
||||
body{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: 'Roboto', sans-serif;
|
||||
line-height: 1.6;
|
||||
background: $background-color;
|
||||
color: $text-color;
|
||||
transition: background-color .5s ease, color .5s ease;
|
||||
&.hidden{
|
||||
overflow: hidden;
|
||||
#app {
|
||||
display: grid;
|
||||
grid-template-rows: var(--header-size);
|
||||
grid-template-columns: var(--header-size) 1fr;
|
||||
|
||||
@include mobile {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
h1,h2,h3 {
|
||||
transition: color .5s ease;
|
||||
}
|
||||
a:any-link {
|
||||
color: inherit;
|
||||
}
|
||||
input, textarea, button{
|
||||
font-family: 'Roboto', sans-serif;
|
||||
}
|
||||
figure{
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
img{
|
||||
display: block;
|
||||
// max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.no-scroll {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.wrapper{
|
||||
position: relative;
|
||||
}
|
||||
.header{
|
||||
position: fixed;
|
||||
z-index: 15;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@include tablet-min{
|
||||
width: calc(100% - 170px);
|
||||
margin-left: 95px;
|
||||
border-top: 0;
|
||||
border-bottom: 0;
|
||||
.header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: 15;
|
||||
}
|
||||
}
|
||||
|
||||
// router view transition
|
||||
.fade-enter-active, .fade-leave-active {
|
||||
transition-property: opacity;
|
||||
transition-duration: 0.25s;
|
||||
}
|
||||
.fade-enter-active {
|
||||
transition-delay: 0.25s;
|
||||
}
|
||||
.fade-enter, .fade-leave-active {
|
||||
opacity: 0
|
||||
.navigation-icons-gutter {
|
||||
position: fixed;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
top: var(--header-size);
|
||||
width: var(--header-size);
|
||||
background-color: var(--background-color-secondary);
|
||||
}
|
||||
|
||||
.content {
|
||||
display: grid;
|
||||
grid-column: 2 / 3;
|
||||
grid-row: 2;
|
||||
z-index: 5;
|
||||
|
||||
@include mobile {
|
||||
grid-column: 1 / 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
490
src/api.js
@@ -1,490 +0,0 @@
|
||||
import axios from 'axios'
|
||||
import storage from '@/storage'
|
||||
import config from '@/config.json'
|
||||
import path from 'path'
|
||||
import store from '@/store'
|
||||
|
||||
const SEASONED_URL = config.SEASONED_URL
|
||||
const ELASTIC_URL = config.ELASTIC_URL
|
||||
const ELASTIC_INDEX = config.ELASTIC_INDEX
|
||||
|
||||
// TODO
|
||||
// - Move autorization token and errors here?
|
||||
|
||||
const checkStatusAndReturnJson = (response) => {
|
||||
if (!response.ok) {
|
||||
throw resp
|
||||
}
|
||||
return response.json()
|
||||
}
|
||||
|
||||
// - - - TMDB - - -
|
||||
|
||||
/**
|
||||
* Fetches tmdb movie by id. Can optionally include cast credits in result object.
|
||||
* @param {number} id
|
||||
* @param {boolean} [credits=false] Include credits
|
||||
* @returns {object} Tmdb response
|
||||
*/
|
||||
const getMovie = (id, checkExistance=false, credits=false, release_dates=false) => {
|
||||
const url = new URL('v2/movie', SEASONED_URL)
|
||||
url.pathname = path.join(url.pathname, id.toString())
|
||||
if (checkExistance) {
|
||||
url.searchParams.append('check_existance', true)
|
||||
}
|
||||
if (credits) {
|
||||
url.searchParams.append('credits', true)
|
||||
}
|
||||
if(release_dates) {
|
||||
url.searchParams.append('release_dates', true)
|
||||
}
|
||||
|
||||
return fetch(url.href)
|
||||
.then(resp => resp.json())
|
||||
.catch(error => { console.error(`api error getting movie: ${id}`); throw error })
|
||||
}
|
||||
|
||||
/**
|
||||
* 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=false, credits=false) => {
|
||||
const url = new URL('v2/show', SEASONED_URL)
|
||||
url.pathname = path.join(url.pathname, id.toString())
|
||||
if (checkExistance) {
|
||||
url.searchParams.append('check_existance', true)
|
||||
}
|
||||
if (credits) {
|
||||
url.searchParams.append('credits', true)
|
||||
}
|
||||
|
||||
return fetch(url.href)
|
||||
.then(resp => resp.json())
|
||||
.catch(error => { console.error(`api error getting show: ${id}`); throw error })
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches tmdb person by id. Can optionally include cast credits in result object.
|
||||
* @param {number} id
|
||||
* @param {boolean} [credits=false] Include credits
|
||||
* @returns {object} Tmdb response
|
||||
*/
|
||||
const getPerson = (id, credits=false) => {
|
||||
const url = new URL('v2/person', SEASONED_URL)
|
||||
url.pathname = path.join(url.pathname, id.toString())
|
||||
if (credits) {
|
||||
url.searchParams.append('credits', true)
|
||||
}
|
||||
|
||||
return fetch(url.href)
|
||||
.then(resp => resp.json())
|
||||
.catch(error => { console.error(`api error getting person: ${id}`); throw error })
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches tmdb list by name.
|
||||
* @param {string} name List the fetch
|
||||
* @param {number} [page=1]
|
||||
* @returns {object} Tmdb list response
|
||||
*/
|
||||
const getTmdbMovieListByName = (name, page=1) => {
|
||||
const url = new URL('v2/movie/' + name, SEASONED_URL)
|
||||
url.searchParams.append('page', page)
|
||||
const headers = { authorization: storage.token }
|
||||
|
||||
return fetch(url.href, { headers: headers })
|
||||
.then(resp => resp.json())
|
||||
// .catch(error => { console.error(`api error getting list: ${name}, page: ${page}`); throw error })
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches requested items.
|
||||
* @param {number} [page=1]
|
||||
* @returns {object} Request response
|
||||
*/
|
||||
const getRequests = (page=1) => {
|
||||
const url = new URL('v2/request', SEASONED_URL)
|
||||
url.searchParams.append('page', page)
|
||||
const headers = { authorization: storage.token }
|
||||
|
||||
return fetch(url.href, { headers: headers })
|
||||
.then(resp => resp.json())
|
||||
// .catch(error => { console.error(`api error getting list: ${name}, page: ${page}`); throw error })
|
||||
}
|
||||
|
||||
|
||||
const getUserRequests = (page=1) => {
|
||||
const url = new URL('v1/user/requests', SEASONED_URL)
|
||||
url.searchParams.append('page', page)
|
||||
|
||||
const headers = { authorization: localStorage.getItem('token') }
|
||||
|
||||
return fetch(url.href, { headers })
|
||||
.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) => {
|
||||
const url = new URL('v2/search', SEASONED_URL)
|
||||
if (mediaType != null && ['movie', 'show', 'person'].includes(mediaType)) {
|
||||
url.pathname += `/${mediaType}`
|
||||
}
|
||||
|
||||
url.searchParams.append('query', query)
|
||||
url.searchParams.append('page', page)
|
||||
url.searchParams.append('adult', adult)
|
||||
|
||||
const headers = { authorization: localStorage.getItem('token') }
|
||||
|
||||
return fetch(url.href, { headers })
|
||||
.then(resp => resp.json())
|
||||
.catch(error => { console.error(`api error searching: ${query}, page: ${page}`); throw error })
|
||||
}
|
||||
|
||||
// - - - Torrents - - -
|
||||
|
||||
/**
|
||||
* Search for torrents by query
|
||||
* @param {string} query
|
||||
* @param {boolean} credits Include credits
|
||||
* @returns {object} Torrent response
|
||||
*/
|
||||
const searchTorrents = (query, authorization_token) => {
|
||||
const url = new URL('/api/v1/pirate/search', SEASONED_URL)
|
||||
url.searchParams.append('query', query)
|
||||
|
||||
const headers = { authorization: storage.token }
|
||||
|
||||
return fetch(url.href, { headers: headers })
|
||||
.then(resp => resp.json())
|
||||
.catch(error => { console.error(`api error searching torrents: ${query}`); throw error })
|
||||
}
|
||||
|
||||
/**
|
||||
* Add magnet to download queue.
|
||||
* @param {string} magnet Magnet link
|
||||
* @param {boolean} name Name of torrent
|
||||
* @param {boolean} tmdb_id
|
||||
* @returns {object} Success/Failure response
|
||||
*/
|
||||
const addMagnet = (magnet, name, tmdb_id) => {
|
||||
const url = new URL('v1/pirate/add', SEASONED_URL)
|
||||
|
||||
const body = JSON.stringify({
|
||||
magnet: magnet,
|
||||
name: name,
|
||||
tmdb_id: tmdb_id
|
||||
})
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
authorization: storage.token
|
||||
}
|
||||
|
||||
return fetch(url.href, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body
|
||||
})
|
||||
.then(resp => resp.json())
|
||||
.catch(error => { console.error(`api error adding magnet: ${name} ${error}`); throw error })
|
||||
}
|
||||
|
||||
// - - - 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
|
||||
* @param {string} [authorization_token] To identify the requesting user
|
||||
* @returns {object} Success/Failure response
|
||||
*/
|
||||
const request = (id, type, authorization_token=undefined) => {
|
||||
const url = new URL('v2/request', SEASONED_URL)
|
||||
// url.pathname = path.join(url.pathname, id.toString())
|
||||
// url.searchParams.append('type', type)
|
||||
|
||||
const headers = {
|
||||
'Authorization': authorization_token,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
const body = {
|
||||
id: id,
|
||||
type: type
|
||||
}
|
||||
|
||||
return fetch(url.href, {
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
body: JSON.stringify(body)
|
||||
})
|
||||
.then(resp => resp.json())
|
||||
.catch(error => { console.error(`api error requesting: ${id}, type: ${type}`); throw error })
|
||||
}
|
||||
|
||||
/**
|
||||
* Check request status by tmdb id and type
|
||||
* @param {number} tmdb id
|
||||
* @param {string} type
|
||||
* @returns {object} Success/Failure response
|
||||
*/
|
||||
const getRequestStatus = (id, type, authorization_token=undefined) => {
|
||||
const url = new URL('v2/request', SEASONED_URL)
|
||||
url.pathname = path.join(url.pathname, id.toString())
|
||||
url.searchParams.append('type', type)
|
||||
|
||||
return fetch(url.href)
|
||||
.then(resp => {
|
||||
const status = resp.status;
|
||||
if (status === 200) { return true }
|
||||
else if (status === 404) { return false }
|
||||
else {
|
||||
console.error(`api error getting request status for id ${id} and type ${type}`)
|
||||
}
|
||||
})
|
||||
.catch(err => Promise.reject(err))
|
||||
}
|
||||
|
||||
const watchLink = (title, year, authorization_token=undefined) => {
|
||||
const url = new URL('v1/plex/watch-link', SEASONED_URL)
|
||||
url.searchParams.append('title', title)
|
||||
url.searchParams.append('year', year)
|
||||
|
||||
const headers = {
|
||||
'Authorization': authorization_token,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
return fetch(url.href, { headers })
|
||||
.then(resp => resp.json())
|
||||
.then(response => response.link)
|
||||
}
|
||||
|
||||
// - - - Seasoned user endpoints - - -
|
||||
|
||||
const register = (username, password) => {
|
||||
const url = new URL('v1/user', SEASONED_URL)
|
||||
const options = {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password })
|
||||
}
|
||||
|
||||
return fetch(url.href, options)
|
||||
.then(resp => resp.json())
|
||||
.catch(error => {
|
||||
console.error('Unexpected error occured before receiving response. Error:', error)
|
||||
// TODO log to sentry the issue here
|
||||
throw error
|
||||
})
|
||||
}
|
||||
|
||||
const login = (username, password, throwError=false) => {
|
||||
const url = new URL('v1/user/login', SEASONED_URL)
|
||||
const options = {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password })
|
||||
}
|
||||
|
||||
return fetch(url.href, options)
|
||||
.then(resp => {
|
||||
if (resp.status == 200)
|
||||
return resp.json();
|
||||
|
||||
if (throwError)
|
||||
throw resp;
|
||||
else
|
||||
console.error("Error occured when trying to sign in.\nError:", resp);
|
||||
})
|
||||
}
|
||||
|
||||
const getSettings = () => {
|
||||
const settingsExists = (value) => {
|
||||
if (value instanceof Object && value.hasOwnProperty('settings'))
|
||||
return value;
|
||||
throw "Settings does not exist in response object.";
|
||||
}
|
||||
const commitSettingsToStore = (response) => {
|
||||
store.dispatch('userModule/setSettings', response.settings)
|
||||
return response
|
||||
}
|
||||
|
||||
const url = new URL('v1/user/settings', SEASONED_URL)
|
||||
|
||||
const authorization_token = localStorage.getItem('token')
|
||||
const headers = authorization_token ? {
|
||||
'Authorization': authorization_token,
|
||||
'Content-Type': 'application/json'
|
||||
} : {}
|
||||
|
||||
return fetch(url.href, { headers })
|
||||
.then(resp => resp.json())
|
||||
.then(settingsExists)
|
||||
.then(commitSettingsToStore)
|
||||
.then(response => response.settings)
|
||||
.catch(error => { console.log('api error getting user settings'); throw error })
|
||||
}
|
||||
|
||||
const updateSettings = (settings) => {
|
||||
const url = new URL('v1/user/settings', SEASONED_URL)
|
||||
|
||||
const authorization_token = localStorage.getItem('token')
|
||||
const headers = authorization_token ? {
|
||||
'Authorization': authorization_token,
|
||||
'Content-Type': 'application/json'
|
||||
} : {}
|
||||
|
||||
return fetch(url.href, {
|
||||
method: 'PUT',
|
||||
headers,
|
||||
body: JSON.stringify(settings)
|
||||
})
|
||||
.then(resp => resp.json())
|
||||
.catch(error => { console.log('api error updating user settings'); throw error })
|
||||
}
|
||||
|
||||
// - - - Authenticate with plex - - -
|
||||
|
||||
const linkPlexAccount = (username, password) => {
|
||||
const url = new URL('v1/user/link_plex', SEASONED_URL)
|
||||
const body = { username, password }
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
authorization: storage.token
|
||||
}
|
||||
|
||||
return fetch(url.href, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(body)
|
||||
})
|
||||
.then(resp => resp.json())
|
||||
.catch(error => { console.error(`api error linking plex account: ${username}`); throw error })
|
||||
}
|
||||
|
||||
const unlinkPlexAccount = (username, password) => {
|
||||
const url = new URL('v1/user/unlink_plex', SEASONED_URL)
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
authorization: storage.token
|
||||
}
|
||||
|
||||
return fetch(url.href, {
|
||||
method: 'POST',
|
||||
headers
|
||||
})
|
||||
.then(resp => resp.json())
|
||||
.catch(error => { console.error(`api error unlinking plex account: ${username}`); throw error })
|
||||
}
|
||||
|
||||
|
||||
// - - - User graphs - - -
|
||||
|
||||
const fetchChart = (urlPath, days, chartType) => {
|
||||
const url = new URL('v1/user' + urlPath, SEASONED_URL)
|
||||
url.searchParams.append('days', days)
|
||||
url.searchParams.append('y_axis', chartType)
|
||||
|
||||
const authorization_token = localStorage.getItem('token')
|
||||
const headers = authorization_token ? {
|
||||
'Authorization': authorization_token,
|
||||
'Content-Type': 'application/json'
|
||||
} : {}
|
||||
|
||||
return fetch(url.href, { headers })
|
||||
.then(resp => resp.json())
|
||||
.catch(error => { console.log('api error fetching chart'); throw error })
|
||||
}
|
||||
|
||||
|
||||
// - - - Random emoji - - -
|
||||
|
||||
const getEmoji = () => {
|
||||
const url = new URL('v1/emoji', SEASONED_URL)
|
||||
|
||||
return fetch(url.href)
|
||||
.then(resp => resp.json())
|
||||
.catch(error => { console.log('api error getting emoji'); throw error })
|
||||
}
|
||||
|
||||
|
||||
// - - - ELASTIC SEARCH - - -
|
||||
// This elastic index contains titles mapped to ids. Lightning search
|
||||
// used for autocomplete
|
||||
|
||||
/**
|
||||
* 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) => {
|
||||
const url = new URL(path.join(ELASTIC_INDEX, '/_search'), ELASTIC_URL)
|
||||
const headers = {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
const body = {
|
||||
"sort" : [
|
||||
{ "popularity" : {"order" : "desc"}},
|
||||
"_score"
|
||||
],
|
||||
"query": {
|
||||
"bool": {
|
||||
"should": [{
|
||||
"match_phrase_prefix": {
|
||||
"original_name": query
|
||||
}
|
||||
},
|
||||
{
|
||||
"match_phrase_prefix": {
|
||||
"original_title": query
|
||||
}
|
||||
}]
|
||||
}
|
||||
},
|
||||
"size": 6
|
||||
}
|
||||
|
||||
return fetch(url.href, {
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
body: JSON.stringify(body)
|
||||
})
|
||||
.then(resp => resp.json())
|
||||
.catch(error => { console.log(`api error searching elasticsearch: ${query}`); throw error })
|
||||
}
|
||||
|
||||
|
||||
|
||||
export {
|
||||
getMovie,
|
||||
getShow,
|
||||
getPerson,
|
||||
getTmdbMovieListByName,
|
||||
searchTmdb,
|
||||
getUserRequests,
|
||||
getRequests,
|
||||
searchTorrents,
|
||||
addMagnet,
|
||||
request,
|
||||
watchLink,
|
||||
getRequestStatus,
|
||||
linkPlexAccount,
|
||||
unlinkPlexAccount,
|
||||
register,
|
||||
login,
|
||||
getSettings,
|
||||
updateSettings,
|
||||
fetchChart,
|
||||
getEmoji,
|
||||
elasticSearchMoviesAndShows
|
||||
}
|
||||
563
src/api.ts
Normal file
@@ -0,0 +1,563 @@
|
||||
import config from "./config";
|
||||
import { IList } from "./interfaces/IList";
|
||||
|
||||
let { SEASONED_URL, ELASTIC_URL, ELASTIC_INDEX } = config;
|
||||
if (!SEASONED_URL) {
|
||||
SEASONED_URL = window.location.origin;
|
||||
}
|
||||
|
||||
// TODO
|
||||
// - Move autorization token and errors here?
|
||||
|
||||
const checkStatusAndReturnJson = response => {
|
||||
if (!response.ok) {
|
||||
throw response;
|
||||
}
|
||||
return response.json();
|
||||
};
|
||||
|
||||
// - - - TMDB - - -
|
||||
|
||||
/**
|
||||
* Fetches tmdb movie by id. Can optionally include cast credits in result object.
|
||||
* @param {number} id
|
||||
* @param {boolean} [credits=false] Include credits
|
||||
* @returns {object} Tmdb response
|
||||
*/
|
||||
const getMovie = (
|
||||
id,
|
||||
checkExistance = false,
|
||||
credits = false,
|
||||
release_dates = false
|
||||
) => {
|
||||
const url = new URL("/api/v2/movie", SEASONED_URL);
|
||||
url.pathname = `${url.pathname}/${id.toString()}`;
|
||||
if (checkExistance) {
|
||||
url.searchParams.append("check_existance", "true");
|
||||
}
|
||||
if (credits) {
|
||||
url.searchParams.append("credits", "true");
|
||||
}
|
||||
if (release_dates) {
|
||||
url.searchParams.append("release_dates", "true");
|
||||
}
|
||||
|
||||
return fetch(url.href)
|
||||
.then(resp => resp.json())
|
||||
.catch(error => {
|
||||
console.error(`api error getting movie: ${id}`);
|
||||
throw error;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 = false, credits = false) => {
|
||||
const url = new URL("/api/v2/show", SEASONED_URL);
|
||||
url.pathname = `${url.pathname}/${id.toString()}`;
|
||||
if (checkExistance) {
|
||||
url.searchParams.append("check_existance", "true");
|
||||
}
|
||||
if (credits) {
|
||||
url.searchParams.append("credits", "true");
|
||||
}
|
||||
|
||||
return fetch(url.href)
|
||||
.then(resp => resp.json())
|
||||
.catch(error => {
|
||||
console.error(`api error getting show: ${id}`);
|
||||
throw error;
|
||||
});
|
||||
};
|
||||
|
||||
function delay(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches tmdb person by id. Can optionally include cast credits in result object.
|
||||
* @param {number} id
|
||||
* @param {boolean} [credits=false] Include credits
|
||||
* @returns {object} Tmdb response
|
||||
*/
|
||||
const getPerson = (id, credits = false) => {
|
||||
const url = new URL("/api/v2/person", SEASONED_URL);
|
||||
url.pathname = `${url.pathname}/${id.toString()}`;
|
||||
if (credits) {
|
||||
url.searchParams.append("credits", "true");
|
||||
}
|
||||
|
||||
return fetch(url.href)
|
||||
.then(resp => resp.json())
|
||||
.catch(error => {
|
||||
console.error(`api error getting person: ${id}`);
|
||||
throw error;
|
||||
});
|
||||
};
|
||||
|
||||
const getCredits = (type, id) => {
|
||||
if (type === "movie") {
|
||||
return getMovieCredits(id);
|
||||
} else if (type === "show") {
|
||||
return getShowCredits(id);
|
||||
} else if (type === "person") {
|
||||
return getPersonCredits(id);
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches tmdb movie credits by id.
|
||||
* @param {number} id
|
||||
* @returns {object} Tmdb response
|
||||
*/
|
||||
const getMovieCredits = id => {
|
||||
const url = new URL("/api/v2/movie", SEASONED_URL);
|
||||
url.pathname = `${url.pathname}/${id.toString()}/credits`;
|
||||
|
||||
return fetch(url.href)
|
||||
.then(resp => resp.json())
|
||||
.catch(error => {
|
||||
console.error(`api error getting movie: ${id}`);
|
||||
throw error;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches tmdb show credits by id.
|
||||
* @param {number} id
|
||||
* @returns {object} Tmdb response
|
||||
*/
|
||||
const getShowCredits = id => {
|
||||
const url = new URL("/api/v2/show", SEASONED_URL);
|
||||
url.pathname = `${url.pathname}/${id.toString()}/credits`;
|
||||
|
||||
return fetch(url.href)
|
||||
.then(resp => resp.json())
|
||||
.catch(error => {
|
||||
console.error(`api error getting show: ${id}`);
|
||||
throw error;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches tmdb person credits by id.
|
||||
* @param {number} id
|
||||
* @returns {object} Tmdb response
|
||||
*/
|
||||
const getPersonCredits = id => {
|
||||
const url = new URL("/api/v2/person", SEASONED_URL);
|
||||
url.pathname = `${url.pathname}/${id.toString()}/credits`;
|
||||
|
||||
return fetch(url.href)
|
||||
.then(resp => resp.json())
|
||||
.catch(error => {
|
||||
console.error(`api error getting person: ${id}`);
|
||||
throw error;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 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: number = 1
|
||||
): Promise<IList> => {
|
||||
const url = new URL("/api/v2/movie/" + name, SEASONED_URL);
|
||||
url.searchParams.append("page", page.toString());
|
||||
|
||||
return fetch(url.href).then(resp => resp.json());
|
||||
// .catch(error => { console.error(`api error getting list: ${name}, page: ${page}`); throw error })
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches requested items.
|
||||
* @param {number} [page=1]
|
||||
* @returns {object} Request response
|
||||
*/
|
||||
const getRequests = (page: number = 1) => {
|
||||
const url = new URL("/api/v2/request", SEASONED_URL);
|
||||
url.searchParams.append("page", page.toString());
|
||||
|
||||
return fetch(url.href).then(resp => resp.json());
|
||||
// .catch(error => { console.error(`api error getting list: ${name}, page: ${page}`); throw error })
|
||||
};
|
||||
|
||||
const getUserRequests = (page = 1) => {
|
||||
const url = new URL("/api/v1/user/requests", SEASONED_URL);
|
||||
url.searchParams.append("page", page.toString());
|
||||
|
||||
return fetch(url.href).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) => {
|
||||
const url = new URL("/api/v2/search", SEASONED_URL);
|
||||
if (mediaType != null && ["movie", "show", "person"].includes(mediaType)) {
|
||||
url.pathname += `/${mediaType}`;
|
||||
}
|
||||
|
||||
url.searchParams.append("query", query);
|
||||
url.searchParams.append("page", page.toString());
|
||||
url.searchParams.append("adult", adult.toString());
|
||||
|
||||
return fetch(url.href)
|
||||
.then(resp => resp.json())
|
||||
.catch(error => {
|
||||
console.error(`api error searching: ${query}, page: ${page}`);
|
||||
throw error;
|
||||
});
|
||||
};
|
||||
|
||||
// - - - Torrents - - -
|
||||
|
||||
/**
|
||||
* Search for torrents by query
|
||||
* @param {string} query
|
||||
* @param {boolean} credits Include credits
|
||||
* @returns {object} Torrent response
|
||||
*/
|
||||
const searchTorrents = query => {
|
||||
const url = new URL("/api/v1/pirate/search", SEASONED_URL);
|
||||
url.searchParams.append("query", query);
|
||||
|
||||
return fetch(url.href)
|
||||
.then(resp => resp.json())
|
||||
.catch(error => {
|
||||
console.error(`api error searching torrents: ${query}`);
|
||||
throw error;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Add magnet to download queue.
|
||||
* @param {string} magnet Magnet link
|
||||
* @param {boolean} name Name of torrent
|
||||
* @param {boolean} tmdb_id
|
||||
* @returns {object} Success/Failure response
|
||||
*/
|
||||
const addMagnet = (magnet, name, tmdb_id) => {
|
||||
const url = new URL("/api/v1/pirate/add", SEASONED_URL);
|
||||
|
||||
const options = {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
magnet: magnet,
|
||||
name: name,
|
||||
tmdb_id: tmdb_id
|
||||
})
|
||||
};
|
||||
|
||||
return fetch(url.href, options)
|
||||
.then(resp => resp.json())
|
||||
.catch(error => {
|
||||
console.error(`api error adding magnet: ${name} ${error}`);
|
||||
throw error;
|
||||
});
|
||||
};
|
||||
|
||||
// - - - 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) => {
|
||||
const url = new URL("/api/v2/request", SEASONED_URL);
|
||||
|
||||
const options = {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ id, type })
|
||||
};
|
||||
|
||||
return fetch(url.href, options)
|
||||
.then(resp => resp.json())
|
||||
.catch(error => {
|
||||
console.error(`api error requesting: ${id}, type: ${type}`);
|
||||
throw error;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Check request status by tmdb id and type
|
||||
* @param {number} tmdb id
|
||||
* @param {string} type
|
||||
* @returns {object} Success/Failure response
|
||||
*/
|
||||
const getRequestStatus = (id, type = undefined) => {
|
||||
const url = new URL("/api/v2/request", SEASONED_URL);
|
||||
url.pathname = `${url.pathname}/${id.toString()}`;
|
||||
url.searchParams.append("type", type);
|
||||
|
||||
return fetch(url.href)
|
||||
.then(resp => {
|
||||
const status = resp.status;
|
||||
if (status === 200) {
|
||||
return true;
|
||||
} else if (status === 404) {
|
||||
return false;
|
||||
} else {
|
||||
console.error(
|
||||
`api error getting request status for id ${id} and type ${type}`
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch(err => Promise.reject(err));
|
||||
};
|
||||
|
||||
const watchLink = (title, year) => {
|
||||
const url = new URL("/api/v1/plex/watch-link", SEASONED_URL);
|
||||
url.searchParams.append("title", title);
|
||||
url.searchParams.append("year", year);
|
||||
|
||||
return fetch(url.href)
|
||||
.then(resp => resp.json())
|
||||
.then(response => response.link);
|
||||
};
|
||||
|
||||
const movieImages = id => {
|
||||
const url = new URL(`v2/movie/${id}/images`, SEASONED_URL);
|
||||
|
||||
return fetch(url.href).then(resp => resp.json());
|
||||
};
|
||||
|
||||
// - - - Seasoned user endpoints - - -
|
||||
|
||||
const register = (username, password) => {
|
||||
const url = new URL("/api/v1/user", SEASONED_URL);
|
||||
const options = {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ username, password })
|
||||
};
|
||||
|
||||
return fetch(url.href, options)
|
||||
.then(resp => resp.json())
|
||||
.catch(error => {
|
||||
console.error(
|
||||
"Unexpected error occured before receiving response. Error:",
|
||||
error
|
||||
);
|
||||
// TODO log to sentry the issue here
|
||||
throw error;
|
||||
});
|
||||
};
|
||||
|
||||
const login = (username, password, throwError = false) => {
|
||||
const url = new URL("/api/v1/user/login", SEASONED_URL);
|
||||
const options = {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ username, password })
|
||||
};
|
||||
|
||||
return fetch(url.href, options).then(resp => {
|
||||
if (resp.status == 200) return resp.json();
|
||||
|
||||
if (throwError) throw resp;
|
||||
else console.error("Error occured when trying to sign in.\nError:", resp);
|
||||
});
|
||||
};
|
||||
|
||||
const logout = (throwError = false) => {
|
||||
const url = new URL("/api/v1/user/logout", SEASONED_URL);
|
||||
const options = { method: "POST" };
|
||||
|
||||
return fetch(url.href, options).then(resp => {
|
||||
if (resp.status == 200) return resp.json();
|
||||
|
||||
if (throwError) throw resp;
|
||||
else console.error("Error occured when trying to log out.\nError:", resp);
|
||||
});
|
||||
};
|
||||
|
||||
const getSettings = () => {
|
||||
const url = new URL("/api/v1/user/settings", SEASONED_URL);
|
||||
|
||||
return fetch(url.href)
|
||||
.then(resp => resp.json())
|
||||
.catch(error => {
|
||||
console.log("api error getting user settings");
|
||||
throw error;
|
||||
});
|
||||
};
|
||||
|
||||
const updateSettings = settings => {
|
||||
const url = new URL("/api/v1/user/settings", SEASONED_URL);
|
||||
|
||||
const options = {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(settings)
|
||||
};
|
||||
|
||||
return fetch(url.href, options)
|
||||
.then(resp => resp.json())
|
||||
.catch(error => {
|
||||
console.log("api error updating user settings");
|
||||
throw error;
|
||||
});
|
||||
};
|
||||
|
||||
// - - - Authenticate with plex - - -
|
||||
|
||||
const linkPlexAccount = (username, password) => {
|
||||
const url = new URL("/api/v1/user/link_plex", SEASONED_URL);
|
||||
const body = { username, password };
|
||||
|
||||
const options = {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body)
|
||||
};
|
||||
|
||||
return fetch(url.href, options)
|
||||
.then(resp => resp.json())
|
||||
.catch(error => {
|
||||
console.error(`api error linking plex account: ${username}`);
|
||||
throw error;
|
||||
});
|
||||
};
|
||||
|
||||
const unlinkPlexAccount = (username, password) => {
|
||||
const url = new URL("/api/v1/user/unlink_plex", SEASONED_URL);
|
||||
|
||||
const options = {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" }
|
||||
};
|
||||
|
||||
return fetch(url.href, options)
|
||||
.then(resp => resp.json())
|
||||
.catch(error => {
|
||||
console.error(`api error unlinking plex account: ${username}`);
|
||||
throw error;
|
||||
});
|
||||
};
|
||||
|
||||
// - - - User graphs - - -
|
||||
|
||||
const fetchChart = (urlPath, days, chartType) => {
|
||||
const url = new URL("/api/v1/user" + urlPath, SEASONED_URL);
|
||||
url.searchParams.append("days", days);
|
||||
url.searchParams.append("y_axis", chartType);
|
||||
|
||||
return fetch(url.href).then(resp => {
|
||||
if (!resp.ok) {
|
||||
console.log("DAMN WE FAILED!", resp);
|
||||
throw Error(resp.statusText);
|
||||
}
|
||||
|
||||
return resp.json();
|
||||
});
|
||||
};
|
||||
|
||||
// - - - Random emoji - - -
|
||||
|
||||
const getEmoji = () => {
|
||||
const url = new URL("/api/v1/emoji", SEASONED_URL);
|
||||
|
||||
return fetch(url.href)
|
||||
.then(resp => resp.json())
|
||||
.catch(error => {
|
||||
console.log("api error getting emoji");
|
||||
throw error;
|
||||
});
|
||||
};
|
||||
|
||||
// - - - ELASTIC SEARCH - - -
|
||||
// This elastic index contains titles mapped to ids. Lightning search
|
||||
// used for autocomplete
|
||||
|
||||
/**
|
||||
* 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, count = 22) => {
|
||||
const url = new URL(`${ELASTIC_INDEX}/_search`, ELASTIC_URL);
|
||||
|
||||
const body = {
|
||||
sort: [{ popularity: { order: "desc" } }, "_score"],
|
||||
query: {
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
match_phrase_prefix: {
|
||||
original_name: query
|
||||
}
|
||||
},
|
||||
{
|
||||
match_phrase_prefix: {
|
||||
original_title: query
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
size: count
|
||||
};
|
||||
|
||||
const options = {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body)
|
||||
};
|
||||
|
||||
return fetch(url.href, options)
|
||||
.then(resp => resp.json())
|
||||
.catch(error => {
|
||||
console.log(`api error searching elasticsearch: ${query}`);
|
||||
throw error;
|
||||
});
|
||||
};
|
||||
|
||||
export {
|
||||
getMovie,
|
||||
getShow,
|
||||
getPerson,
|
||||
getMovieCredits,
|
||||
getShowCredits,
|
||||
getPersonCredits,
|
||||
getCredits,
|
||||
getTmdbMovieListByName,
|
||||
searchTmdb,
|
||||
getUserRequests,
|
||||
getRequests,
|
||||
searchTorrents,
|
||||
addMagnet,
|
||||
request,
|
||||
watchLink,
|
||||
movieImages,
|
||||
getRequestStatus,
|
||||
linkPlexAccount,
|
||||
unlinkPlexAccount,
|
||||
register,
|
||||
login,
|
||||
logout,
|
||||
getSettings,
|
||||
updateSettings,
|
||||
fetchChart,
|
||||
getEmoji,
|
||||
elasticSearchMoviesAndShows
|
||||
};
|
||||
|
Before Width: | Height: | Size: 1.9 KiB |
@@ -1,75 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<section class="not-found">
|
||||
<h1 class="not-found__title">Page Not Found</h1>
|
||||
</section>
|
||||
<seasoned-button class="button" @click="goBack">go back to previous page</seasoned-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import store from '@/store'
|
||||
import SeasonedButton from '@/components/ui/SeasonedButton'
|
||||
|
||||
export default {
|
||||
components: { SeasonedButton },
|
||||
methods: {
|
||||
goBack() {
|
||||
this.$router.go(-1)
|
||||
}
|
||||
},
|
||||
created() {
|
||||
if (this.$popup.isOpen == true)
|
||||
this.$popup.close()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "./src/scss/variables";
|
||||
@import "./src/scss/media-queries";
|
||||
|
||||
.button {
|
||||
font-size: 1.2rem;
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: calc(50% + 46px);
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
@include mobile {
|
||||
top: 60%;
|
||||
left: 50%;
|
||||
font-size: 1rem;
|
||||
width: content;
|
||||
}
|
||||
}
|
||||
|
||||
.not-found {
|
||||
display: flex;
|
||||
height: calc(100vh - var(--header-size));
|
||||
background: url('~assets/pulp-fiction.jpg') no-repeat 50% 50%;
|
||||
background-size: cover;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
height: calc(100vh - var(--header-size));
|
||||
width: 100%;
|
||||
pointer-events: none;
|
||||
background: $background-40;
|
||||
}
|
||||
&__title {
|
||||
margin-top: 30vh;
|
||||
font-size: 2.5rem;
|
||||
font-weight: 500;
|
||||
color: $text-color;
|
||||
position: relative;
|
||||
|
||||
@include tablet-min {
|
||||
font-size: 3.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,316 +0,0 @@
|
||||
<template>
|
||||
<div class="wrapper" v-if="hasPlexUser">
|
||||
<h1>Your watch activity</h1>
|
||||
|
||||
<div class="filter">
|
||||
<h2>Filter</h2>
|
||||
|
||||
<div class="filter-item">
|
||||
<label class="desktop-only">Days:</label>
|
||||
<input class="dayinput"
|
||||
v-model="days"
|
||||
placeholder="number of days"
|
||||
type="number"
|
||||
pattern="[0-9]*"
|
||||
:style="{maxWidth: `${3 + (0.5 * days.length)}rem`}"/>
|
||||
<!-- <datalist id="days">
|
||||
<option v-for="index in 1500" :value="index" :key="index"></option>
|
||||
</datalist> -->
|
||||
</div>
|
||||
|
||||
<toggle-button class="filter-item" :options="chartTypes" :selected.sync="selectedChartDataType" />
|
||||
</div>
|
||||
|
||||
<div class="chart-section">
|
||||
<h3 class="chart-header">Activity per day:</h3>
|
||||
<div class="chart">
|
||||
<canvas ref="activityCanvas"></canvas>
|
||||
</div>
|
||||
|
||||
<h3 class="chart-header">Activity per day of week:</h3>
|
||||
<div class="chart">
|
||||
<canvas ref="playsByDayOfWeekCanvas"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<h1>Must be authenticated</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import store from '@/store'
|
||||
import ToggleButton from '@/components/ui/ToggleButton';
|
||||
import { fetchChart } from '@/api'
|
||||
|
||||
var Chart = require('chart.js');
|
||||
Chart.defaults.global.elements.point.radius = 0
|
||||
Chart.defaults.global.elements.point.hitRadius = 10
|
||||
Chart.defaults.global.elements.point.pointHoverRadius = 10
|
||||
Chart.defaults.global.elements.point.hoverBorderWidth = 4
|
||||
|
||||
export default {
|
||||
components: { ToggleButton },
|
||||
data() {
|
||||
return {
|
||||
days: 30,
|
||||
selectedChartDataType: 'plays',
|
||||
charts: [{
|
||||
name: 'Watch activity',
|
||||
ref: 'activityCanvas',
|
||||
data: null,
|
||||
urlPath: '/plays_by_day',
|
||||
graphType: 'line'
|
||||
}, {
|
||||
name: 'Plays by day of week',
|
||||
ref: 'playsByDayOfWeekCanvas',
|
||||
data: null,
|
||||
urlPath: '/plays_by_dayofweek',
|
||||
graphType: 'bar'
|
||||
}],
|
||||
chartData: [{
|
||||
type: 'plays',
|
||||
tooltipLabel: 'Play count',
|
||||
},{
|
||||
type: 'duration',
|
||||
tooltipLabel: 'Watched duration',
|
||||
valueConvertFunction: this.convertSecondsToHumanReadable
|
||||
}],
|
||||
gridColor: getComputedStyle(document.documentElement).getPropertyValue('--text-color-5')
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
hasPlexUser() {
|
||||
return store.getters['userModule/plex_userid'] != null ? true : false
|
||||
},
|
||||
chartTypes() {
|
||||
return this.chartData.map(chart => chart.type)
|
||||
},
|
||||
selectedChartType() {
|
||||
return this.chartData.filter(data => data.type == this.selectedChartDataType)[0]
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
hasPlexUser(newValue, oldValue) {
|
||||
if (newValue != oldValue && newValue == true) {
|
||||
this.fetchChartData(this.charts)
|
||||
}
|
||||
},
|
||||
days(newValue) {
|
||||
if (newValue !== '') {
|
||||
this.fetchChartData(this.charts)
|
||||
}
|
||||
},
|
||||
selectedChartDataType(selectedChartDataType) {
|
||||
this.fetchChartData(this.charts)
|
||||
}
|
||||
},
|
||||
beforeMount() {
|
||||
if (typeof(this.days) == 'number') {
|
||||
this.days = this.days.toString()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
fetchChartData(charts) {
|
||||
if (this.hasPlexUser == false) {
|
||||
return
|
||||
}
|
||||
|
||||
for (let chart of charts) {
|
||||
|
||||
|
||||
|
||||
fetchChart(chart.urlPath, this.days, this.selectedChartType.type)
|
||||
.then(data => {
|
||||
this.series = data.data.series.filter(group => group.name === 'TV')[0].data; // plays pr date in groups (movie/tv/music)
|
||||
this.categories = data.data.categories; // dates
|
||||
|
||||
const x_labels = data.data.categories.map(date => {
|
||||
if (date.match(/[0-9]{4}-[0-9]{2}-[0-9]{2}/)) {
|
||||
const [year, month, day] = date.split('-')
|
||||
return `${day}.${month}`
|
||||
}
|
||||
|
||||
return date
|
||||
})
|
||||
let y_activityMovies = data.data.series.filter(group => group.name === 'Movies')[0].data
|
||||
let y_activityTV = data.data.series.filter(group => group.name === 'TV')[0].data
|
||||
|
||||
const datasets = [{
|
||||
label: `Movies watch last ${ this.days } days`,
|
||||
data: y_activityMovies,
|
||||
backgroundColor: 'rgba(54, 162, 235, 0.2)',
|
||||
borderColor: 'rgba(54, 162, 235, 1)',
|
||||
borderWidth: 1
|
||||
},
|
||||
{
|
||||
label: `Shows watch last ${ this.days } days`,
|
||||
data: y_activityTV,
|
||||
backgroundColor: 'rgba(255, 159, 64, 0.2)',
|
||||
borderColor: 'rgba(255, 159, 64, 1)',
|
||||
borderWidth: 1
|
||||
}
|
||||
]
|
||||
|
||||
if (chart.data == null) {
|
||||
this.generateChart(chart, x_labels, datasets)
|
||||
} else {
|
||||
chart.data.clear();
|
||||
chart.data.data.labels = x_labels;
|
||||
chart.data.data.datasets = datasets;
|
||||
chart.data.update();
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
generateChart(chart, labels, datasets) {
|
||||
const chartInstance = new Chart(this.$refs[chart.ref], {
|
||||
type: chart.graphType,
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: datasets
|
||||
},
|
||||
options: {
|
||||
// hitRadius: 8,
|
||||
maintainAspectRatio: false,
|
||||
tooltips: {
|
||||
callbacks: {
|
||||
title: (tooltipItem, data) => `Watch date: ${tooltipItem[0].label}`,
|
||||
label: (tooltipItem, data) => {
|
||||
let label = data.datasets[tooltipItem.datasetIndex].label
|
||||
let value = tooltipItem.value;
|
||||
let text = 'Duration watched'
|
||||
|
||||
const context = label.split(' ')[0]
|
||||
if (context) {
|
||||
text = `${context} ${this.selectedChartType.tooltipLabel.toLowerCase()}`
|
||||
}
|
||||
|
||||
if (this.selectedChartType.valueConvertFunction) {
|
||||
value = this.selectedChartType.valueConvertFunction(tooltipItem.value)
|
||||
}
|
||||
|
||||
return ` ${text}: ${value}`
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
yAxes: [{
|
||||
gridLines: {
|
||||
color: this.gridColor
|
||||
},
|
||||
stacked: chart.graphType === 'bar',
|
||||
ticks: {
|
||||
// suggestedMax: 10000,
|
||||
callback: (value, index, values) => {
|
||||
if (this.selectedChartType.valueConvertFunction) {
|
||||
return this.selectedChartType.valueConvertFunction(value, values)
|
||||
}
|
||||
return value
|
||||
},
|
||||
beginAtZero: true
|
||||
}
|
||||
}],
|
||||
xAxes: [{
|
||||
stacked: chart.graphType === 'bar',
|
||||
gridLines: {
|
||||
display: false,
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
chart.data = chartInstance;
|
||||
},
|
||||
convertSecondsToHumanReadable(value, values=null) {
|
||||
const highestValue = values ? values[0] : value;
|
||||
|
||||
// minutes
|
||||
if (highestValue < 3600) {
|
||||
const minutes = Math.floor(value / 60);
|
||||
|
||||
value = `${minutes} m`
|
||||
}
|
||||
// hours and minutes
|
||||
else if (highestValue > 3600 && highestValue < 86400) {
|
||||
const hours = Math.floor(value / 3600);
|
||||
const minutes = Math.floor(value % 3600 / 60);
|
||||
|
||||
value = hours != 0 ? `${hours} h ${minutes} m` : `${minutes} m`
|
||||
}
|
||||
// days and hours
|
||||
else if (highestValue > 86400 && highestValue < 31557600) {
|
||||
const days = Math.floor(value / 86400);
|
||||
const hours = Math.floor(value % 86400 / 3600);
|
||||
|
||||
value = days != 0 ? `${days} d ${hours} h` : `${hours} h`
|
||||
}
|
||||
// years and days
|
||||
else if (highestValue > 31557600) {
|
||||
const years = Math.floor(value / 31557600);
|
||||
const days = Math.floor(value % 31557600 / 86400);
|
||||
|
||||
value = years != 0 ? `${years} y ${days} d` : `${days} d`
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "./src/scss/variables";
|
||||
|
||||
.wrapper {
|
||||
padding: 2rem;
|
||||
|
||||
@include mobile-only {
|
||||
padding: 0 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
.filter {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
margin-bottom: 2rem;
|
||||
|
||||
h2 {
|
||||
margin-bottom: 0.5rem;
|
||||
width: 100%;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
|
||||
&-item:not(:first-of-type) {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.dayinput {
|
||||
font-size: 1.2rem;
|
||||
max-width: 3rem;
|
||||
background-color: $background-ui;
|
||||
color: $text-color;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-section {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.chart {
|
||||
position: relative;
|
||||
height: 35vh;
|
||||
width: 90vw;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.chart-header {
|
||||
font-weight: 300;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
44
src/components/CastList.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<div class="cast">
|
||||
<ol class="persons">
|
||||
<CastListItem v-for="person in cast" :person="person" :key="person.id" />
|
||||
</ol>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CastListItem from "src/components/CastListItem";
|
||||
|
||||
export default {
|
||||
name: "CastList",
|
||||
components: { CastListItem },
|
||||
props: {
|
||||
cast: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.cast {
|
||||
position: relative;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
ol {
|
||||
overflow-x: scroll;
|
||||
padding: 0;
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
|
||||
scrollbar-width: none; /* for Firefox */
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none; /* for Chrome, Safari, and Opera */
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
121
src/components/CastListItem.vue
Normal file
@@ -0,0 +1,121 @@
|
||||
<template>
|
||||
<li class="card">
|
||||
<a @click="openCastItem">
|
||||
<img class="persons--image" :src="pictureUrl" />
|
||||
<p class="name">{{ person.name || person.title }}</p>
|
||||
<p class="meta">{{ person.character || person.year }}</p>
|
||||
</a>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions } from "vuex";
|
||||
|
||||
export default {
|
||||
name: "CastListItem",
|
||||
props: {
|
||||
person: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions("popup", ["open"]),
|
||||
openCastItem() {
|
||||
let { id, type } = this.person;
|
||||
|
||||
if (type) {
|
||||
this.open({ id, type });
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
pictureUrl() {
|
||||
const { profile_path, poster_path, poster } = this.person;
|
||||
if (profile_path) return "https://image.tmdb.org/t/p/w185" + profile_path;
|
||||
else if (poster_path)
|
||||
return "https://image.tmdb.org/t/p/w185" + poster_path;
|
||||
else if (poster) return "https://image.tmdb.org/t/p/w185" + poster;
|
||||
|
||||
return "/assets/no-image_small.svg";
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
li a p:first-of-type {
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
li.card p {
|
||||
font-size: 1em;
|
||||
padding: 0 10px;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-height: calc(10px + ((16px * var(--line-height)) * 3));
|
||||
}
|
||||
|
||||
li.card {
|
||||
margin: 10px;
|
||||
margin-right: 4px;
|
||||
padding-bottom: 10px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
|
||||
min-width: 140px;
|
||||
width: 140px;
|
||||
background-color: var(--background-color-secondary);
|
||||
color: var(--text-color);
|
||||
|
||||
transition: all 0.3s ease;
|
||||
transform: scale(0.97) translateZ(0);
|
||||
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
|
||||
&:first-of-type {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
|
||||
transform: scale(1.03);
|
||||
}
|
||||
|
||||
.name {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.character {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.meta {
|
||||
font-size: 0.9em;
|
||||
color: var(--text-color-70);
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
// margin-top: auto;
|
||||
max-height: calc((0.9em * var(--line-height)) * 1);
|
||||
}
|
||||
|
||||
a {
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-height: 210px;
|
||||
background-color: var(--background-color);
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,87 +0,0 @@
|
||||
<template>
|
||||
<section>
|
||||
<LandingBanner />
|
||||
|
||||
<div v-for="list in lists">
|
||||
<list-header :title="list.title" :link="'/list/' + list.route" />
|
||||
|
||||
<results-list :results="list.data" :shortList="true" />
|
||||
<loader v-if="!list.data.length" />
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import LandingBanner from "@/components/LandingBanner";
|
||||
import ListHeader from "@/components/ListHeader";
|
||||
import ResultsList from "@/components/ResultsList";
|
||||
import Loader from "@/components/ui/Loader";
|
||||
|
||||
import { getTmdbMovieListByName, getRequests } from "@/api";
|
||||
|
||||
export default {
|
||||
name: "home",
|
||||
components: { LandingBanner, ResultsList, ListHeader, Loader },
|
||||
data() {
|
||||
return {
|
||||
imageFile: "/pulp-fiction.jpg",
|
||||
requests: [],
|
||||
nowplaying: [],
|
||||
upcoming: [],
|
||||
popular: []
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
lists() {
|
||||
return [
|
||||
{
|
||||
title: "Requests",
|
||||
route: "request",
|
||||
data: this.requests
|
||||
},
|
||||
{
|
||||
title: "Now playing",
|
||||
route: "now_playing",
|
||||
data: this.nowplaying
|
||||
},
|
||||
{
|
||||
title: "Upcoming",
|
||||
route: "upcoming",
|
||||
data: this.upcoming
|
||||
},
|
||||
{
|
||||
title: "Popular",
|
||||
route: "popular",
|
||||
data: this.popular
|
||||
}
|
||||
];
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
fetchRequests() {
|
||||
getRequests().then(results => (this.requests = results.results));
|
||||
},
|
||||
fetchNowPlaying() {
|
||||
getTmdbMovieListByName("now_playing").then(
|
||||
results => (this.nowplaying = results.results)
|
||||
);
|
||||
},
|
||||
fetchUpcoming() {
|
||||
getTmdbMovieListByName("upcoming").then(
|
||||
results => (this.upcoming = results.results)
|
||||
);
|
||||
},
|
||||
fetchPopular() {
|
||||
getTmdbMovieListByName("popular").then(
|
||||
results => (this.popular = results.results)
|
||||
);
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.fetchRequests();
|
||||
this.fetchNowPlaying();
|
||||
this.fetchUpcoming();
|
||||
this.fetchPopular();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -1,14 +1,28 @@
|
||||
<template>
|
||||
<header v-bind:style="{ 'background-image': 'url(' + imageFile + ')' }">
|
||||
<header
|
||||
:class="{ expanded, noselect: true }"
|
||||
v-bind:style="{ 'background-image': 'url(' + imageFile + ')' }"
|
||||
>
|
||||
<div class="container">
|
||||
<h1 class="title">Request new movies or tv shows for plex</h1>
|
||||
<strong class="subtitle">Made with Vue.js</strong>
|
||||
<h1 class="title">Request movies or tv shows</h1>
|
||||
<strong class="subtitle"
|
||||
>Create a profile to track and view requests</strong
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="expand-icon" @click="expanded = !expanded">
|
||||
<IconExpand v-if="!expanded" />
|
||||
<IconShrink v-else />
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import IconExpand from "../icons/IconExpand.vue";
|
||||
import IconShrink from "../icons/IconShrink.vue";
|
||||
|
||||
export default {
|
||||
components: { IconExpand, IconShrink },
|
||||
props: {
|
||||
image: {
|
||||
type: String,
|
||||
@@ -17,24 +31,35 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
imageFile: "/pulp-fiction.jpg"
|
||||
images: [
|
||||
"pulp-fiction.jpg",
|
||||
"arrival.jpg",
|
||||
"dune.jpg",
|
||||
"mandalorian.jpg"
|
||||
],
|
||||
imageFile: undefined,
|
||||
expanded: false
|
||||
};
|
||||
},
|
||||
beforeMount() {
|
||||
if (this.image && this.image.length > 0) {
|
||||
this.imageFile = this.image;
|
||||
} else {
|
||||
this.imageFile = `/assets/${
|
||||
this.images[Math.floor(Math.random() * this.images.length)]
|
||||
}`;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "./src/scss/variables";
|
||||
@import "./src/scss/media-queries";
|
||||
@import "src/scss/variables";
|
||||
@import "src/scss/media-queries";
|
||||
|
||||
header {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
height: 25vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -43,8 +68,48 @@ header {
|
||||
background-position: 50% 50%;
|
||||
position: relative;
|
||||
|
||||
@include tablet-min {
|
||||
height: 284px;
|
||||
&.expanded {
|
||||
height: calc(100vh - var(--header-size));
|
||||
width: calc(100vw - var(--header-size));
|
||||
|
||||
@include mobile {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
&:before {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.title,
|
||||
.subtitle {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.expand-icon {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
transition: all 0.5s ease-in-out;
|
||||
height: 1.8rem;
|
||||
width: 1.8rem;
|
||||
fill: var(--text-color-50);
|
||||
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 1rem;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
fill: var(--text-color-90);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.expand-icon {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&:before {
|
||||
@@ -54,8 +119,8 @@ header {
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: $background-70;
|
||||
transition: background-color 0.5s ease;
|
||||
background-color: var(--background-70);
|
||||
transition: inherit;
|
||||
}
|
||||
|
||||
.container {
|
||||
@@ -71,9 +136,10 @@ header {
|
||||
letter-spacing: 0.5px;
|
||||
color: $text-color;
|
||||
margin: 0;
|
||||
opacity: 1;
|
||||
|
||||
@include tablet-min {
|
||||
font-size: 28px;
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,9 +149,10 @@ header {
|
||||
font-weight: 300;
|
||||
color: $text-color-70;
|
||||
margin: 5px 0;
|
||||
opacity: 1;
|
||||
|
||||
@include tablet-min {
|
||||
font-size: 16px;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,25 @@
|
||||
<template>
|
||||
<header :class="{ 'sticky': sticky }">
|
||||
<h2>{{ title }}</h2>
|
||||
<header>
|
||||
<h2>{{ prettify }}</h2>
|
||||
<h3>{{ subtitle }}</h3>
|
||||
|
||||
<div v-if="info instanceof Array" class="flex flex-direction-column">
|
||||
<span v-for="item in info" class="info">{{ item }}</span>
|
||||
</div>
|
||||
<span v-else class="info">{{ info }}</span>
|
||||
<router-link v-if="link" :to="link" class='view-more' :aria-label="`View all ${title}`">
|
||||
<router-link
|
||||
v-if="shortList"
|
||||
:to="urlify"
|
||||
class="view-more"
|
||||
:aria-label="`View all ${title}`"
|
||||
>
|
||||
View All
|
||||
</router-link>
|
||||
</header>
|
||||
|
||||
<div v-else-if="info">
|
||||
<div v-if="info instanceof Array" class="flex flex-direction-column">
|
||||
<span v-for="item in info" :key="item" class="info">{{ item }}</span>
|
||||
</div>
|
||||
|
||||
<span v-else class="info">{{ info }}</span>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -19,10 +29,10 @@ export default {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
sticky: {
|
||||
type: Boolean,
|
||||
subtitle: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: true
|
||||
default: null
|
||||
},
|
||||
info: {
|
||||
type: [String, Array],
|
||||
@@ -31,34 +41,43 @@ export default {
|
||||
link: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
shortList: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
urlify: function () {
|
||||
return `/list/${this.title.toLowerCase().replace(" ", "_")}`;
|
||||
},
|
||||
prettify: function () {
|
||||
return this.title.includes("_")
|
||||
? this.title.split("_").join(" ")
|
||||
: this.title;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import './src/scss/variables';
|
||||
@import './src/scss/media-queries';
|
||||
@import './src/scss/main';
|
||||
@import "src/scss/variables";
|
||||
@import "src/scss/media-queries";
|
||||
@import "src/scss/main";
|
||||
|
||||
header {
|
||||
width: 100%;
|
||||
min-height: 80px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-left: 0.75rem;
|
||||
padding-right: 0.75rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background-color: $background-color;
|
||||
|
||||
&.sticky {
|
||||
background-color: $background-color;
|
||||
|
||||
position: sticky;
|
||||
position: -webkit-sticky;
|
||||
top: $header-size;
|
||||
z-index: 4;
|
||||
}
|
||||
position: sticky;
|
||||
position: -webkit-sticky;
|
||||
top: $header-size;
|
||||
z-index: 1;
|
||||
|
||||
h2 {
|
||||
font-size: 1.4rem;
|
||||
@@ -72,16 +91,16 @@ header {
|
||||
.view-more {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 300;
|
||||
letter-spacing: .5px;
|
||||
letter-spacing: 0.5px;
|
||||
color: $text-color-70;
|
||||
text-decoration: none;
|
||||
transition: color .5s ease;
|
||||
transition: color 0.5s ease;
|
||||
cursor: pointer;
|
||||
|
||||
&:after{
|
||||
&:after {
|
||||
content: " →";
|
||||
}
|
||||
&:hover{
|
||||
&:hover {
|
||||
color: $text-color;
|
||||
}
|
||||
}
|
||||
@@ -89,18 +108,18 @@ header {
|
||||
.info {
|
||||
font-size: 13px;
|
||||
font-weight: 300;
|
||||
letter-spacing: .5px;
|
||||
letter-spacing: 0.5px;
|
||||
color: $text-color;
|
||||
text-decoration: none;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
@include tablet-min {
|
||||
padding-left: 1.25rem;;
|
||||
padding-left: 1.25rem;
|
||||
}
|
||||
|
||||
@include desktop-lg-min {
|
||||
padding-left: 1.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<list-header :title="listTitle" :info="info" :sticky="true" />
|
||||
|
||||
<results-list :results="results" v-if="results" />
|
||||
|
||||
<loader v-if="!results.length" />
|
||||
|
||||
<div v-if="page < totalPages" class="fullwidth-button">
|
||||
<seasoned-button @click="loadMore">load more</seasoned-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
import ListHeader from '@/components/ListHeader'
|
||||
import ResultsList from '@/components/ResultsList'
|
||||
import SeasonedButton from '@/components/ui/SeasonedButton'
|
||||
import Loader from '@/components/ui/Loader'
|
||||
import { getTmdbMovieListByName, getRequests } from '@/api'
|
||||
import store from '@/store'
|
||||
|
||||
export default {
|
||||
components: { ListHeader, ResultsList, SeasonedButton, Loader },
|
||||
data() {
|
||||
return {
|
||||
legalTmdbLists: [ 'now_playing', 'upcoming', 'popular' ],
|
||||
results: [],
|
||||
page: 1,
|
||||
totalPages: 0,
|
||||
totalResults: 0,
|
||||
loading: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
listTitle() {
|
||||
if (this.results.length === 0)
|
||||
return ''
|
||||
|
||||
const routeListName = this.$route.params.name
|
||||
console.log('routelistname', routeListName)
|
||||
return routeListName.includes('_') ? routeListName.split('_').join(' ') : routeListName
|
||||
},
|
||||
info() {
|
||||
if (this.results.length === 0)
|
||||
return [null, null]
|
||||
return [this.pageCount, this.resultCount]
|
||||
},
|
||||
resultCount() {
|
||||
const loadedResults = this.results.length
|
||||
const totalResults = this.totalResults < 10000 ? this.totalResults : '∞'
|
||||
return `${loadedResults} of ${totalResults} results`
|
||||
},
|
||||
pageCount() {
|
||||
return `Page ${this.page} of ${this.totalPages}`
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
loadMore() {
|
||||
console.log(this.$route)
|
||||
this.loading = true;
|
||||
this.page++
|
||||
|
||||
window.history.replaceState({}, 'search', `/#/${this.$route.fullPath}?page=${this.page}`)
|
||||
this.init()
|
||||
},
|
||||
init() {
|
||||
const routeListName = this.$route.params.name
|
||||
|
||||
if (routeListName === 'request') {
|
||||
getRequests(this.page)
|
||||
.then(results => {
|
||||
this.results = this.results.concat(...results.results)
|
||||
this.page = results.page
|
||||
this.totalPages = results.total_pages
|
||||
this.totalResults = results.total_results
|
||||
})
|
||||
} else if (this.legalTmdbLists.includes(routeListName)) {
|
||||
getTmdbMovieListByName(routeListName, this.page)
|
||||
.then(results => {
|
||||
this.results = this.results.concat(...results.results)
|
||||
this.page = results.page
|
||||
this.totalPages = results.total_pages
|
||||
this.totalResults = results.total_results
|
||||
})
|
||||
} else {
|
||||
// TODO handle if list is not found
|
||||
console.log('404 this is not a tmdb list')
|
||||
}
|
||||
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
created() {
|
||||
if (this.results.length === 0)
|
||||
this.init()
|
||||
|
||||
store.dispatch('documentTitle/updateTitle', `${this.$router.history.current.name} ${this.$route.params.name}`)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.fullwidth-button {
|
||||
width: 100%;
|
||||
margin: 1rem 0;
|
||||
padding-bottom: 2rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
@@ -1,12 +0,0 @@
|
||||
<template>
|
||||
<div class="container info">
|
||||
<movie :id="$route.params.id" :type="'page'"></movie>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Movie from './Movie';
|
||||
export default {
|
||||
components: { Movie }
|
||||
}
|
||||
</script>
|
||||
@@ -1,104 +0,0 @@
|
||||
<template>
|
||||
<div class="movie-popup" @click="$popup.close()">
|
||||
<div class="movie-popup__box" @click.stop>
|
||||
<movie :id="id" :type="type"></movie>
|
||||
<button class="movie-popup__close" @click="$popup.close()"></button>
|
||||
</div>
|
||||
<i class="loader"></i>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Movie from './Movie';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
id: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
components: { Movie },
|
||||
methods: {
|
||||
checkEventForEscapeKey(event) {
|
||||
if (event.keyCode == 27) {
|
||||
this.$popup.close()
|
||||
}
|
||||
}
|
||||
},
|
||||
created(){
|
||||
window.addEventListener('keyup', this.checkEventForEscapeKey)
|
||||
document.getElementsByTagName("body")[0].classList += " no-scroll";
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('keyup', this.checkEventForEscapeKey)
|
||||
document.getElementsByTagName("body")[0].classList.remove("no-scroll");
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "./src/scss/variables";
|
||||
@import "./src/scss/media-queries";
|
||||
|
||||
.movie-popup{
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 20;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
background: rgba($dark, 0.93);
|
||||
-webkit-overflow-scrolling: touch;
|
||||
overflow: auto;
|
||||
&__box{
|
||||
width: 100%;
|
||||
max-width: 768px;
|
||||
position: relative;
|
||||
z-index: 5;
|
||||
background: $background-color-secondary;
|
||||
padding-bottom: 50px;
|
||||
@include tablet-min{
|
||||
padding-bottom: 0;
|
||||
margin: 40px auto;
|
||||
}
|
||||
}
|
||||
&__close{
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
transition: background 0.5s ease;
|
||||
cursor: pointer;
|
||||
&:before,
|
||||
&:after{
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 19px;
|
||||
left: 10px;
|
||||
width: 20px;
|
||||
height: 2px;
|
||||
background: $white;
|
||||
}
|
||||
&:before{
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
&:after{
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
&:hover{
|
||||
background: $green;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,249 +0,0 @@
|
||||
<template>
|
||||
<li class="movie-item" :class="{ shortList: shortList }">
|
||||
<figure class="movie-item__poster">
|
||||
<img
|
||||
class="movie-item__img"
|
||||
ref="poster-image"
|
||||
@click="openMoviePopup(movie.id, movie.type)"
|
||||
:alt="posterAltText"
|
||||
:data-src="poster"
|
||||
src="~assets/placeholder.png"
|
||||
/>
|
||||
|
||||
<div v-if="movie.download" class="progress">
|
||||
<progress :value="movie.download.progress" max="100"></progress>
|
||||
<span>{{ movie.download.state }}: {{ movie.download.progress }}%</span>
|
||||
</div>
|
||||
</figure>
|
||||
|
||||
<div class="movie-item__info">
|
||||
<p v-if="movie.title || movie.name">{{ movie.title || movie.name }}</p>
|
||||
<p v-if="movie.year">{{ movie.year }}</p>
|
||||
<p v-if="movie.type == 'person'">
|
||||
Known for: {{ movie.known_for_department }}
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import img from "../directives/v-image";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
movie: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
shortList: {
|
||||
type: Boolean,
|
||||
required: false
|
||||
}
|
||||
},
|
||||
directives: {
|
||||
img: img
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
poster: undefined,
|
||||
observed: false,
|
||||
posterSizes: [
|
||||
{
|
||||
id: "w500",
|
||||
minWidth: 500
|
||||
},
|
||||
{
|
||||
id: "w342",
|
||||
minWidth: 342
|
||||
},
|
||||
{
|
||||
id: "w185",
|
||||
minWidth: 185
|
||||
},
|
||||
{
|
||||
id: "w154",
|
||||
minWidth: 0
|
||||
}
|
||||
]
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
posterAltText: function () {
|
||||
const type = this.movie.type || "";
|
||||
const title = this.movie.title || this.movie.name;
|
||||
return this.movie.poster
|
||||
? `Poster for ${type} ${title}`
|
||||
: `Missing image for ${type} ${title}`;
|
||||
}
|
||||
},
|
||||
beforeMount() {
|
||||
if (this.movie.poster != null) {
|
||||
this.poster = "https://image.tmdb.org/t/p/w500" + this.movie.poster;
|
||||
} else {
|
||||
this.poster = "/no-image.png";
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
const poster = this.$refs["poster-image"];
|
||||
if (poster == null) return;
|
||||
|
||||
const imageObserver = new IntersectionObserver((entries, imgObserver) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting && this.observed == false) {
|
||||
const lazyImage = entry.target;
|
||||
lazyImage.src = lazyImage.dataset.src;
|
||||
lazyImage.className = lazyImage.className + " is-loaded";
|
||||
this.observed = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
imageObserver.observe(poster);
|
||||
},
|
||||
methods: {
|
||||
openMoviePopup(id, type) {
|
||||
this.$popup.open(id, type);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "./src/scss/variables";
|
||||
@import "./src/scss/media-queries";
|
||||
@import "./src/scss/main";
|
||||
|
||||
.movie-item {
|
||||
padding: 10px;
|
||||
width: 50%;
|
||||
background-color: $background-color;
|
||||
transition: background-color 0.5s ease;
|
||||
|
||||
@include tablet-min {
|
||||
padding: 15px;
|
||||
width: 33%;
|
||||
}
|
||||
@include tablet-landscape-min {
|
||||
padding: 15px;
|
||||
width: 25%;
|
||||
}
|
||||
@include desktop-min {
|
||||
padding: 15px;
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
@include desktop-lg-min {
|
||||
padding: 15px;
|
||||
width: 12.5%;
|
||||
}
|
||||
|
||||
&:hover &__info > p {
|
||||
color: $text-color;
|
||||
}
|
||||
|
||||
&__poster {
|
||||
text-decoration: none;
|
||||
color: $text-color-70;
|
||||
font-weight: 300;
|
||||
|
||||
> img {
|
||||
width: 100%;
|
||||
opacity: 0;
|
||||
transform: scale(0.97) translateZ(0);
|
||||
transition: opacity 1s ease, transform 0.5s ease;
|
||||
&.is-loaded {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.03);
|
||||
box-shadow: 0 0 10px rgba($dark, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__info {
|
||||
padding-top: 15px;
|
||||
font-weight: 300;
|
||||
|
||||
> p {
|
||||
color: $text-color-70;
|
||||
margin: 0;
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.5px;
|
||||
transition: color 0.5s ease;
|
||||
cursor: pointer;
|
||||
@include mobile-ls-min {
|
||||
font-size: 12px;
|
||||
}
|
||||
@include tablet-min {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.no-image {
|
||||
background-color: var(--text-color);
|
||||
color: var(--background-color);
|
||||
width: 100%;
|
||||
height: 383px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
span {
|
||||
font-size: 1.5rem;
|
||||
width: 70%;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "./src/scss/variables";
|
||||
|
||||
.progress {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin-bottom: 0.8rem;
|
||||
|
||||
> progress {
|
||||
width: 95%;
|
||||
}
|
||||
|
||||
> span {
|
||||
position: absolute;
|
||||
font-size: 1rem;
|
||||
line-height: 1.4rem;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
progress {
|
||||
border-radius: 4px;
|
||||
height: 1.4rem;
|
||||
}
|
||||
progress::-webkit-progress-bar {
|
||||
background-color: rgba($black, 0.55);
|
||||
border-radius: 4px;
|
||||
}
|
||||
progress::-webkit-progress-value {
|
||||
background-color: $green-70;
|
||||
border-radius: 4px;
|
||||
}
|
||||
progress::-moz-progress-bar {
|
||||
/* style rules */
|
||||
background-color: green;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,314 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<nav class="nav">
|
||||
<router-link class="nav__logo" :to="{name: 'home'}" exact title="Vue.js — TMDb App">
|
||||
<svg class="nav__logo-image">
|
||||
<use xlink:href="#svgLogo"></use>
|
||||
</svg>
|
||||
</router-link>
|
||||
|
||||
<div class="nav__hamburger" @click="toggleNav">
|
||||
<div v-for="_ in 3" class="bar"></div>
|
||||
</div>
|
||||
|
||||
<ul class="nav__list">
|
||||
<li class="nav__item" v-for="item in listTypes">
|
||||
<router-link class="nav__link" :to="'/list/' + item.route">
|
||||
<div class="nav__link-wrap">
|
||||
<svg class="nav__link-icon">
|
||||
<use :xlink:href="'#icon_' + item.route"></use>
|
||||
</svg>
|
||||
<span class="nav__link-title">{{ item.title }}</span>
|
||||
</div>
|
||||
</router-link>
|
||||
</li>
|
||||
|
||||
<li class="nav__item nav__item--profile">
|
||||
<router-link class="nav__link nav__link--profile" :to="{name: 'signin'}" v-if="!userLoggedIn">
|
||||
<div class="nav__link-wrap">
|
||||
<svg class="nav__link-icon">
|
||||
<use xlink:href="#iconLogin"></use>
|
||||
</svg>
|
||||
<span class="nav__link-title">Sign in</span>
|
||||
</div>
|
||||
</router-link>
|
||||
|
||||
<router-link class="nav__link nav__link--profile" :to="{name: 'profile'}" v-if="userLoggedIn">
|
||||
<div class="nav__link-wrap">
|
||||
<svg class="nav__link-icon">
|
||||
<use xlink:href="#iconLogin"></use>
|
||||
</svg>
|
||||
<span class="nav__link-title">Profile</span>
|
||||
</div>
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<div class="spacer"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import storage from '@/storage'
|
||||
|
||||
export default {
|
||||
data(){
|
||||
return {
|
||||
listTypes: storage.homepageLists,
|
||||
userLoggedIn: localStorage.getItem('token') ? true : false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setUserStatus(){
|
||||
this.userLoggedIn = localStorage.getItem('token') ? true : false;
|
||||
},
|
||||
toggleNav(){
|
||||
document.querySelector('.nav__hamburger').classList.toggle('nav__hamburger--active');
|
||||
document.querySelector('.nav__list').classList.toggle('nav__list--active');
|
||||
}
|
||||
},
|
||||
created(){
|
||||
// TODO move this to state manager
|
||||
eventHub.$on('setUserStatus', this.setUserStatus);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "./src/scss/variables";
|
||||
@import "./src/scss/media-queries";
|
||||
|
||||
.icon {
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
@include mobile-only {
|
||||
width: 100%;
|
||||
height: $header-size;
|
||||
}
|
||||
}
|
||||
|
||||
.nav {
|
||||
transition: background .5s ease;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
z-index: 10;
|
||||
display: block;
|
||||
color: $text-color;
|
||||
background-color: $background-color-secondary;
|
||||
|
||||
@include tablet-min{
|
||||
width: 95px;
|
||||
height: 100vh;
|
||||
}
|
||||
&__logo {
|
||||
width: 55px;
|
||||
height: $header-size;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: $background-nav-logo;
|
||||
@include tablet-min{
|
||||
width: 95px;
|
||||
}
|
||||
&-image{
|
||||
width: 35px;
|
||||
height: 31px;
|
||||
fill: $green;
|
||||
transition: transform 0.5s ease;
|
||||
@include tablet-min{
|
||||
width: 45px;
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
&:hover &-image {
|
||||
transform: scale(1.04);
|
||||
}
|
||||
}
|
||||
&__hamburger {
|
||||
display: block;
|
||||
position: fixed;
|
||||
width: 55px;
|
||||
height: 50px;
|
||||
top: 0;
|
||||
right: 0;
|
||||
cursor: pointer;
|
||||
z-index: 10;
|
||||
border-left: 1px solid $background-color;
|
||||
@include tablet-min{
|
||||
display: none;
|
||||
}
|
||||
.bar {
|
||||
position: absolute;
|
||||
width: 23px;
|
||||
height: 1px;
|
||||
background-color: $text-color-70;
|
||||
transition: all 300ms ease;
|
||||
&:nth-child(1) {
|
||||
left: 16px;
|
||||
top: 17px;
|
||||
}
|
||||
&:nth-child(2) {
|
||||
left: 16px;
|
||||
top: 25px;
|
||||
&:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
width: 23px;
|
||||
height: 1px;
|
||||
transition: all 300ms ease;
|
||||
}
|
||||
}
|
||||
&:nth-child(3) {
|
||||
right: 15px;
|
||||
top: 33px;
|
||||
}
|
||||
}
|
||||
&--active {
|
||||
.bar{
|
||||
&:nth-child(1),
|
||||
&:nth-child(3){
|
||||
width: 0;
|
||||
}
|
||||
&:nth-child(2) {
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
&:nth-child(2):after {
|
||||
transform: rotate(-90deg);
|
||||
// background: rgba($c-dark, 0.5);
|
||||
background-color: $text-color-70;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&__list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 50px;
|
||||
border-top: 1px solid $background-color;
|
||||
@include mobile-only {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
font-size: 0;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
background-color: $background-95;
|
||||
text-align: left;
|
||||
&--active{
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
@include tablet-min {
|
||||
display: flex;
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 100%;
|
||||
border-top: 0;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
&__item {
|
||||
transition: background .5s ease, color .5s ease, border .5s ease;
|
||||
background-color: $background-color-secondary;
|
||||
color: $text-color-70;
|
||||
|
||||
@include mobile-only {
|
||||
flex: 0 0 50%;
|
||||
text-align: center;
|
||||
border-bottom: 1px solid $background-color;
|
||||
&:nth-child(odd){
|
||||
border-right: 1px solid $background-color;
|
||||
|
||||
&:last-child {
|
||||
// flex: 0 0 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
@include tablet-min {
|
||||
width: 100%;
|
||||
border-bottom: 1px solid $text-color-5;
|
||||
|
||||
&--profile {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
top: 0;
|
||||
width: $header-size;
|
||||
height: $header-size;
|
||||
border-bottom: 0;
|
||||
border-left: 1px solid $background-color;
|
||||
}
|
||||
}
|
||||
&:hover, .is-active {
|
||||
color: $text-color;
|
||||
background-color: $background-color;
|
||||
}
|
||||
}
|
||||
&__link {
|
||||
background-color: inherit; // a elements have a transparent background
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 7px;
|
||||
font-weight: 300;
|
||||
text-decoration: none;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
&-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@include mobile-only {
|
||||
font-size: 10px;
|
||||
padding: 20px 0;
|
||||
}
|
||||
@include tablet-min {
|
||||
width: 95px;
|
||||
height: 95px;
|
||||
font-size: 9px;
|
||||
&--profile {
|
||||
width: 75px;
|
||||
height: 75px;
|
||||
background-color: $background-color-secondary;
|
||||
}
|
||||
}
|
||||
&-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
fill: $text-color-70;
|
||||
@include tablet-min {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
}
|
||||
&-title {
|
||||
margin-top: 5px;
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
&:hover &-icon, &.is-active &-icon {
|
||||
fill: $text-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,63 +0,0 @@
|
||||
<template>
|
||||
<div class="persons">
|
||||
<div class="persons--image" :style="{
|
||||
'background-image': 'url(' + getPicture(info) + ')' }"></div>
|
||||
<span>{{ info.name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'Person',
|
||||
components: {
|
||||
|
||||
},
|
||||
props: {
|
||||
info: Object
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
}
|
||||
},
|
||||
created() {},
|
||||
beforeMount() {},
|
||||
computed: {
|
||||
|
||||
},
|
||||
methods: {
|
||||
getPicture: (person) => {
|
||||
if (person)
|
||||
return 'https://image.tmdb.org/t/p/w185' + person.profile_path;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.persons {
|
||||
display: flex;
|
||||
// border: 1px solid black;
|
||||
flex-direction: column;
|
||||
|
||||
margin: 0 0.5rem;
|
||||
|
||||
span {
|
||||
font-size: 0.6rem;
|
||||
}
|
||||
|
||||
&--image {
|
||||
border-radius: 50%;
|
||||
height: 70px;
|
||||
width: 70px;
|
||||
// height: auto;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: 50% 50%;
|
||||
}
|
||||
|
||||
&--name {
|
||||
}
|
||||
}
|
||||
</style>
|
||||
125
src/components/Popup.vue
Normal file
@@ -0,0 +1,125 @@
|
||||
<template>
|
||||
<div v-if="isOpen" class="movie-popup" @click="close">
|
||||
<div class="movie-popup__box" @click.stop>
|
||||
<person v-if="type === 'person'" :id="id" type="person" />
|
||||
<movie v-else :id="id" :type="type"></movie>
|
||||
<button class="movie-popup__close" @click="close"></button>
|
||||
</div>
|
||||
<i class="loader"></i>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapGetters } from "vuex";
|
||||
import Movie from "@/components/popup/Movie";
|
||||
import Person from "@/components/popup/Person";
|
||||
|
||||
export default {
|
||||
components: { Movie, Person },
|
||||
computed: {
|
||||
...mapGetters("popup", ["isOpen", "id", "type"])
|
||||
},
|
||||
watch: {
|
||||
isOpen(value) {
|
||||
value
|
||||
? document.getElementsByTagName("body")[0].classList.add("no-scroll")
|
||||
: document
|
||||
.getElementsByTagName("body")[0]
|
||||
.classList.remove("no-scroll");
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions("popup", ["close", "open"]),
|
||||
checkEventForEscapeKey(event) {
|
||||
if (event.keyCode == 27) this.close();
|
||||
}
|
||||
},
|
||||
created() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
let id = null;
|
||||
let type = null;
|
||||
|
||||
if (params.has("movie")) {
|
||||
id = Number(params.get("movie"));
|
||||
type = "movie";
|
||||
} else if (params.has("show")) {
|
||||
id = Number(params.get("show"));
|
||||
type = "show";
|
||||
} else if (params.has("person")) {
|
||||
id = Number(params.get("person"));
|
||||
type = "person";
|
||||
}
|
||||
|
||||
if (id && type) {
|
||||
this.open({ id, type });
|
||||
}
|
||||
|
||||
window.addEventListener("keyup", this.checkEventForEscapeKey);
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener("keyup", this.checkEventForEscapeKey);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "src/scss/variables";
|
||||
@import "src/scss/media-queries";
|
||||
|
||||
.movie-popup {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 20;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba($dark, 0.93);
|
||||
-webkit-overflow-scrolling: touch;
|
||||
overflow: auto;
|
||||
|
||||
&__box {
|
||||
max-width: 768px;
|
||||
position: relative;
|
||||
z-index: 5;
|
||||
margin: 8vh auto;
|
||||
|
||||
@include mobile {
|
||||
margin: 0 0 50px 0;
|
||||
}
|
||||
}
|
||||
&__close {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
transition: background 0.5s ease;
|
||||
cursor: pointer;
|
||||
z-index: 5;
|
||||
|
||||
&:before,
|
||||
&:after {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 19px;
|
||||
left: 10px;
|
||||
width: 20px;
|
||||
height: 2px;
|
||||
background: $white;
|
||||
}
|
||||
&:before {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
&:after {
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
&:hover {
|
||||
background: $green;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,150 +0,0 @@
|
||||
<template>
|
||||
<section class="profile">
|
||||
<div class="profile__content" v-if="userLoggedIn">
|
||||
<header class="profile__header">
|
||||
<h2 class="profile__title">{{ emoji }} Welcome {{ username }}</h2>
|
||||
|
||||
<div class="button--group">
|
||||
<seasoned-button @click="toggleSettings">{{ showSettings ? 'hide settings' : 'show settings' }}</seasoned-button>
|
||||
|
||||
<seasoned-button @click="logOut">Log out</seasoned-button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<settings v-if="showSettings"></settings>
|
||||
|
||||
<list-header title="User requests" :info="resultCount" />
|
||||
<results-list v-if="results" :results="results" />
|
||||
</div>
|
||||
|
||||
<section class="not-found" v-if="!userLoggedIn">
|
||||
<div class="not-found__content">
|
||||
<h2 class="not-found__title">Authentication Request Failed</h2>
|
||||
<router-link :to="{name: 'signin'}" exact title="Sign in here">
|
||||
<button class="not-found__button button">Sign In</button>
|
||||
</router-link>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import storage from '@/storage'
|
||||
import store from '@/store'
|
||||
import ListHeader from '@/components/ListHeader'
|
||||
import ResultsList from '@/components/ResultsList'
|
||||
import Settings from '@/components/Settings'
|
||||
import SeasonedButton from '@/components/ui/SeasonedButton'
|
||||
|
||||
import { getEmoji, getUserRequests } from '@/api'
|
||||
|
||||
export default {
|
||||
components: { ListHeader, ResultsList, Settings, SeasonedButton },
|
||||
data(){
|
||||
return{
|
||||
userLoggedIn: '',
|
||||
emoji: '',
|
||||
results: undefined,
|
||||
totalResults: undefined,
|
||||
showSettings: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
resultCount() {
|
||||
if (this.results === undefined)
|
||||
return
|
||||
|
||||
const loadedResults = this.results.length
|
||||
const totalResults = this.totalResults < 10000 ? this.totalResults : '∞'
|
||||
return `${loadedResults} of ${totalResults} results`
|
||||
},
|
||||
username: () => store.getters['userModule/username']
|
||||
},
|
||||
methods: {
|
||||
toggleSettings() {
|
||||
this.showSettings = this.showSettings ? false : true;
|
||||
|
||||
if (this.showSettings) {
|
||||
this.$router.replace({ query: { settings: true} })
|
||||
} else {
|
||||
this.$router.replace({ name: 'profile' })
|
||||
}
|
||||
},
|
||||
logOut(){
|
||||
this.$router.push('logout')
|
||||
}
|
||||
},
|
||||
created(){
|
||||
if(!localStorage.getItem('token')){
|
||||
this.userLoggedIn = false;
|
||||
} else {
|
||||
this.userLoggedIn = true;
|
||||
|
||||
this.showSettings = window.location.toString().includes('settings=true')
|
||||
|
||||
getUserRequests()
|
||||
.then(results => {
|
||||
this.results = results.results
|
||||
this.totalResults = results.total_results
|
||||
})
|
||||
|
||||
getEmoji()
|
||||
.then(resp => {
|
||||
const { emoji } = resp
|
||||
this.emoji = emoji
|
||||
store.dispatch('documentTitle/updateEmoji', emoji)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "./src/scss/variables";
|
||||
@import "./src/scss/media-queries";
|
||||
|
||||
.button--group {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
// DUPLICATE CODE
|
||||
.profile{
|
||||
&__header{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid $text-color-5;
|
||||
|
||||
@include mobile-only {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
|
||||
.button--group {
|
||||
padding-top: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
@include tablet-min{
|
||||
padding: 29px 30px;
|
||||
}
|
||||
@include tablet-landscape-min{
|
||||
padding: 29px 50px;
|
||||
}
|
||||
@include desktop-min{
|
||||
padding: 29px 60px;
|
||||
}
|
||||
}
|
||||
&__title{
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
line-height: 16px;
|
||||
color: $text-color;
|
||||
font-weight: 300;
|
||||
@include tablet-min{
|
||||
font-size: 18px;
|
||||
line-height: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,110 +0,0 @@
|
||||
<template>
|
||||
<section>
|
||||
<h1>Register new user</h1>
|
||||
|
||||
<seasoned-input placeholder="username" icon="Email" type="username" :value.sync="username" @enter="submit"/>
|
||||
|
||||
<seasoned-input placeholder="password" icon="Keyhole" type="password" :value.sync="password" @enter="submit"/>
|
||||
<seasoned-input placeholder="repeat password" icon="Keyhole" type="password" :value.sync="passwordRepeat" @enter="submit"/>
|
||||
|
||||
<seasoned-button @click="submit">Register</seasoned-button>
|
||||
<router-link class="link" to="/signin">Have a user? Sign in here</router-link>
|
||||
|
||||
<seasoned-messages :messages.sync="messages"></seasoned-messages>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { register } from '@/api'
|
||||
import SeasonedButton from '@/components/ui/SeasonedButton'
|
||||
import SeasonedInput from '@/components/ui/SeasonedInput'
|
||||
import SeasonedMessages from '@/components/ui/SeasonedMessages'
|
||||
|
||||
export default {
|
||||
components: { SeasonedButton, SeasonedInput, SeasonedMessages },
|
||||
data() {
|
||||
return {
|
||||
messages: [],
|
||||
username: null,
|
||||
password: null,
|
||||
passwordRepeat: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
submit() {
|
||||
this.messages = [];
|
||||
let { username, password, passwordRepeat } = this;
|
||||
|
||||
if (username == null || username.length == 0) {
|
||||
this.messages.push({ type: 'error', title: 'Missing username' })
|
||||
return
|
||||
} else if (password == null || password.length == 0) {
|
||||
this.messages.push({ type: 'error', title: 'Missing password' })
|
||||
return
|
||||
} else if (passwordRepeat == null || passwordRepeat.length == 0) {
|
||||
this.messages.push({ type: 'error', title: 'Missing repeat password' })
|
||||
return
|
||||
} else if (passwordRepeat != password) {
|
||||
this.messages.push({ type: 'error', title: 'Passwords do not match' })
|
||||
return
|
||||
}
|
||||
|
||||
this.registerUser(username, password)
|
||||
},
|
||||
registerUser(username, password) {
|
||||
register(username, password, true)
|
||||
.then(data => {
|
||||
if (data.success){
|
||||
localStorage.setItem('token', data.token);
|
||||
const jwtData = parseJwt(data.token)
|
||||
localStorage.setItem('username', jwtData['username']);
|
||||
localStorage.setItem('admin', jwtData['admin'] || false);
|
||||
|
||||
eventHub.$emit('setUserStatus');
|
||||
this.$router.push({ name: 'profile' })
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
if (error.status === 401) {
|
||||
this.messages.push({ type: 'error', title: 'Access denied', message: 'Incorrect username or password' })
|
||||
}
|
||||
else {
|
||||
this.messages.push({ type: 'error', title: 'Unexpected error', message: error.message })
|
||||
}
|
||||
});
|
||||
},
|
||||
logOut(){
|
||||
localStorage.clear();
|
||||
eventHub.$emit('setUserStatus');
|
||||
this.$router.push({ name: 'home' });
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "./src/scss/variables";
|
||||
|
||||
section {
|
||||
padding: 1.3rem;
|
||||
|
||||
@include tablet-min {
|
||||
padding: 4rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
line-height: 16px;
|
||||
color: $text-color;
|
||||
font-weight: 300;
|
||||
margin-bottom: 20px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.link {
|
||||
display: block;
|
||||
width: max-content;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,15 +1,26 @@
|
||||
<template>
|
||||
<ul class="results" :class="{'shortList': shortList}">
|
||||
<movies-list-item v-for='movie in results' :movie="movie" />
|
||||
</ul>
|
||||
<template>
|
||||
<div>
|
||||
<ul
|
||||
v-if="results && results.length"
|
||||
class="results"
|
||||
:class="{ shortList: shortList }"
|
||||
>
|
||||
<results-list-item
|
||||
v-for="(movie, index) in results"
|
||||
:key="`${movie.type}-${movie.id}-${index}`"
|
||||
:movie="movie"
|
||||
/>
|
||||
</ul>
|
||||
|
||||
<span v-else-if="!loading" class="no-results">No results found</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
import MoviesListItem from '@/components/MoviesListItem'
|
||||
import ResultsListItem from "@/components/ResultsListItem";
|
||||
|
||||
export default {
|
||||
components: { MoviesListItem },
|
||||
components: { ResultsListItem },
|
||||
props: {
|
||||
results: {
|
||||
type: Array,
|
||||
@@ -19,50 +30,54 @@ export default {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "src/scss/media-queries";
|
||||
@import "src/scss/main";
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import './src/scss/media-queries';
|
||||
.no-results {
|
||||
width: 100%;
|
||||
display: block;
|
||||
text-align: center;
|
||||
margin: 1.5rem;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.results {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(225px, 1fr));
|
||||
grid-auto-rows: auto;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
|
||||
&.shortList > li {
|
||||
display: none;
|
||||
@include mobile {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
&:nth-child(-n+4) {
|
||||
display: block;
|
||||
&.shortList {
|
||||
overflow: auto;
|
||||
grid-auto-flow: column;
|
||||
max-width: 100vw;
|
||||
|
||||
@include noscrollbar;
|
||||
|
||||
> li {
|
||||
min-width: 225px;
|
||||
}
|
||||
|
||||
@include tablet-min {
|
||||
max-width: calc(100vw - var(--header-size));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include tablet-min {
|
||||
.results.shortList > li:nth-child(-n+6) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
@include tablet-landscape-min {
|
||||
.results.shortList > li:nth-child(-n+8) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
@include desktop-min {
|
||||
.results.shortList > li:nth-child(-n+10) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
@include desktop-lg-min {
|
||||
.results.shortList > li:nth-child(-n+16) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
</style>
|
||||
|
||||
180
src/components/ResultsListItem.vue
Normal file
@@ -0,0 +1,180 @@
|
||||
<template>
|
||||
<li class="movie-item" ref="list-item">
|
||||
<figure ref="poster" class="movie-item__poster" @click="openMoviePopup">
|
||||
<img
|
||||
class="movie-item__img"
|
||||
:alt="posterAltText"
|
||||
:data-src="poster"
|
||||
src="/assets/placeholder.png"
|
||||
/>
|
||||
|
||||
<div v-if="movie.download" class="progress">
|
||||
<progress :value="movie.download.progress" max="100"></progress>
|
||||
<span>{{ movie.download.state }}: {{ movie.download.progress }}%</span>
|
||||
</div>
|
||||
</figure>
|
||||
|
||||
<div class="movie-item__info">
|
||||
<p v-if="movie.title || movie.name" class="movie-item__title">
|
||||
{{ movie.title || movie.name }}
|
||||
</p>
|
||||
<p v-if="movie.year">{{ movie.year }}</p>
|
||||
<p v-if="movie.type == 'person'">
|
||||
Known for: {{ movie.known_for_department }}
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions } from "vuex";
|
||||
import img from "../directives/v-image";
|
||||
import { buildImageProxyUrl } from "../utils";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
movie: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
directives: {
|
||||
img: img
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
poster: null,
|
||||
observed: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
posterAltText: function () {
|
||||
const type = this.movie.type || "";
|
||||
const title = this.movie.title || this.movie.name;
|
||||
return this.movie.poster
|
||||
? `Poster for ${type} ${title}`
|
||||
: `Missing image for ${type} ${title}`;
|
||||
},
|
||||
imageWidth() {
|
||||
if (this.image)
|
||||
return Math.ceil(this.image.getBoundingClientRect().width);
|
||||
},
|
||||
imageHeight() {
|
||||
if (this.image)
|
||||
return Math.ceil(this.image.getBoundingClientRect().height);
|
||||
}
|
||||
},
|
||||
beforeMount() {
|
||||
if (this.movie.poster == null) {
|
||||
this.poster = "/assets/no-image.svg";
|
||||
return;
|
||||
}
|
||||
|
||||
this.poster = `https://image.tmdb.org/t/p/w500${this.movie.poster}`;
|
||||
// this.poster = this.buildProxyURL(
|
||||
// this.imageWidth,
|
||||
// this.imageHeight,
|
||||
// assetUrl
|
||||
// );
|
||||
},
|
||||
mounted() {
|
||||
const poster = this.$refs["poster"];
|
||||
this.image = poster.getElementsByTagName("img")[0];
|
||||
if (this.image == null) return;
|
||||
|
||||
const imageObserver = new IntersectionObserver((entries, imgObserver) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting && this.observed == false) {
|
||||
const lazyImage = entry.target;
|
||||
lazyImage.src = lazyImage.dataset.src;
|
||||
poster.className = poster.className + " is-loaded";
|
||||
this.observed = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
imageObserver.observe(this.image);
|
||||
},
|
||||
methods: {
|
||||
...mapActions("popup", ["open"]),
|
||||
openMoviePopup() {
|
||||
this.open({
|
||||
id: this.movie.id,
|
||||
type: this.movie.type
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "src/scss/variables";
|
||||
@import "src/scss/media-queries";
|
||||
@import "src/scss/main";
|
||||
|
||||
.movie-item {
|
||||
padding: 15px;
|
||||
width: 100%;
|
||||
background-color: var(--background-color);
|
||||
|
||||
&:hover &__info > p {
|
||||
color: $text-color;
|
||||
}
|
||||
|
||||
&__poster {
|
||||
text-decoration: none;
|
||||
color: $text-color-70;
|
||||
font-weight: 300;
|
||||
position: relative;
|
||||
transform: scale(0.97) translateZ(0);
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--background-color);
|
||||
transition: 1s background-color ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.03);
|
||||
box-shadow: 0 0 10px rgba($dark, 0.1);
|
||||
}
|
||||
|
||||
&.is-loaded::before {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&__info {
|
||||
padding-top: 10px;
|
||||
font-weight: 300;
|
||||
|
||||
> p {
|
||||
color: $text-color-70;
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
letter-spacing: 0.5px;
|
||||
transition: color 0.5s ease;
|
||||
cursor: pointer;
|
||||
@include mobile-ls-min {
|
||||
font-size: 12px;
|
||||
}
|
||||
@include tablet-min {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
202
src/components/ResultsSection.vue
Normal file
@@ -0,0 +1,202 @@
|
||||
<template>
|
||||
<div ref="resultSection" class="resultSection">
|
||||
<list-header v-bind="{ title, info, shortList }" />
|
||||
|
||||
<div
|
||||
v-if="!loadedPages.includes(1) && loading == false"
|
||||
class="button-container"
|
||||
>
|
||||
<seasoned-button @click="loadLess" class="load-button" :fullWidth="true"
|
||||
>load previous</seasoned-button
|
||||
>
|
||||
</div>
|
||||
|
||||
<results-list v-bind="{ results, shortList, loading }" />
|
||||
<loader v-if="loading" />
|
||||
|
||||
<div ref="loadMoreButton" class="button-container">
|
||||
<seasoned-button
|
||||
class="load-button"
|
||||
v-if="!loading && !shortList && page != totalPages && results.length"
|
||||
@click="loadMore"
|
||||
:fullWidth="true"
|
||||
>load more</seasoned-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ListHeader from "@/components/ListHeader";
|
||||
import ResultsList from "@/components/ResultsList";
|
||||
import SeasonedButton from "@/components/ui/SeasonedButton";
|
||||
import store from "@/store";
|
||||
import { getTmdbMovieListByName } from "@/api";
|
||||
import Loader from "@/components/ui/Loader";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
apiFunction: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
shortList: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
components: { ListHeader, ResultsList, SeasonedButton, Loader },
|
||||
data() {
|
||||
return {
|
||||
results: [],
|
||||
page: 1,
|
||||
loadedPages: [],
|
||||
totalPages: -1,
|
||||
totalResults: 0,
|
||||
loading: true,
|
||||
autoLoad: false,
|
||||
observer: undefined
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
info() {
|
||||
if (this.results.length === 0) return [null, null];
|
||||
return [this.pageCount, this.resultCount];
|
||||
},
|
||||
resultCount() {
|
||||
const loadedResults = this.results.length;
|
||||
const totalResults = this.totalResults < 10000 ? this.totalResults : "∞";
|
||||
return `${loadedResults} of ${totalResults} results`;
|
||||
},
|
||||
pageCount() {
|
||||
return `Page ${this.page} of ${this.totalPages}`;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
loadMore() {
|
||||
if (!this.autoLoad) {
|
||||
this.autoLoad = true;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
let maxPage = [...this.loadedPages].slice(-1)[0];
|
||||
|
||||
if (maxPage == NaN) return;
|
||||
this.page = maxPage + 1;
|
||||
this.getListResults();
|
||||
},
|
||||
loadLess() {
|
||||
this.loading = true;
|
||||
const minPage = this.loadedPages[0];
|
||||
if (minPage === 1) return;
|
||||
|
||||
this.page = minPage - 1;
|
||||
this.getListResults(true);
|
||||
},
|
||||
updateQueryParams() {
|
||||
let params = new URLSearchParams(window.location.search);
|
||||
if (params.has("page")) {
|
||||
params.set("page", this.page);
|
||||
} else if (this.page > 1) {
|
||||
params.append("page", this.page);
|
||||
}
|
||||
|
||||
window.history.replaceState(
|
||||
{},
|
||||
"search",
|
||||
`${window.location.protocol}//${window.location.hostname}${
|
||||
window.location.port ? `:${window.location.port}` : ""
|
||||
}${window.location.pathname}${
|
||||
params.toString().length ? `?${params}` : ""
|
||||
}`
|
||||
);
|
||||
},
|
||||
getPageFromUrl() {
|
||||
return new URLSearchParams(window.location.search).get("page");
|
||||
},
|
||||
getListResults(front = false) {
|
||||
this.apiFunction(this.page)
|
||||
.then(results => {
|
||||
if (!front) this.results = this.results.concat(...results.results);
|
||||
else this.results = results.results.concat(...this.results);
|
||||
this.page = results.page;
|
||||
this.loadedPages.push(this.page);
|
||||
this.loadedPages = this.loadedPages.sort((a, b) => a - b);
|
||||
this.totalPages = results.total_pages;
|
||||
this.totalResults = results.total_results;
|
||||
})
|
||||
.then(this.updateQueryParams)
|
||||
.finally(() => (this.loading = false));
|
||||
},
|
||||
setupAutoloadObserver() {
|
||||
this.observer = new IntersectionObserver(this.handleButtonIntersection, {
|
||||
root: this.$refs.resultSection.$el,
|
||||
rootMargin: "0px",
|
||||
threshold: 0
|
||||
});
|
||||
|
||||
this.observer.observe(this.$refs.loadMoreButton);
|
||||
},
|
||||
handleButtonIntersection(entries) {
|
||||
entries.map(entry =>
|
||||
entry.isIntersecting && this.autoLoad ? this.loadMore() : null
|
||||
);
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.page = this.getPageFromUrl() || this.page;
|
||||
if (this.results.length === 0) this.getListResults();
|
||||
|
||||
if (!this.shortList) {
|
||||
store.dispatch(
|
||||
"documentTitle/updateTitle",
|
||||
`${this.$router.history.current.name} ${this.title}`
|
||||
);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (!this.shortList) {
|
||||
this.setupAutoloadObserver();
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.observer = undefined;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "src/scss/media-queries";
|
||||
|
||||
.resultSection {
|
||||
background-color: var(--background-color);
|
||||
}
|
||||
|
||||
.button-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.load-button {
|
||||
margin: 2rem 0;
|
||||
|
||||
@include mobile {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
margin-bottom: 4rem;
|
||||
|
||||
@include mobile {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,122 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<list-header :title="title" :info="resultCount" :sticky="true" />
|
||||
|
||||
<results-list :results="results" />
|
||||
|
||||
<div v-if="page < totalPages" class="fullwidth-button">
|
||||
<seasoned-button @click="loadMore">load more</seasoned-button>
|
||||
</div>
|
||||
|
||||
<div class="notFound" v-if="results.length == 0 && loading == false">
|
||||
<h1 class="notFound-title">No results for search: <b>{{ query }}</b></h1>
|
||||
</div>
|
||||
|
||||
<loader v-if="loading" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.notFound {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&-title {
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { searchTmdb } from '@/api'
|
||||
import ListHeader from '@/components/ListHeader'
|
||||
import ResultsList from '@/components/ResultsList'
|
||||
import SeasonedButton from '@/components/ui/SeasonedButton'
|
||||
import Loader from '@/components/ui/Loader'
|
||||
|
||||
export default {
|
||||
components: { ListHeader, ResultsList, SeasonedButton, Loader },
|
||||
props: {
|
||||
propQuery: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
propPage: {
|
||||
type: Number,
|
||||
required: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
query: String,
|
||||
title: String,
|
||||
page: Number,
|
||||
adult: undefined,
|
||||
mediaType: null,
|
||||
totalPages: 0,
|
||||
results: [],
|
||||
totalResults: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
resultCount() {
|
||||
const loadedResults = this.results.length
|
||||
const totalResults = this.totalResults < 10000 ? this.totalResults : '∞'
|
||||
return `${loadedResults} of ${totalResults} results`
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
search(query=this.query, page=this.page, adult=this.adult, mediaType=this.mediaType) {
|
||||
searchTmdb(query, page, adult, mediaType)
|
||||
.then(this.parseResponse)
|
||||
},
|
||||
parseResponse(data) {
|
||||
if (this.results.length > 0) {
|
||||
this.results.push(...data.results)
|
||||
} else {
|
||||
this.results = data.results
|
||||
}
|
||||
|
||||
this.totalPages = data.total_pages
|
||||
this.totalResults = data.total_results || data.results.length
|
||||
|
||||
this.loading = false
|
||||
},
|
||||
loadMore() {
|
||||
this.page++
|
||||
|
||||
window.history.replaceState({}, 'search', `/#/search?query=${this.query}&page=${this.page}`)
|
||||
this.search()
|
||||
}
|
||||
},
|
||||
created() {
|
||||
const { query, page, adult, media_type } = this.$route.query
|
||||
|
||||
if (!query) {
|
||||
// abort
|
||||
console.error('abort, no query')
|
||||
}
|
||||
this.query = decodeURIComponent(query)
|
||||
this.page = page || 1
|
||||
this.adult = adult || this.adult
|
||||
this.mediaType = media_type || this.mediaType
|
||||
this.title = `Search results: ${this.query}`
|
||||
|
||||
this.search()
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.fullwidth-button {
|
||||
width: 100%;
|
||||
margin: 1rem 0;
|
||||
padding-bottom: 2rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,376 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="search">
|
||||
<input
|
||||
ref="input"
|
||||
type="text"
|
||||
placeholder="Search for movie or show"
|
||||
aria-label="Search input for finding a movie or show"
|
||||
autocorrect="off"
|
||||
autocapitalize="off"
|
||||
tabindex="1"
|
||||
v-model="query"
|
||||
@input="handleInput"
|
||||
@click="focus = true"
|
||||
@keydown.escape="handleEscape"
|
||||
@keyup.enter="handleSubmit"
|
||||
@keydown.up="navigateUp"
|
||||
@keydown.down="navigateDown" />
|
||||
|
||||
<svg class="search-icon" fill="currentColor" @click="handleSubmit"><use xlink:href="#iconSearch"></use></svg>
|
||||
</div>
|
||||
|
||||
<transition name="fade">
|
||||
<div class="dropdown" v-if="!disabled && focus && query.length > 0">
|
||||
<div class="filter">
|
||||
<h2>Filter your search:</h2>
|
||||
|
||||
<div class="filter-items">
|
||||
<toggle-button :options="searchTypes" :selected.sync="selectedSearchType" />
|
||||
|
||||
<label>Adult
|
||||
<input type="checkbox" value="adult" v-model="adult">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="dropdown-results" v-if="elasticSearchResults.length">
|
||||
<ul v-for="(item, index) in elasticSearchResults"
|
||||
@click="openResult(item, index + 1)"
|
||||
:class="{ active: index + 1 === selectedResult}">
|
||||
|
||||
{{ item.name }}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div v-else class="dropdown">
|
||||
<div class="dropdown-results">
|
||||
<h2 class="not-found">No results for query: <b>{{ query }}</b></h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<seasoned-button class="end-section" fullWidth="true"
|
||||
@click="focus = false" :active="elasticSearchResults.length + 1 === selectedResult">
|
||||
close
|
||||
</seasoned-button>
|
||||
</div>
|
||||
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SeasonedButton from '@/components/ui/SeasonedButton'
|
||||
import ToggleButton from '@/components/ui/ToggleButton';
|
||||
|
||||
import { elasticSearchMoviesAndShows } from '@/api'
|
||||
import config from '@/config.json'
|
||||
|
||||
export default {
|
||||
name: 'SearchInput',
|
||||
components: {
|
||||
SeasonedButton,
|
||||
ToggleButton
|
||||
},
|
||||
props: ['value'],
|
||||
data() {
|
||||
return {
|
||||
adult: true,
|
||||
searchTypes: ['all', 'movie', 'show', 'person'],
|
||||
selectedSearchType: 'all',
|
||||
|
||||
query: this.value,
|
||||
focus: false,
|
||||
disabled: false,
|
||||
scrollListener: undefined,
|
||||
scrollDistance: 0,
|
||||
elasticSearchResults: [],
|
||||
selectedResult: 0,
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
focus: function(val) {
|
||||
if (val === true) {
|
||||
window.addEventListener('scroll', this.disableFocus)
|
||||
} else {
|
||||
window.removeEventListener('scroll', this.disableFocus)
|
||||
this.scrollDistance = 0
|
||||
}
|
||||
},
|
||||
adult: function(value) {
|
||||
this.handleInput()
|
||||
}
|
||||
},
|
||||
beforeMount() {
|
||||
const elasticUrl = config.ELASTIC_URL
|
||||
if (elasticUrl === undefined || elasticUrl === false || elasticUrl === '') {
|
||||
this.disabled = true
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
console.log('scroll eventlistener not removed, destroying!')
|
||||
window.removeEventListener('scroll', this.disableFocus)
|
||||
},
|
||||
methods: {
|
||||
navigateDown() {
|
||||
this.focus = true
|
||||
this.selectedResult++
|
||||
},
|
||||
navigateUp() {
|
||||
this.focus = true
|
||||
this.selectedResult--
|
||||
const input = this.$refs.input;
|
||||
const textLength = input.value.length
|
||||
|
||||
setTimeout(() => {
|
||||
input.focus()
|
||||
input.setSelectionRange(textLength, textLength + 1)
|
||||
}, 1)
|
||||
},
|
||||
openResult(item, index) {
|
||||
this.selectedResult = index;
|
||||
this.$popup.open(item.id, item.type)
|
||||
},
|
||||
handleInput(e){
|
||||
this.selectedResult = 0
|
||||
this.$emit('input', this.query);
|
||||
|
||||
if (! this.focus) {
|
||||
this.focus = true;
|
||||
}
|
||||
|
||||
elasticSearchMoviesAndShows(this.query)
|
||||
.then(resp => {
|
||||
const data = resp.hits.hits
|
||||
|
||||
let results = data.map(item => {
|
||||
const index = item._index.slice(0, -1)
|
||||
if (index === 'movie' || item._source.original_title) {
|
||||
return {
|
||||
name: item._source.original_title,
|
||||
id: item._source.id,
|
||||
adult: item._source.adult,
|
||||
type: 'movie'
|
||||
}
|
||||
} else if (index === 'show' || item._source.original_name) {
|
||||
return {
|
||||
name: item._source.original_name,
|
||||
id: item._source.id,
|
||||
adult: item._source.adult,
|
||||
type: 'show'
|
||||
}
|
||||
}
|
||||
})
|
||||
results = this.removeDuplicates(results)
|
||||
this.elasticSearchResults = results
|
||||
})
|
||||
},
|
||||
removeDuplicates(searchResults) {
|
||||
let filteredResults = []
|
||||
searchResults.map(result => {
|
||||
const numberOfDuplicates = filteredResults.filter(filterItem => filterItem.id == result.id)
|
||||
if (numberOfDuplicates.length >= 1) {
|
||||
return null
|
||||
}
|
||||
filteredResults.push(result)
|
||||
})
|
||||
|
||||
if (this.adult == false) {
|
||||
filteredResults = filteredResults.filter(result => result.adult == false)
|
||||
}
|
||||
|
||||
return filteredResults
|
||||
},
|
||||
handleSubmit() {
|
||||
let searchResults = this.elasticSearchResults
|
||||
|
||||
if (this.selectedResult > searchResults.length) {
|
||||
this.focus = false
|
||||
this.selectedResult = 0
|
||||
} else if (this.selectedResult > 0) {
|
||||
const resultItem = searchResults[this.selectedResult - 1]
|
||||
this.$popup.open(resultItem.id, resultItem.type)
|
||||
} else {
|
||||
const encodedQuery = encodeURI(this.query.replace('/ /g, "+"'))
|
||||
const media_type = this.selectedSearchType !== 'all' ? this.selectedSearchType : null
|
||||
this.$router.push({ name: 'search', query: { query: encodedQuery, adult: this.adult, media_type }});
|
||||
this.focus = false
|
||||
this.selectedResult = 0
|
||||
}
|
||||
},
|
||||
handleEscape() {
|
||||
if (this.$popup.isOpen) {
|
||||
console.log('THIS WAS FUCKOING OPEN!')
|
||||
} else {
|
||||
this.focus = false
|
||||
}
|
||||
},
|
||||
disableFocus(_) {
|
||||
this.focus = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "./src/scss/variables";
|
||||
@import "./src/scss/media-queries";
|
||||
@import './src/scss/main';
|
||||
|
||||
|
||||
.fade-enter-active {
|
||||
transition: opacity .2s;
|
||||
}
|
||||
.fade-leave-active {
|
||||
transition: opacity .2s;
|
||||
}
|
||||
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.filter {
|
||||
// background-color: rgba(004, 122, 125, 0.2);
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 1rem 2rem;
|
||||
|
||||
h2 {
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
&-items {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
> :not(:first-child) {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hr {
|
||||
display: block;
|
||||
height: 1px;
|
||||
border: 0;
|
||||
border-bottom: 1px solid $text-color-50;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
z-index: 5;
|
||||
min-height: $header-size;
|
||||
right: 0px;
|
||||
background-color: $background-color-secondary;
|
||||
|
||||
@include mobile-only {
|
||||
position: fixed;
|
||||
top: 50px;
|
||||
padding-top: 20px;
|
||||
width: calc(100%);
|
||||
}
|
||||
|
||||
.not-found {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
&-results {
|
||||
padding-left: 60px;
|
||||
width: 100%;
|
||||
|
||||
@include mobile-only {
|
||||
padding-left: 45px;
|
||||
}
|
||||
|
||||
> ul {
|
||||
font-size: 1.3rem;
|
||||
padding: 0;
|
||||
margin: 0.2rem 0;
|
||||
width: calc(100% - 25px);
|
||||
max-width: fit-content;
|
||||
|
||||
list-style: none;
|
||||
color: rgba(0, 0, 0, 0.5);
|
||||
text-transform: capitalize;
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid transparent;
|
||||
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
color: $text-color-50;
|
||||
|
||||
&.active, &:hover, &:active {
|
||||
color: $text-color;
|
||||
border-bottom: 2px solid $text-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search {
|
||||
height: $header-size;
|
||||
display: flex;
|
||||
position: fixed;
|
||||
flex-wrap: wrap;
|
||||
z-index: 5;
|
||||
border: 0;
|
||||
background-color: $background-color-secondary;
|
||||
|
||||
// TODO check if this is for mobile
|
||||
width: calc(100% - 110px);
|
||||
top: 0;
|
||||
right: 55px;
|
||||
|
||||
@include tablet-min{
|
||||
position: relative;
|
||||
width: 100%;
|
||||
right: 0px;
|
||||
}
|
||||
|
||||
input {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 13px 0 13px 45px;
|
||||
outline: none;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
background-color: $background-color-secondary;
|
||||
font-weight: 300;
|
||||
font-size: 19px;
|
||||
color: $text-color;
|
||||
transition: background-color .5s ease, color .5s ease;
|
||||
|
||||
@include tablet-min {
|
||||
padding: 13px 30px 13px 60px;
|
||||
}
|
||||
}
|
||||
|
||||
&-icon{
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
fill: $text-color-50;
|
||||
transition: fill 0.5s ease;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
left: 15px;
|
||||
top: 15px;
|
||||
|
||||
@include tablet-min{
|
||||
top: 27px;
|
||||
left: 25px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,198 +0,0 @@
|
||||
<template>
|
||||
<section class="profile">
|
||||
<div class="profile__content" v-if="userLoggedIn">
|
||||
<section class='settings'>
|
||||
<h3 class='settings__header'>Plex account</h3>
|
||||
|
||||
<div v-if="!hasPlexUser">
|
||||
<span class="settings__info">Sign in to your plex account to get information about recently added movies and to see your watch history</span>
|
||||
|
||||
<form class="form">
|
||||
<seasoned-input placeholder="plex username" icon="Email" :value.sync="plexUsername"/>
|
||||
<seasoned-input placeholder="plex password" icon="Keyhole" type="password"
|
||||
:value.sync="plexPassword" @submit="authenticatePlex" />
|
||||
|
||||
<seasoned-button @click="authenticatePlex">link plex account</seasoned-button>
|
||||
</form>
|
||||
</div>
|
||||
<div v-else>
|
||||
<span class="settings__info">Awesome, your account is already authenticated with plex! Enjoy viewing your seasoned search history, plex watch history and real-time torrent download progress.</span>
|
||||
<seasoned-button @click="unauthenticatePlex">un-link plex account</seasoned-button>
|
||||
</div>
|
||||
<seasoned-messages :messages.sync="messages" />
|
||||
|
||||
<hr class='setting__divider'>
|
||||
|
||||
<h3 class='settings__header'>Change password</h3>
|
||||
<form class="form">
|
||||
<seasoned-input placeholder="new password" icon="Keyhole" type="password"
|
||||
:value.sync="newPassword" />
|
||||
|
||||
<seasoned-input placeholder="repeat new password" icon="Keyhole" type="password"
|
||||
:value.sync="newPasswordRepeat" />
|
||||
|
||||
<seasoned-button @click="changePassword">change password</seasoned-button>
|
||||
</form>
|
||||
|
||||
<hr class='setting__divider'>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="not-found" v-else>
|
||||
<div class="not-found__content">
|
||||
<h2 class="not-found__title">Authentication Request Failed</h2>
|
||||
<router-link :to="{name: 'signin'}" exact title="Sign in here">
|
||||
<button class="not-found__button button">Sign In</button>
|
||||
</router-link>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import store from '@/store'
|
||||
import storage from '@/storage'
|
||||
import SeasonedInput from '@/components/ui/SeasonedInput'
|
||||
import SeasonedButton from '@/components/ui/SeasonedButton'
|
||||
import SeasonedMessages from '@/components/ui/SeasonedMessages'
|
||||
|
||||
import { getSettings, updateSettings, linkPlexAccount, unlinkPlexAccount } from '@/api'
|
||||
|
||||
export default {
|
||||
components: { SeasonedInput, SeasonedButton, SeasonedMessages },
|
||||
data(){
|
||||
return{
|
||||
userLoggedIn: '',
|
||||
messages: [],
|
||||
plexUsername: null,
|
||||
plexPassword: null,
|
||||
newPassword: null,
|
||||
newPasswordRepeat: null,
|
||||
emoji: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
hasPlexUser: function() {
|
||||
return this.settings && this.settings['plex_userid']
|
||||
},
|
||||
settings: {
|
||||
get: () => {
|
||||
return store.getters['userModule/settings']
|
||||
},
|
||||
set: function(newSettings) {
|
||||
store.dispatch('userModule/setSettings', newSettings)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setValue(l, t) {
|
||||
this[l] = t
|
||||
},
|
||||
changePassword() {
|
||||
return
|
||||
},
|
||||
async authenticatePlex() {
|
||||
let username = this.plexUsername
|
||||
let password = this.plexPassword
|
||||
|
||||
const response = await linkPlexAccount(username, password)
|
||||
|
||||
this.messages.push({
|
||||
type: response.success ? 'success' : 'error',
|
||||
title: response.success ? 'Authenticated with plex' : 'Something went wrong',
|
||||
message: response.message
|
||||
})
|
||||
|
||||
if (response.success)
|
||||
getSettings().then(settings => this.settings = settings)
|
||||
},
|
||||
async unauthenticatePlex() {
|
||||
const response = await unlinkPlexAccount()
|
||||
|
||||
this.messages.push({
|
||||
type: response.success ? 'success' : 'error',
|
||||
title: response.success ? 'Unlinked plex account ' : 'Something went wrong',
|
||||
message: response.message
|
||||
})
|
||||
|
||||
if (response.success)
|
||||
getSettings().then(settings => this.settings = settings)
|
||||
}
|
||||
},
|
||||
created(){
|
||||
const token = localStorage.getItem('token') || false;
|
||||
if (token){
|
||||
this.userLoggedIn = true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "./src/scss/variables";
|
||||
@import "./src/scss/media-queries";
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
// DUPLICATE CODE
|
||||
.form {
|
||||
> div:last-child {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
&__group{
|
||||
justify-content: unset;
|
||||
&__input-icon {
|
||||
margin-top: 8px;
|
||||
height: 22px;
|
||||
width: 22px;
|
||||
}
|
||||
&-input {
|
||||
padding: 10px 5px 10px 45px;
|
||||
height: 40px;
|
||||
font-size: 17px;
|
||||
width: 75%;
|
||||
@include desktop-min {
|
||||
width: 400px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.settings {
|
||||
padding: 3rem;
|
||||
|
||||
@include mobile-only {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
&__header {
|
||||
margin: 0;
|
||||
line-height: 16px;
|
||||
color: $text-color;
|
||||
font-weight: 300;
|
||||
margin-bottom: 20px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
&__info {
|
||||
display: block;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
hr {
|
||||
display: block;
|
||||
height: 1px;
|
||||
border: 0;
|
||||
border-bottom: 1px solid $text-color-50;
|
||||
margin-top: 30px;
|
||||
margin-bottom: 70px;
|
||||
margin-left: 20px;
|
||||
width: 96%;
|
||||
text-align: left;
|
||||
}
|
||||
span {
|
||||
font-weight: 200;
|
||||
size: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,114 +0,0 @@
|
||||
<template>
|
||||
<section>
|
||||
<h1>Sign in</h1>
|
||||
|
||||
<seasoned-input placeholder="username"
|
||||
icon="Email"
|
||||
type="email"
|
||||
@enter="submit"
|
||||
:value.sync="username" />
|
||||
<seasoned-input placeholder="password" icon="Keyhole" type="password" :value.sync="password" @enter="submit"/>
|
||||
|
||||
<seasoned-button @click="submit">sign in</seasoned-button>
|
||||
<router-link class="link" to="/register">Don't have a user? Register here</router-link>
|
||||
|
||||
<seasoned-messages :messages.sync="messages"></seasoned-messages>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
<script>
|
||||
import { login } from '@/api'
|
||||
import storage from '../storage'
|
||||
import SeasonedInput from '@/components/ui/SeasonedInput'
|
||||
import SeasonedButton from '@/components/ui/SeasonedButton'
|
||||
import SeasonedMessages from '@/components/ui/SeasonedMessages'
|
||||
import { parseJwt } from '@/utils'
|
||||
|
||||
export default {
|
||||
components: { SeasonedInput, SeasonedButton, SeasonedMessages },
|
||||
data(){
|
||||
return{
|
||||
messages: [],
|
||||
username: null,
|
||||
password: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setValue(l, t) {
|
||||
this[l] = t
|
||||
},
|
||||
submit() {
|
||||
this.messages = [];
|
||||
let username = this.username;
|
||||
let password = this.password;
|
||||
|
||||
if (username == null || username.length == 0) {
|
||||
this.messages.push({ type: 'error', title: 'Missing username' })
|
||||
return
|
||||
}
|
||||
|
||||
if (password == null || password.length == 0) {
|
||||
this.messages.push({ type: 'error', title: 'Missing password' })
|
||||
return
|
||||
}
|
||||
|
||||
this.signin(username, password)
|
||||
},
|
||||
signin(username, password) {
|
||||
login(username, password, true)
|
||||
.then(data => {
|
||||
if (data.success){
|
||||
const jwtData = parseJwt(data.token)
|
||||
localStorage.setItem('token', data.token);
|
||||
localStorage.setItem('username', jwtData['username']);
|
||||
localStorage.setItem('admin', jwtData['admin'] || false);
|
||||
|
||||
eventHub.$emit('setUserStatus');
|
||||
this.$router.push({ name: 'profile' })
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
if (error.status === 401) {
|
||||
this.messages.push({ type: 'error', title: 'Access denied', message: 'Incorrect username or password' })
|
||||
}
|
||||
else {
|
||||
this.messages.push({ type: 'error', title: 'Unexpected error', message: error.message })
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
created(){
|
||||
document.title = 'Sign in' + storage.pageTitlePostfix;
|
||||
storage.backTitle = document.title;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "./src/scss/variables";
|
||||
|
||||
section {
|
||||
padding: 1.3rem;
|
||||
|
||||
@include tablet-min {
|
||||
padding: 4rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
line-height: 16px;
|
||||
color: $text-color;
|
||||
font-weight: 300;
|
||||
margin-bottom: 20px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.link {
|
||||
display: block;
|
||||
width: max-content;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,108 +1,151 @@
|
||||
<template>
|
||||
<div v-if="show" class="container">
|
||||
<h2 class="torrentHeader-text">Searching for: {{ editedSearchQuery || query }}</h2>
|
||||
<!-- <div class="torrentHeader">
|
||||
<span class="torrentHeader-text">Searching for: </span>
|
||||
<h2 class="torrentHeader-text editable">
|
||||
Searching for:
|
||||
<span :contenteditable="!edit" @input="this.handleInput">{{
|
||||
query
|
||||
}}</span>
|
||||
|
||||
<IconSearch
|
||||
class="icon"
|
||||
v-if="editedSearchQuery && editedSearchQuery.length"
|
||||
/>
|
||||
<IconEdit v-else class="icon" @click="() => (this.edit = !this.edit)" />
|
||||
</h2>
|
||||
|
||||
<span id="search" :contenteditable="editSearchQuery ? true : false" class="torrentHeader-text editable">{{ editedSearchQuery || query }}</span>
|
||||
|
||||
|
||||
<svg v-if="!editSearchQuery" class="torrentHeader-editIcon" @click="toggleEditSearchQuery">
|
||||
<use xlink:href="#icon_radar"></use>
|
||||
</svg>
|
||||
|
||||
<svg v-else class="torrentHeader-editIcon" @click="toggleEditSearchQuery">
|
||||
<use xlink:href="#icon_check"></use>
|
||||
</svg>
|
||||
|
||||
</div> -->
|
||||
|
||||
<div v-if="listLoaded">
|
||||
<div v-if="!loading">
|
||||
<div v-if="torrents.length > 0">
|
||||
<!-- <ul class="filter">
|
||||
<li class="filter-item" v-for="(item, index) in release_types" @click="applyFilter(item, index)" :class="{'active': item === selectedRelaseType}">{{ item }}</li>
|
||||
</ul> -->
|
||||
|
||||
<toggle-button :options="release_types" :selected.sync="selectedRelaseType" class="toggle"></toggle-button>
|
||||
|
||||
|
||||
<table>
|
||||
<tr class="table__header noselect">
|
||||
<th @click="sortTable('name')" :class="selectedSortableClass('name')">
|
||||
<thead class="table__header noselect">
|
||||
<tr>
|
||||
<th
|
||||
v-for="column in columns"
|
||||
:key="column"
|
||||
@click="sortTable(column)"
|
||||
:class="column === selectedColumn ? 'active' : null"
|
||||
>
|
||||
{{ column }}
|
||||
<span v-if="prevCol === column && direction">↑</span>
|
||||
<span v-if="prevCol === column && !direction">↓</span>
|
||||
</th>
|
||||
</tr>
|
||||
<!-- <th
|
||||
@click="sortTable('name')"
|
||||
:class="selectedSortableClass('name')"
|
||||
>
|
||||
<span>Name</span>
|
||||
<span v-if="prevCol === 'name' && direction">↑</span>
|
||||
<span v-if="prevCol === 'name' && !direction">↓</span>
|
||||
</th>
|
||||
<th @click="sortTable('seed')" :class="selectedSortableClass('seed')">
|
||||
<th
|
||||
@click="sortTable('seed')"
|
||||
:class="selectedSortableClass('seed')"
|
||||
>
|
||||
<span>Seed</span>
|
||||
<span v-if="prevCol === 'seed' && direction">↑</span>
|
||||
<span v-if="prevCol === 'seed' && !direction">↓</span>
|
||||
</th>
|
||||
<th @click="sortTable('size')" :class="selectedSortableClass('size')">
|
||||
<th
|
||||
@click="sortTable('size')"
|
||||
:class="selectedSortableClass('size')"
|
||||
>
|
||||
<span>Size</span>
|
||||
<span v-if="prevCol === 'size' && direction">↑</span>
|
||||
<span v-if="prevCol === 'size' && !direction">↓</span>
|
||||
</th>
|
||||
|
||||
<th>
|
||||
<span>Magnet</span>
|
||||
</th>
|
||||
</tr>
|
||||
<tr v-for="torrent in torrents" class="table__content">
|
||||
<td @click="expand($event, torrent.name)">{{ torrent.name }}</td>
|
||||
<td @click="expand($event, torrent.name)">{{ torrent.seed }}</td>
|
||||
<td @click="expand($event, torrent.name)">{{ torrent.size }}</td>
|
||||
<td @click="sendTorrent(torrent.magnet, torrent.name, $event)" class="download">
|
||||
<svg class="download__icon"><use xlink:href="#iconUnmatched"></use></svg>
|
||||
</td>
|
||||
</tr>
|
||||
</th> -->
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="torrent in torrents"
|
||||
class="table__content"
|
||||
:key="torrent.magnet"
|
||||
>
|
||||
<td @click="expand($event, torrent.name)">{{ torrent.name }}</td>
|
||||
<td @click="expand($event, torrent.name)">{{ torrent.seed }}</td>
|
||||
<td @click="expand($event, torrent.name)">{{ torrent.size }}</td>
|
||||
<td
|
||||
@click="sendTorrent(torrent.magnet, torrent.name, $event)"
|
||||
class="download"
|
||||
>
|
||||
<IconMagnet />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div style="
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 1rem;
|
||||
">
|
||||
<seasonedButton @click="resetTorrentsAndToggleEditSearchQuery">Edit search query</seasonedButton>
|
||||
<div style="display: flex; justify-content: center; padding: 1rem">
|
||||
<seasonedButton @click="resetTorrentsAndToggleEditSearchQuery"
|
||||
>Edit search query</seasonedButton
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else style="display: flex;
|
||||
padding-bottom: 2rem;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
align-items: center;">
|
||||
<h2>No results found</h2>
|
||||
<br />
|
||||
<div
|
||||
v-else
|
||||
style="
|
||||
display: flex;
|
||||
padding-bottom: 2rem;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
"
|
||||
>
|
||||
<h2>No results found</h2>
|
||||
<br />
|
||||
|
||||
<div class="editQuery" v-if="editSearchQuery">
|
||||
<div class="editQuery" v-if="editSearchQuery">
|
||||
<seasonedInput
|
||||
placeholder="Torrent query"
|
||||
:value.sync="editedSearchQuery"
|
||||
@enter="fetchTorrents(editedSearchQuery)"
|
||||
/>
|
||||
|
||||
<seasonedInput placeholder="Torrent query" icon="_torrents" :value.sync="editedSearchQuery" @enter="fetchTorrents(editedSearchQuery)" />
|
||||
<div style="height: 45px; width: 5px"></div>
|
||||
|
||||
<div style="height: 45px; width: 5px;"></div>
|
||||
<seasonedButton @click="fetchTorrents(editedSearchQuery)"
|
||||
>Search</seasonedButton
|
||||
>
|
||||
</div>
|
||||
|
||||
<seasonedButton @click="fetchTorrents(editedSearchQuery)">Search</seasonedButton>
|
||||
<seasonedButton
|
||||
@click="toggleEditSearchQuery"
|
||||
:active="editSearchQuery ? true : false"
|
||||
>Edit search query</seasonedButton
|
||||
>
|
||||
</div>
|
||||
|
||||
<seasonedButton @click="toggleEditSearchQuery" :active="editSearchQuery ? true : false">Edit search query</seasonedButton>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="torrentloader"><i></i></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import storage from '@/storage'
|
||||
import store from '@/store'
|
||||
import { sortableSize } from '@/utils'
|
||||
import { searchTorrents, addMagnet } from '@/api'
|
||||
import store from "@/store";
|
||||
import { sortableSize } from "@/utils";
|
||||
import { searchTorrents, addMagnet } from "@/api";
|
||||
|
||||
import SeasonedButton from '@/components/ui/SeasonedButton'
|
||||
import SeasonedInput from '@/components/ui/SeasonedInput'
|
||||
import ToggleButton from '@/components/ui/ToggleButton'
|
||||
import IconMagnet from "../icons/IconMagnet";
|
||||
import IconEdit from "../icons/IconEdit";
|
||||
import IconSearch from "../icons/IconSearch";
|
||||
|
||||
import SeasonedButton from "@/components/ui/SeasonedButton";
|
||||
import SeasonedInput from "@/components/ui/SeasonedInput";
|
||||
import ToggleButton from "@/components/ui/ToggleButton";
|
||||
|
||||
export default {
|
||||
components: { SeasonedButton, SeasonedInput, ToggleButton },
|
||||
components: {
|
||||
IconMagnet,
|
||||
IconEdit,
|
||||
IconSearch,
|
||||
SeasonedButton,
|
||||
SeasonedInput,
|
||||
ToggleButton
|
||||
},
|
||||
props: {
|
||||
query: {
|
||||
type: String,
|
||||
@@ -118,175 +161,208 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
listLoaded: false,
|
||||
edit: true,
|
||||
loading: false,
|
||||
torrents: [],
|
||||
torrentResponse: undefined,
|
||||
currentPage: 0,
|
||||
prevCol: '',
|
||||
prevCol: "",
|
||||
direction: false,
|
||||
release_types: ['all'],
|
||||
selectedRelaseType: 'all',
|
||||
release_types: ["all"],
|
||||
selectedRelaseType: "all",
|
||||
editSearchQuery: false,
|
||||
editedSearchQuery: ''
|
||||
}
|
||||
editedSearchQuery: "",
|
||||
|
||||
columns: ["name", "seed", "size", "magnet"],
|
||||
selectedColumn: null
|
||||
};
|
||||
},
|
||||
beforeMount() {
|
||||
if (localStorage.getItem('admin')) {
|
||||
this.fetchTorrents()
|
||||
}
|
||||
store.dispatch('torrentModule/reset')
|
||||
created() {
|
||||
this.fetchTorrents().then(_ => this.sortTable("size"));
|
||||
},
|
||||
watch: {
|
||||
selectedRelaseType: function(newValue) {
|
||||
this.applyFilter(newValue)
|
||||
selectedRelaseType: function (newValue) {
|
||||
this.applyFilter(newValue);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
selectedSortableClass(headerName) {
|
||||
return headerName === this.prevCol ? 'active' : ''
|
||||
return headerName === this.prevCol ? "active" : "";
|
||||
},
|
||||
resetTorrentsAndToggleEditSearchQuery() {
|
||||
this.torrents = []
|
||||
this.toggleEditSearchQuery()
|
||||
this.torrents = [];
|
||||
this.toggleEditSearchQuery();
|
||||
},
|
||||
toggleEditSearchQuery() {
|
||||
this.editSearchQuery = !this.editSearchQuery;
|
||||
},
|
||||
expand(event, name) {
|
||||
const existingExpandedElement = document.getElementsByClassName('expanded')[0]
|
||||
const existingExpandedElement =
|
||||
document.getElementsByClassName("expanded")[0];
|
||||
|
||||
const clickedElement = event.target.parentNode;
|
||||
const scopedStyleDataVariable = Object.keys(clickedElement.dataset)[0]
|
||||
const scopedStyleDataVariable = Object.keys(clickedElement.dataset)[0];
|
||||
|
||||
if (existingExpandedElement) {
|
||||
const expandedSibling = event.target.parentNode.nextSibling.className === 'expanded'
|
||||
const expandedSibling =
|
||||
event.target.parentNode.nextSibling.className === "expanded";
|
||||
|
||||
existingExpandedElement.remove()
|
||||
const table = document.getElementsByTagName('table')[0]
|
||||
table.style.display = 'block'
|
||||
existingExpandedElement.remove();
|
||||
const table = document.getElementsByTagName("table")[0];
|
||||
table.style.display = "block";
|
||||
|
||||
if (expandedSibling) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const nameRow = document.createElement('tr')
|
||||
const nameCol = document.createElement('td')
|
||||
nameRow.className = 'expanded'
|
||||
const nameRow = document.createElement("tr");
|
||||
const nameCol = document.createElement("td");
|
||||
nameRow.className = "expanded";
|
||||
nameRow.dataset[scopedStyleDataVariable] = "";
|
||||
nameCol.innerText = name
|
||||
nameCol.innerText = name;
|
||||
nameCol.dataset[scopedStyleDataVariable] = "";
|
||||
|
||||
nameRow.appendChild(nameCol)
|
||||
nameRow.appendChild(nameCol);
|
||||
|
||||
clickedElement.insertAdjacentElement('afterend', nameRow)
|
||||
clickedElement.insertAdjacentElement("afterend", nameRow);
|
||||
},
|
||||
sendTorrent(magnet, name, event){
|
||||
sendTorrent(magnet, name, event) {
|
||||
this.$notifications.info({
|
||||
title: 'Adding torrent 🦜',
|
||||
title: "Adding torrent 🦜",
|
||||
description: this.query,
|
||||
timeout: 3000
|
||||
})
|
||||
});
|
||||
|
||||
event.target.parentNode.classList.add('active')
|
||||
event.target.parentNode.classList.add("active");
|
||||
addMagnet(magnet, name, this.tmdb_id)
|
||||
.catch((resp) => { console.log('error:', resp.data) })
|
||||
.then((resp) => {
|
||||
console.log('addTorrent resp: ', resp)
|
||||
this.$notifications.success({
|
||||
title: 'Torrent added 🎉',
|
||||
description: this.query,
|
||||
timeout: 3000
|
||||
.catch(resp => {
|
||||
console.log("error:", resp.data);
|
||||
})
|
||||
})
|
||||
.then(resp => {
|
||||
console.log("addTorrent resp: ", resp);
|
||||
this.$notifications.success({
|
||||
title: "Torrent added 🎉",
|
||||
description: this.query,
|
||||
timeout: 3000
|
||||
});
|
||||
});
|
||||
},
|
||||
sortTable(col, sameDirection=false) {
|
||||
sortTable(col, sameDirection = false) {
|
||||
if (this.prevCol === col && sameDirection === false) {
|
||||
this.direction = !this.direction
|
||||
this.direction = !this.direction;
|
||||
}
|
||||
|
||||
switch (col) {
|
||||
case 'name':
|
||||
this.sortName()
|
||||
break
|
||||
case 'seed':
|
||||
this.sortSeed()
|
||||
break
|
||||
case 'size':
|
||||
this.sortSize()
|
||||
break
|
||||
}
|
||||
this.prevCol = col
|
||||
if (col === "name") this.sortName();
|
||||
else if (col === "seed") this.sortSeed();
|
||||
else if (col === "size") this.sortSize();
|
||||
|
||||
this.prevCol = col;
|
||||
},
|
||||
sortName() {
|
||||
const torrentsCopy = [...this.torrents]
|
||||
sortName() {
|
||||
const torrentsCopy = [...this.torrents];
|
||||
if (this.direction) {
|
||||
this.torrents = torrentsCopy.sort((a, b) => (a.name < b.name) ? 1 : -1)
|
||||
this.torrents = torrentsCopy.sort((a, b) => (a.name < b.name ? 1 : -1));
|
||||
} else {
|
||||
this.torrents = torrentsCopy.sort((a, b) => (a.name > b.name) ? 1 : -1)
|
||||
this.torrents = torrentsCopy.sort((a, b) => (a.name > b.name ? 1 : -1));
|
||||
}
|
||||
},
|
||||
sortSeed() {
|
||||
const torrentsCopy = [...this.torrents]
|
||||
sortSeed() {
|
||||
const torrentsCopy = [...this.torrents];
|
||||
if (this.direction) {
|
||||
this.torrents = torrentsCopy.sort((a, b) => parseInt(a.seed) - parseInt(b.seed));
|
||||
this.torrents = torrentsCopy.sort(
|
||||
(a, b) => parseInt(a.seed) - parseInt(b.seed)
|
||||
);
|
||||
} else {
|
||||
this.torrents = torrentsCopy.sort((a, b) => parseInt(b.seed) - parseInt(a.seed));
|
||||
this.torrents = torrentsCopy.sort(
|
||||
(a, b) => parseInt(b.seed) - parseInt(a.seed)
|
||||
);
|
||||
}
|
||||
},
|
||||
sortSize() {
|
||||
const torrentsCopy = [...this.torrents]
|
||||
sortSize() {
|
||||
const torrentsCopy = [...this.torrents];
|
||||
if (this.direction) {
|
||||
this.torrents = torrentsCopy.sort((a, b) => parseInt(sortableSize(a.size)) - parseInt(sortableSize(b.size)));
|
||||
this.torrents = torrentsCopy.sort(
|
||||
(a, b) =>
|
||||
parseInt(sortableSize(a.size)) - parseInt(sortableSize(b.size))
|
||||
);
|
||||
} else {
|
||||
this.torrents = torrentsCopy.sort((a, b) => parseInt(sortableSize(b.size)) - parseInt(sortableSize(a.size)));
|
||||
this.torrents = torrentsCopy.sort(
|
||||
(a, b) =>
|
||||
parseInt(sortableSize(b.size)) - parseInt(sortableSize(a.size))
|
||||
);
|
||||
}
|
||||
},
|
||||
findRelaseTypes() {
|
||||
this.torrents.forEach(item => this.release_types.push(...item.release_type))
|
||||
this.release_types = [...new Set(this.release_types)]
|
||||
this.torrents.forEach(item =>
|
||||
this.release_types.push(...item.release_type)
|
||||
);
|
||||
this.release_types = [...new Set(this.release_types)];
|
||||
},
|
||||
applyFilter(item, index) {
|
||||
this.selectedRelaseType = item;
|
||||
const torrents = [...this.torrentResponse]
|
||||
const torrents = [...this.torrentResponse];
|
||||
|
||||
if (item === 'all') {
|
||||
this.torrents = torrents
|
||||
this.sortTable(this.prevCol, true)
|
||||
return
|
||||
if (item === "all") {
|
||||
this.torrents = torrents;
|
||||
this.sortTable(this.prevCol, true);
|
||||
return;
|
||||
}
|
||||
|
||||
this.torrents = torrents.filter(torrent => torrent.release_type.includes(item))
|
||||
this.sortTable(this.prevCol, true)
|
||||
this.torrents = torrents.filter(torrent =>
|
||||
torrent.release_type.includes(item)
|
||||
);
|
||||
this.sortTable(this.prevCol, true);
|
||||
},
|
||||
updateResultCountInStore() {
|
||||
store.dispatch('torrentModule/setResults', this.torrents)
|
||||
store.dispatch('torrentModule/setResultCount', this.torrentResponse.length)
|
||||
store.dispatch("torrentModule/setResults", this.torrents);
|
||||
store.dispatch(
|
||||
"torrentModule/setResultCount",
|
||||
this.torrentResponse.length
|
||||
);
|
||||
},
|
||||
fetchTorrents(query=undefined){
|
||||
this.listLoaded = false;
|
||||
filterDeadTorrents(torrents) {
|
||||
return torrents.filter(torrent => {
|
||||
if (isNaN(torrent.seed)) return false;
|
||||
return parseInt(torrent.seed) > 0;
|
||||
});
|
||||
},
|
||||
fetchTorrents(query = undefined) {
|
||||
this.loading = true;
|
||||
this.editSearchQuery = false;
|
||||
|
||||
searchTorrents(query || this.query, 'all', this.currentPage, storage.token)
|
||||
return searchTorrents(query || this.query)
|
||||
.then(data => {
|
||||
this.torrentResponse = [...data.results];
|
||||
this.torrents = data.results;
|
||||
this.listLoaded = true;
|
||||
const { results } = data;
|
||||
if (results) {
|
||||
this.torrentResponse = results;
|
||||
this.torrents = this.filterDeadTorrents(results);
|
||||
} else {
|
||||
this.torrents = [];
|
||||
}
|
||||
})
|
||||
.then(this.updateResultCountInStore)
|
||||
.then(this.findRelaseTypes)
|
||||
.catch(e => {
|
||||
const error = e.toString()
|
||||
this.errorMessage = error.indexOf('401') != -1 ? 'Permission denied' : 'Nothing found';
|
||||
this.listLoaded = true;
|
||||
console.log("e:", e);
|
||||
const error = e.toString();
|
||||
this.errorMessage =
|
||||
error.indexOf("401") != -1 ? "Permission denied" : "Nothing found";
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
handleInput(event) {
|
||||
this.editedSearchQuery = event.target.innerText;
|
||||
console.log("edit text:", this.editedSearchQuery);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "./src/scss/variables";
|
||||
@import "src/scss/variables";
|
||||
.expanded {
|
||||
display: flex;
|
||||
padding: 0.25rem 1rem;
|
||||
@@ -301,11 +377,153 @@ export default {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
$checkboxSize: 20px;
|
||||
$ui-border-width: 2px;
|
||||
|
||||
.checkbox {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-bottom: $checkboxSize * 0.5;
|
||||
|
||||
input[type="checkbox"] {
|
||||
display: block;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
|
||||
+ div {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
padding-left: 1.25rem;
|
||||
font-size: 20px;
|
||||
line-height: $checkboxSize + $ui-border-width * 2;
|
||||
left: $checkboxSize;
|
||||
cursor: pointer;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
left: -$checkboxSize;
|
||||
border: $ui-border-width solid var(--color-green);
|
||||
width: $checkboxSize;
|
||||
height: $checkboxSize;
|
||||
}
|
||||
|
||||
&::after {
|
||||
transition: all 0.3s ease;
|
||||
content: "";
|
||||
position: absolute;
|
||||
display: inline-block;
|
||||
left: -$checkboxSize + $ui-border-width;
|
||||
top: $ui-border-width;
|
||||
width: $checkboxSize + $ui-border-width;
|
||||
height: $checkboxSize + $ui-border-width;
|
||||
}
|
||||
}
|
||||
|
||||
&:checked {
|
||||
+ div::after {
|
||||
background-color: var(--color-green);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover:not(checked) {
|
||||
+ div::after {
|
||||
background-color: var(--color-green);
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus {
|
||||
+ div::before {
|
||||
outline: 2px solid Highlight;
|
||||
outline-style: auto;
|
||||
outline-color: -webkit-focus-ring-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style lang="scss" scoped>
|
||||
@import "./src/scss/variables";
|
||||
@import "./src/scss/media-queries";
|
||||
@import "./src/scss/elements";
|
||||
@import "src/scss/variables";
|
||||
@import "src/scss/media-queries";
|
||||
@import "src/scss/elements";
|
||||
|
||||
h2 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
thead {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
color: var(--background-color);
|
||||
text-transform: uppercase;
|
||||
cursor: pointer;
|
||||
background-color: var(--text-color);
|
||||
letter-spacing: 0.8px;
|
||||
font-size: 1rem;
|
||||
border: 1px solid var(--text-color-90);
|
||||
|
||||
th:first-of-type {
|
||||
border-top-left-radius: 8px;
|
||||
}
|
||||
|
||||
th:last-of-type {
|
||||
border-top-right-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
tr > td:first-of-type {
|
||||
white-space: unset;
|
||||
}
|
||||
|
||||
tr > td:not(td:first-of-type) {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
tr > td:last-of-type {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
tr td:first-of-type {
|
||||
border-left: 1px solid var(--text-color-90);
|
||||
}
|
||||
|
||||
tr td:last-of-type {
|
||||
border-right: 1px solid var(--text-color-90);
|
||||
}
|
||||
|
||||
tr:last-of-type {
|
||||
td {
|
||||
border-bottom: 1px solid var(--text-color-90);
|
||||
}
|
||||
|
||||
td:first-of-type {
|
||||
border-bottom-left-radius: 8px;
|
||||
}
|
||||
td:last-of-type {
|
||||
border-bottom-right-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
tr:nth-child(even) {
|
||||
background-color: var(--background-70);
|
||||
}
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 0.35rem 0.25rem;
|
||||
white-space: nowrap;
|
||||
|
||||
svg {
|
||||
width: 24px;
|
||||
fill: var(--text-color);
|
||||
}
|
||||
}
|
||||
|
||||
.toggle {
|
||||
max-width: unset !important;
|
||||
@@ -323,17 +541,21 @@ export default {
|
||||
justify-content: center;
|
||||
padding-bottom: 20px;
|
||||
|
||||
|
||||
&-text {
|
||||
font-weight: 400;
|
||||
text-transform: uppercase;
|
||||
font-size: 14px;
|
||||
color: $green;
|
||||
font-size: 20px;
|
||||
// color: $green;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
|
||||
@include tablet-min {
|
||||
font-size: 16px
|
||||
.icon {
|
||||
vertical-align: text-top;
|
||||
margin-left: 1rem;
|
||||
fill: var(--text-color);
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
// stroke: white !important;
|
||||
}
|
||||
|
||||
&.editable {
|
||||
@@ -355,59 +577,67 @@ export default {
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
// border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
margin-top: 1rem;
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
// table-layout: fixed;
|
||||
}
|
||||
|
||||
.table__content, .table__header {
|
||||
display: flex;
|
||||
padding: 0;
|
||||
border-left: 1px solid $text-color;
|
||||
border-right: 1px solid $text-color;
|
||||
border-bottom: 1px solid $text-color;
|
||||
// .table__content,
|
||||
// .table__header {
|
||||
// display: flex;
|
||||
// padding: 0;
|
||||
// border-left: 1px solid $text-color;
|
||||
// border-right: 1px solid $text-color;
|
||||
// border-bottom: 1px solid $text-color;
|
||||
|
||||
th, td {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-basis: 100%;
|
||||
// th,
|
||||
// td {
|
||||
// display: flex;
|
||||
// flex-direction: column;
|
||||
// flex-basis: 100%;
|
||||
|
||||
padding: 0.4rem;
|
||||
// padding: 0.4rem;
|
||||
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
min-width: 75px;
|
||||
}
|
||||
// white-space: nowrap;
|
||||
// text-overflow: ellipsis;
|
||||
// overflow: hidden;
|
||||
// min-width: 75px;
|
||||
// }
|
||||
|
||||
th:first-child, td:first-child {
|
||||
flex: 1;
|
||||
}
|
||||
// th:first-child,
|
||||
// td:first-child {
|
||||
// flex: 1;
|
||||
// }
|
||||
|
||||
th:not(:first-child), td:not(:first-child) {
|
||||
flex: 0.2;
|
||||
}
|
||||
// th:not(:first-child),
|
||||
// td:not(:first-child) {
|
||||
// flex: 0.2;
|
||||
// }
|
||||
|
||||
th:nth-child(2), td:nth-child(2) {
|
||||
flex: 0.1;
|
||||
}
|
||||
// th:nth-child(2),
|
||||
// td:nth-child(2) {
|
||||
// flex: 0.1;
|
||||
// }
|
||||
|
||||
@include mobile-only {
|
||||
th:first-child, td:first-child {
|
||||
display: none;
|
||||
// @include mobile-only {
|
||||
// th:first-child,
|
||||
// td:first-child {
|
||||
// display: none;
|
||||
|
||||
&.show {
|
||||
display: block;
|
||||
align: flex-end;
|
||||
}
|
||||
}
|
||||
// &.show {
|
||||
// display: block;
|
||||
// align: flex-end;
|
||||
// }
|
||||
// }
|
||||
|
||||
th:not(:first-child), td:not(:first-child) {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
// th:not(:first-child),
|
||||
// td:not(:first-child) {
|
||||
// flex: 1;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
.table__content {
|
||||
td:not(:last-child) {
|
||||
@@ -422,58 +652,54 @@ table {
|
||||
border-bottom-right-radius: 3px;
|
||||
}
|
||||
|
||||
.table__header {
|
||||
color: $text-color;
|
||||
text-transform: uppercase;
|
||||
cursor: pointer;
|
||||
background-color: $background-color-secondary;
|
||||
// .table__header {
|
||||
// color: $text-color;
|
||||
// text-transform: uppercase;
|
||||
// cursor: pointer;
|
||||
// background-color: $background-color-secondary;
|
||||
|
||||
border-top: 1px solid $text-color;
|
||||
border-top-left-radius: 3px;
|
||||
border-top-right-radius: 3px;
|
||||
// border-top: 1px solid $text-color;
|
||||
// border-top-left-radius: 3px;
|
||||
// border-top-right-radius: 3px;
|
||||
|
||||
th {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.7px;
|
||||
// font-size: 1.08rem;
|
||||
font-size: 15px;
|
||||
// th {
|
||||
// display: flex;
|
||||
// flex-direction: row;
|
||||
// font-weight: 400;
|
||||
// letter-spacing: 0.7px;
|
||||
// // font-size: 1.08rem;
|
||||
// font-size: 15px;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
min-width: 0.2rem;
|
||||
}
|
||||
|
||||
span:first-child {
|
||||
margin-right: 0.6rem;
|
||||
}
|
||||
span:nth-child(2) {
|
||||
margin-right: 0.1rem;
|
||||
}
|
||||
}
|
||||
|
||||
th:not(:last-child) {
|
||||
border-right: 1px solid $text-color;
|
||||
}
|
||||
|
||||
}
|
||||
// &::before {
|
||||
// content: "";
|
||||
// min-width: 0.2rem;
|
||||
// }
|
||||
|
||||
// span:first-child {
|
||||
// margin-right: 0.6rem;
|
||||
// }
|
||||
// span:nth-child(2) {
|
||||
// margin-right: 0.1rem;
|
||||
// }
|
||||
// }
|
||||
|
||||
// th:not(:last-child) {
|
||||
// border-right: 1px solid $text-color;
|
||||
// }
|
||||
// }
|
||||
|
||||
.editQuery {
|
||||
display: flex;
|
||||
width: 70%;
|
||||
justify-content: center;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
@include mobile-only {
|
||||
width: 90%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.download {
|
||||
|
||||
&__icon {
|
||||
fill: $text-color-70;
|
||||
height: 1.2rem;
|
||||
@@ -506,7 +732,7 @@ table {
|
||||
&:after {
|
||||
border: 5px solid $green;
|
||||
border-radius: 50%;
|
||||
content: '';
|
||||
content: "";
|
||||
left: 10px;
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
@@ -514,6 +740,8 @@ table {
|
||||
}
|
||||
}
|
||||
@keyframes load {
|
||||
100% { transform: rotate(360deg); }
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
262
src/components/header/AutocompleteDropdown.vue
Normal file
@@ -0,0 +1,262 @@
|
||||
<template>
|
||||
<transition name="shut">
|
||||
<ul class="dropdown">
|
||||
<li
|
||||
v-for="result in searchResults"
|
||||
:key="`${result.index}-${result.title}-${result.type}`"
|
||||
@click="openPopup(result)"
|
||||
:class="
|
||||
`result di-${result.index} ${result.index === index ? 'active' : ''}`
|
||||
"
|
||||
>
|
||||
<IconMovie v-if="result.type == 'movie'" class="type-icon" />
|
||||
<IconShow v-if="result.type == 'show'" class="type-icon" />
|
||||
<span class="title">{{ result.title }}</span>
|
||||
</li>
|
||||
|
||||
<li
|
||||
v-if="searchResults.length"
|
||||
:class="`info di-${searchResults.length}`"
|
||||
>
|
||||
<span> Select from list or press enter to search </span>
|
||||
</li>
|
||||
</ul>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions } from "vuex";
|
||||
import IconMovie from "src/icons/IconMovie";
|
||||
import IconShow from "src/icons/IconShow";
|
||||
import IconPerson from "src/icons/IconPerson";
|
||||
import { elasticSearchMoviesAndShows } from "@/api";
|
||||
|
||||
export default {
|
||||
components: { IconMovie, IconShow, IconPerson },
|
||||
props: {
|
||||
query: {
|
||||
type: String,
|
||||
default: null,
|
||||
required: false
|
||||
},
|
||||
index: {
|
||||
type: Number,
|
||||
default: -1,
|
||||
required: false
|
||||
},
|
||||
results: {
|
||||
type: Array,
|
||||
default: [],
|
||||
required: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
query(newQuery) {
|
||||
if (newQuery && newQuery.length > 1) this.fetchAutocompleteResults();
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
searchResults: [],
|
||||
keyboardNavigationIndex: 0,
|
||||
numberOfResults: 10
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
...mapActions("popup", ["open"]),
|
||||
openPopup(result) {
|
||||
const { id, type } = result;
|
||||
this.open({ id, type });
|
||||
},
|
||||
fetchAutocompleteResults() {
|
||||
this.keyboardNavigationIndex = 0;
|
||||
this.searchResults = [];
|
||||
|
||||
elasticSearchMoviesAndShows(this.query, this.numberOfResults).then(
|
||||
resp => {
|
||||
const data = resp.hits.hits;
|
||||
|
||||
let results = data.map(item => {
|
||||
let index = null;
|
||||
if (item._source.log.file.path.includes("movie")) index = "movie";
|
||||
if (item._source.log.file.path.includes("series")) index = "show";
|
||||
|
||||
if (index === "movie" || index === "show") {
|
||||
return {
|
||||
title:
|
||||
item._source.original_name || item._source.original_title,
|
||||
id: item._source.id,
|
||||
adult: item._source.adult,
|
||||
type: index
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
results = this.removeDuplicates(results);
|
||||
results = results.map((el, index) => {
|
||||
return { ...el, index };
|
||||
});
|
||||
|
||||
this.$emit("update:results", results);
|
||||
this.searchResults = results;
|
||||
}
|
||||
);
|
||||
},
|
||||
removeDuplicates(searchResults) {
|
||||
let filteredResults = [];
|
||||
searchResults.map(result => {
|
||||
if (result === undefined) return;
|
||||
const numberOfDuplicates = filteredResults.filter(
|
||||
filterItem => filterItem.id == result.id
|
||||
);
|
||||
if (numberOfDuplicates.length >= 1) {
|
||||
return null;
|
||||
}
|
||||
filteredResults.push(result);
|
||||
});
|
||||
|
||||
if (this.adult == false) {
|
||||
filteredResults = filteredResults.filter(
|
||||
result => result.adult == false
|
||||
);
|
||||
}
|
||||
|
||||
return filteredResults;
|
||||
}
|
||||
},
|
||||
created() {
|
||||
if (this.query) this.fetchAutocompleteResults();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "src/scss/variables";
|
||||
@import "src/scss/media-queries";
|
||||
@import "src/scss/main";
|
||||
$sizes: 22;
|
||||
|
||||
@for $i from 0 through $sizes {
|
||||
.dropdown .di-#{$i} {
|
||||
visibility: visible;
|
||||
transform-origin: top center;
|
||||
animation: scaleZ 200ms calc(50ms * #{$i}) ease-in forwards;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes scaleZ {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scale(0);
|
||||
}
|
||||
80% {
|
||||
transform: scale(1.07);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
top: var(--header-size);
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
max-width: 720px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: wrap;
|
||||
z-index: 5;
|
||||
|
||||
margin-top: -1px;
|
||||
border-top: none;
|
||||
padding: 0;
|
||||
|
||||
@include mobile {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
max-width: 100vw;
|
||||
}
|
||||
|
||||
@include tablet-min {
|
||||
top: unset;
|
||||
--gutter: 1.5rem;
|
||||
max-width: calc(100% - (2 * var(--gutter)));
|
||||
margin: -1px var(--gutter) 0 var(--gutter);
|
||||
}
|
||||
|
||||
@include desktop {
|
||||
max-width: 720px;
|
||||
}
|
||||
}
|
||||
|
||||
li.result {
|
||||
background-color: var(--background-95);
|
||||
color: var(--text-color-50);
|
||||
padding: 0.5rem 2rem;
|
||||
list-style: none;
|
||||
|
||||
opacity: 0;
|
||||
height: 56px;
|
||||
width: 100%;
|
||||
visibility: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.5rem 2rem;
|
||||
|
||||
font-size: 1.4rem;
|
||||
text-transform: capitalize;
|
||||
list-style: none;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
|
||||
transition: color 0.1s ease, fill 0.4s ease;
|
||||
|
||||
span {
|
||||
overflow-x: hidden;
|
||||
text-overflow: ellipsis;
|
||||
transition: inherit;
|
||||
}
|
||||
|
||||
&.active,
|
||||
&:hover,
|
||||
&:active {
|
||||
color: var(--text-color);
|
||||
border-bottom: 2px solid var(--color-green);
|
||||
|
||||
.type-icon {
|
||||
fill: var(--text-color);
|
||||
}
|
||||
}
|
||||
|
||||
.type-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
margin-right: 1rem;
|
||||
transition: inherit;
|
||||
fill: var(--text-color-50);
|
||||
}
|
||||
}
|
||||
|
||||
li.info {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 0 1rem;
|
||||
color: var(--text-color-50);
|
||||
background-color: var(--background-95);
|
||||
color: var(--text-color-50);
|
||||
font-size: 0.6rem;
|
||||
height: 16px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.shut-leave-to {
|
||||
height: 0px;
|
||||
transition: height 0.4s ease;
|
||||
flex-wrap: no-wrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
126
src/components/header/NavigationHeader.vue
Normal file
@@ -0,0 +1,126 @@
|
||||
<template>
|
||||
<nav>
|
||||
<a v-if="isHome" class="nav__logo" href="/">
|
||||
<TmdbLogo class="logo" />
|
||||
</a>
|
||||
<router-link v-else class="nav__logo" to="/" exact>
|
||||
<TmdbLogo class="logo" />
|
||||
</router-link>
|
||||
|
||||
<SearchInput />
|
||||
|
||||
<Hamburger class="mobile-only" />
|
||||
|
||||
<NavigationIcon class="desktop-only" :route="profileRoute" />
|
||||
|
||||
<div class="navigation-icons-grid mobile-only" :class="{ open: isOpen }">
|
||||
<NavigationIcons>
|
||||
<NavigationIcon :route="profileRoute" />
|
||||
</NavigationIcons>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapActions } from "vuex";
|
||||
import TmdbLogo from "@/icons/tmdb-logo";
|
||||
import IconProfile from "@/icons/IconProfile";
|
||||
import IconProfileLock from "@/icons/IconProfileLock";
|
||||
import IconSettings from "@/icons/IconSettings";
|
||||
import IconActivity from "@/icons/IconActivity";
|
||||
import SearchInput from "@/components/header/SearchInput";
|
||||
import NavigationIcons from "src/components/header/NavigationIcons";
|
||||
import NavigationIcon from "src/components/header/NavigationIcon";
|
||||
import Hamburger from "@/components/ui/Hamburger";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NavigationIcons,
|
||||
NavigationIcon,
|
||||
SearchInput,
|
||||
TmdbLogo,
|
||||
IconProfile,
|
||||
IconProfileLock,
|
||||
IconSettings,
|
||||
IconActivity,
|
||||
Hamburger
|
||||
},
|
||||
computed: {
|
||||
...mapGetters("user", ["loggedIn"]),
|
||||
...mapGetters("hamburger", ["isOpen"]),
|
||||
isHome() {
|
||||
return this.$route.path === "/";
|
||||
},
|
||||
profileRoute() {
|
||||
return {
|
||||
title: !this.loggedIn ? "Signin" : "Profile",
|
||||
route: !this.loggedIn ? "/signin" : "/profile",
|
||||
icon: !this.loggedIn ? IconProfileLock : IconProfile
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "src/scss/variables";
|
||||
@import "src/scss/media-queries";
|
||||
|
||||
.spacer {
|
||||
@include mobile-only {
|
||||
width: 100%;
|
||||
height: $header-size;
|
||||
}
|
||||
}
|
||||
|
||||
nav {
|
||||
display: grid;
|
||||
grid-template-columns: var(--header-size) 1fr var(--header-size);
|
||||
|
||||
> * {
|
||||
z-index: 10;
|
||||
}
|
||||
}
|
||||
|
||||
.nav__logo {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.logo {
|
||||
padding: 1rem;
|
||||
fill: var(--color-green);
|
||||
width: var(--header-size);
|
||||
height: var(--header-size);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: $background-nav-logo;
|
||||
transition: transform 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.08);
|
||||
}
|
||||
|
||||
@include mobile {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.navigation-icons-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
position: fixed;
|
||||
top: var(--header-size);
|
||||
left: 0;
|
||||
width: 100%;
|
||||
background-color: $background-95;
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
transition: opacity 0.4s ease;
|
||||
|
||||
&.open {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
81
src/components/header/NavigationIcon.vue
Normal file
@@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<router-link
|
||||
:to="{ path: route.route }"
|
||||
:key="route.title"
|
||||
v-if="route.requiresAuth == undefined || (route.requiresAuth && loggedIn)"
|
||||
>
|
||||
<li class="navigation-link" :class="{ active: route.route == active }">
|
||||
<component class="navigation-icon" :is="route.icon"></component>
|
||||
<span>{{ route.title }}</span>
|
||||
</li>
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapActions } from "vuex";
|
||||
|
||||
export default {
|
||||
name: "NavigationIcon",
|
||||
props: {
|
||||
active: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
route: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters("user", ["loggedIn"])
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "src/scss/media-queries";
|
||||
|
||||
.navigation-link {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
min-height: var(--header-size);
|
||||
list-style: none;
|
||||
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;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&.active {
|
||||
background-color: var(--background-color);
|
||||
|
||||
span,
|
||||
.navigation-icon {
|
||||
color: var(--text-color);
|
||||
fill: var(--text-color);
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
text-transform: uppercase;
|
||||
font-size: 11px;
|
||||
margin-top: 0.25rem;
|
||||
color: var(--text-color-70);
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.navigation-icon {
|
||||
width: 28px;
|
||||
fill: var(--text-color-70);
|
||||
transition: inherit;
|
||||
}
|
||||
</style>
|
||||
103
src/components/header/NavigationIcons.vue
Normal file
@@ -0,0 +1,103 @@
|
||||
<template>
|
||||
<ul class="navigation-icons">
|
||||
<NavigationIcon
|
||||
v-for="route in routes"
|
||||
:key="route.route"
|
||||
:route="route"
|
||||
:active="activeRoute"
|
||||
/>
|
||||
<slot></slot>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import NavigationIcon from "@/components/header/NavigationIcon";
|
||||
import IconInbox from "@/icons/IconInbox";
|
||||
import IconNowPlaying from "@/icons/IconNowPlaying";
|
||||
import IconPopular from "@/icons/IconPopular";
|
||||
import IconUpcoming from "@/icons/IconUpcoming";
|
||||
import IconSettings from "@/icons/IconSettings";
|
||||
import IconActivity from "@/icons/IconActivity";
|
||||
|
||||
export default {
|
||||
name: "NavigationIcons",
|
||||
components: {
|
||||
NavigationIcon,
|
||||
IconInbox,
|
||||
IconPopular,
|
||||
IconNowPlaying,
|
||||
IconUpcoming,
|
||||
IconSettings,
|
||||
IconActivity
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
routes: [
|
||||
{
|
||||
title: "Requests",
|
||||
route: "/list/requests",
|
||||
icon: IconInbox
|
||||
},
|
||||
{
|
||||
title: "Now Playing",
|
||||
route: "/list/now_playing",
|
||||
icon: IconNowPlaying
|
||||
},
|
||||
{
|
||||
title: "Popular",
|
||||
route: "/list/popular",
|
||||
icon: IconPopular
|
||||
},
|
||||
{
|
||||
title: "Upcoming",
|
||||
route: "/list/upcoming",
|
||||
icon: IconUpcoming
|
||||
},
|
||||
{
|
||||
title: "Activity",
|
||||
route: "/activity",
|
||||
requiresAuth: true,
|
||||
icon: IconActivity
|
||||
},
|
||||
{
|
||||
title: "Settings",
|
||||
route: "/profile?settings=true",
|
||||
requiresAuth: true,
|
||||
icon: IconSettings
|
||||
}
|
||||
],
|
||||
activeRoute: null
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
$route() {
|
||||
this.activeRoute = window.location.pathname;
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.activeRoute = window.location.pathname;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "src/scss/media-queries";
|
||||
|
||||
.navigation-icons {
|
||||
display: grid;
|
||||
grid-column: 1fr;
|
||||
padding-left: 0;
|
||||
margin: 0;
|
||||
background-color: var(--background-color-secondary);
|
||||
z-index: 15;
|
||||
width: 100%;
|
||||
|
||||
@include desktop {
|
||||
grid-template-rows: var(--header-size);
|
||||
}
|
||||
|
||||
@include mobile {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
281
src/components/header/SearchInput.vue
Normal file
@@ -0,0 +1,281 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="search" :class="{ active: focusingInput }">
|
||||
<IconSearch class="search-icon" tabindex="-1" />
|
||||
|
||||
<input
|
||||
ref="input"
|
||||
type="text"
|
||||
placeholder="Search for movie or show"
|
||||
aria-label="Search input for finding a movie or show"
|
||||
autocorrect="off"
|
||||
autocapitalize="off"
|
||||
tabindex="0"
|
||||
v-model="query"
|
||||
@input="handleInput"
|
||||
@click="focusingInput = true"
|
||||
@keydown.escape="handleEscape"
|
||||
@keyup.enter="handleSubmit"
|
||||
@keydown.up="navigateUp"
|
||||
@keydown.down="navigateDown"
|
||||
@focus="focusingInput = true"
|
||||
@blur="focusingInput = false"
|
||||
/>
|
||||
|
||||
<IconClose
|
||||
tabindex="0"
|
||||
aria-label="button"
|
||||
v-if="query && query.length"
|
||||
@click="resetQuery"
|
||||
@keydown.enter.stop="resetQuery"
|
||||
class="close-icon"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<AutocompleteDropdown
|
||||
v-if="showAutocompleteResults"
|
||||
:query="query"
|
||||
:index="dropdownIndex"
|
||||
:results.sync="dropdownResults"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapGetters } from "vuex";
|
||||
import SeasonedButton from "@/components/ui/SeasonedButton";
|
||||
import AutocompleteDropdown from "@/components/header/AutocompleteDropdown";
|
||||
|
||||
import IconSearch from "src/icons/IconSearch";
|
||||
import IconClose from "src/icons/IconClose";
|
||||
import config from "@/config";
|
||||
|
||||
export default {
|
||||
name: "SearchInput",
|
||||
components: {
|
||||
SeasonedButton,
|
||||
AutocompleteDropdown,
|
||||
IconClose,
|
||||
IconSearch
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
query: null,
|
||||
disabled: false,
|
||||
dropdownIndex: -1,
|
||||
dropdownResults: [],
|
||||
focusingInput: false,
|
||||
showAutocomplete: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters("popup", ["isOpen"]),
|
||||
showAutocompleteResults() {
|
||||
return (
|
||||
!this.disabled &&
|
||||
this.focusingInput &&
|
||||
this.query &&
|
||||
this.query.length > 0
|
||||
);
|
||||
}
|
||||
},
|
||||
created() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
if (params && params.has("query")) {
|
||||
this.query = decodeURIComponent(params.get("query"));
|
||||
}
|
||||
|
||||
const elasticUrl = config.ELASTIC_URL;
|
||||
if (elasticUrl === undefined || elasticUrl === false || elasticUrl === "") {
|
||||
this.disabled = true;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions("popup", ["open"]),
|
||||
navigateDown() {
|
||||
if (this.dropdownIndex < this.dropdownResults.length - 1) {
|
||||
this.dropdownIndex++;
|
||||
}
|
||||
},
|
||||
navigateUp() {
|
||||
if (this.dropdownIndex > -1) this.dropdownIndex--;
|
||||
|
||||
const input = this.$refs.input;
|
||||
const textLength = input.value.length;
|
||||
|
||||
setTimeout(() => {
|
||||
input.focus();
|
||||
input.setSelectionRange(textLength, textLength + 1);
|
||||
}, 1);
|
||||
},
|
||||
search() {
|
||||
const encodedQuery = encodeURI(this.query.replace('/ /g, "+"'));
|
||||
|
||||
this.$router.push({
|
||||
name: "search",
|
||||
query: {
|
||||
...this.$route.query,
|
||||
query: encodedQuery
|
||||
}
|
||||
});
|
||||
},
|
||||
resetQuery(event) {
|
||||
this.query = "";
|
||||
this.$refs.input.focus();
|
||||
},
|
||||
handleInput(e) {
|
||||
this.$emit("input", this.query);
|
||||
this.dropdownIndex = -1;
|
||||
},
|
||||
handleSubmit() {
|
||||
if (!this.query || this.query.length == 0) return;
|
||||
|
||||
if (this.dropdownIndex >= 0) {
|
||||
const resultItem = this.dropdownResults[this.dropdownIndex];
|
||||
|
||||
console.log("resultItem:", resultItem);
|
||||
this.open({
|
||||
id: resultItem.id,
|
||||
type: resultItem.type
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.search();
|
||||
this.$refs.input.blur();
|
||||
this.dropdownIndex = -1;
|
||||
},
|
||||
handleEscape() {
|
||||
if (!this.isOpen) {
|
||||
this.$refs.input.blur();
|
||||
this.dropdownIndex = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "src/scss/variables";
|
||||
@import "src/scss/media-queries";
|
||||
@import "src/scss/main";
|
||||
|
||||
.close-icon {
|
||||
position: absolute;
|
||||
top: calc(50% - 12px);
|
||||
right: 0;
|
||||
cursor: pointer;
|
||||
fill: var(--text-color);
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
|
||||
@include tablet-min {
|
||||
right: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.filter {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 1rem 2rem;
|
||||
|
||||
h2 {
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
&-items {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
> :not(:first-child) {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hr {
|
||||
display: block;
|
||||
height: 1px;
|
||||
border: 0;
|
||||
border-bottom: 1px solid $text-color-50;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.search.active {
|
||||
input {
|
||||
border-color: var(--color-green);
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
fill: var(--color-green);
|
||||
}
|
||||
}
|
||||
|
||||
.search {
|
||||
height: $header-size;
|
||||
display: flex;
|
||||
position: fixed;
|
||||
flex-wrap: wrap;
|
||||
z-index: 5;
|
||||
border: 0;
|
||||
background-color: $background-color-secondary;
|
||||
|
||||
// TODO check if this is for mobile
|
||||
width: calc(100% - 110px);
|
||||
top: 0;
|
||||
right: 55px;
|
||||
|
||||
@include tablet-min {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
right: 0px;
|
||||
}
|
||||
|
||||
input {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 13px 28px 13px 45px;
|
||||
outline: none;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
background-color: $background-color-secondary;
|
||||
font-weight: 300;
|
||||
font-size: 18px;
|
||||
color: $text-color;
|
||||
border-bottom: 1px solid transparent;
|
||||
|
||||
&:focus {
|
||||
// border-bottom: 1px solid var(--color-green);
|
||||
border-color: var(--color-green);
|
||||
}
|
||||
|
||||
@include tablet-min {
|
||||
font-size: 24px;
|
||||
padding: 13px 40px 13px 60px;
|
||||
}
|
||||
}
|
||||
|
||||
&-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
fill: var(--text-color-50);
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
left: 15px;
|
||||
top: calc(50% - 10px);
|
||||
|
||||
@include tablet-min {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
top: calc(50% - 12px);
|
||||
left: 22px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
82
src/components/popup/ActionButton.vue
Normal file
@@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<li
|
||||
class="sidebar-list-element"
|
||||
@click="event => $emit('click', event)"
|
||||
:class="{ active, disabled }"
|
||||
>
|
||||
<slot></slot>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
active: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "src/scss/media-queries";
|
||||
|
||||
li.sidebar-list-element {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-color-50);
|
||||
font-size: 12px;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid var(--text-color-5);
|
||||
cursor: pointer;
|
||||
|
||||
&:first-of-type {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
div > svg,
|
||||
svg {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
margin-right: 1rem;
|
||||
transition: all 0.3s ease;
|
||||
fill: var(--text-color-70);
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&.active {
|
||||
color: var(--text-color);
|
||||
|
||||
div > svg,
|
||||
svg {
|
||||
fill: var(--text-color);
|
||||
transform: scale(1.1, 1.1);
|
||||
}
|
||||
}
|
||||
|
||||
&.active > div > svg,
|
||||
&.active > svg {
|
||||
fill: var(--color-green);
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.pending {
|
||||
color: #f8bd2d;
|
||||
}
|
||||
|
||||
.meta {
|
||||
margin-left: auto;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
124
src/components/popup/Description.vue
Normal file
@@ -0,0 +1,124 @@
|
||||
<template>
|
||||
<div
|
||||
id="description"
|
||||
class="movie-description noselect"
|
||||
@click="overflow ? (truncated = !truncated) : null"
|
||||
>
|
||||
<span ref="description" :class="{ truncated }">{{ description }}</span>
|
||||
|
||||
<button v-if="description && overflow" class="truncate-toggle">
|
||||
<IconArrowDown :class="{ rotate: !truncated }" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import IconArrowDown from "../../icons/IconArrowDown";
|
||||
export default {
|
||||
components: { IconArrowDown },
|
||||
props: {
|
||||
description: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
truncated: true,
|
||||
overflow: false
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.checkDescriptionOverflowing();
|
||||
},
|
||||
methods: {
|
||||
checkDescriptionOverflowing() {
|
||||
const descriptionEl = document.getElementById("description");
|
||||
if (!descriptionEl) return;
|
||||
|
||||
const { height, width } = descriptionEl.getBoundingClientRect();
|
||||
const { fontSize, lineHeight } = getComputedStyle(descriptionEl);
|
||||
|
||||
const elementWithoutOverflow = document.createElement("div");
|
||||
elementWithoutOverflow.setAttribute(
|
||||
"style",
|
||||
`max-width: ${Math.ceil(
|
||||
width + 10
|
||||
)}px; display: block; font-size: ${fontSize}; line-height: ${lineHeight};`
|
||||
);
|
||||
// Don't know why need to add 10px to width, but works out perfectly
|
||||
|
||||
elementWithoutOverflow.classList.add("dummy-non-overflow");
|
||||
elementWithoutOverflow.innerText = this.description;
|
||||
|
||||
document.body.appendChild(elementWithoutOverflow);
|
||||
const elemWithoutOverflowHeight =
|
||||
elementWithoutOverflow.getBoundingClientRect()["height"];
|
||||
|
||||
this.overflow = elemWithoutOverflowHeight > height;
|
||||
this.removeElements(document.querySelectorAll(".dummy-non-overflow"));
|
||||
},
|
||||
removeElements: elems => elems.forEach(el => el.remove())
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "src/scss/media-queries";
|
||||
|
||||
.movie-description {
|
||||
font-weight: 300;
|
||||
font-size: 13px;
|
||||
line-height: 1.8;
|
||||
margin-bottom: 20px;
|
||||
transition: all 1s ease;
|
||||
|
||||
@include tablet-min {
|
||||
margin-bottom: 30px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
span.truncated {
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
-webkit-line-clamp: 4;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.truncate-toggle {
|
||||
border: none;
|
||||
background: none;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
color: var(--text-color);
|
||||
margin-top: 1rem;
|
||||
cursor: pointer;
|
||||
|
||||
svg {
|
||||
transition: 0.4s ease all;
|
||||
height: 22px;
|
||||
width: 22px;
|
||||
fill: var(--text-color);
|
||||
|
||||
&.rotate {
|
||||
transform: rotateX(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: "";
|
||||
flex: 1;
|
||||
border-bottom: 1px solid var(--text-color-50);
|
||||
}
|
||||
&::before {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
&::after {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
58
src/components/popup/Detail.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<div class="movie-detail">
|
||||
<h2 class="title">{{ title }}</h2>
|
||||
<span v-if="detail" class="info">{{ detail }}</span>
|
||||
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
detail: {
|
||||
required: false,
|
||||
default: null
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "src/scss/media-queries";
|
||||
|
||||
.movie-detail {
|
||||
margin-bottom: 20px;
|
||||
|
||||
&:last-of-type {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
@include tablet-min {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
h2.title {
|
||||
margin: 0;
|
||||
font-weight: 400;
|
||||
text-transform: uppercase;
|
||||
font-size: 1.2rem;
|
||||
color: var(--color-green);
|
||||
|
||||
@include mobile {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
}
|
||||
|
||||
span.info {
|
||||
font-weight: 300;
|
||||
font-size: 1rem;
|
||||
letter-spacing: 0.8px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -10,12 +10,15 @@
|
||||
<img
|
||||
class="movie-item__img is-loaded"
|
||||
ref="poster-image"
|
||||
src="~assets/placeholder.png"
|
||||
src="/assets/placeholder.png"
|
||||
/>
|
||||
</figure>
|
||||
|
||||
<h1 class="movie__title" v-if="movie">{{ movie.title }}</h1>
|
||||
<loading-placeholder v-else :count="1" />
|
||||
<div v-if="movie" class="movie__title">
|
||||
<h1>{{ movie.title || movie.name }}</h1>
|
||||
<i>{{ movie.tagline }}</i>
|
||||
</div>
|
||||
<loading-placeholder v-else :count="2" />
|
||||
</header>
|
||||
|
||||
<!-- Siderbar and movie info -->
|
||||
@@ -23,50 +26,58 @@
|
||||
<div class="movie__wrap movie__wrap--main">
|
||||
<!-- SIDEBAR ACTIONS -->
|
||||
<div class="movie__actions" v-if="movie">
|
||||
<sidebar-list-element
|
||||
:iconRef="'#iconNot_exsits'"
|
||||
:active="matched"
|
||||
:iconRefActive="'#iconExists'"
|
||||
:textActive="'Already in plex 🎉'"
|
||||
>
|
||||
Not yet in plex
|
||||
</sidebar-list-element>
|
||||
<sidebar-list-element
|
||||
@click="sendRequest"
|
||||
:iconRef="'#iconSent'"
|
||||
:active="requested"
|
||||
:textActive="'Requested to be downloaded'"
|
||||
>
|
||||
Request to be downloaded?
|
||||
</sidebar-list-element>
|
||||
<action-button :active="matched" :disabled="true">
|
||||
<IconThumbsUp v-if="matched" />
|
||||
<IconThumbsDown v-else />
|
||||
{{ !matched ? "Not yet available" : "Already available 🎉" }}
|
||||
</action-button>
|
||||
|
||||
<sidebar-list-element
|
||||
v-if="isPlexAuthenticated && matched"
|
||||
@click="openInPlex"
|
||||
:iconString="'⏯ '"
|
||||
>
|
||||
Watch in plex now!
|
||||
</sidebar-list-element>
|
||||
<action-button @click="sendRequest" :active="requested">
|
||||
<transition name="fade" mode="out-in">
|
||||
<div v-if="!requested" key="request"><IconRequest /></div>
|
||||
<div v-else key="requested"><IconRequested /></div>
|
||||
</transition>
|
||||
{{ !requested ? `Request ${this.type}?` : "Already requested" }}
|
||||
</action-button>
|
||||
|
||||
<sidebar-list-element
|
||||
v-if="admin"
|
||||
<action-button v-if="plexId && matched" @click="openInPlex">
|
||||
<IconPlay />
|
||||
Open and watch in plex now!
|
||||
</action-button>
|
||||
|
||||
<action-button
|
||||
v-if="credits && credits.cast && credits.cast.length"
|
||||
:active="showCast"
|
||||
@click="() => (showCast = !showCast)"
|
||||
>
|
||||
<IconProfile class="icon" />
|
||||
{{ showCast ? "Hide cast" : "Show cast" }}
|
||||
</action-button>
|
||||
|
||||
<action-button
|
||||
v-if="admin === true"
|
||||
@click="showTorrents = !showTorrents"
|
||||
:iconRef="'#icon_torrents'"
|
||||
:active="showTorrents"
|
||||
:supplementaryText="numberOfTorrentResults"
|
||||
>
|
||||
<IconBinoculars />
|
||||
Search for torrents
|
||||
</sidebar-list-element>
|
||||
<sidebar-list-element @click="openTmdb" :iconRef="'#icon_info'">
|
||||
<span v-if="numberOfTorrentResults" class="meta">{{
|
||||
numberOfTorrentResults
|
||||
}}</span>
|
||||
</action-button>
|
||||
|
||||
<action-button @click="openTmdb">
|
||||
<IconInfo />
|
||||
See more info
|
||||
</sidebar-list-element>
|
||||
</action-button>
|
||||
</div>
|
||||
|
||||
<!-- Loading placeholder -->
|
||||
<div class="movie__actions text-input__loading" v-else>
|
||||
<div
|
||||
v-for="index in admin ? Array(4) : Array(3)"
|
||||
class="movie__actions-link"
|
||||
v-for="_ in admin ? Array(4) : Array(3)"
|
||||
:key="index"
|
||||
>
|
||||
<div
|
||||
class="movie__actions-text text-input__loading--line"
|
||||
@@ -78,70 +89,63 @@
|
||||
<!-- MOVIE INFO -->
|
||||
<div class="movie__info">
|
||||
<!-- Loading placeholder -->
|
||||
<div
|
||||
class="movie__description noselect"
|
||||
@click="truncatedDescription = !truncatedDescription"
|
||||
v-if="!loading"
|
||||
>
|
||||
<span :class="truncatedDescription ? 'truncated' : null">{{
|
||||
movie.overview
|
||||
}}</span>
|
||||
<button class="truncate-toggle"><i>⬆</i></button>
|
||||
</div>
|
||||
<div v-else class="movie__description">
|
||||
<div v-if="loading">
|
||||
<loading-placeholder :count="5" />
|
||||
</div>
|
||||
|
||||
<Description
|
||||
v-if="!loading && movie && movie.overview"
|
||||
:description="movie.overview"
|
||||
/>
|
||||
|
||||
<div class="movie__details" v-if="movie">
|
||||
<div v-if="movie.year">
|
||||
<h2 class="title">Release Date</h2>
|
||||
<div class="text">{{ movie.year }}</div>
|
||||
</div>
|
||||
|
||||
<div v-if="movie.rating">
|
||||
<h2 class="title">Rating</h2>
|
||||
<div class="text">{{ movie.rating }}</div>
|
||||
</div>
|
||||
|
||||
<div v-if="movie.type == 'show'">
|
||||
<h2 class="title">Seasons</h2>
|
||||
<div class="text">{{ movie.seasons }}</div>
|
||||
</div>
|
||||
|
||||
<div v-if="movie.genres">
|
||||
<h2 class="title">Genres</h2>
|
||||
<div class="text">{{ movie.genres.join(", ") }}</div>
|
||||
</div>
|
||||
|
||||
<div v-if="movie.type == 'show'">
|
||||
<h2 class="title">Production status</h2>
|
||||
<div class="text">{{ movie.production_status }}</div>
|
||||
</div>
|
||||
|
||||
<div v-if="movie.type == 'show'">
|
||||
<h2 class="title">Runtime</h2>
|
||||
<div class="text">{{ movie.runtime[0] }} minutes</div>
|
||||
</div>
|
||||
<Detail
|
||||
v-if="movie.year"
|
||||
title="Release date"
|
||||
:detail="movie.year"
|
||||
/>
|
||||
<Detail v-if="movie.rating" title="Rating" :detail="movie.rating" />
|
||||
<Detail
|
||||
v-if="movie.type == 'show'"
|
||||
title="Seasons"
|
||||
:detail="movie.seasons"
|
||||
/>
|
||||
<Detail
|
||||
v-if="movie.genres && movie.genres.length"
|
||||
title="Genres"
|
||||
:detail="movie.genres.join(', ')"
|
||||
/>
|
||||
<Detail
|
||||
v-if="
|
||||
movie.production_status &&
|
||||
movie.production_status !== 'Released'
|
||||
"
|
||||
title="Production status"
|
||||
:detail="movie.production_status"
|
||||
/>
|
||||
<Detail
|
||||
v-if="movie.runtime"
|
||||
title="Runtime"
|
||||
:detail="humanMinutes(movie.runtime)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TODO: change this classname, this is general -->
|
||||
|
||||
<div class="movie__admin" v-if="movie && movie.credits">
|
||||
<h2 class="movie__details-title">Cast</h2>
|
||||
<div style="display: flex; flex-wrap: wrap">
|
||||
<person
|
||||
v-for="cast in movie.credits.cast"
|
||||
:info="cast"
|
||||
style="flex-basis: 0"
|
||||
></person>
|
||||
</div>
|
||||
<div
|
||||
class="movie__admin"
|
||||
v-if="showCast && credits && credits.cast && credits.cast.length"
|
||||
>
|
||||
<Detail title="cast">
|
||||
<CastList :cast="credits.cast" />
|
||||
</Detail>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TORRENT LIST -->
|
||||
<TorrentList
|
||||
v-if="movie"
|
||||
v-if="movie && admin"
|
||||
:show="showTorrents"
|
||||
:query="title"
|
||||
:tmdb_id="id"
|
||||
@@ -152,18 +156,29 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import storage from "@/storage";
|
||||
import { mapGetters } from "vuex";
|
||||
import img from "@/directives/v-image";
|
||||
import TorrentList from "./TorrentList";
|
||||
import Person from "./Person";
|
||||
import SidebarListElement from "./ui/sidebarListElem";
|
||||
import IconProfile from "@/icons/IconProfile";
|
||||
import IconThumbsUp from "@/icons/IconThumbsUp";
|
||||
import IconThumbsDown from "@/icons/IconThumbsDown";
|
||||
import IconInfo from "@/icons/IconInfo";
|
||||
import IconRequest from "@/icons/IconRequest";
|
||||
import IconRequested from "@/icons/IconRequested";
|
||||
import IconBinoculars from "@/icons/IconBinoculars";
|
||||
import IconPlay from "@/icons/IconPlay";
|
||||
import TorrentList from "@/components/TorrentList";
|
||||
import CastList from "@/components/CastList";
|
||||
import Detail from "@/components/popup/Detail";
|
||||
import ActionButton from "@/components/popup/ActionButton";
|
||||
import Description from "@/components/popup/Description";
|
||||
import store from "@/store";
|
||||
import LoadingPlaceholder from "./ui/LoadingPlaceholder";
|
||||
import LoadingPlaceholder from "@/components/ui/LoadingPlaceholder";
|
||||
|
||||
import {
|
||||
getMovie,
|
||||
getPerson,
|
||||
getShow,
|
||||
getPerson,
|
||||
getCredits,
|
||||
request,
|
||||
getRequestStatus,
|
||||
watchLink
|
||||
@@ -181,7 +196,22 @@ export default {
|
||||
type: String
|
||||
}
|
||||
},
|
||||
components: { TorrentList, Person, LoadingPlaceholder, SidebarListElement },
|
||||
components: {
|
||||
Description,
|
||||
Detail,
|
||||
ActionButton,
|
||||
IconProfile,
|
||||
IconThumbsUp,
|
||||
IconThumbsDown,
|
||||
IconRequest,
|
||||
IconRequested,
|
||||
IconInfo,
|
||||
IconBinoculars,
|
||||
IconPlay,
|
||||
TorrentList,
|
||||
CastList,
|
||||
LoadingPlaceholder
|
||||
},
|
||||
directives: { img: img }, // TODO decide to remove or use
|
||||
data() {
|
||||
return {
|
||||
@@ -192,22 +222,17 @@ export default {
|
||||
poster: undefined,
|
||||
backdrop: undefined,
|
||||
matched: false,
|
||||
userLoggedIn: storage.sessionId ? true : false,
|
||||
requested: false,
|
||||
admin: localStorage.getItem("admin") == "true" ? true : false,
|
||||
showTorrents: false,
|
||||
showCast: false,
|
||||
credits: [],
|
||||
compact: false,
|
||||
loading: true,
|
||||
truncatedDescription: true
|
||||
loading: true
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
id: function (val) {
|
||||
if (this.type === "movie") {
|
||||
this.fetchMovie(val);
|
||||
} else {
|
||||
this.fetchShow(val);
|
||||
}
|
||||
this.fetchByType();
|
||||
},
|
||||
backdrop: function (backdrop) {
|
||||
if (backdrop != null) {
|
||||
@@ -221,15 +246,32 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters("user", ["loggedIn", "admin", "plexId"]),
|
||||
numberOfTorrentResults: () => {
|
||||
let numTorrents = store.getters["torrentModule/resultCount"];
|
||||
return numTorrents !== null ? numTorrents + " results" : null;
|
||||
},
|
||||
isPlexAuthenticated: () => {
|
||||
return store.getters["userModule/isPlexAuthenticated"];
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async fetchByType() {
|
||||
try {
|
||||
let response;
|
||||
if (this.type === "movie") {
|
||||
response = await getMovie(this.id, true, false);
|
||||
} else if (this.type === "show") {
|
||||
response = await getShow(this.id, false, false);
|
||||
} else {
|
||||
this.$router.push({ name: "404" });
|
||||
}
|
||||
|
||||
this.parseResponse(response);
|
||||
} catch (error) {
|
||||
this.$router.push({ name: "404" });
|
||||
}
|
||||
|
||||
// async get credits
|
||||
getCredits(this.type, this.id).then(credits => (this.credits = credits));
|
||||
},
|
||||
parseResponse(movie) {
|
||||
this.loading = false;
|
||||
this.movie = { ...movie };
|
||||
@@ -248,21 +290,37 @@ export default {
|
||||
setPosterSrc() {
|
||||
const poster = this.$refs["poster-image"];
|
||||
if (this.poster == null) {
|
||||
poster.src = "/no-image.png";
|
||||
poster.src = "/assets/no-image.svg";
|
||||
return;
|
||||
}
|
||||
|
||||
poster.src = `${this.ASSET_URL}${this.ASSET_SIZES[0]}${this.poster}`;
|
||||
},
|
||||
humanMinutes(minutes) {
|
||||
if (minutes instanceof Array) {
|
||||
minutes = minutes[0];
|
||||
}
|
||||
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const minutesLeft = minutes - hours * 60;
|
||||
|
||||
if (minutesLeft == 0) {
|
||||
return hours > 1 ? `${hours} hours` : `${hours} hour`;
|
||||
} else if (hours == 0) {
|
||||
return `${minutesLeft} min`;
|
||||
}
|
||||
|
||||
return `${hours}h ${minutesLeft}m`;
|
||||
},
|
||||
sendRequest() {
|
||||
request(this.id, this.type, storage.token).then(resp => {
|
||||
request(this.id, this.type).then(resp => {
|
||||
if (resp.success) {
|
||||
this.requested = true;
|
||||
}
|
||||
});
|
||||
},
|
||||
openInPlex() {
|
||||
watchLink(this.title, this.movie.year, storage.token).then(
|
||||
watchLink(this.title, this.movie.year).then(
|
||||
watchLink => (window.location = watchLink)
|
||||
);
|
||||
},
|
||||
@@ -273,27 +331,9 @@ export default {
|
||||
}
|
||||
},
|
||||
created() {
|
||||
store.dispatch("torrentModule/setResultCount", null);
|
||||
this.prevDocumentTitle = store.getters["documentTitle/title"];
|
||||
|
||||
if (this.type === "movie") {
|
||||
getMovie(this.id, true)
|
||||
.then(this.parseResponse)
|
||||
.catch(error => {
|
||||
this.$router.push({ name: "404" });
|
||||
});
|
||||
} else if (this.type == "person") {
|
||||
getPerson(this.id, true)
|
||||
.then(this.parseResponse)
|
||||
.catch(error => {
|
||||
this.$router.push({ name: "404" });
|
||||
});
|
||||
} else {
|
||||
getShow(this.id, true)
|
||||
.then(this.parseResponse)
|
||||
.catch(error => {
|
||||
this.$router.push({ name: "404" });
|
||||
});
|
||||
}
|
||||
this.fetchByType();
|
||||
},
|
||||
beforeDestroy() {
|
||||
store.dispatch("documentTitle/updateTitle", this.prevDocumentTitle);
|
||||
@@ -302,14 +342,13 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "./src/scss/loading-placeholder";
|
||||
@import "./src/scss/variables";
|
||||
@import "./src/scss/media-queries";
|
||||
@import "./src/scss/main";
|
||||
@import "src/scss/loading-placeholder";
|
||||
@import "src/scss/variables";
|
||||
@import "src/scss/media-queries";
|
||||
@import "src/scss/main";
|
||||
|
||||
header {
|
||||
$duration: 0.2s;
|
||||
height: 250px;
|
||||
transform: scaleY(1);
|
||||
transition: height $duration ease;
|
||||
transform-origin: top;
|
||||
@@ -318,23 +357,32 @@ header {
|
||||
background-repeat: no-repeat;
|
||||
background-position: 50% 50%;
|
||||
background-color: $background-color;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
height: 350px;
|
||||
|
||||
@include tablet-min {
|
||||
height: 350px;
|
||||
@include mobile {
|
||||
grid-template-columns: 1fr;
|
||||
height: 250px;
|
||||
place-items: center;
|
||||
}
|
||||
&:before {
|
||||
|
||||
* {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 0;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: $background-dark-85;
|
||||
}
|
||||
|
||||
@include mobile {
|
||||
&.compact {
|
||||
height: 100px;
|
||||
@@ -346,53 +394,21 @@ header {
|
||||
display: none;
|
||||
|
||||
@include desktop {
|
||||
background: $background-color;
|
||||
height: 0;
|
||||
background: var(--background-color);
|
||||
height: auto;
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: calc(45% - 40px);
|
||||
top: 40px;
|
||||
left: 40px;
|
||||
width: calc(100% - 80px);
|
||||
margin: 40px;
|
||||
|
||||
> img {
|
||||
width: 100%;
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.truncate-toggle {
|
||||
border: none;
|
||||
background: none;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
color: $text-color;
|
||||
|
||||
> i {
|
||||
font-style: unset;
|
||||
font-size: 0.7rem;
|
||||
transition: 0.3s ease all;
|
||||
transform: rotateY(180deg);
|
||||
}
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: "";
|
||||
flex: 1;
|
||||
border-bottom: 1px solid $text-color-50;
|
||||
}
|
||||
&::before {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
&::after {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.movie {
|
||||
&__wrap {
|
||||
display: flex;
|
||||
&--header {
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
@@ -426,32 +442,33 @@ header {
|
||||
&__title {
|
||||
position: relative;
|
||||
padding: 20px;
|
||||
color: $green;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
height: fit-content;
|
||||
|
||||
@include tablet-min {
|
||||
width: 55%;
|
||||
text-align: left;
|
||||
margin-left: 45%;
|
||||
padding: 30px 30px 30px 40px;
|
||||
padding: 140px 30px 0 40px;
|
||||
}
|
||||
h1 {
|
||||
color: var(--color-green);
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
font-size: 24px;
|
||||
margin-bottom: 0;
|
||||
|
||||
@include tablet-min {
|
||||
font-size: 30px;
|
||||
}
|
||||
}
|
||||
}
|
||||
&__main {
|
||||
min-height: calc(100vh - 250px);
|
||||
@include tablet-min {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
height: 100%;
|
||||
i {
|
||||
display: block;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
margin-top: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
&__actions {
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
@@ -479,53 +496,15 @@ header {
|
||||
&__info {
|
||||
margin-left: 0;
|
||||
}
|
||||
&__description {
|
||||
font-weight: 300;
|
||||
font-size: 13px;
|
||||
line-height: 1.8;
|
||||
margin-bottom: 20px;
|
||||
|
||||
& .truncated {
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
-webkit-line-clamp: 4;
|
||||
-webkit-box-orient: vertical;
|
||||
|
||||
& + .truncate-toggle > i {
|
||||
transform: rotateY(0deg) rotateZ(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
@include tablet-min {
|
||||
margin-bottom: 30px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
&__details {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
> div {
|
||||
margin-bottom: 20px;
|
||||
margin-right: 20px;
|
||||
@include tablet-min {
|
||||
margin-bottom: 30px;
|
||||
margin-right: 30px;
|
||||
}
|
||||
& .title {
|
||||
margin: 0;
|
||||
font-weight: 400;
|
||||
text-transform: uppercase;
|
||||
font-size: 14px;
|
||||
color: $green;
|
||||
@include tablet-min {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
& .text {
|
||||
font-weight: 300;
|
||||
font-size: 14px;
|
||||
margin-top: 5px;
|
||||
> * {
|
||||
margin-right: 30px;
|
||||
|
||||
@include mobile {
|
||||
margin-right: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -553,4 +532,13 @@ header {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.4s;
|
||||
}
|
||||
.fade-enter,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
300
src/components/popup/Person.vue
Normal file
@@ -0,0 +1,300 @@
|
||||
<template>
|
||||
<section class="person">
|
||||
<header ref="header">
|
||||
<div class="info">
|
||||
<h1 v-if="person">
|
||||
{{ person.title || person.name }}
|
||||
</h1>
|
||||
<div v-else>
|
||||
<loading-placeholder :count="1" />
|
||||
<loading-placeholder :count="1" lineClass="short" :top="3.5" />
|
||||
</div>
|
||||
|
||||
<span class="known-for" v-if="person && person['known_for_department']">
|
||||
{{
|
||||
person.known_for_department === "Acting"
|
||||
? "Actor"
|
||||
: person.known_for_department
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<figure class="person__poster">
|
||||
<img
|
||||
class="person-item__img is-loaded"
|
||||
ref="poster-image"
|
||||
src="/assets/placeholder.png"
|
||||
/>
|
||||
</figure>
|
||||
</header>
|
||||
|
||||
<div v-if="loading">
|
||||
<loading-placeholder :count="6" />
|
||||
<loading-placeholder lineClass="short" :top="3" />
|
||||
<loading-placeholder :count="6" lineClass="fullwidth" />
|
||||
<loading-placeholder lineClass="short" :top="4.5" />
|
||||
<loading-placeholder />
|
||||
</div>
|
||||
|
||||
<div v-if="person">
|
||||
<Detail v-if="age" title="Age" :detail="age" />
|
||||
|
||||
<Detail
|
||||
v-if="person"
|
||||
title="Born"
|
||||
:detail="person.place_of_birth ? person.place_of_birth : '(Not found)'"
|
||||
/>
|
||||
|
||||
<Detail v-if="person.biography" title="Biography">
|
||||
<Description :description="person.biography" />
|
||||
</Detail>
|
||||
|
||||
<Detail
|
||||
title="movies"
|
||||
:detail="`Credited in ${movieCredits.length} movies`"
|
||||
v-if="credits"
|
||||
>
|
||||
<CastList :cast="movieCredits" />
|
||||
</Detail>
|
||||
|
||||
<Detail
|
||||
title="shows"
|
||||
:detail="`Credited in ${showCredits.length} shows`"
|
||||
v-if="credits"
|
||||
>
|
||||
<CastList :cast="showCredits" />
|
||||
</Detail>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import img from "@/directives/v-image";
|
||||
import CastList from "@/components/CastList";
|
||||
import Detail from "@/components/popup/Detail";
|
||||
import Description from "@/components/popup/Description";
|
||||
import LoadingPlaceholder from "@/components/ui/LoadingPlaceholder";
|
||||
|
||||
import { getPerson, getPersonCredits } from "@/api";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
id: {
|
||||
required: true,
|
||||
type: Number
|
||||
},
|
||||
type: {
|
||||
required: false,
|
||||
type: String,
|
||||
default: "person"
|
||||
}
|
||||
},
|
||||
components: {
|
||||
Detail,
|
||||
Description,
|
||||
CastList,
|
||||
LoadingPlaceholder
|
||||
},
|
||||
directives: { img: img }, // TODO decide to remove or use
|
||||
data() {
|
||||
return {
|
||||
ASSET_URL: "https://image.tmdb.org/t/p/",
|
||||
ASSET_SIZES: ["w500", "w780", "original"],
|
||||
person: undefined,
|
||||
loading: true,
|
||||
credits: undefined
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
backdrop: function (backdrop) {
|
||||
if (backdrop != null) {
|
||||
const style = {
|
||||
backgroundImage:
|
||||
"url(" + this.ASSET_URL + this.ASSET_SIZES[1] + backdrop + ")"
|
||||
};
|
||||
|
||||
Object.assign(this.$refs.header.style, style);
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
age: function () {
|
||||
if (!this.person || !this.person.birthday) {
|
||||
return;
|
||||
}
|
||||
|
||||
const today = new Date().getFullYear();
|
||||
const birthYear = new Date(this.person.birthday).getFullYear();
|
||||
return `${today - birthYear} years old`;
|
||||
},
|
||||
movieCredits: function () {
|
||||
const { cast } = this.credits;
|
||||
if (!cast) return;
|
||||
|
||||
return cast
|
||||
.filter(l => l.type === "movie")
|
||||
.filter((item, pos, self) => self.indexOf(item) == pos)
|
||||
.sort((a, b) => a.popularity < b.popularity);
|
||||
},
|
||||
showCredits: function () {
|
||||
const { cast } = this.credits;
|
||||
if (!cast) return;
|
||||
|
||||
const alreadyExists = (item, pos, self) => {
|
||||
const names = self.map(item => item.title);
|
||||
return names.indexOf(item.title) == pos;
|
||||
};
|
||||
|
||||
return cast
|
||||
.filter(item => item.type === "show")
|
||||
.filter(alreadyExists)
|
||||
.sort((a, b) => a.popularity < b.popularity);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
parseResponse(person) {
|
||||
this.loading = false;
|
||||
this.person = { ...person };
|
||||
this.title = person.title;
|
||||
this.poster = person.poster;
|
||||
if (person.credits) this.credits = person.credits;
|
||||
|
||||
this.setPosterSrc();
|
||||
},
|
||||
setPosterSrc() {
|
||||
const poster = this.$refs["poster-image"];
|
||||
if (this.poster == null) {
|
||||
poster.src = "/assets/no-image.svg";
|
||||
return;
|
||||
}
|
||||
|
||||
poster.src = `${this.ASSET_URL}${this.ASSET_SIZES[0]}${this.poster}`;
|
||||
}
|
||||
},
|
||||
created() {
|
||||
getPerson(this.id, false)
|
||||
.then(this.parseResponse)
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
this.$router.push({ name: "404" });
|
||||
});
|
||||
|
||||
getPersonCredits(this.id)
|
||||
.then(credits => (this.credits = credits))
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "src/scss/loading-placeholder";
|
||||
@import "src/scss/variables";
|
||||
@import "src/scss/media-queries";
|
||||
@import "src/scss/main";
|
||||
|
||||
section.person {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
padding: 40px;
|
||||
background-color: var(--background-color);
|
||||
|
||||
@include mobile {
|
||||
padding: 50px 20px 10px;
|
||||
}
|
||||
|
||||
&:before {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: -130px;
|
||||
left: -100px;
|
||||
z-index: 1;
|
||||
width: 1000px;
|
||||
height: 500px;
|
||||
transform: rotate(21deg);
|
||||
background-color: #062541;
|
||||
|
||||
@include mobile {
|
||||
// top: -52vw;
|
||||
top: -215px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
header {
|
||||
$duration: 0.2s;
|
||||
transition: height $duration ease;
|
||||
position: relative;
|
||||
background-color: transparent;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
height: 350px;
|
||||
z-index: 2;
|
||||
|
||||
@include mobile {
|
||||
height: 180px;
|
||||
}
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 30px;
|
||||
padding-left: 0;
|
||||
text-align: left;
|
||||
|
||||
@include mobile {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: $green;
|
||||
width: 100%;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
font-size: 30px;
|
||||
margin-top: 0;
|
||||
|
||||
@include mobile {
|
||||
font-size: 24px;
|
||||
margin: 10px 0;
|
||||
// padding: 30px 30px 30px 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.known-for {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.person__poster {
|
||||
display: block;
|
||||
border-radius: 10px;
|
||||
background-color: grey;
|
||||
animation: pulse 1s infinite ease-in-out;
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
background-color: rgba(165, 165, 165, 0.1);
|
||||
}
|
||||
50% {
|
||||
background-color: rgba(165, 165, 165, 0.3);
|
||||
}
|
||||
100% {
|
||||
background-color: rgba(165, 165, 165, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
> img {
|
||||
border-radius: 10px;
|
||||
width: 100%;
|
||||
|
||||
@include mobile {
|
||||
max-width: 225px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,49 +1,44 @@
|
||||
<template>
|
||||
|
||||
<div class="darkToggle">
|
||||
<span @click="toggleDarkmode()">{{ darkmodeToggleIcon }}</span>
|
||||
<span @click="toggleDarkmode">{{ darkmodeToggleIcon }}</span>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
|
||||
data() {
|
||||
return {
|
||||
darkmode: this.supported
|
||||
}
|
||||
darkmode: this.systemDarkModeEnabled()
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
toggleDarkmode() {
|
||||
this.darkmode = !this.darkmode;
|
||||
document.body.className = this.darkmode ? 'dark' : 'light'
|
||||
document.body.className = this.darkmode ? "dark" : "light";
|
||||
},
|
||||
supported() {
|
||||
const computedStyle = window.getComputedStyle(document.body)
|
||||
if (computedStyle['colorScheme'] != null)
|
||||
return computedStyle.colorScheme.includes('dark')
|
||||
return false
|
||||
systemDarkModeEnabled() {
|
||||
const computedStyle = window.getComputedStyle(document.body);
|
||||
if (computedStyle["colorScheme"] != null) {
|
||||
return computedStyle.colorScheme.includes("dark");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
darkmodeToggleIcon() {
|
||||
return this.darkmode ? '🌝' : '🌚'
|
||||
return this.darkmode ? "🌝" : "🌚";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.darkToggle {
|
||||
height: 25px;
|
||||
width: 25px;
|
||||
cursor: pointer;
|
||||
// background-color: red;
|
||||
position: fixed;
|
||||
margin-bottom: 10px;
|
||||
margin-bottom: 1.5rem;
|
||||
margin-right: 2px;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
@@ -54,4 +49,4 @@ export default {
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
82
src/components/ui/Hamburger.vue
Normal file
@@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<div
|
||||
class="nav__hamburger"
|
||||
:class="{ open: isOpen }"
|
||||
@click="toggle"
|
||||
@keydown.enter="toggle"
|
||||
tabindex="0"
|
||||
>
|
||||
<div v-for="(_, index) in 3" :key="index" class="bar"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapActions } from "vuex";
|
||||
|
||||
export default {
|
||||
computed: { ...mapGetters("hamburger", ["isOpen"]) },
|
||||
methods: { ...mapActions("hamburger", ["toggle"]) }
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "src/scss/media-queries";
|
||||
|
||||
.nav__hamburger {
|
||||
display: block;
|
||||
position: relative;
|
||||
width: var(--header-size);
|
||||
height: var(--header-size);
|
||||
cursor: pointer;
|
||||
border-left: 1px solid var(--background-color);
|
||||
background-color: var(--background-color-secondary);
|
||||
|
||||
@include tablet-min {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.bar {
|
||||
position: absolute;
|
||||
width: 23px;
|
||||
height: 1px;
|
||||
background-color: var(--text-color-70);
|
||||
transition: all 300ms ease;
|
||||
&:nth-child(1) {
|
||||
left: 16px;
|
||||
top: 17px;
|
||||
}
|
||||
&:nth-child(2) {
|
||||
left: 16px;
|
||||
top: 25px;
|
||||
&:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
width: 23px;
|
||||
height: 1px;
|
||||
transition: all 300ms ease;
|
||||
}
|
||||
}
|
||||
&:nth-child(3) {
|
||||
right: 15px;
|
||||
top: 33px;
|
||||
}
|
||||
}
|
||||
&.open {
|
||||
.bar {
|
||||
&:nth-child(1),
|
||||
&:nth-child(3) {
|
||||
width: 0;
|
||||
}
|
||||
&:nth-child(2) {
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
&:nth-child(2):after {
|
||||
transform: rotate(-90deg);
|
||||
background-color: var(--text-color-70);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -7,7 +7,7 @@
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "./src/scss/variables";
|
||||
@import "src/scss/variables";
|
||||
|
||||
.loader {
|
||||
display: flex;
|
||||
@@ -16,7 +16,7 @@
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&--icon{
|
||||
&--icon {
|
||||
border: 2px solid $text-color-70;
|
||||
border-radius: 50%;
|
||||
display: block;
|
||||
@@ -32,7 +32,7 @@
|
||||
&:after {
|
||||
border: 7px solid $green-90;
|
||||
border-radius: 50%;
|
||||
content: '';
|
||||
content: "";
|
||||
left: 8px;
|
||||
position: absolute;
|
||||
top: 22px;
|
||||
@@ -40,7 +40,9 @@
|
||||
}
|
||||
}
|
||||
@keyframes load {
|
||||
100% { transform: rotate(360deg); }
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="text-input__loading">
|
||||
<div class="text-input__loading--line" :class="lineClass" v-for="_ in Array(count)"></div>
|
||||
</div>
|
||||
<div class="text-input__loading" :style="`margin-top: ${top}rem`">
|
||||
<div
|
||||
class="text-input__loading--line"
|
||||
:class="lineClass"
|
||||
v-for="l in Array(count)"
|
||||
:key="l"
|
||||
></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -11,17 +14,21 @@ export default {
|
||||
props: {
|
||||
count: {
|
||||
type: Number,
|
||||
require: true
|
||||
default: 1,
|
||||
require: false
|
||||
},
|
||||
lineClass: {
|
||||
type: String,
|
||||
default: ''
|
||||
default: ""
|
||||
},
|
||||
top: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "./src/scss/loading-placeholder";
|
||||
|
||||
</style>
|
||||
@import "src/scss/loading-placeholder";
|
||||
</style>
|
||||
|
||||
@@ -1,31 +1,39 @@
|
||||
<template>
|
||||
<button type="button" @click="emit('click')" :class="{ active: active }">
|
||||
<button
|
||||
type="button"
|
||||
@click="emit('click')"
|
||||
:class="{ active: active, fullwidth: fullWidth }"
|
||||
>
|
||||
<slot></slot>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'seasonedButton',
|
||||
name: "seasonedButton",
|
||||
props: {
|
||||
active: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false
|
||||
},
|
||||
fullWidth: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
emit() {
|
||||
this.$emit('click')
|
||||
this.$emit("click");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "./src/scss/variables";
|
||||
@import "./src/scss/media-queries";
|
||||
@import "src/scss/variables";
|
||||
@import "src/scss/media-queries";
|
||||
|
||||
button {
|
||||
display: inline-block;
|
||||
@@ -43,14 +51,25 @@ button {
|
||||
background: $background-color-secondary;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
transition: background 0.5s ease, color 0.5s ease, border-color .5s ease;
|
||||
transition: background 0.5s ease, color 0.5s ease, border-color 0.5s ease;
|
||||
|
||||
@include desktop {
|
||||
font-size: 0.8rem;
|
||||
padding: 6px 20px 5px 20px;
|
||||
}
|
||||
|
||||
&:focus, &:active, &.active {
|
||||
&.fullwidth {
|
||||
font-size: 14px;
|
||||
width: 40%;
|
||||
|
||||
@include mobile {
|
||||
width: 60%;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus,
|
||||
&:active,
|
||||
&.active {
|
||||
background: $text-color;
|
||||
color: $background-color;
|
||||
}
|
||||
|
||||
@@ -1,116 +1,134 @@
|
||||
<template>
|
||||
<div class="group" :class="{ completed: value }">
|
||||
<svg class="group__input-icon"><use v-bind="{'xlink:href':'#icon' + icon}"></use></svg>
|
||||
<input class="group__input" :type="tempType || type" @input="handleInput" v-model="inputValue"
|
||||
:placeholder="placeholder" @keyup.enter="submit" />
|
||||
|
||||
<i v-if="value && type === 'password'" @click="toggleShowPassword" class="group__input-show noselect">show</i>
|
||||
<div class="group" :class="{ completed: value, focus }">
|
||||
<component :is="inputIcon" v-if="inputIcon" />
|
||||
|
||||
<input
|
||||
class="input"
|
||||
:type="tempType || type"
|
||||
@input="handleInput"
|
||||
v-model="inputValue"
|
||||
:placeholder="placeholder"
|
||||
@keyup.enter="event => $emit('enter', event)"
|
||||
@focus="focus = true"
|
||||
@blur="focus = false"
|
||||
/>
|
||||
|
||||
<i
|
||||
v-if="value && type === 'password'"
|
||||
@click="toggleShowPassword"
|
||||
@keydown.enter="toggleShowPassword"
|
||||
class="show noselect"
|
||||
tabindex="0"
|
||||
>{{ tempType == "password" ? "show" : "hide" }}</i
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import IconKey from "../../icons/IconKey";
|
||||
import IconEmail from "../../icons/IconEmail";
|
||||
|
||||
export default {
|
||||
components: { IconKey, IconEmail },
|
||||
props: {
|
||||
placeholder: { type: String },
|
||||
icon: { type: String },
|
||||
type: { type: String, default: 'text' },
|
||||
type: { type: String, default: "text" },
|
||||
value: { type: String, default: undefined }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
inputValue: this.value || undefined,
|
||||
tempType: undefined
|
||||
tempType: this.type,
|
||||
focus: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
inputIcon() {
|
||||
if (this.type === "password") return IconKey;
|
||||
if (this.type === "email") return IconEmail;
|
||||
return false;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
submit(event) {
|
||||
this.$emit('enter')
|
||||
},
|
||||
handleInput(event) {
|
||||
if (this.value !== undefined) {
|
||||
this.$emit('update:value', this.inputValue)
|
||||
this.$emit("update:value", this.inputValue);
|
||||
} else {
|
||||
this.$emit('change', this.inputValue, event)
|
||||
this.$emit("change", this.inputValue, event);
|
||||
}
|
||||
},
|
||||
toggleShowPassword() {
|
||||
if (this.tempType === 'text') {
|
||||
this.tempType = 'password'
|
||||
if (this.tempType === "text") {
|
||||
this.tempType = "password";
|
||||
} else {
|
||||
this.tempType = 'text'
|
||||
this.tempType = "text";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import "./src/scss/variables";
|
||||
@import "./src/scss/media-queries";
|
||||
@import "src/scss/variables";
|
||||
@import "src/scss/media-queries";
|
||||
|
||||
.group{
|
||||
.group {
|
||||
display: flex;
|
||||
margin-bottom: 1rem;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
max-width: 35rem;
|
||||
border: 1px solid var(--text-color-50);
|
||||
background-color: var(--background-color-secondary);
|
||||
|
||||
&:hover, &:focus {
|
||||
.group__input {
|
||||
border-color: $text-color;
|
||||
&.completed,
|
||||
&.focus,
|
||||
&:hover,
|
||||
&:focus {
|
||||
border-color: var(--text-color);
|
||||
|
||||
&-icon {
|
||||
fill: $text-color;
|
||||
}
|
||||
svg {
|
||||
fill: var(--text-color);
|
||||
}
|
||||
}
|
||||
|
||||
&.completed {
|
||||
.group__input {
|
||||
border-color: $text-color;
|
||||
|
||||
&-icon {
|
||||
fill: $text-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__input {
|
||||
width: 100%;
|
||||
max-width: 35rem;
|
||||
padding: 10px 10px 10px 45px;
|
||||
outline: none;
|
||||
background-color: $background-color-secondary;
|
||||
color: $text-color;
|
||||
font-weight: 100;
|
||||
font-size: 1.2rem;
|
||||
border: 1px solid $text-color-50;
|
||||
margin: 0;
|
||||
margin-left: -2.2rem !important;
|
||||
z-index: 3;
|
||||
transition: color .5s ease, background-color .5s ease, border .5s ease;
|
||||
|
||||
border-radius: 0;
|
||||
-webkit-appearance: none;
|
||||
|
||||
&-show {
|
||||
position: relative;
|
||||
left: -50px;
|
||||
z-index: 11;
|
||||
margin: auto 0;
|
||||
height: 100%;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
color: $text-color-50;
|
||||
}
|
||||
}
|
||||
|
||||
&__input-icon {
|
||||
svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
fill: $text-color-50;
|
||||
transition: fill 0.5s ease;
|
||||
fill: var(--text-color-50);
|
||||
pointer-events: none;
|
||||
margin-top: 10px;
|
||||
margin-left: 10px;
|
||||
z-index: 8;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
outline: none;
|
||||
background-color: var(--background-color-secondary);
|
||||
color: var(--text-color);
|
||||
font-weight: 100;
|
||||
font-size: 1.2rem;
|
||||
margin: 0;
|
||||
z-index: 3;
|
||||
border: none;
|
||||
|
||||
border-radius: 0;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
.show {
|
||||
position: absolute;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
right: 20px;
|
||||
z-index: 11;
|
||||
margin: auto 0;
|
||||
height: 100%;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
color: var(--text-color-50);
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
<template>
|
||||
<transition-group name="fade">
|
||||
<div class="message" v-for="(message, index) in reversedMessages" :class="message.type || 'warning'" :key="index">
|
||||
<div
|
||||
class="message"
|
||||
v-for="(message, index) in reversedMessages"
|
||||
:key="`${index}-${message.title}-${message.type}}`"
|
||||
:class="message.type || 'warning'"
|
||||
>
|
||||
<span class="pinstripe"></span>
|
||||
<div>
|
||||
<h2 class="title">{{ message.title || defaultTitles[message.type] }}</h2>
|
||||
<span v-if="message.message" class="message">{{ message.message }}</span>
|
||||
<h2 class="title">
|
||||
{{ message.title || defaultTitles[message.type] }}
|
||||
</h2>
|
||||
<span v-if="message.message" class="message">{{
|
||||
message.message
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
<button class="dismiss" @click="clicked(message)">X</button>
|
||||
@@ -13,7 +22,6 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props: {
|
||||
messages: {
|
||||
@@ -24,37 +32,36 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
defaultTitles: {
|
||||
error: 'Unexpected error',
|
||||
warning: 'Something went wrong',
|
||||
undefined: 'Something went wrong'
|
||||
error: "Unexpected error",
|
||||
warning: "Something went wrong",
|
||||
undefined: "Something went wrong"
|
||||
},
|
||||
localMessages: [...this.messages]
|
||||
}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
reversedMessages() {
|
||||
return [...this.messages].reverse()
|
||||
return [...this.messages].reverse();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
clicked(e) {
|
||||
const removedMessage = [...this.messages].filter(mes => mes !== e)
|
||||
this.$emit('update:messages', removedMessage)
|
||||
const removedMessage = [...this.messages].filter(mes => mes !== e);
|
||||
this.$emit("update:messages", removedMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "./src/scss/variables";
|
||||
@import "./src/scss/media-queries";
|
||||
@import "src/scss/variables";
|
||||
@import "src/scss/media-queries";
|
||||
|
||||
.fade-enter-active {
|
||||
transition: opacity .4s;
|
||||
transition: opacity 0.4s;
|
||||
}
|
||||
.fade-leave-active {
|
||||
transition: opacity .1s;
|
||||
transition: opacity 0.1s;
|
||||
}
|
||||
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
|
||||
opacity: 0;
|
||||
@@ -72,7 +79,6 @@ export default {
|
||||
> div {
|
||||
margin: 10px 24px;
|
||||
width: 100%;
|
||||
|
||||
}
|
||||
|
||||
.title {
|
||||
@@ -81,12 +87,12 @@ export default {
|
||||
margin: 0;
|
||||
font-size: 1.3rem;
|
||||
color: $text-color;
|
||||
transition: color .5s ease;
|
||||
transition: color 0.5s ease;
|
||||
}
|
||||
.message {
|
||||
font-weight: 300;
|
||||
color: $text-color-70;
|
||||
transition: color .5s ease;
|
||||
transition: color 0.5s ease;
|
||||
margin: 0.2rem 0 0.5rem;
|
||||
}
|
||||
|
||||
@@ -101,7 +107,6 @@ export default {
|
||||
span {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.pinstripe {
|
||||
@@ -126,7 +131,7 @@ export default {
|
||||
margin-top: 0.5rem;
|
||||
margin-right: 0.5rem;
|
||||
color: $text-color-70;
|
||||
transition: color .5s ease;
|
||||
transition: color 0.5s ease;
|
||||
|
||||
&:hover {
|
||||
color: $text-color;
|
||||
@@ -140,7 +145,7 @@ export default {
|
||||
background-color: $color-success-highlight;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&.error {
|
||||
background-color: $color-error;
|
||||
|
||||
@@ -151,11 +156,10 @@ export default {
|
||||
|
||||
&.warning {
|
||||
background-color: $color-warning;
|
||||
|
||||
|
||||
.pinstripe {
|
||||
background-color: $color-warning-highlight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
<template>
|
||||
<div v-html="require(`@/assets/icons/${ icon }.svg`)"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ['icon']
|
||||
}
|
||||
</script>
|
||||
@@ -1,14 +1,19 @@
|
||||
<template>
|
||||
<div class="toggle-container">
|
||||
<button v-for="option in options" class="toggle-button" @click="toggle(option)"
|
||||
<button
|
||||
v-for="option in options"
|
||||
:key="option"
|
||||
class="toggle-button"
|
||||
@click="toggle(option)"
|
||||
:class="toggleValue === option ? 'selected' : null"
|
||||
>{{ option }}</button>
|
||||
>
|
||||
{{ option }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
export default {
|
||||
export default {
|
||||
props: {
|
||||
options: {
|
||||
Array,
|
||||
@@ -23,38 +28,35 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
toggleValue: this.selected || this.options[0]
|
||||
}
|
||||
},
|
||||
beforeMount() {
|
||||
this.toggle(this.toggleValue)
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
toggle(toggleValue) {
|
||||
this.toggleValue = toggleValue;
|
||||
if (this.selected !== undefined) {
|
||||
this.$emit('update:selected', toggleValue)
|
||||
this.$emit("update:selected", toggleValue);
|
||||
this.$emit("change", toggleValue);
|
||||
} else {
|
||||
this.$emit('change', toggleValue)
|
||||
this.$emit("change", toggleValue);
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "./src/scss/variables";
|
||||
@import "src/scss/variables";
|
||||
|
||||
$background: $background-ui;
|
||||
$background-selected: $background-color-secondary;
|
||||
|
||||
.toggle-container {
|
||||
width: 100%;
|
||||
max-width: 15rem;
|
||||
display: flex;
|
||||
overflow-x: scroll;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
// padding: 0.2rem;
|
||||
background-color: $background;
|
||||
border: 2px solid $background;
|
||||
border-radius: 8px;
|
||||
@@ -65,36 +67,20 @@ $background-selected: $background-color-secondary;
|
||||
font-size: 1rem;
|
||||
line-height: 1rem;
|
||||
font-weight: normal;
|
||||
width: 100%;
|
||||
padding: 0.5rem 0;
|
||||
padding: 0.5rem;
|
||||
border: 0;
|
||||
color: $text-color;
|
||||
// background-color: $text-color-5;
|
||||
background-color: $background;
|
||||
text-transform: capitalize;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
flex: 1 0 auto;
|
||||
|
||||
&.selected {
|
||||
color: $text-color;
|
||||
// background-color: $background-color-secondary;
|
||||
background-color: $background-selected;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
// &:first-of-type, &:last-of-type {
|
||||
// border-left: 4px solid $background;
|
||||
// border-right: 4px solid $background;
|
||||
// }
|
||||
|
||||
|
||||
// &:first-of-type {
|
||||
// border-top-left-radius: 4px;
|
||||
// border-bottom-left-radius: 4px;
|
||||
// }
|
||||
|
||||
// &:last-of-type {
|
||||
// border-top-right-radius: 4px;
|
||||
// border-bottom-right-radius: 4px;
|
||||
// }
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<a @click="$emit('click')">
|
||||
<li>
|
||||
<figure v-if="iconRef" :class="activeClassIfActive">
|
||||
<svg class="icon"><use :xlink:href="iconRefNameIfActive"/></svg>
|
||||
</figure>
|
||||
|
||||
<span class="text" :class="activeClassIfActive">{{ contentTextToDisplay }}</span>
|
||||
|
||||
<span v-if="supplementaryText" class="supplementary-text">
|
||||
{{ supplementaryText }}
|
||||
</span>
|
||||
</li>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// TODO if a image is hovered and we can't set the hover color we want to
|
||||
// go into it and change the fill
|
||||
export default {
|
||||
props: {
|
||||
iconRef: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
iconRefActive: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
active: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
textActive: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
supplementaryText: {
|
||||
type: String,
|
||||
required: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
iconRefNameIfActive() {
|
||||
const { iconRefActive, iconRef, active } = this
|
||||
|
||||
if ((iconRefActive && iconRef) && active) {
|
||||
return iconRefActive
|
||||
}
|
||||
return iconRef
|
||||
},
|
||||
contentTextToDisplay() {
|
||||
const { textActive, active, $slots } = this
|
||||
|
||||
if (textActive && active)
|
||||
return textActive
|
||||
|
||||
if ($slots.default && $slots.default.length > 0)
|
||||
return $slots.default[0].text
|
||||
|
||||
return ''
|
||||
},
|
||||
activeClassIfActive() {
|
||||
return this.active ? 'active' : ''
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "./src/scss/variables";
|
||||
@import "./src/scss/media-queries";
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
text-transform: uppercase;
|
||||
color: $text-color-50;
|
||||
transition: color 0.5s ease;
|
||||
font-size: 11px;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid $text-color-5;
|
||||
|
||||
&:hover {
|
||||
color: $text-color;
|
||||
cursor: pointer;
|
||||
|
||||
.icon {
|
||||
fill: $text-color;
|
||||
cursor: pointer;
|
||||
transform: scale(1.1, 1.1);
|
||||
}
|
||||
}
|
||||
.active {
|
||||
color: $text-color;
|
||||
|
||||
.icon {
|
||||
fill: $green;
|
||||
}
|
||||
}
|
||||
.pending {
|
||||
color: #f8bd2d;
|
||||
}
|
||||
|
||||
.text {
|
||||
margin-left: 26px;
|
||||
}
|
||||
|
||||
.supplementary-text {
|
||||
flex-grow: 1;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
figure {
|
||||
position: absolute;
|
||||
|
||||
> svg {
|
||||
position: relative;
|
||||
top: 50%;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin: 0 7px 0 0;
|
||||
fill: $text-color-50;
|
||||
transition: fill 0.5s ease, transform 0.5s ease;
|
||||
|
||||
& .waiting {
|
||||
transform: scale(0.8, 0.8);
|
||||
}
|
||||
& .pending {
|
||||
fill: #f8bd2d;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
9
src/config.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import type IConfig from "./interfaces/IConfig";
|
||||
|
||||
const config: IConfig = {
|
||||
SEASONED_URL: "",
|
||||
ELASTIC_URL: "https://elastic.kevinmidboe.com/",
|
||||
ELASTIC_INDEX: "shows,movies"
|
||||
};
|
||||
|
||||
export default config;
|
||||
16
src/icons/IconActivity.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
style="transition: stroke-width 0.5s ease"
|
||||
>
|
||||
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline>
|
||||
</svg>
|
||||
</template>
|
||||
8
src/icons/IconArrowDown.vue
Normal file
@@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<path
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
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"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
16
src/icons/IconBinoculars.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<path
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
d="M31.313 18.896v0l-5.071-10.846c-0.004-0.008-0.008-0.017-0.012-0.029-0.542-1.154-1.529-2.021-2.696-2.429l-1.129-1.921c-0.004-0.004-0.008-0.013-0.012-0.017-0.358-0.608-1.021-0.987-1.725-0.987-1.104 0-2 0.896-2 2v2.071c-0.825 0.842-1.333 1.996-1.333 3.263v1.025c-0.392-0.229-0.85-0.358-1.333-0.358s-0.942 0.129-1.333 0.358v-1.025c0-1.267-0.508-2.421-1.333-3.263v-2.071c0-1.104-0.896-2-2-2-0.704 0-1.367 0.379-1.725 0.987-0.004 0.004-0.008 0.013-0.012 0.017l-1.129 1.921c-1.171 0.408-2.158 1.275-2.696 2.429-0.004 0.008-0.008 0.017-0.013 0.025l-5.071 10.85c-0.442 0.946-0.688 1.996-0.688 3.104 0 4.042 3.292 7.333 7.333 7.333 3.942 0 7.167-3.125 7.325-7.029 0.396 0.229 0.85 0.363 1.342 0.363 0.488 0 0.946-0.133 1.342-0.363 0.158 3.904 3.383 7.029 7.325 7.029 4.042 0 7.333-3.292 7.333-7.333 0-1.108-0.246-2.158-0.688-3.104zM26.5 14.9c-0.587-0.15-1.2-0.233-1.833-0.233-1.771 0-3.396 0.629-4.667 1.679v-6.346c0-1.104 0.896-2 2-2 0.767 0 1.471 0.446 1.804 1.133 0.004 0.008 0.008 0.012 0.008 0.021l2.688 5.746zM20.667 4c0.233 0 0.446 0.117 0.567 0.317 0.004 0.004 0.004 0.008 0.008 0.013l0.592 1.008c-0.654 0.025-1.275 0.183-1.833 0.446v-1.117c0-0.367 0.3-0.667 0.667-0.667zM16 12c0.733 0 1.333 0.6 1.333 1.333v4.358c-0.392-0.229-0.85-0.358-1.333-0.358s-0.942 0.129-1.333 0.358v-4.358c0-0.733 0.6-1.333 1.333-1.333zM10.767 4.317c0.121-0.2 0.333-0.317 0.567-0.317 0.367 0 0.667 0.3 0.667 0.667v1.117c-0.558-0.267-1.179-0.425-1.833-0.446l0.592-1.008c0.004-0.004 0.004-0.008 0.008-0.013zM8.188 9.154c0.004-0.008 0.004-0.012 0.008-0.021 0.333-0.688 1.037-1.133 1.804-1.133 1.104 0 2 0.896 2 2v6.346c-1.271-1.050-2.896-1.679-4.667-1.679-0.633 0-1.246 0.079-1.833 0.233l2.688-5.746zM7.333 26.667c-2.575 0-4.667-2.092-4.667-4.667s2.092-4.667 4.667-4.667 4.667 2.092 4.667 4.667-2.092 4.667-4.667 4.667zM16 21.333c-0.733 0-1.333-0.6-1.333-1.333s0.6-1.333 1.333-1.333c0.733 0 1.333 0.6 1.333 1.333s-0.6 1.333-1.333 1.333zM24.667 26.667c-2.575 0-4.667-2.092-4.667-4.667s2.092-4.667 4.667-4.667 4.667 2.092 4.667 4.667-2.092 4.667-4.667 4.667z"
|
||||
/>
|
||||
<path
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
d="M5.333 22v-0.667h-1.333v0.667c0 1.837 1.496 3.333 3.333 3.333h0.667v-1.333h-0.667c-1.104 0-2-0.896-2-2z"
|
||||
/>
|
||||
<path
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
d="M22.667 22v-0.667h-1.333v0.667c0 1.837 1.496 3.333 3.333 3.333h0.667v-1.333h-0.667c-1.104 0-2-0.896-2-2z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
15
src/icons/IconClose.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<svg
|
||||
id="icon-cross"
|
||||
viewBox="0 0 32 32"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@click="$emit('click')"
|
||||
@keydown="event => $emit('keydown', event)"
|
||||
style="transition-duration: 0s;"
|
||||
>
|
||||
<path
|
||||
fill="inherit"
|
||||
d="M27.942 5.942l-1.883-1.883-10.058 10.054-10.058-10.054-1.883 1.883 10.054 10.058-10.054 10.058 1.883 1.883 10.058-10.054 10.058 10.054 1.883-1.883-10.054-10.058z"
|
||||
></path>
|
||||
</svg>
|
||||
</template>
|
||||
12
src/icons/IconEdit.vue
Normal file
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<svg
|
||||
@click="$emit('click')"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 32 32"
|
||||
>
|
||||
<path
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
d="M30.229 1.771c-1.142-1.142-2.658-1.771-4.275-1.771s-3.133 0.629-4.275 1.771l-18.621 18.621c-0.158 0.158-0.275 0.358-0.337 0.575l-2.667 9.333c-0.133 0.467-0.004 0.967 0.338 1.308 0.254 0.254 0.596 0.392 0.942 0.392 0.121 0 0.246-0.017 0.367-0.050l9.333-2.667c0.217-0.063 0.417-0.179 0.575-0.337l18.621-18.621c2.358-2.362 2.358-6.196 0-8.554zM6.079 21.137l14.392-14.392 4.779 4.779-14.387 14.396-4.783-4.783zM21.413 5.804l1.058-1.058 4.779 4.779-1.058 1.058-4.779-4.779zM5.167 22.108l4.725 4.725-6.617 1.892 1.892-6.617zM28.346 8.438l-0.15 0.15-4.783-4.783 0.15-0.15c0.642-0.637 1.488-0.988 2.392-0.988s1.75 0.35 2.392 0.992c1.317 1.317 1.317 3.458 0 4.779z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
8
src/icons/IconEmail.vue
Normal file
@@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<path
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
d="M30.742 9.771c-0.804-1.904-1.958-3.617-3.429-5.083-1.471-1.471-3.179-2.621-5.083-3.429-1.975-0.833-4.071-1.258-6.229-1.258s-4.254 0.425-6.229 1.258c-1.904 0.804-3.617 1.958-5.083 3.429-1.471 1.471-2.621 3.179-3.429 5.083-0.833 1.975-1.258 4.071-1.258 6.229s0.425 4.254 1.258 6.229c0.804 1.904 1.958 3.617 3.429 5.083 1.471 1.471 3.179 2.621 5.083 3.429 1.975 0.833 4.071 1.258 6.229 1.258h6.667v-2.667h-6.667c-7.35 0-13.333-5.983-13.333-13.333s5.983-13.333 13.333-13.333c7.35 0 13.333 5.983 13.333 13.333v0.667c0 1.837-1.496 3.333-3.333 3.333s-3.333-1.496-3.333-3.333v-7.333h-2.667v1.338c-1.117-0.838-2.5-1.338-4-1.338-3.675 0-6.667 2.992-6.667 6.667s2.992 6.667 6.667 6.667c2.079 0 3.938-0.958 5.162-2.454 1.092 1.488 2.854 2.454 4.837 2.454 3.308 0 6-2.692 6-6v-0.667c0-2.158-0.425-4.254-1.258-6.229zM16 20c-2.204 0-4-1.796-4-4s1.796-4 4-4 4 1.796 4 4-1.796 4-4 4z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
22
src/icons/IconExpand.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<svg
|
||||
id="icon-full-screen-enter"
|
||||
viewBox="0 0 32 32"
|
||||
width="100%"
|
||||
height="100%"
|
||||
style="transition-duration: 0s;"
|
||||
>
|
||||
<path
|
||||
style="transition-duration: 0s;"
|
||||
d="M29.333 2.667h-26.667c-1.471 0-2.667 1.196-2.667 2.667v21.333c0 1.471 1.196 2.667 2.667 2.667h26.667c1.471 0 2.667-1.196 2.667-2.667v-21.333c0-1.471-1.196-2.667-2.667-2.667zM29.333 26.667h-26.667v-21.333h26.667v21.333c0.004 0 0 0 0 0z"
|
||||
></path>
|
||||
<path
|
||||
style="transition-duration: 0s;"
|
||||
d="M11.333 17.058l-4.667 4.667v-1.725h-1.333v3.333c0 0.367 0.3 0.667 0.667 0.667h3.333v-1.333h-1.725l4.667-4.667-0.942-0.942z"
|
||||
></path>
|
||||
<path
|
||||
style="transition-duration: 0s;"
|
||||
d="M26 8h-3.333v1.333h1.725l-4.667 4.667 0.942 0.942 4.667-4.667v1.725h1.333v-3.333c0-0.367-0.3-0.667-0.667-0.667z"
|
||||
></path>
|
||||
</svg>
|
||||
</template>
|
||||
35
src/icons/IconInbox.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<path
|
||||
fill="inherit"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
d="M31.671 16.054v0l-6.421-11.708c-0.117-0.213-0.342-0.346-0.583-0.346h-17.333c-0.242 0-0.467 0.133-0.583 0.346l-6.421 11.708c-0.208 0.379-0.329 0.817-0.329 1.279v8c0 1.471 1.196 2.667 2.667 2.667h26.667c1.471 0 2.667-1.196 2.667-2.667v-8c0-0.462-0.121-0.9-0.329-1.279zM29.333 25.333h-26.667v-8h8.167c0.592 2.296 2.683 4 5.167 4 2.479 0 4.571-1.704 5.167-4h8.167v8zM20.667 14.667c-0.367 0-0.667 0.3-0.667 0.667v0.667c0 2.204-1.796 4-4 4s-4-1.796-4-4v-0.667c0-0.367-0.3-0.667-0.667-0.667h-8.667c-0.021 0-0.038 0-0.058 0l5.121-9.333h16.546l5.121 9.333c-0.021 0-0.038 0-0.058 0h-8.671z"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<!-- <svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
style="transition: stroke-width 0.5s ease"
|
||||
>
|
||||
<polyline points="22 12 16 12 14 15 10 15 8 12 2 12"></polyline>
|
||||
<path
|
||||
d="M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"
|
||||
></path>
|
||||
</svg> -->
|
||||
</template>
|
||||
|
||||
<!-- <style lang="scss">
|
||||
svg {
|
||||
fill: var(--text-color);
|
||||
stroke: none;
|
||||
}
|
||||
</style>
|
||||
-->
|
||||
8
src/icons/IconInfo.vue
Normal file
@@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<path
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
d="M30.742 9.771c-0.804-1.904-1.958-3.617-3.429-5.083-1.471-1.471-3.179-2.621-5.083-3.429-1.975-0.833-4.071-1.258-6.229-1.258s-4.254 0.425-6.229 1.258c-1.904 0.804-3.617 1.958-5.083 3.429-1.471 1.471-2.621 3.179-3.429 5.083-0.833 1.975-1.258 4.071-1.258 6.229s0.425 4.254 1.258 6.229c0.804 1.904 1.958 3.617 3.429 5.083 1.471 1.471 3.179 2.621 5.083 3.429 1.975 0.833 4.071 1.258 6.229 1.258s4.254-0.425 6.229-1.258c1.904-0.804 3.617-1.958 5.083-3.429 1.471-1.471 2.621-3.179 3.429-5.083 0.833-1.975 1.258-4.071 1.258-6.229s-0.425-4.254-1.258-6.229zM27.438 9.15c-0.171 0.083-0.342 0.192-0.508 0.321l-0.087 0.067-0.063 0.092c-0.625 0.925-1.604 1.208-2.254 1.008-0.35-0.108-0.529-0.321-0.529-0.637 0-0.9-0.479-1.6-0.863-2.167-0.333-0.492-0.533-0.804-0.467-1.033 0.058-0.2 0.358-0.612 1.629-1.233 1.25 0.996 2.317 2.213 3.142 3.583zM16 2.667c0.271 0 0.538 0.008 0.8 0.025-0.133 0.087-0.292 0.158-0.471 0.242-0.521 0.237-1.238 0.567-1.613 1.483l-0.012 0.025-0.008 0.025c-0.858 2.717-2.008 3.783-2.771 4.487-0.546 0.504-1.017 0.942-1.025 1.667-0.008 0.629 0.329 1.296 1.242 2.458 0.729 0.929 1.429 1.4 2.142 1.45 0.9 0.058 1.533-0.558 2.046-1.054 0.258-0.25 0.525-0.512 0.725-0.579 0.054-0.017 0.175-0.058 0.479 0.242 1.108 1.108 2.012 1.4 2.675 1.617 0.613 0.2 0.892 0.292 1.204 0.887 0.521 0.987 1.308 1.371 1.883 1.65 0.629 0.304 0.708 0.383 0.708 0.708 0 0.212 0.008 0.442 0.012 0.679 0.025 0.85 0.058 2.137-0.325 2.533-0.054 0.058-0.146 0.121-0.354 0.121-1.329 0-1.863 1.183-2.217 1.967-0.1 0.225-0.267 0.587-0.375 0.704-0.871-0.121-1.938 0.875-3.592 2.492-0.367 0.358-0.85 0.833-1.233 1.167 0.042-0.442 0.142-1.104 0.358-2.046 0.292-1.279 0.708-2.658 1.008-3.354 0.196-0.454 0.25-1.163-0.608-1.942-0.462-0.417-1.1-0.792-1.721-1.154-0.446-0.258-0.863-0.504-1.183-0.746-0.358-0.271-0.425-0.408-0.433-0.433 0-0.246 0.046-0.525 0.088-0.821 0.171-1.113 0.425-2.796-1.821-3.779-0.233-0.104-0.479-0.2-0.713-0.292-1.879-0.742-3.817-1.508-4.162-6.667 2.392-2.325 5.662-3.763 9.267-3.763zM3.817 21.413c0.796 0.137 1.375-0.446 1.804-0.875 0.142-0.142 0.438-0.438 0.558-0.467 0.058 0.025 0.479 0.25 1.204 2.167 0.425 1.125 0.446 2.283 0.467 3.621 0.004 0.225 0.008 0.454 0.013 0.696-1.742-1.346-3.142-3.113-4.046-5.142zM9.229 27.483c-0.033-0.571-0.042-1.117-0.054-1.65-0.025-1.404-0.046-2.725-0.554-4.071-0.742-1.958-1.371-2.825-2.175-3-0.779-0.167-1.354 0.413-1.775 0.833-0.171 0.171-0.521 0.521-0.633 0.5-0.046-0.008-0.446-0.137-1.163-1.742-0.138-0.767-0.208-1.55-0.208-2.354 0-3.104 1.067-5.967 2.854-8.233 0.242 1.792 0.721 3.175 1.446 4.196 1.004 1.417 2.296 1.925 3.433 2.375 0.233 0.092 0.454 0.179 0.671 0.275 1.308 0.571 1.208 1.242 1.037 2.354-0.050 0.337-0.104 0.688-0.104 1.033 0 0.988 1.108 1.633 2.279 2.321 0.558 0.329 1.137 0.667 1.496 0.992 0.308 0.279 0.292 0.396 0.279 0.425-0.35 0.813-0.817 2.367-1.129 3.783-0.171 0.767-0.283 1.45-0.338 1.979-0.075 0.779-0.017 1.242 0.192 1.546 0.075 0.108 0.175 0.196 0.287 0.258-2.121-0.15-4.108-0.796-5.842-1.821zM16 29.333c-0.067 0-0.129 0-0.196 0 0.525-0.188 1.167-0.8 2.275-1.883 0.533-0.521 1.083-1.058 1.571-1.475 0.613-0.521 0.871-0.625 0.942-0.642 0.288 0.042 0.788 0.012 1.212-0.525 0.217-0.271 0.367-0.604 0.525-0.958 0.375-0.833 0.6-1.179 1.004-1.179 0.521 0 0.975-0.183 1.308-0.525 0.779-0.8 0.738-2.233 0.704-3.5-0.008-0.229-0.012-0.446-0.012-0.642 0-1.204-0.846-1.613-1.462-1.908-0.496-0.242-0.967-0.467-1.283-1.067-0.567-1.079-1.283-1.313-1.975-1.537-0.592-0.192-1.262-0.412-2.146-1.292-0.587-0.588-1.208-0.779-1.846-0.563-0.488 0.162-0.863 0.529-1.229 0.887-0.371 0.358-0.717 0.7-1.025 0.679-0.175-0.012-0.558-0.15-1.183-0.942-0.637-0.813-0.963-1.354-0.958-1.613 0.004-0.15 0.229-0.367 0.6-0.708 0.808-0.75 2.162-2.004 3.129-5.033 0.167-0.392 0.438-0.529 0.925-0.754 0.5-0.229 1.125-0.517 1.483-1.267 1.704 0.308 3.296 0.938 4.708 1.825-0.988 0.563-1.508 1.108-1.688 1.729-0.246 0.846 0.229 1.542 0.646 2.154 0.325 0.475 0.633 0.925 0.633 1.412 0 0.9 0.563 1.633 1.471 1.912 0.246 0.075 0.521 0.117 0.808 0.117 0.958 0 2.079-0.446 2.875-1.554 0.087-0.063 0.171-0.108 0.246-0.146 0.817 1.713 1.271 3.633 1.271 5.662 0 7.35-5.983 13.333-13.333 13.333z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
20
src/icons/IconKey.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
d="M24 13.333v-2.667c0-2.137-0.833-4.146-2.342-5.658s-3.521-2.342-5.658-2.342-4.146 0.833-5.658 2.342-2.342 3.521-2.342 5.658v2.667c-1.471 0-2.667 1.196-2.667 2.667v10.667c0 1.471 1.196 2.667 2.667 2.667h16c1.471 0 2.667-1.196 2.667-2.667v-10.667c0-1.471-1.196-2.667-2.667-2.667zM10.667 10.667c0-2.942 2.392-5.333 5.333-5.333s5.333 2.392 5.333 5.333v2.667h-10.667v-2.667zM24 26.667h-16v-10.667h16v10.667c0.004 0 0 0 0 0z"
|
||||
/>
|
||||
<path
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
d="M12 20c-0.733 0-1.333 0.6-1.333 1.333s0.6 1.333 1.333 1.333 1.333-0.6 1.333-1.333-0.6-1.333-1.333-1.333zM12 21.333c0 0 0 0 0 0v0z"
|
||||
/>
|
||||
<path
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
d="M16 20c-0.733 0-1.333 0.6-1.333 1.333s0.6 1.333 1.333 1.333c0.733 0 1.333-0.6 1.333-1.333s-0.6-1.333-1.333-1.333zM16 21.333c0 0 0 0 0 0v0z"
|
||||
/>
|
||||
<path
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
d="M20 20c-0.733 0-1.333 0.6-1.333 1.333s0.6 1.333 1.333 1.333 1.333-0.6 1.333-1.333-0.6-1.333-1.333-1.333zM20 21.333c0 0 0 0 0 0v0z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
8
src/icons/IconMagnet.vue
Normal file
@@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<path
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
d="M31.608 13.725l-4-4c-0.304-0.304-0.729-0.442-1.154-0.375-0.421 0.067-0.788 0.333-0.979 0.713-2.938 5.804-5.517 9.617-8.354 12.358-0.004 0.004-0.012 0.012-0.017 0.017-1.008 1.008-2.346 1.563-3.771 1.563s-2.762-0.554-3.771-1.563c-1.008-1.008-1.563-2.346-1.563-3.771s0.554-2.762 1.563-3.771c0.004-0.004 0.012-0.012 0.017-0.017 2.742-2.842 6.55-5.417 12.358-8.354 0.383-0.192 0.646-0.558 0.712-0.979s-0.071-0.85-0.375-1.154l-4-4c-0.404-0.404-1.021-0.504-1.538-0.25-2.271 1.125-4.475 2.438-6.554 3.9-2.175 1.529-4.279 3.271-6.258 5.179-0.004 0.004-0.013 0.012-0.017 0.017-2.521 2.521-3.908 5.867-3.908 9.429s1.387 6.908 3.904 9.429c2.521 2.517 5.867 3.904 9.429 3.904s6.908-1.387 9.429-3.904c0.004-0.004 0.012-0.012 0.017-0.017 1.908-1.979 3.65-4.088 5.179-6.258 1.462-2.079 2.775-4.283 3.904-6.558 0.254-0.517 0.15-1.133-0.254-1.537zM17.075 2.962l2.025 2.025c-1.188 0.629-2.292 1.246-3.317 1.854l-2-2c1.071-0.667 2.167-1.296 3.292-1.879zM20.867 26.217c-2.012 2.008-4.688 3.117-7.533 3.117-2.85 0-5.529-1.108-7.542-3.125-4.154-4.154-4.158-10.917-0.008-15.075 2.183-2.104 4.454-3.942 6.858-5.55l1.971 1.971c-2.879 1.804-5.117 3.571-6.946 5.463-1.504 1.508-2.333 3.517-2.333 5.65 0 2.137 0.833 4.146 2.342 5.658 1.512 1.508 3.521 2.342 5.658 2.342 2.133 0 4.138-0.829 5.65-2.333 1.892-1.829 3.663-4.067 5.463-6.946l1.971 1.971c-1.608 2.404-3.446 4.675-5.55 6.858zM27.158 18.212l-1.996-1.996c0.608-1.025 1.225-2.129 1.854-3.317l2.025 2.025c-0.587 1.125-1.217 2.221-1.883 3.288z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
8
src/icons/IconMovie.vue
Normal file
@@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<path
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
d="M32 10.667c0-3.675-2.992-6.667-6.667-6.667-1.050 0-2.1 0.25-3.029 0.725-0.271 0.138-0.533 0.296-0.783 0.471-0.333-0.546-0.729-1.058-1.196-1.521-1.512-1.508-3.521-2.342-5.658-2.342s-4.146 0.833-5.658 2.342-2.342 3.521-2.342 5.658c0 1.262 0.3 2.517 0.871 3.633 0.446 0.875 1.062 1.671 1.796 2.329v1.35l-7.475-3.204c-0.413-0.175-0.883-0.133-1.258 0.113s-0.6 0.662-0.6 1.113v14.667c0 0.475 0.254 0.917 0.662 1.154 0.208 0.121 0.438 0.179 0.671 0.179 0.229 0 0.458-0.058 0.662-0.175l7.338-4.196v1.704c0 1.471 1.196 2.667 2.667 2.667h17.333c1.471 0 2.667-1.196 2.667-2.667v-13.333c0-0.567-0.175-1.088-0.479-1.521 0.317-0.783 0.479-1.625 0.479-2.479zM29.333 25.333h-17.333v-10.667h17.333v10.667zM25.333 6.667c2.204 0 4 1.796 4 4 0 0.458-0.079 0.908-0.229 1.333h-6.892c0.3-0.846 0.454-1.746 0.454-2.667 0-0.517-0.050-1.021-0.142-1.517 0.746-0.737 1.742-1.15 2.808-1.15zM14.667 4c2.942 0 5.333 2.392 5.333 5.333 0 0.95-0.246 1.863-0.712 2.667h-7.287c-0.6 0-1.154 0.2-1.6 0.537-0.688-0.912-1.067-2.025-1.067-3.204 0-2.942 2.392-5.333 5.333-5.333zM2.667 27.038v-10.35l6.667 2.858v3.679l-6.667 3.813zM12 28.004c0 0 0-0.004 0 0v-1.337h17.333v1.333l-17.333 0.004z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
38
src/icons/IconNowPlaying.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<path
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="inherit"
|
||||
d="M31.608 12.392l-3.642-3.642c-0.521-0.521-1.367-0.521-1.887 0-0.375 0.375-0.879 0.583-1.413 0.583s-1.038-0.208-1.413-0.588c-0.779-0.779-0.779-2.050 0-2.829 0.521-0.521 0.521-1.367 0-1.888l-3.646-3.638c-0.25-0.25-0.587-0.392-0.942-0.392s-0.692 0.142-0.942 0.392l-17.333 17.333c-0.521 0.521-0.521 1.367 0 1.887l3.642 3.642c0.25 0.25 0.588 0.392 0.942 0.392s0.692-0.142 0.942-0.392c0.379-0.379 0.879-0.587 1.412-0.587s1.037 0.208 1.412 0.587 0.592 0.879 0.592 1.413c0 0.533-0.208 1.038-0.588 1.413-0.25 0.25-0.392 0.587-0.392 0.942s0.142 0.692 0.392 0.942l3.642 3.642c0.258 0.258 0.6 0.392 0.942 0.392s0.683-0.129 0.942-0.392l17.333-17.333c0.525-0.517 0.525-1.358 0.004-1.879zM13.333 28.779l-1.896-1.896c0.954-1.767 0.688-4.029-0.804-5.521-0.883-0.875-2.054-1.363-3.3-1.363 0 0 0 0 0 0-0.787 0-1.546 0.196-2.221 0.558l-1.892-1.892 15.446-15.446 1.896 1.896c-0.954 1.767-0.688 4.029 0.804 5.521 0.908 0.908 2.104 1.367 3.3 1.367 0.767 0 1.529-0.188 2.221-0.558l1.896 1.896-15.45 15.438z"
|
||||
/>
|
||||
<path
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="inherit"
|
||||
d="M17.137 8.196c-0.258-0.258-0.683-0.258-0.942 0l-8 8c-0.258 0.258-0.258 0.683 0 0.942l6.667 6.667c0.129 0.129 0.3 0.196 0.471 0.196s0.342-0.067 0.471-0.196l8-8c0.258-0.258 0.258-0.683 0-0.942l-6.667-6.667zM15.333 22.392l-5.725-5.725 7.058-7.058 5.725 5.725-7.058 7.058z"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<!-- <svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
style="transition: stroke-width 0.5s ease"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<polygon points="10 8 16 12 10 16 10 8"></polygon>
|
||||
</svg> -->
|
||||
</template>
|
||||
|
||||
<!-- <style lang="scss" scoped>
|
||||
svg {
|
||||
fill: var(--text-color);
|
||||
stroke: none;
|
||||
}
|
||||
</style>
|
||||
-->
|
||||
12
src/icons/IconPerson.vue
Normal file
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<path
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
d="M16 18.667c4.413 0 8-3.588 8-8s-3.587-8-8-8-8 3.588-8 8 3.588 8 8 8zM16 5.333c2.942 0 5.333 2.392 5.333 5.333s-2.392 5.333-5.333 5.333c-2.942 0-5.333-2.392-5.333-5.333s2.392-5.333 5.333-5.333z"
|
||||
/>
|
||||
<path
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
d="M26.279 22.758c-1.842-1.858-5.204-2.758-10.279-2.758s-8.438 0.9-10.279 2.758c-1.721 1.733-1.721 3.846-1.721 5.242v0.667c0 0.367 0.3 0.667 0.667 0.667h22.667c0.367 0 0.667-0.3 0.667-0.667v-0.667c0-1.396 0-3.508-1.721-5.242zM6.667 28c0-1.183 0-2.413 0.946-3.363 0.563-0.567 1.429-1.017 2.583-1.342 1.475-0.417 3.429-0.629 5.804-0.629s4.329 0.212 5.804 0.629c1.154 0.325 2.021 0.775 2.583 1.342 0.946 0.95 0.946 2.179 0.946 3.363h-18.667z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
12
src/icons/IconPlay.vue
Normal file
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
d="M31.333 4h-30.667c-0.367 0-0.667 0.3-0.667 0.667v20c0 0.367 0.3 0.667 0.667 0.667h16.417c1.579 1.642 3.796 2.667 6.25 2.667 4.779 0 8.667-3.887 8.667-8.667v-14.667c0-0.367-0.3-0.667-0.667-0.667zM5.333 5.333v2.667h-2.667v-2.667h2.667zM2.667 17.333h2.667v2.667h-2.667v-2.667zM2.667 16v-2.667h2.667v2.667h-2.667zM2.667 12v-2.667h2.667v2.667h-2.667zM2.667 24v-2.667h2.667v2.667h-2.667zM29.333 12h-1.387c-0.404-0.254-0.833-0.479-1.279-0.667v-2h2.667v2.667zM8 6.667h16v4.025c-0.221-0.017-0.442-0.025-0.667-0.025-4.779 0-8.667 3.888-8.667 8.667 0 1.179 0.238 2.308 0.667 3.333h-7.333v-16zM23.333 26.667c-4.042 0-7.333-3.292-7.333-7.333s3.292-7.333 7.333-7.333 7.333 3.292 7.333 7.333-3.292 7.333-7.333 7.333zM29.333 8h-2.667v-2.667h2.667v2.667z"
|
||||
/>
|
||||
<path
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
d="M27.675 18.762l-6.667-4c-0.204-0.125-0.462-0.125-0.671-0.008s-0.337 0.342-0.337 0.579v8c0 0.242 0.129 0.462 0.337 0.579 0.1 0.058 0.217 0.087 0.329 0.087 0.121 0 0.238-0.033 0.342-0.096l6.667-4c0.2-0.121 0.325-0.337 0.325-0.571s-0.121-0.45-0.325-0.571zM21.333 22.154v-5.642l4.704 2.821-4.704 2.821z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
29
src/icons/IconPopular.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<path
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="inherit"
|
||||
d="M21.333 20h-2.667c-0.738 0-1.333 0.596-1.333 1.333v10c0 0.367 0.3 0.667 0.667 0.667h4c0.367 0 0.667-0.3 0.667-0.667v-10c0-0.738-0.596-1.333-1.333-1.333zM18.667 30.667v-8h1.333v8h-1.333z"
|
||||
/>
|
||||
<path
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="inherit"
|
||||
d="M13.333 14.667h-2.667c-0.738 0-1.333 0.596-1.333 1.333v15.333c0 0.367 0.3 0.667 0.667 0.667h4c0.367 0 0.667-0.3 0.667-0.667v-15.333c0-0.738-0.596-1.333-1.333-1.333zM10.667 30.667v-13.333h1.333v13.333h-1.333z"
|
||||
/>
|
||||
<path
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="inherit"
|
||||
d="M5.333 20h-2.667c-0.738 0-1.333 0.596-1.333 1.333v10c0 0.367 0.3 0.667 0.667 0.667h4c0.367 0 0.667-0.3 0.667-0.667v-10c0-0.738-0.596-1.333-1.333-1.333zM2.667 30.667v-8h1.333v8h-1.333z"
|
||||
/>
|
||||
<path
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="inherit"
|
||||
d="M29.333 9.333h-2.667c-0.738 0-1.333 0.596-1.333 1.333v20.667c0 0.367 0.3 0.667 0.667 0.667h4c0.367 0 0.667-0.3 0.667-0.667v-20.667c0-0.738-0.596-1.333-1.333-1.333zM26.667 30.667v-18.667h1.333v18.667h-1.333z"
|
||||
/>
|
||||
<path
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="inherit"
|
||||
d="M31.333 0h-3.333v1.333h1.592l-11.738 9.267-9.004-2.571c-0.208-0.058-0.429-0.012-0.6 0.121l-7.188 5.75 0.833 1.042 6.917-5.533 9.004 2.571c0.204 0.058 0.429 0.017 0.596-0.117l12.254-9.679v1.817h1.333v-3.333c0-0.367-0.3-0.667-0.667-0.667z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
14
src/icons/IconProfile.vue
Normal file
@@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<path
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="inherit"
|
||||
d="M16 18.667c4.413 0 8-3.588 8-8s-3.587-8-8-8-8 3.588-8 8 3.588 8 8 8zM16 5.333c2.942 0 5.333 2.392 5.333 5.333s-2.392 5.333-5.333 5.333c-2.942 0-5.333-2.392-5.333-5.333s2.392-5.333 5.333-5.333z"
|
||||
/>
|
||||
<path
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="inherit"
|
||||
d="M26.279 22.758c-1.842-1.858-5.204-2.758-10.279-2.758s-8.438 0.9-10.279 2.758c-1.721 1.733-1.721 3.846-1.721 5.242v0.667c0 0.367 0.3 0.667 0.667 0.667h22.667c0.367 0 0.667-0.3 0.667-0.667v-0.667c0-1.396 0-3.508-1.721-5.242zM6.667 28c0-1.183 0-2.413 0.946-3.363 0.563-0.567 1.429-1.017 2.583-1.342 1.475-0.417 3.429-0.629 5.804-0.629s4.329 0.212 5.804 0.629c1.154 0.325 2.021 0.775 2.583 1.342 0.946 0.95 0.946 2.179 0.946 3.363h-18.667z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
16
src/icons/IconProfileLock.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<path
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
d="M12 16c4.413 0 8-3.588 8-8s-3.587-8-8-8-8 3.587-8 8 3.588 8 8 8zM12 2.667c2.942 0 5.333 2.392 5.333 5.333s-2.392 5.333-5.333 5.333-5.333-2.392-5.333-5.333 2.392-5.333 5.333-5.333z"
|
||||
/>
|
||||
<path
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
d="M23.333 14.667c-2.617 0-4.971 1.167-6.558 3.008-1.367-0.225-2.975-0.342-4.775-0.342-5.075 0-8.438 0.9-10.279 2.758-1.721 1.733-1.721 3.846-1.721 5.242v0.667c0 0.367 0.3 0.667 0.667 0.667h14.667c1.308 3.129 4.4 5.333 8 5.333 4.779 0 8.667-3.887 8.667-8.667s-3.887-8.667-8.667-8.667zM2.667 25.333c0-1.183 0-2.413 0.946-3.363 0.563-0.567 1.429-1.017 2.583-1.342 1.475-0.417 3.429-0.629 5.804-0.629 1.196 0 2.292 0.054 3.267 0.163-0.387 0.983-0.6 2.054-0.6 3.171 0 0.688 0.079 1.358 0.233 2h-12.233zM23.333 30.667c-4.042 0-7.333-3.292-7.333-7.333s3.292-7.333 7.333-7.333 7.333 3.292 7.333 7.333-3.292 7.333-7.333 7.333z"
|
||||
/>
|
||||
<path
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
d="M27.333 22.667h-0.667v-0.667c0-1.837-1.496-3.333-3.333-3.333s-3.333 1.496-3.333 3.333v0.667h-0.667c-0.367 0-0.667 0.3-0.667 0.667v4c0 0.367 0.3 0.667 0.667 0.667h8c0.367 0 0.667-0.3 0.667-0.667v-4c0-0.367-0.3-0.667-0.667-0.667zM21.333 22c0-1.104 0.896-2 2-2s2 0.896 2 2v0.667h-4v-0.667zM26.667 26.667h-6.667v-2.667h6.667v2.667z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
17
src/icons/IconRequest.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<path
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
d="M23.988 13.863c0 0 0 0 0 0 0-0.004 0-0.008-0.004-0.012 0 0 0 0 0-0.004s0-0.004 0-0.008c0 0 0 0 0 0 0-0.004 0-0.004-0.004-0.008 0 0 0-0.004 0-0.004 0-0.004 0-0.004 0-0.008 0 0 0-0.004 0-0.004s0-0.004 0-0.004c0 0 0-0.004 0-0.004 0-0.004 0-0.004-0.004-0.008 0 0 0-0.004 0-0.004s0-0.004 0-0.004c0 0 0-0.004 0-0.004 0-0.004 0-0.004-0.004-0.008 0 0 0 0 0-0.004s0-0.004-0.004-0.008c0 0 0 0 0-0.004s-0.004-0.004-0.004-0.008c0 0 0 0 0 0-0.050-0.121-0.133-0.229-0.246-0.304 0 0 0 0 0 0-0.004-0.004-0.012-0.008-0.017-0.012 0 0 0 0 0 0-0.004 0-0.004-0.004-0.008-0.004 0 0 0 0 0 0-0.004 0-0.004-0.004-0.008-0.004 0 0 0 0-0.004 0 0 0 0 0-0.004 0-0.004-0.004-0.012-0.008-0.017-0.008 0 0 0 0 0 0-0.133-0.071-0.279-0.092-0.421-0.071 0 0 0 0 0 0-0.004 0-0.008 0-0.008 0s0 0 0 0c-0.004 0-0.004 0-0.008 0 0 0 0 0-0.004 0s-0.008 0-0.008 0c0 0 0 0 0 0-0.004 0-0.004 0-0.008 0 0 0 0 0-0.004 0s-0.008 0-0.008 0.004c0 0 0 0-0.004 0s-0.004 0-0.008 0.004c0 0 0 0 0 0-0.004 0-0.008 0-0.008 0.004 0 0 0 0 0 0-0.008 0-0.012 0.004-0.021 0.008 0 0 0 0 0 0-0.067 0.021-0.133 0.058-0.192 0.1l-14.679 10.662c-0.208 0.154-0.313 0.413-0.262 0.667s0.242 0.458 0.492 0.521l4.946 1.238 1.238 4.946c0.067 0.262 0.287 0.462 0.554 0.5 0.029 0.004 0.063 0.008 0.092 0.008 0.238 0 0.458-0.125 0.579-0.337l2.383-4.175 4.804 1.796c0.204 0.075 0.433 0.050 0.613-0.075s0.288-0.329 0.288-0.55v-14.654c0-0.050-0.004-0.1-0.012-0.15zM10.213 24.367l9.7-7.054-6.171 7.933-3.529-0.879zM15.579 29.563l-0.854-3.408 6.033-7.758-3.358 7.975-1.821 3.192zM18.883 26.288l3.783-8.988v10.404l-3.783-1.417z"
|
||||
/>
|
||||
<path
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
d="M28.488 3.513c-2.267-2.263-5.283-3.513-8.488-3.513-2.5 0-4.9 0.762-6.933 2.204-1.667 1.179-2.983 2.737-3.863 4.554-0.396-0.058-0.796-0.092-1.204-0.092-2.138 0-4.146 0.833-5.658 2.342s-2.342 3.521-2.342 5.658c0 1.858 0.65 3.667 1.829 5.096 1.163 1.408 2.788 2.383 4.571 2.746l0.529-2.613c-2.471-0.504-4.263-2.7-4.263-5.229 0-2.942 2.392-5.333 5.333-5.333 1.8 0 3.462 0.896 4.454 2.4l2.225-1.467c-0.75-1.137-1.754-2.042-2.917-2.662 1.608-2.996 4.775-4.938 8.238-4.938 5.146 0 9.333 4.188 9.333 9.333 0 2.225-0.938 4.367-2.571 5.875l1.808 1.958c1.071-0.988 1.913-2.163 2.504-3.488 0.613-1.371 0.925-2.833 0.925-4.35 0-3.2-1.25-6.217-3.512-8.483z"
|
||||
/>
|
||||
|
||||
<!-- <path
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
d="M31.542 1.658c-0.396-0.342-0.95-0.425-1.425-0.208l-29.333 13.333c-0.512 0.233-0.821 0.758-0.779 1.317s0.425 1.029 0.963 1.183l8.367 2.387v9.663c0 0.587 0.383 1.104 0.946 1.275 0.129 0.038 0.258 0.058 0.387 0.058 0.438 0 0.858-0.217 1.108-0.596l4.683-7.017 6.946 3.475c0.354 0.175 0.767 0.188 1.129 0.029s0.637-0.467 0.746-0.846l6.667-22.667c0.146-0.504-0.012-1.046-0.404-1.387zM5.183 15.713l18.771-8.533-12.804 10.246c-0.037-0.017-0.079-0.029-0.117-0.042l-5.85-1.671zM12 24.929v-6.262c0-0.067-0.004-0.133-0.017-0.2l13.963-11.171-10.971 13.183c-0.029 0.038-0.058 0.075-0.083 0.113l-2.892 4.338zM23.171 23.429l-6.296-3.15 11.167-13.421-4.871 16.571z"
|
||||
/> -->
|
||||
</svg>
|
||||
</template>
|
||||
17
src/icons/IconRequested.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<path
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
d="M31.904 16.087c0.063-0.471 0.096-0.946 0.096-1.421 0-3.204-1.25-6.221-3.512-8.488-2.267-2.263-5.283-3.513-8.488-3.513-2.5 0-4.9 0.763-6.933 2.204-1.667 1.179-2.983 2.737-3.863 4.554-0.396-0.058-0.796-0.092-1.204-0.092-2.138 0-4.146 0.833-5.658 2.342s-2.342 3.521-2.342 5.658 0.833 4.146 2.342 5.658c1.513 1.508 3.521 2.342 5.658 2.342h8.033c1.542 2.404 4.238 4 7.3 4 4.779 0 8.667-3.887 8.667-8.667 0-0.992-0.167-1.946-0.475-2.833 0.175-0.567 0.304-1.154 0.379-1.746zM8 22.667c-2.942 0-5.333-2.392-5.333-5.333s2.392-5.333 5.333-5.333c1.8 0 3.463 0.896 4.454 2.4l2.225-1.467c-0.75-1.137-1.754-2.042-2.917-2.662 1.608-2.996 4.775-4.938 8.238-4.938 5.063 0 9.196 4.050 9.329 9.083-1.558-1.496-3.671-2.417-5.996-2.417-4.779 0-8.667 3.887-8.667 8.667 0 0.688 0.079 1.358 0.233 2h-6.9zM23.333 28c-4.042 0-7.333-3.292-7.333-7.333s3.292-7.333 7.333-7.333 7.333 3.292 7.333 7.333-3.292 7.333-7.333 7.333z"
|
||||
/>
|
||||
<path
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
d="M22 22.392l-2.667-2.667-0.942 0.942 3.137 3.137c0.129 0.129 0.3 0.196 0.471 0.196s0.342-0.067 0.471-0.196l5.804-5.804-0.942-0.942-5.333 5.333z"
|
||||
/>
|
||||
|
||||
<!-- <path
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
d="M31.542 1.658c-0.396-0.342-0.95-0.425-1.425-0.208l-29.333 13.333c-0.512 0.233-0.821 0.758-0.779 1.317s0.425 1.029 0.963 1.183l8.367 2.387v9.663c0 0.587 0.383 1.104 0.946 1.275 0.129 0.038 0.258 0.058 0.387 0.058 0.438 0 0.858-0.217 1.108-0.596l4.683-7.017 6.946 3.475c0.354 0.175 0.767 0.188 1.129 0.029s0.637-0.467 0.746-0.846l6.667-22.667c0.146-0.504-0.012-1.046-0.404-1.387zM5.183 15.713l18.771-8.533-12.804 10.246c-0.037-0.017-0.079-0.029-0.117-0.042l-5.85-1.671zM12 24.929v-6.262c0-0.067-0.004-0.133-0.017-0.2l13.963-11.171-10.971 13.183c-0.029 0.038-0.058 0.075-0.083 0.113l-2.892 4.338zM23.171 23.429l-6.296-3.15 11.167-13.421-4.871 16.571z"
|
||||
/> -->
|
||||
</svg>
|
||||
</template>
|
||||
13
src/icons/IconSearch.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 32 32"
|
||||
style="transition-duration: 0s;"
|
||||
>
|
||||
<path
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="inherit"
|
||||
d="M21.388 21.141c-0.045 0.035-0.089 0.073-0.132 0.116s-0.080 0.085-0.116 0.132c-1.677 1.617-3.959 2.611-6.473 2.611-2.577 0-4.909-1.043-6.6-2.733s-2.733-4.023-2.733-6.6 1.043-4.909 2.733-6.6 4.023-2.733 6.6-2.733 4.909 1.043 6.6 2.733 2.733 4.023 2.733 6.6c0 2.515-0.993 4.796-2.612 6.475zM28.943 27.057l-4.9-4.9c1.641-2.053 2.624-4.657 2.624-7.491 0-3.313-1.344-6.315-3.515-8.485s-5.172-3.515-8.485-3.515-6.315 1.344-8.485 3.515-3.515 5.172-3.515 8.485 1.344 6.315 3.515 8.485 5.172 3.515 8.485 3.515c2.833 0 5.437-0.983 7.491-2.624l4.9 4.9c0.521 0.521 1.365 0.521 1.885 0s0.521-1.365 0-1.885z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
24
src/icons/IconSettings.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
style="transition: stroke-width 0.5s ease"
|
||||
>
|
||||
<line x1="4" y1="21" x2="4" y2="14"></line>
|
||||
<line x1="4" y1="10" x2="4" y2="3"></line>
|
||||
<line x1="12" y1="21" x2="12" y2="12"></line>
|
||||
<line x1="12" y1="8" x2="12" y2="3"></line>
|
||||
<line x1="20" y1="21" x2="20" y2="16"></line>
|
||||
<line x1="20" y1="12" x2="20" y2="3"></line>
|
||||
<line x1="1" y1="14" x2="7" y2="14"></line>
|
||||
<line x1="9" y1="8" x2="15" y2="8"></line>
|
||||
<line x1="17" y1="16" x2="23" y2="16"></line>
|
||||
</svg>
|
||||
</template>
|
||||
18
src/icons/IconShow.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<path
|
||||
d="M29.333 6.667h-26.667c-1.471 0-2.667 1.196-2.667 2.667v18.667c0 1.471 1.196 2.667 2.667 2.667h26.667c1.471 0 2.667-1.196 2.667-2.667v-18.667c0-1.471-1.196-2.667-2.667-2.667zM29.333 28h-26.667v-18.667h26.667v18.667z"
|
||||
></path>
|
||||
<path
|
||||
d="M4.667 26.667h17.333c0.367 0 0.667-0.3 0.667-0.667v-14.667c0-0.367-0.3-0.667-0.667-0.667h-17.333c-0.367 0-0.667 0.3-0.667 0.667v14.667c0 0.367 0.3 0.667 0.667 0.667zM5.333 12h16v13.333h-16v-13.333z"
|
||||
></path>
|
||||
<path
|
||||
d="M26 16c1.104 0 2-0.896 2-2s-0.896-2-2-2-2 0.896-2 2 0.896 2 2 2zM26 13.333c0.367 0 0.667 0.3 0.667 0.667s-0.3 0.667-0.667 0.667-0.667-0.3-0.667-0.667 0.3-0.667 0.667-0.667z"
|
||||
></path>
|
||||
<path d="M24 24h4v1.333h-4v-1.333z"></path>
|
||||
<path d="M24 21.333h4v1.333h-4v-1.333z"></path>
|
||||
<path
|
||||
d="M22.917 2.229l-0.688-1.146-6.854 4.112-5.508-4.129-0.8 1.067 5.867 4.4c0.117 0.088 0.258 0.133 0.4 0.133 0.117 0 0.238-0.033 0.342-0.096l7.242-4.342z"
|
||||
></path>
|
||||
</svg>
|
||||
</template>
|
||||
18
src/icons/IconShrink.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<svg
|
||||
id="icon-full-screen-exit"
|
||||
viewBox="0 0 32 32"
|
||||
width="100%"
|
||||
height="100%"
|
||||
>
|
||||
<path
|
||||
d="M29.333 2.667h-26.667c-1.471 0-2.667 1.196-2.667 2.667v21.333c0 1.471 1.196 2.667 2.667 2.667h26.667c1.471 0 2.667-1.196 2.667-2.667v-21.333c0-1.471-1.196-2.667-2.667-2.667zM29.333 26.667h-26.667v-21.333h26.667v21.333c0.004 0 0 0 0 0z"
|
||||
></path>
|
||||
<path
|
||||
d="M26 7.725l-4.667 4.667v-1.725h-1.333v3.333c0 0.367 0.3 0.667 0.667 0.667h3.333v-1.333h-1.725l4.667-4.667-0.942-0.942z"
|
||||
></path>
|
||||
<path
|
||||
d="M11.333 17.333h-3.333v1.333h1.725l-4.667 4.667 0.942 0.942 4.667-4.667v1.725h1.333v-3.333c0-0.367-0.3-0.667-0.667-0.667z"
|
||||
></path>
|
||||
</svg>
|
||||
</template>
|
||||
8
src/icons/IconThumbsDown.vue
Normal file
@@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<path
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
d="M28 13.333c0-0.708-0.188-1.4-0.538-2 0.346-0.6 0.538-1.292 0.538-2 0-1.208-0.546-2.329-1.442-3.075 0.071-0.3 0.108-0.612 0.108-0.925 0-2.204-1.796-4-4-4h-3.621c-3.842 0-7.683 0.946-11.104 2.729 0 0-0.004 0-0.004 0.004-2.429 1.279-3.938 3.792-3.938 6.563v4.671c0 1.137 0.263 2.275 0.763 3.292 0.504 1.025 1.246 1.925 2.146 2.613 1.237 0.942 2.417 1.542 3.554 2.117 2.175 1.104 3.896 1.979 5.412 4.962 0.313 0.783 0.817 1.417 1.475 1.846 0.708 0.462 1.529 0.633 2.321 0.483 0.921-0.175 1.717-0.767 2.246-1.671 0.508-0.871 0.758-2.004 0.742-3.358 0-1.283-0.296-2.863-1.029-4.25h2.375c2.204 0 4-1.796 4-4 0-0.708-0.188-1.4-0.538-2 0.346-0.6 0.533-1.292 0.533-2zM22.667 6.667h-2.667v1.333h4c0.208 0 0.404 0.046 0.587 0.137 0.454 0.221 0.746 0.683 0.746 1.196 0 0.329-0.121 0.646-0.337 0.887-0.254 0.283-0.613 0.442-0.992 0.446 0 0-0.004 0-0.004 0h-2.667v1.333h2.667c0 0 0.004 0 0.004 0 0.325 0 0.637 0.121 0.879 0.333 0.288 0.254 0.45 0.617 0.45 1s-0.163 0.746-0.45 1c-0.242 0.213-0.554 0.333-0.883 0.333h-2.667v1.333h2.667c0.379 0 0.742 0.163 0.996 0.446 0.217 0.242 0.337 0.558 0.337 0.887 0 0.733-0.6 1.333-1.333 1.333h-5.333c-0.6 0-1.125 0.4-1.283 0.979s0.083 1.192 0.6 1.5c1.379 0.829 2.008 2.887 2.008 4.45 0 0.004 0 0.012 0 0.017 0.021 1.633-0.475 2.321-0.813 2.387-0.254 0.046-0.629-0.188-0.833-0.725-0.017-0.046-0.033-0.087-0.058-0.129-1.917-3.813-4.304-5.025-6.617-6.2-1.033-0.525-2.1-1.067-3.146-1.863-1.162-0.883-1.858-2.296-1.858-3.779v-4.675c0-1.775 0.963-3.388 2.508-4.2 3.042-1.587 6.454-2.429 9.871-2.429h3.621c0.733 0 1.333 0.6 1.333 1.333 0 0.229-0.058 0.45-0.163 0.646-0.238 0.425-0.688 0.688-1.171 0.688z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
8
src/icons/IconThumbsUp.vue
Normal file
@@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<path
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
d="M27.462 16.667c0.346-0.6 0.538-1.292 0.538-2 0-2.204-1.796-4-4-4h-2.375c0.733-1.387 1.029-2.967 1.029-4.25 0.017-1.358-0.233-2.487-0.742-3.358-0.525-0.904-1.321-1.5-2.246-1.671-0.788-0.15-1.613 0.025-2.321 0.483-0.654 0.425-1.163 1.063-1.475 1.846-1.517 2.983-3.237 3.858-5.412 4.962-1.137 0.579-2.313 1.175-3.554 2.117-0.904 0.688-1.646 1.588-2.146 2.612-0.5 1.017-0.763 2.154-0.763 3.292v4.671c0 2.771 1.508 5.283 3.938 6.563 0 0 0.004 0 0.004 0.004 3.421 1.788 7.263 2.729 11.104 2.729h3.625c2.204 0 4-1.796 4-4 0-0.317-0.038-0.625-0.108-0.925 0.896-0.746 1.442-1.867 1.442-3.075 0-0.708-0.188-1.4-0.538-2 0.346-0.6 0.538-1.292 0.538-2s-0.188-1.4-0.538-2zM23.837 26.021c0.108 0.196 0.163 0.417 0.163 0.646 0 0.733-0.6 1.333-1.333 1.333h-3.621c-3.412 0-6.825-0.837-9.871-2.429-1.55-0.817-2.508-2.425-2.508-4.2v-4.671c0-1.483 0.696-2.896 1.858-3.779 1.050-0.796 2.117-1.338 3.146-1.863 2.308-1.175 4.696-2.387 6.617-6.2 0.021-0.042 0.042-0.083 0.058-0.129 0.2-0.533 0.579-0.771 0.833-0.725 0.337 0.063 0.833 0.75 0.813 2.388 0 0.004 0 0.013 0 0.017 0 1.563-0.629 3.621-2.008 4.45-0.512 0.308-0.758 0.921-0.6 1.5 0.158 0.575 0.683 0.975 1.283 0.975h5.333c0.733 0 1.333 0.6 1.333 1.333 0 0.329-0.121 0.646-0.337 0.887-0.254 0.283-0.617 0.446-0.996 0.446h-2.667v1.333h2.667c0.325 0 0.642 0.121 0.883 0.333 0.288 0.254 0.45 0.617 0.45 1s-0.163 0.746-0.45 1c-0.242 0.212-0.554 0.333-0.879 0.333 0 0-0.004 0-0.004 0v0h-2.667v1.333h2.667c0 0 0.004 0 0.004 0 0.375 0 0.738 0.163 0.992 0.446 0.217 0.242 0.337 0.558 0.337 0.887 0 0.512-0.292 0.975-0.746 1.196-0.183 0.092-0.383 0.137-0.587 0.137h-4v1.333h2.667c0.483 0 0.933 0.262 1.171 0.688z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
40
src/icons/IconUpcoming.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<svg id="icon-calendar4" viewBox="0 0 32 32" width="100%" height="100%">
|
||||
<path fill="inherit" d="M0 1.333h6.667v2.667h-6.667v-2.667z"></path>
|
||||
<path fill="inherit" d="M12 1.333h8v2.667h-8v-2.667z"></path>
|
||||
<path fill="inherit" d="M25.333 1.333h6.667v2.667h-6.667v-2.667z"></path>
|
||||
<path fill="inherit" d="M8 0h2.667v9.333h-2.667v-9.333z"></path>
|
||||
<path fill="inherit" d="M21.333 0h2.667v9.333h-2.667v-9.333z"></path>
|
||||
<path fill="inherit" d="M12 5.333h8v2.667h-8v-2.667z"></path>
|
||||
<path
|
||||
fill="inherit"
|
||||
d="M29.333 5.333h-4v2.667h4v21.333h-20v-6c0-0.367-0.3-0.667-0.667-0.667h-6v-14.667h4v-2.667h-4c-1.471 0-2.667 1.196-2.667 2.667v14.667c0 5.146 4.188 9.333 9.333 9.333h20c1.471 0 2.667-1.196 2.667-2.667v-21.333c0-1.471-1.196-2.667-2.667-2.667zM2.8 24h5.2v5.2c-2.608-0.533-4.667-2.592-5.2-5.2z"
|
||||
></path>
|
||||
<path fill="inherit" d="M14.667 12h2.667v1.333h-2.667v-1.333z"></path>
|
||||
<path fill="inherit" d="M18.667 12h2.667v1.333h-2.667v-1.333z"></path>
|
||||
<path fill="inherit" d="M22.667 12h2.667v1.333h-2.667v-1.333z"></path>
|
||||
<path fill="inherit" d="M10.667 14.667h2.667v1.333h-2.667v-1.333z"></path>
|
||||
<path fill="inherit" d="M14.667 14.667h2.667v1.333h-2.667v-1.333z"></path>
|
||||
<path fill="inherit" d="M18.667 14.667h2.667v1.333h-2.667v-1.333z"></path>
|
||||
<path fill="inherit" d="M22.667 14.667h2.667v1.333h-2.667v-1.333z"></path>
|
||||
<path fill="inherit" d="M6.667 14.667h2.667v1.333h-2.667v-1.333z"></path>
|
||||
<path fill="inherit" d="M10.667 17.333h2.667v1.333h-2.667v-1.333z"></path>
|
||||
<path fill="inherit" d="M14.667 17.333h2.667v1.333h-2.667v-1.333z"></path>
|
||||
<path fill="inherit" d="M18.667 17.333h2.667v1.333h-2.667v-1.333z"></path>
|
||||
<path fill="inherit" d="M22.667 17.333h2.667v1.333h-2.667v-1.333z"></path>
|
||||
<path fill="inherit" d="M6.667 17.333h2.667v1.333h-2.667v-1.333z"></path>
|
||||
<path fill="inherit" d="M10.667 20h2.667v1.333h-2.667v-1.333z"></path>
|
||||
<path fill="inherit" d="M14.667 20h2.667v1.333h-2.667v-1.333z"></path>
|
||||
<path fill="inherit" d="M18.667 20h2.667v1.333h-2.667v-1.333z"></path>
|
||||
<path fill="inherit" d="M22.667 20h2.667v1.333h-2.667v-1.333z"></path>
|
||||
<path fill="inherit" d="M6.667 20h2.667v1.333h-2.667v-1.333z"></path>
|
||||
<path fill="inherit" d="M10.667 22.667h2.667v1.333h-2.667v-1.333z"></path>
|
||||
<path fill="inherit" d="M14.667 22.667h2.667v1.333h-2.667v-1.333z"></path>
|
||||
<path fill="inherit" d="M18.667 22.667h2.667v1.333h-2.667v-1.333z"></path>
|
||||
<path fill="inherit" d="M22.667 22.667h2.667v1.333h-2.667v-1.333z"></path>
|
||||
<path fill="inherit" d="M10.667 25.333h2.667v1.333h-2.667v-1.333z"></path>
|
||||
<path fill="inherit" d="M14.667 25.333h2.667v1.333h-2.667v-1.333z"></path>
|
||||
<path fill="inherit" d="M18.667 25.333h2.667v1.333h-2.667v-1.333z"></path>
|
||||
<path fill="inherit" d="M22.667 25.333h2.667v1.333h-2.667v-1.333z"></path>
|
||||
</svg>
|
||||
</template>
|
||||
70
src/icons/tmdb-logo.vue
Normal file
@@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<svg
|
||||
version="1.1"
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 225 199"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
style="outline-offset: -5px"
|
||||
>
|
||||
<title>TMDB Logo</title>
|
||||
|
||||
<path
|
||||
d="M202.545 185.929C215.58 185.929 224.402 177.107 224.402 164.071V21.8571C224.402 8.82143 215.58 0 202.545 0H21.8571C8.82143 0 0 8.82143 0 21.8571V198.937L11.2143 185.937V21.8571C11.2241 15.9833 15.9833 11.2241 21.8571 11.2143H202.545C208.418 11.2241 213.178 15.9833 213.188 21.8571V164.071C213.178 169.945 208.418 174.704 202.545 174.714H38.4911L27.2768 185.929L27.2054 185.839"
|
||||
fill="#01D277"
|
||||
/>
|
||||
<path
|
||||
d="M42.1677 67.911H44.0133C44.9484 67.911 45.699 67.7137 46.265 67.3192C46.8556 66.9247 47.1509 66.2589 47.1509 65.3219C47.1509 64.3849 46.8556 63.7192 46.265 63.3247C45.699 62.9301 44.9484 62.7329 44.0133 62.7329H42.1677V67.911ZM44.0133 57.5548C45.3914 57.5548 46.6218 57.7644 47.7046 58.1836C48.8119 58.5781 49.747 59.1329 50.5099 59.8479C51.2727 60.5384 51.851 61.3644 52.2448 62.326C52.6631 63.263 52.8723 64.2616 52.8723 65.3219C52.8723 66.8014 52.5278 68.0466 51.8387 69.0575C51.1743 70.0685 50.2884 70.8575 49.181 71.4247L58.04 83.2603L58.1686 83.4452H51.9025L51.7649 83.2603L44.0133 73.089H42.1677V83.4452H41.9832H37V83.2603V57.7397L37.1846 57.5548H44.0133Z"
|
||||
fill="#01D277"
|
||||
/>
|
||||
<path
|
||||
d="M76.6677 57.5548H76.8523V62.7329H76.6677H64.3021V67.3562H73.3456H73.5302V72.5342H73.3456H64.3021V78.0857H77.0368H77.2214V83.4452H77.0368H59.1344V83.2603L59.0419 57.5548H59.3189H76.6677Z"
|
||||
fill="#01D277"
|
||||
/>
|
||||
<path
|
||||
d="M94.7735 72.3493L96.8775 74.1986C97.4435 73.1137 97.7264 71.8808 97.7264 70.5C97.7264 69.3411 97.5173 68.2685 97.0989 67.2822C96.7052 66.2959 96.1392 65.4452 95.401 64.7301C94.6873 63.9904 93.8507 63.411 92.8909 62.9918C91.9312 62.5726 90.8977 62.363 89.7903 62.363C88.6829 62.363 87.6494 62.5726 86.6897 62.9918C85.73 63.411 84.881 63.9904 84.1427 64.7301C83.4291 65.4452 82.8631 66.2959 82.4448 67.2822C82.051 68.2685 81.8542 69.3411 81.8542 70.5C81.8542 71.6589 82.051 72.7315 82.4448 73.7178C82.8631 74.7041 83.4291 75.5671 84.1427 76.3068C84.881 77.0219 85.73 77.589 86.6897 78.0082C87.6494 78.4274 88.6829 78.637 89.7903 78.637C90.9961 78.637 92.1281 78.3904 93.1862 77.8973L91.2668 76.2329V75.863L94.4043 72.3493H94.7735ZM103.079 79.9315L99.9412 83.4452H99.5721L97.505 81.6699C96.3976 82.4096 95.1918 82.989 93.8876 83.4082C92.6079 83.8027 91.2422 84 89.7903 84C87.8955 84 86.1114 83.6548 84.438 82.9644C82.7893 82.274 81.3497 81.3247 80.1193 80.1164C78.8889 78.8836 77.9169 77.4534 77.2032 75.826C76.4896 74.174 76.1328 72.3986 76.1328 70.5C76.1328 68.6014 76.4896 66.8384 77.2032 65.211C77.9169 63.5589 78.8889 62.1288 80.1193 60.9205C81.3497 59.6877 82.7893 58.726 84.438 58.0356C86.1114 57.3452 87.8955 57 89.7903 57C91.6851 57 93.4569 57.3452 95.1057 58.0356C96.779 58.726 98.2309 59.6877 99.4613 60.9205C100.692 62.1288 101.664 63.5589 102.377 65.211C103.091 66.8384 103.448 68.6014 103.448 70.5C103.448 71.9055 103.251 73.237 102.857 74.4945C102.464 75.7274 101.91 76.8863 101.196 77.9712L103.079 79.5616V79.9315Z"
|
||||
fill="#01D277"
|
||||
/>
|
||||
<path
|
||||
d="M110.657 57.5548H110.842V71.2397C110.842 72.4973 111.002 73.5945 111.322 74.5315C111.642 75.4438 112.085 76.2082 112.651 76.8247C113.217 77.4164 113.881 77.8726 114.644 78.1932C115.407 78.489 116.231 78.637 117.117 78.637C118.003 78.637 118.827 78.489 119.59 78.1932C120.353 77.8726 121.017 77.4164 121.583 76.8247C122.149 76.2082 122.592 75.4438 122.912 74.5315C123.232 73.5945 123.392 72.4973 123.392 71.2397V57.5548H123.577H128.375H128.56V71.9795C128.56 73.8781 128.252 75.5795 127.637 77.0836C127.046 78.563 126.234 79.8205 125.201 80.8562C124.167 81.8671 122.949 82.6438 121.546 83.1863C120.168 83.7288 118.692 84 117.117 84C115.542 84 114.053 83.7288 112.651 83.1863C111.272 82.6438 110.067 81.8671 109.033 80.8562C108 79.8205 107.175 78.563 106.56 77.0836C105.969 75.5795 105.674 73.8781 105.674 71.9795V57.5548H105.859H110.657Z"
|
||||
fill="#01D277"
|
||||
/>
|
||||
<path
|
||||
d="M149.055 57.5548H149.239V62.7329H149.055H136.689V67.3562H145.733H145.917V72.5342H145.733H136.689V78.0236H149.424H149.608V83.4452H149.424H131.521V83.2603V57.5548H131.706H149.055Z"
|
||||
fill="#01D277"
|
||||
/>
|
||||
<path
|
||||
d="M161.497 64.0274C161.079 63.4603 160.611 63.0411 160.094 62.7699C159.602 62.4986 159.122 62.363 158.655 62.363C158.015 62.363 157.547 62.5356 157.252 62.8808C156.957 63.2014 156.809 63.6452 156.809 64.2123C156.809 64.7055 157.08 65.137 157.621 65.5068C158.163 65.8767 158.827 66.2466 159.614 66.6164C160.427 66.9863 161.3 67.4055 162.235 67.874C163.195 68.3178 164.069 68.8849 164.856 69.5753C165.668 70.2658 166.345 71.1041 166.886 72.0904C167.428 73.0767 167.698 74.2726 167.698 75.6781C167.698 76.7877 167.477 77.8479 167.034 78.8589C166.615 79.8699 166.013 80.7575 165.225 81.5219C164.462 82.2616 163.552 82.8658 162.494 83.3342C161.435 83.7781 160.279 84 159.024 84C158.064 84 157.129 83.8767 156.219 83.6301C155.308 83.3836 154.471 83.0137 153.709 82.5205C152.946 82.0274 152.269 81.4356 151.678 80.7452C151.112 80.0301 150.669 79.2041 150.349 78.2671L154.594 75.4192H154.964C155.431 76.6521 156.034 77.5027 156.772 77.9712C157.535 78.4151 158.286 78.637 159.024 78.637C159.959 78.637 160.71 78.3781 161.276 77.8603C161.866 77.3425 162.161 76.6151 162.161 75.6781C162.161 74.9384 161.891 74.3096 161.349 73.7918C160.808 73.274 160.131 72.7932 159.319 72.3493C158.532 71.8808 157.658 71.4247 156.698 70.9808C155.763 70.537 154.89 70.0192 154.078 69.4274C153.29 68.811 152.626 68.0959 152.084 67.2822C151.543 66.4438 151.272 65.4205 151.272 64.2123C151.272 63.1521 151.457 62.1781 151.826 61.2904C152.22 60.4027 152.749 59.6507 153.413 59.0342C154.078 58.3932 154.853 57.9 155.739 57.5548C156.649 57.1849 157.621 57 158.655 57C160.23 57 161.571 57.3329 162.678 57.9986C163.786 58.6644 164.844 59.6877 165.853 61.0685L161.866 64.0274H161.497Z"
|
||||
fill="#01D277"
|
||||
/>
|
||||
<path
|
||||
d="M187.815 57.5548H188V62.5479V62.7329H180.248V83.2603V83.4452H175.081V83.2603V62.7329H167.329V62.5479V57.5548H167.514H187.815Z"
|
||||
fill="#01D277"
|
||||
/>
|
||||
<path
|
||||
d="M61.9051 108.406L49.9478 94.7412H48V126.338H54.149V108.97L61.9051 117.327L69.6613 108.97L69.6263 126.338H75.7666V94.7412H73.8625L61.9051 108.406Z"
|
||||
fill="#01D277"
|
||||
/>
|
||||
<path
|
||||
d="M96.831 94.7412C76.4049 94.7412 76.4049 126.338 96.831 126.338C117.257 126.338 117.257 94.7412 96.831 94.7412ZM96.831 119.995C84.9585 119.995 84.9585 101.048 96.831 101.048C108.703 101.048 108.703 119.995 96.831 119.995Z"
|
||||
fill="#01D277"
|
||||
/>
|
||||
<path
|
||||
d="M158.109 95.6987H152.364V126.338H158.109V95.6987Z"
|
||||
fill="#01D277"
|
||||
/>
|
||||
<path
|
||||
d="M169.222 120.21V114.082H179.357V107.954H169.222V101.827H180.551V95.6987H162.896V126.338H181.088V120.21H169.222Z"
|
||||
fill="#01D277"
|
||||
/>
|
||||
<path
|
||||
d="M132.262 112.402L124.095 95.6987H116.938L131.565 127.295H132.959L147.577 95.6987H140.42L132.262 112.402Z"
|
||||
fill="#01D277"
|
||||
/>
|
||||
<path
|
||||
d="M42.425 121.5H42.6V126.4H42.425H37.7V126.225V121.5H37.875H42.425Z"
|
||||
fill="#01D277"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||