Compare commits

...

28 Commits

Author SHA1 Message Date
1bed14c8c3 Removed tmdb type from request 2018-08-13 00:32:38 +02:00
161c6db991 When adding a magnet, additional information name and tmdb id is also sent in request body. 2018-08-13 00:20:30 +02:00
b37d2cac8c Fixed merge. Commented out history mode. 2018-08-12 23:24:36 +02:00
5f827787a2 Changed base to root directory and added temperoray redirect for old url. 2018-08-12 23:23:14 +02:00
5ff25889e4 Stricter href to /# path 2018-08-12 23:22:16 +02:00
64fd1b338d Added port varialbe 2018-08-12 23:21:23 +02:00
72bd648dae Added script type to imporing build script. 2018-08-12 23:20:48 +02:00
2f0ab80ed5 Added vue-js-modal to package. 2018-07-28 18:45:04 +02:00
12b207f342 Updated router base directory. 2018-07-28 18:43:22 +02:00
a0021f3c39 Merge branch 'master' of github.com:KevinMidboe/seasonedRequest 2018-07-28 18:40:22 +02:00
105b56d2e6 Webpack dev server now gets a static port to run on. 2018-07-28 18:37:23 +02:00
a9439fdb29 Demo-modal for settings 2018-07-25 19:00:54 +02:00
c62a82a32d lock file 2018-07-25 18:50:07 +02:00
6d42769f54 Added settings page for users. 2018-07-25 18:49:54 +02:00
c8a80d6b05 Added filter toggle for requested. 2018-07-25 18:46:08 +02:00
86d70f3c69 Removed unused urls and removed rating function. 2018-07-25 18:41:48 +02:00
3888af6051 Added vue package vue-js-modal 2018-07-25 18:18:13 +02:00
7e339cb007 Added ISSUE_TEMPLATE for issue. 2018-07-25 17:54:24 +02:00
8c38f82dbe Added filtering of requested movies. Also change the order of movies in sidebar and added support for rating of movie. Change path destinations to be more clear. 2018-07-25 17:53:01 +02:00
4cbfd0a788 Create ISSUE_TEMPLATE.md 2018-03-22 01:18:22 +01:00
aa9bfa12d9 Re-wrote to support my api for requesting new movies. 2018-03-20 21:50:39 +01:00
1c45c1ea95 Added dist folder to gitignore 2018-03-20 21:47:44 +01:00
301c7accdb Added the following svg icons; requests, nowplaying, inmatched, sent, matched, info, does not exsist and exsists 2018-01-20 11:00:20 +01:00
Dmytro Barylo
17f06cffa7 Readme update 2017-10-03 00:25:12 +03:00
Dmytro Barylo
22f69c2e6a Node server configured 2017-10-03 00:09:36 +03:00
Dmytro Barylo
c5cf0bd61a Movie route fix 2017-10-02 23:30:28 +03:00
Dmytro Barylo
79ec76afc7 Update README.md 2017-06-29 15:58:52 +03:00
Dmytro Barylo
270260127f README and LICENSE update 2017-03-21 17:12:39 +02:00
40 changed files with 15615 additions and 195 deletions

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
.DS_Store
node_modules/
npm-debug.log
dist/

19
ISSUE_TEMPLATE.md Normal file
View File

@@ -0,0 +1,19 @@
## What kind of an issue is this?
- [ ] Bug report
- [ ] Feature request
## Expected behaviour?
## Current behaviour?
*if this is a bug report*
## Steps to reproduce behaviour?
*if this is a bug report*
## Screenshot? 📷
*A image tells a thousands words*

22
LICENSE Normal file
View File

@@ -0,0 +1,22 @@
Copyright (c) 2017 Dmytro Barylo
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -1,8 +1,12 @@
# The Movie Database App
A Vue.js project. [Live Demo Here] (http://tmdb.dmytrobarylo.com/)
A Vue.js project.
![](http://tmdb.dmytrobarylo.com/demo.gif)
![](https://github.com/dmtrbrl/tmdb-app/blob/master/docs/demo.gif)
## Demo
[TMDB Vue App](https://tmdb-vue-app.herokuapp.com/)
## Build Setup
@@ -18,4 +22,7 @@ npm run build
```
For detailed explanation on how things work, consult the [docs for vue-loader](http://vuejs.github.io/vue-loader).
This app uses [history mode] (https://router.vuejs.org/en/essentials/history-mode.html)
This app uses [history mode](https://router.vuejs.org/en/essentials/history-mode.html)
## License
[MIT](https://github.com/dmtrbrl/tmdb-app/blob/master/LICENSE)

18
dist/build.js vendored

File diff suppressed because one or more lines are too long

1
dist/build.js.map vendored

File diff suppressed because one or more lines are too long

BIN
dist/no-image.png vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

BIN
dist/placeholder.png vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

BIN
dist/pulp-fiction.jpg vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 279 KiB

BIN
docs/demo.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

File diff suppressed because one or more lines are too long

6999
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,19 +1,25 @@
{
"name": "tmdb-app",
"description": "The Movie Database app ",
"version": "1.0.0",
"version": "1.1.0",
"author": "Dmytro Barylo",
"private": true,
"scripts": {
"dev": "cross-env NODE_ENV=development webpack-dev-server --open --hot",
"build": "cross-env NODE_ENV=production webpack --progress --hide-modules"
"dev": "cross-env NODE_ENV=development webpack-dev-server --hot --port=5000",
"build": "cross-env NODE_ENV=production webpack --progress --hide-modules",
"start": "node server.js"
},
"dependencies": {
"ag-grid-vue": "^17.0.0",
"axios": "^0.15.3",
"connect-history-api-fallback": "^1.3.0",
"debounce": "^1.0.0",
"express": "^4.16.1",
"numeral": "^2.0.4",
"vue": "^2.1.0",
"vue-axios": "^1.2.2",
"vue-data-tablee": "^0.12.1",
"vue-js-modal": "^1.3.16",
"vue-router": "^2.2.1"
},
"devDependencies": {

21
server.js Normal file
View File

@@ -0,0 +1,21 @@
var express = require('express');
var path = require('path');
var history = require('connect-history-api-fallback');
app = express();
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.listen(port);

View File

@@ -3,19 +3,20 @@
<navigation></navigation>
<header class="header">
<div class="header__search">
<input class="header__search-input" type="text" v-model.trim="searchQuery" @keyup.enter="search" @blur="search" placeholder="Search for a movie...">
<input class="header__search-input" type="text" v-model.trim="searchQuery" @keyup.enter="search" placeholder="Search for a movie or show...">
<svg class="header__search-icon">
<use xlink:href="#iconSearch"></use>
</svg>
</div>
</header>
<movie-popup v-if="moviePopupIsVisible" @close="closeMoviePopup" :id="moviePopupId"></movie-popup>
<movie-popup v-if="moviePopupIsVisible" @close="closeMoviePopup" :id="moviePopupId" :type="moviePopupType"></movie-popup>
<section class="main">
<transition name="fade" @after-leave="afterLeave">
<router-view name="list-router-view" :type="'page'" :mode="'collection'" :key="$route.params.category"></router-view>
<router-view name="search-router-view" :type="'page'" :mode="'search'" :key="$route.params.query"></router-view>
<router-view name="user-requests-router-view" :type="'page'" :mode="'user-requests'"></router-view>
<router-view name="page-router-view"></router-view>
</transition>
</section>
@@ -36,6 +37,7 @@ export default {
moviePopupIsVisible: false,
moviePopupHistoryVisible: false,
moviePopupId: 0,
moviePopupType: 'movie',
searchQuery: ''
}
},
@@ -58,17 +60,20 @@ export default {
}.bind(this));
},
setUserStatus(){
storage.sessionId = localStorage.getItem('session_id') || null;
storage.userId = localStorage.getItem('user_id') || null;
storage.token = localStorage.getItem('token') || null;
storage.username = localStorage.getItem('username') || null;
storage.admin = localStorage.getItem('admin') || null;
},
// Movie Popup Methods
openMoviePopup(id, newMoviePopup){
openMoviePopup(id, type, newMoviePopup){
console.log('app openMoviePopup:', type)
if(newMoviePopup){
storage.backTitle = document.title;
}
storage.createMoviePopup = newMoviePopup;
this.moviePopupIsVisible = true;
this.moviePopupId = id;
this.moviePopupType = type;
document.querySelector('body').classList.add('hidden');
},
closeMoviePopup(){
@@ -232,8 +237,8 @@ img{
}
}
&-icon{
width: 14px;
height: 14px;
width: 19px;
height: 19px;
fill: rgba($c-dark, 0.5);
transition: fill 0.5s ease;
pointer-events: none;
@@ -288,7 +293,16 @@ img{
font-size: 12px;
padding: 6px 20px 5px 20px;
}
body:not(.touch) &:hover{
&:active, &:hover{
background: $c-dark;
color: $c-white;
}
body:not(.touch) &:hover, &:focus{
background: $c-dark;
color: $c-white;
}
&__active {
@extend .button;
background: $c-dark;
color: $c-white;
}

BIN
src/assets/arrival.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 279 KiB

After

Width:  |  Height:  |  Size: 275 KiB

BIN
src/assets/star-wars.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 423 KiB

View File

@@ -39,12 +39,19 @@ export default {
height: 100%;
background: rgba($c-light, 0.7);
}
&-shortList{
width: 100%;
}
&__content{
width: 100%;
padding: 0 20px;
text-align: center;
@include tablet-min{
padding: 0 40px 40px 0;
padding: 20px 0 0 0;
}
&-shortList {
width: 100%;
}
}
&__title{

View File

@@ -1,18 +1,12 @@
<template>
<section class="home">
<header class="home__header">
<header class="home__header" v-bind:style="{ 'background-image': 'url(' + imageFile + ')' }">
<div class="home__header-wrap">
<h1 class="home__header-title">The Movie DB App</h1>
<h1 class="home__header-title">Request new movies or tv shows for plex</h1>
<strong class="home__header-subtitle">Made with Vue.js</strong>
<a href="https://github.com/dmtrbrl/tmdb-app" target="_blank" class="home__header-link">
<svg class="home__header-link-icon">
<use xlink:href="#iconGithub"></use>
</svg>
<span>View Code</span>
</a>
</div>
</header>
<movies-list v-for="item in listTypes" v-if="item.isCategory" :type="'component'" :mode="'collection'" :category="item.query" :shortList="true"></movies-list>
<movies-list v-for="item in listTypes" v-if="item.isCategory" :type="'component'" :mode="item.type" :category="item.query" :shortList="true"></movies-list>
</section>
</template>
@@ -25,7 +19,8 @@ export default {
components: { MoviesList },
data(){
return {
listTypes: storage.listTypes
listTypes: storage.listTypes,
imageFile: 'dist/pulp-fiction.jpg'
}
},
created(){
@@ -50,9 +45,9 @@ export default {
background-position: 50% 50%;
position: relative;
background-color: $c-dark;
background-image: url('~assets/pulp-fiction.jpg');
background-image: url('~assets/arrival.jpg');
@include tablet-min{
height: 384px;
height: 284px;
}
&:before{
content: "";

View File

@@ -10,44 +10,97 @@
<div class="movie__title">
<h1 class="movie__title-text">
{{ movie.title }}
<span v-if="movie.tagline">{{ movie.tagline }}</span>
<!-- <span>{{ movie.type }}</span> -->
</h1>
<span>
</span>
</div>
</div>
</header>
<div class="movie__main">
<div class="movie__wrap movie__wrap--main" :class="{'movie__wrap--page': type=='page'}">
<div class="movie__actions" v-if="userLoggedIn && favoriteChecked">
<a href="#" class="movie__actions-link" :class="{'active' : favorite === true}" @click.prevent="toggleFavorite">
<svg class="movie__actions-icon" :class="{'waiting' : favorite === ''}">
<use xlink:href="#iconFavorite"></use>
<!-- <div class="movie__ratings">
<p>here</p>
</div> -->
<!-- <div class="movie__actions" v-if="userLoggedIn && favoriteChecked"> -->
<div class="movie__actions">
<a class="movie__actions-link" v-if="matched" :class="{'active' : matched}">
<svg class="movie__actions-icon">
<use xlink:href="#iconExsits"></use>
</svg>
<span class="movie__actions-text" v-if="favorite === ''">Wait...</span>
<span class="movie__actions-text" v-else-if="favorite">Marked as Favorite</span>
<span class="movie__actions-text" v-else>Mark as Favorite?</span>
<span class="movie__actions-text"> Already in plex &nbsp;🎉</span>
</a>
<a class="movie__actions-link" v-else="matched">
<svg class="movie__actions-icon">
<use xlink:href="#iconNot_exsits"></use>
</svg>
<span class="movie__actions-text"> Not in plex yet</span>
</a>
<a class="movie__actions-link" :class="{'active' : requested}" v-if="this.requested">
<svg class="movie__actions-icon">
<use xlink:href="#iconSent"></use>
</svg>
<span class="movie__actions-text"> Requested to be downloaded</span>
</a>
<a class="movie__actions-link" v-else="this.requested" @click.prevent="sendRequest">
<svg class="movie__actions-icon" :class="{'waiting' : requested}">
<use xlink:href="#iconUnmatched"></use>
</svg>
<span class="movie__actions-text"> Request to be downloaded?</span>
</a>
<a class="movie__actions-link" @click="showTorrents=true" v-if="admin==='true'" :class="{'active' : showTorrents}">
<svg class="movie__actions-icon">
<use xlink:href="#icon_torrents"></use>
</svg>
<span class="movie__actions-text"> Search for torrents</span>
</a>
<a class="movie__actions-link" @click.prevent="openTmdb">
<svg class="movie__actions-icon">
<use xlink:href="#icon_info"></use>
</svg>
<span class="movie__actions-text"> See more info</span>
</a>
</div>
<div class="movie__info">
<div v-if="movie.overview" class="movie__description">
{{ movie.overview }}
<div v-if="movie.summary" class="movie__description">
{{ movie.summary }}
</div>
<div class="movie__details">
<div v-if="movie.genres.length" class="movie__details-block">
<!-- <div v-if="movie.genres.length" class="movie__details-block">
<h2 class="movie__details-title">
Genres
</h2>
<div class="movie__details-text">
{{ nestedDataToString(movie.genres) }}
</div>
</div> -->
<div v-if="movie.year" class="movie__details-block">
<h2 class="movie__details-title">Release Date</h2>
<div class="movie__details-text">{{ movie.year }}</div>
</div>
<div v-if="movie.release_date" class="movie__details-block">
<h2 class="movie__details-title">
Release Date
</h2>
<div class="movie__details-text" v-formatDate="movie.release_date"></div>
<!-- <div v-if="movie.score" class="movie__details-block">
<h2 class="movie__details-title">Rating</h2>
<div class="movie__details-text">{{ rating }}</div>
</div> -->
<div v-if="movie.type == 'show'" class="movie__details-block">
<h2 class="movie__details-title">Seasons</h2>
<div class="movie__details-text">{{ movie.seasons }}</div>
</div>
</div>
</div>
<!-- <TableDemo class="movie__admin">This is it</TableDemo> -->
<div class="movie__admin" v-if="admin == 'true' && showTorrents">
<h2 class="movie__admin-title">torrents: {{ movie.title }}</h2>
<TorrentList :query="movie.title" :tmdb_id="movie.id"></TorrentList>
</div>
</div>
</div>
</div>
@@ -59,9 +112,11 @@ import axios from 'axios'
import storage from '../storage.js'
import img from '../directives/v-image.js'
import formatDate from '../directives/v-formatDate.js'
import TorrentList from './TorrentList.vue'
export default {
props: ['id', 'type'],
props: ['id', 'type', 'mediaType'],
components: { TorrentList },
directives: {
img: img,
formatDate: formatDate
@@ -74,30 +129,37 @@ export default {
movieBackdropSrc: '',
userLoggedIn: storage.sessionId ? true : false,
favoriteChecked: false,
favorite: ''
requested: false,
admin: localStorage.getItem('admin'),
showTorrents: false
}
},
computed: {
loaded(){
return this.movieLoaded ? true : false;
}
},
// computed: {
// loaded(){
// return this.movieLoaded ? true : false;
// }
// },
methods: {
fetchMovie(id){
axios.get(`https://api.themoviedb.org/3/movie/${id}?api_key=${storage.apiKey}&language=en-US`)
this.id = id;
(this.mediaType == 'show') ? this.tmdbType = 'show' : this.tmdbType = 'movie'
axios.get(`https://api.kevinmidboe.com/api/v1/plex/request/${id}?type=${this.mediaType}`)
.then(function(resp){
let movie = resp.data;
this.movie = movie;
this.poster();
this.backdrop();
this.matched = this.movie.matchedInPlex;
this.requested = this.movie.requested;
if(this.userLoggedIn){
this.checkIfInFavorites(movie.id);
this.movieLoaded = true;
} else {
this.movieLoaded = true;
}
// Push state
if(storage.createMoviePopup){
storage.moviePath = '/movie/' + id;
storage.moviePath = this.mediaType + '/' + id;
history.pushState({ popup: true }, null, storage.moviePath);
storage.createMoviePopup = false;
}
@@ -105,17 +167,19 @@ export default {
document.title = this.movie.title + storage.pageTitlePostfix;
}.bind(this))
.catch(function(error) {
console.log(error.response)
this.$router.push({ name: '404' });
}.bind(this));
},
poster() {
// Change the poster resolution
if(this.movie.poster_path){
this.moviePosterSrc = 'https://image.tmdb.org/t/p/w600_and_h900_bestv2' + this.movie.poster_path;
this.moviePosterSrc = 'https://image.tmdb.org/t/p/w300' + this.movie.poster_path;
}
},
backdrop(){
if(this.movie.backdrop_path){
this.movieBackdropSrc = 'https://image.tmdb.org/t/p/w500' + this.movie.backdrop_path;
if(this.movie.background_path){
this.movieBackdropSrc = 'https://image.tmdb.org/t/p/w500' + this.movie.background_path;
}
},
nestedDataToString(data) {
@@ -125,13 +189,15 @@ export default {
return resultString;
},
checkIfInFavorites(id){
axios.get(`https://api.themoviedb.org/3/movie/${id}/account_states?api_key=${storage.apiKey}&session_id=${storage.sessionId}`)
// Change to check in plex
axios.get(`https://api.themoviedb.org/3/${this.tmdbType}/${id}/account_states?api_key=${storage.apiKey}&session_id=${storage.sessionId}`)
.then(function(resp){
this.favorite = resp.data.favorite;
this.favoriteChecked = true;
this.movieLoaded = true;
}.bind(this))
},
// Toggle the downloading status if admin
toggleFavorite(){
let favoriteInvert = !this.favorite;
this.favorite = '';
@@ -144,7 +210,35 @@ export default {
this.favorite = favoriteInvert;
eventHub.$emit('updateFavorite');
}.bind(this));
}
},
// Send a request for a specific movie
sendRequest(){
this.requested = ''
axios({
method: 'post', //you can set what request you want to be
url: `https://api.kevinmidboe.com/api/v1/plex/request/${this.id}?type=${this.mediaType}`,
headers: {
authorization: storage.token
}
})
// axios.post(`https://api.kevinmidboe.com/api/v1/plex/request/${this.id}?type=${this.mediaType}`, {}, {
// authorization: storage.token
// })
// axios.post(`https://api.kevinmidboe.com/api/v1/plex/request/${this.id}?api_key=${storage.apiKey}&session_id=${storage.sessionId}`, {
.then(function(resp){
if (resp.data.success)
this.requested = true;
else
this.requested = false;
}.bind(this));
},
openTmdb(){
window.location.replace('https://www.themoviedb.org/' + this.tmdbType + '/' + this.id)
},
// Search torrents by query
// searchForTorrents() {
// axios.get(`https://apollo.kevinmidboe.com/api/v1/plex/request/${id}?type=${'movie'}&api_key=${storage.apiKey}&language=en-US`)
// },
},
watch: {
id: function(val){
@@ -153,6 +247,7 @@ export default {
},
created(){
this.fetchMovie(this.id);
console.log('admin: ', this.admin)
}
}
</script>
@@ -292,19 +387,26 @@ export default {
&.active{
color: $c-dark;
}
&.pending{
color: #f8bd2d;
}
}
&-icon{
width: 16px;
height: 16px;
width: 18px;
height: 18px;
margin: 0 10px 0 0;
fill: rgba($c-dark, 0.5);
transition: fill 0.5s ease, transform 0.5s ease;
&.waiting{
transform: scale(0.8, 0.8);
}
&.pending{
fill: #f8bd2d;
}
}
&-link:hover &-icon{
fill: rgba($c-dark, 0.75);
cursor: pointer;
}
&-link.active &-icon{
fill: $c-green;
@@ -312,6 +414,7 @@ export default {
&-text{
display: block;
padding-top: 2px;
cursor: pointer;
}
}
&__info{
@@ -339,10 +442,15 @@ export default {
}
}
&__details{
&-block{
float: left;
}
&-block:not(:last-child){
margin-bottom: 20px;
margin-right: 20px;
@include tablet-min{
margin-bottom: 30px;
margin-right: 30px;
}
}
&-title{
@@ -361,5 +469,28 @@ export default {
margin-top: 5px;
}
}
&__admin{
width: 100%;
padding: 20px;
order: 2;
@include tablet-min{
order: 3;
padding: 40px;
padding-top: 0px;
width: 100%;
}
&-title{
margin: 0;
font-weight: 400;
text-transform: uppercase;
text-align: center;
font-size: 14px;
color: $c-green;
padding-bottom: 20px;
@include tablet-min{
font-size: 16px;
}
}
}
}
</style>

View File

@@ -1,7 +1,7 @@
<template>
<div class="movie-popup" @click="$emit('close')">
<div class="movie-popup__box" @click.stop>
<movie :id="id"></movie>
<movie :id="id" :mediaType="type"></movie>
<button class="movie-popup__close" @click="$emit('close')"></button>
</div>
<i class="loader"></i>
@@ -12,7 +12,7 @@
import Movie from './Movie.vue';
export default {
props: ['id'],
props: ['id', 'type'],
components: { Movie },
created(){
window.addEventListener('keyup', function(e){

View File

@@ -4,19 +4,38 @@
<header class="movies__header">
<h2 class="movies__title">{{ listTitle }}</h2>
<span class="movies__results" v-if="!shortList">{{ countResults }}</span>
<router-link v-if="shortList" class="movies__link" :to="{name: 'home-category', params: {category: category}}">
<router-link v-if="shortList && mode != 'user-requests'" class="movies__link" :to="{name: 'home-category', params: {category: category}}">
View All
</router-link>
<router-link v-if="shortList && mode == 'user-requests'" class="movies__link" :to="{name: 'user-requests'}">
View All
</router-link>
<span v-if="!shortList && (this.$route.params.category === 'requests' || mode == 'user-requests')" class="movies__filters">
<button type="button" class="button" @click="toggleFilter">Filter</button>
<span class="movies__filters__button-spacing"></span>
<!-- <button type="button" class="button" @click="sort">Sort</button> -->
<span class="movies__filters__button-spacing"></span>
<div class="form__group">
<input v-model="filter_query" class="form__group-input" placeholder="Filter by search"/>
</div>
</span>
</header>
<ul v-if="showFilter" class="movies__filters-list">
<li v-for="(item, index) in filters.status.elms" @click="applyFilter(item, index)" :class="{'active': index === filters.status.selected}">{{ item }}</li>
</ul>
<ul class="movies__list">
<movies-list-item class="movies__item" v-for="(movie, index) in movies" :movie="movie"></movies-list-item>
</ul>
<div class="movies__nav" v-if="!shortList" :class="{'is-hidden' : currentPage == pages}">
<button @click="loadMore" class="button">Load More</button>
<button @click="loadMore" class="button">Load Mores</button>
</div>
</div>
<i v-if="!listLoaded" class="loader"></i>
<section v-if="!movies.length" class="not-found">
<section v-if="!movies.length && !shortList" class="not-found">
<div class="not-found__content">
<h2 class="not-found__title" v-if="mode == 'search'">Nothing Found</h2>
<h2 class="not-found__title" v-if="mode == 'favorite'">You haven't added any favorite movies</h2>
@@ -37,20 +56,30 @@ let removed;
export default {
props: ['type', 'mode', 'category', 'shortList'],
components: { MoviesListItem },
beforeRouteLeave (to, from, next) {
if(from.name == 'search'){
eventHub.$emit('setSearchQuery', true);
}
next();
},
// beforeRouteLeave (to, from, next) {
// if(from.name == 'search'){
// eventHub.$emit('setSearchQuery', true);
// }
// next();
// },
data() {
return {
listTitle: '',
movies: [],
unfiltered_movies: [],
pages: '',
filter: '',
filter_query: '',
results: '',
currentPage: 1,
listLoaded: false
listLoaded: false,
showFilter: false,
filters: {
status: {
elms: ['all', 'requested', 'downloading', 'downloaded'],
selected: 0,
}
}
}
},
computed: {
@@ -61,13 +90,18 @@ export default {
return this.$route.params.query || '';
},
request(){
console.log('todays mode is: ', this.mode);
if(this.mode == 'search'){
return `https://api.themoviedb.org/3/search/movie?api_key=${storage.apiKey}&language=en-US&query=${this.query}&page=${this.currentPage}`;
return `https://api.kevinmidboe.com/api/v1/plex/request?query=${this.query}&page=${this.currentPage}`;
} else if(this.mode == 'requests' || this.$route.params.category == 'requests') {
return `https://api.kevinmidboe.com/api/v1/plex/requests/all?page=${this.currentPage}&status=${this.filter}`;
} else if(this.mode == 'collection') {
let caregory = this.$route.params.category || this.category;
return `https://api.themoviedb.org/3/movie/${caregory}?api_key=${storage.apiKey}&language=en-US&page=${this.currentPage}`;
} else if(this.mode == 'favorite') {
return `https://api.themoviedb.org/3/account/${storage.userId}/favorite/movies?api_key=${storage.apiKey}&session_id=${storage.sessionId}&language=en-US&sort_by=created_at.desc&page=${this.currentPage}`;
let category = this.$route.params.category || this.category;
return `https://api.kevinmidboe.com/api/v1/tmdb/list/${category}?page=${this.currentPage}`;
} else if(this.mode == 'history') {
return 'https://api.kevinmidboe.com/api/v1/user/history';
} else if(this.mode == 'user-requests') {
return 'https://api.kevinmidboe.com/api/v1/user/requests';
}
},
countResults(){
@@ -80,9 +114,13 @@ export default {
},
methods: {
fetchCategory(){
axios.get(this.request)
axios.get(this.request, {
headers: {authorization: storage.token},
})
.then(function(resp){
let data = resp.data;
console.log('data: ', data)
if(this.shortList){
this.movies = data.results.slice(0, 5);
this.pages = 1;
@@ -92,6 +130,7 @@ export default {
this.pages = data.total_pages;
this.results = data.total_results;
}
this.unfiltered_movies = this.movies;
this.listLoaded = true;
// Change Page title
if(this.type == 'page'){
@@ -111,32 +150,29 @@ export default {
this.movies = newData;
}.bind(this));
},
updateFavorite(){
if(this.mode == 'favorite'){
let promises = [], movies = [], pages, results;
for(let i = 1; i <= this.currentPage; i++){
promises.push(axios.get(`https://api.themoviedb.org/3/account/${storage.userId}/favorite/movies?api_key=${storage.apiKey}&session_id=${storage.sessionId}&language=en-US&sort_by=created_at.desc&page=${i}`))
}
axios.all(promises).then(function(results) {
results.forEach(function(resp) {
let data = resp.data;
movies = movies.concat(data.results);
pages = data.total_pages;
results = data.total_results;
});
this.movies = movies;
this.pages = pages;
if(this.currentPage > pages){
this.currentPage -= 1;
}
this.results = results;
}.bind(this));
}
// sort() {
// console.log(this.showFilters)
// },
toggleFilter(item, index){
this.showFilter = this.showFilter ? false : true;
// this.results = this.results.filter(result => result.status != 'downloaded')
},
applyFilter(item, index) {
this.filter = item;
this.filters.status.selected = index;
console.log('applied query filter: ', item, index)
this.fetchCategory()
}
},
watch: {
query(value){
this.fetchCategory(value);
filter_query: function(val, oldVal) {
let movies = this.unfiltered_movies;
val = val.toLowerCase()
if (val.length > 0)
movies = movies.filter(movie => movie.title.toLowerCase().startsWith(val))
if (movies.length > 0)
this.movies = movies;
}
},
created(){
@@ -144,18 +180,36 @@ export default {
if(this.mode == 'search'){
this.listTitle = storage.categories['search'];
eventHub.$emit('setSearchQuery');
} else if(this.mode == 'requests') {
this.listTitle = storage.categories['requests'];
} else if(this.mode == 'collection') {
let caregory = this.$route.params.category || this.category;
this.listTitle = storage.categories[caregory];
let category = this.$route.params.category || this.category;
this.listTitle = storage.categories[category]; // <-- this
} else if(this.mode == 'favorite') {
this.listTitle = storage.categories['favorite'];
} else if(this.mode == 'user-requests') {
this.listTitle = storage.categories['user-requests'];
}
this.fetchCategory();
eventHub.$on('updateFavorite', this.updateFavorite);
}
}
</script>
<style lang="scss" scoped>
@import "./src/scss/media-queries";
.form__group-input {
padding: 10px 5px 10px 15px;
margin-left: 0;
height: 38px;
width: 150px;
font-size: 15px;
@include desktop-min {
width: 200px;
font-size: 17px;
}
}
</style>
<style lang="scss">
@import "./src/scss/variables";
@import "./src/scss/media-queries";
@@ -172,9 +226,11 @@ export default {
}
&__header{
display: flex;
flex-flow: row wrap;
align-items: center;
justify-content: space-between;
padding: 20px 10px;
@include tablet-min{
padding: 23px 15px;
}
@@ -191,6 +247,8 @@ export default {
line-height: 16px;
color: $c-dark;
font-weight: 300;
flex-basis: 50%;
@include tablet-min{
font-size: 18px;
line-height: 18px;
@@ -201,6 +259,12 @@ export default {
font-weight: 300;
letter-spacing: 0.5px;
color: rgba($c-dark, 0.5);
text-align: right;
flex-basis: 50%;
@include mobile-only {
display: none;
}
}
&__link{
font-size: 12px;
@@ -216,6 +280,71 @@ export default {
color: $c-dark;
}
}
&__filters{
margin-top: 10px;
line-height: 22px;
color: $c-dark;
font-size: 18px;
display: flex;
justify-content: flex-end;
transition: opacity 1s ease;
&__button-spacing {
@include tablet-min {
width: 15px;
}
@include mobile-only {
width: 10px;
}
}
&-list {
margin: 0px 10px;
padding: 0;
list-style: none;
border: solid 1px;
border-radius: 2px;
overflow: hidden;
display: flex;
transition: color 0.2s ease;
justify-content: space-evenly;
@include tablet-min{
margin: 0px 15px;
}
@include tablet-landscape-min {
margin: 0px 25px;
}
@include desktop-min{
margin: 0px 30px;
}
li {
padding: 6px 14px;
background-color: $c-white;
transition: color 0.2s ease;
font-size: 13px;
font-weight: 200;
text-transform: capitalize;
text-align: center;
width: 100%;
&:nth-child(n+2) {
border-left: solid 1px;
}
&.active, &:hover {
border-color: transparent;
background-color: #091c24;
color: $c-white;
cursor: pointer;
}
@include tablet-min {
font-size: 16px;
}
}
}
&-toggle {
margin-left: 15px;
}
}
&__list{
padding: 0;
margin: 0;

View File

@@ -1,12 +1,13 @@
<template>
<li class="movies-item">
<a class="movies-item__link" :class="{'no-image': noImage}" :href="'/movie/' + movie.id" @click.prevent="openMoviePopup(movie.id, true)">
<a class="movies-item__link" :class="{'no-image': noImage}" href="#" @click.prevent="openMoviePopup(movie.id, movie.type, true)">
<figure class="movies-item__poster">
<img v-if="!noImage" class="movies-item__img" src="~assets/placeholder.png" v-img="poster()" alt="">
<img v-if="noImage" class="movies-item__img is-loaded" src="~assets/no-image.png" alt="">
</figure>
<div class="movies-item__content">
<p class="movies-item__title">{{ movie.title }}</p>
<p class="movies-item__title">{{ movie.year }}</p>
</div>
</a>
</li>
@@ -28,13 +29,14 @@ export default {
methods: {
poster() {
if(this.movie.poster_path){
return 'https://image.tmdb.org/t/p/w370_and_h556_bestv2' + this.movie.poster_path;
return 'https://image.tmdb.org/t/p/w300' + this.movie.poster_path;
} else {
this.noImage = true;
}
},
openMoviePopup(id, event){
eventHub.$emit('openMoviePopup', id, event);
openMoviePopup(id, type, event){
console.log('open:', id, type, event)
eventHub.$emit('openMoviePopup', id, type, event);
}
}
}

View File

@@ -12,7 +12,7 @@
</div>
<ul class="nav__list">
<li class="nav__item" v-for="item in listTypes" v-if="item.isCategory">
<router-link class="nav__link" :to="{name: 'home-category', params: {category: item.query}}">
<router-link class="nav__link" :to="{name: item.name, params: {mode: item.type, category: item.query}}">
<div class="nav__link-wrap">
<svg class="nav__link-icon">
<use :xlink:href="'#icon_' + item.query"></use>
@@ -22,14 +22,22 @@
</router-link>
</li>
<li class="nav__item nav__item--profile">
<div class="nav__link nav__link--profile" @click="requestToken" v-if="!userLoggedIn">
<!-- <div class="nav__link nav__link--profile" @click="requestToken" v-if="!userLoggedIn">
<div class="nav__link-wrap">
<svg class="nav__link-icon">
<use xlink:href="#iconLogin"></use>
</svg>
<span class="nav__link-title">Log In</span>
</div>
</div>
</div> -->
<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">
@@ -50,12 +58,12 @@ export default {
data(){
return {
listTypes: storage.listTypes,
userLoggedIn: storage.sessionId ? true : false
userLoggedIn: localStorage.getItem('token') ? true : false
}
},
methods: {
setUserStatus(){
this.userLoggedIn = storage.sessionId ? true : false;
this.userLoggedIn = localStorage.getItem('token') ? true : false;
},
requestToken(){
eventHub.$emit('requestToken');

View File

@@ -1,17 +1,29 @@
<template>
<section class="profile">
<div class="profile__content" v-if="userLoggedIn === true">
<div class="profile__content" v-if="userLoggedIn">
<header class="profile__header">
<h2 class="profile__title">Hello {{ userName }}</h2>
<button class="button" @click="logOut">Log Out</button>
<h2 class="profile__title">{{ emoji }} Welcome {{ userName }}</h2>
<div>
<router-link class="" :to="{name: 'settings'}">
</router-link>
<button v-if="showSettings" class="button__active" @click="toggleSettings" style="margin-right: 2em;">Hide settings</button>
<button v-else class="button" @click="toggleSettings" style="margin-right: 2em;">Show settings</button>
<button class="button" @click="logOut">Log Out</button>
</div>
</header>
<movies-list :type="'component'" :mode="'favorite'"></movies-list>
<settings v-if="showSettings"></settings>
<movies-list v-for="item in listTypes" v-if="!showSettings && item.isProfileContent" :type="'component'" :mode="item.type" :category="item.query" :shortList="true"></movies-list>
<!-- <movies-list v-for="item in listTypes" v-if="item.isCategory" :type="'component'" :mode="item.type" :shortList="true"></movies-list> -->
<!-- <created-lists></created-lists> -->
</div>
<section class="not-found" v-if="userLoggedIn === false">
<section class="not-found" v-if="!userLoggedIn">
<div class="not-found__content">
<h2 class="not-found__title">Authentication Request Failed</h2>
<button class="not-found__button button" @click="requestToken">Log In</button>
<router-link :to="{name: 'signin'}" exact title="Sign in here">
<button class="not-found__button button">Sign In</button>
</router-link>
</div>
</section>
</section>
@@ -21,32 +33,21 @@
import axios from 'axios'
import storage from '../storage.js'
import MoviesList from './MoviesList.vue'
import Settings from './Settings.vue'
// import CreatedLists from './CreatedLists.vue'
export default {
components: { MoviesList },
components: { MoviesList, Settings },
data(){
return{
userLoggedIn: '',
userName: ''
userName: '',
emoji: '',
showSettings: false,
listTypes: storage.listTypes
}
},
methods: {
requestPermission(){
let query = location.search.substring(1);
if(query.length){
let params = query.split('&');
let token = params[0].split('=')[1];
let status = params[1].split('=')[0];
if(status == 'approved'){
this.createSession(token);
} else {
this.userLoggedIn = false;
}
} else {
this.userLoggedIn = false;
}
},
createSession(token){
axios.get(`https://api.themoviedb.org/3/authentication/session/new?api_key=${storage.apiKey}&request_token=${token}`)
.then(function(resp){
@@ -60,20 +61,21 @@ export default {
}
}.bind(this));
},
getUserInfo(){
axios.get(`https://api.themoviedb.org/3/account?api_key=${storage.apiKey}&session_id=${storage.sessionId}`)
getNewEmoji(){
axios.get(`https://api.kevinmidboe.com/api/v1/emoji`)
.then(function(resp){
let data = resp.data;
this.userName = data.username;
if (!localStorage.getItem('user_id')) localStorage.setItem('user_id', data.id);
this.emoji = resp.data.emoji;
}.bind(this))
.catch(function (error) {
this.logOut();
}.bind(this));
},
getUserInfo(){
this.userName = localStorage.getItem('username');
},
requestToken(){
eventHub.$emit('requestToken');
},
toggleSettings() {
this.showSettings = this.showSettings ? false : true;
},
logOut(){
localStorage.clear();
eventHub.$emit('setUserStatus');
@@ -83,11 +85,12 @@ export default {
created(){
document.title = 'Profile' + storage.pageTitlePostfix;
storage.backTitle = document.title;
if(!storage.sessionId){
this.requestPermission();
if(!localStorage.getItem('token')){
this.userLoggedIn = false;
} else {
this.userLoggedIn = true;
this.getUserInfo();
this.getNewEmoji();
}
}
}
@@ -97,14 +100,6 @@ export default {
@import "./src/scss/variables";
@import "./src/scss/media-queries";
.profile{
&__content{
.wrapper{
min-height: calc(100vh - 175px);
@include tablet-min{
min-height: calc(100vh - 171px);
}
}
}
&__header{
display: flex;
align-items: center;

269
src/components/Register.vue Normal file
View File

@@ -0,0 +1,269 @@
<template>
<section class="profile">
<div class="profile__content">
<header class="profile__header">
<h2 class="profile__title">Register new user</h2>
</header>
<form class="form">
<div class="form__buffer"></div>
<div class="center">
<div class="form__group">
<svg class="form__group__input-icon">
<use xlink:href="#iconEmail"></use>
</svg>
<input class="form__group-input" type="username" ref="username" placeholder="Username" >
</div>
<div class="form__group">
<svg class="form__group__input-icon">
<use xlink:href="#iconKeyhole"></use>
</svg>
<input class="form__group-input" type="password" ref="password" placeholder="Password">
</div>
<div class="form__group">
<svg class="form__group__input-icon">
<use xlink:href="#iconKeyhole"></use>
</svg>
<input class="form__group-input" type="password" ref="password_re" placeholder="Repeat password">
</div>
<transition name="message-fade">
<div class="message" :class="messageClass" v-if="showMessage">
<span class="message-text">{{ messageText }}</span>
<span class="message-dismiss" v-on:click="dismissMessage">X</span>
</div>
</transition>
<div class="form__group">
<button type="button" class="button" v-on:click="requestNewUser">Register</button>
</div>
</div>
</form>
<div class="form__group">
<router-link class="form__group-link" :to="{name: 'signin'}" exact title="Sign in here">
<span class="form__group-signin">Sign in here</span>
</router-link>
</div>
<!-- <created-lists></created-lists> -->
</div>
<section class="not-found" v-if="userLoggedIn === false">
<div class="not-found__content">
<h2 class="not-found__title">Authentication Request Failed</h2>
<button class="not-found__button button" @click="requestToken">Log In</button>
</div>
</section>
</section>
</template>
<script>
import axios from 'axios'
import storage from '../storage.js'
import MoviesList from './MoviesList.vue'
// import CreatedLists from './CreatedLists.vue'
export default {
components: { MoviesList },
data(){
return{
userLoggedIn: '',
userName: '',
showMessage: false,
messageClass: 'message-success',
messageText: 'hello world'
}
},
methods: {
requestNewUser(){
let username = this.$refs.username.value;
let password = this.$refs.password.value;
let password_re = this.$refs.password_re.value;
let verifyCredentials = this.checkCredentials(username, password, password_re);
if (verifyCredentials.verified) {
axios.post(`https://api.kevinmidboe.com/api/v1/user`, {
username: username,
password: password
})
.then(function(resp) {
let data = resp.data;
if (data.success){
this.msg(data.message, 'success');
localStorage.setItem('token', data.token);
localStorage.setItem('username', username);
localStorage.setItem('admin', data.admin)
eventHub.$emit('setUserStatus');
this.$router.push({ name: 'profile' })
}
}.bind(this))
.catch(function(error){
this.msg(error.response.data.error, 'warning')
}.bind(this));
}
else {
this.msg(verifyCredentials.reason, 'warning');
}
},
checkCredentials(username, password, password_re) {
if (password !== password_re) {
return {
verified: false,
reason: 'Passwords do not match'
}
}
else if (username === undefined) {
return {
verified: false,
reason: 'Please insert username'
}
}
else {
return {
verified: true,
reason: 'Verified credentials'
}
}
},
msg(text, status){
if (status === 'warning')
this.messageClass = 'message-warning';
else if (status === 'success')
this.messageClass = 'message-success';
else
this.messageClass = 'message-info';
this.messageText = text;
this.showMessage = true;
// setTimeout(() => this.showMessage = false, 3500);
},
dismissMessage(){
this.showMessage = false;
},
logOut(){
localStorage.clear();
eventHub.$emit('setUserStatus');
this.$router.push({ name: 'home' });
}
},
created(){
document.title = 'Profile' + storage.pageTitlePostfix;
storage.backTitle = document.title;
},
mounted(){
// this.$refs.email.focus();
}
}
</script>
<style lang="scss">
@import "./src/scss/variables";
@import "./src/scss/media-queries";
.message-enter-active {
transition: all .3s ease;
}
.message-fade-leave-active {
transition: all .8s cubic-bezier(1.0, 0.5, 0.8, 1.0);
}
.message-fade-enter, .message-fade-leave-to {
opacity: 0;
}
.message{
width: 75%;
max-width: 35rem;
margin: 0 auto;
margin-bottom: 1rem;
padding: 12px 15px 12px 15px;
position: relative;
&-text{
font-weight: 300;
}
&-dismiss{
position: absolute;
font-size: 17px;
font-weight: 100;
top: 0;
right: 0;
margin-top: 2px;
margin-right: 5px;
cursor: pointer;
}
}
.message-warning{
background-color: #f2dede;
border: 1px solid #b75b91;
color: #b75b91;
}
.message-success{
background-color: #dff0d9;
border: 1px solid #3e7549;
color: #3e7549;
}
.center {
justify-content: center;
}
.form{
z-index: 15;
background-color: $c-light;
display: flex;
flex-direction: column;
@include tablet-min{
}
&__buffer{
width: 100%;
height: 4rem;
}
&__group{
display: flex;
justify-content: center;
@include tablet-min{
}
&-input{
width: 75%;
max-width: 35rem;
padding: 15px 10px 15px 45px;
outline: none;
background-color: $c-white;
color: $c-dark;
font-weight: 100;
font-size: 20px;
border: 1px solid $c-dark;
margin-left: -2.2rem;
z-index: 3;
&:focus, &:hover {
border-color: $c-dark;
}
}
&-input[type="username"] {
margin-bottom: 3rem;
}
&__input-icon{
width: 24px;
height: 24px;
fill: rgba($c-dark, 0.5);
transition: fill 0.5s ease;
pointer-events: none;
margin-top: 15px;
margin-left: 15px;
z-index: 8;
}
&-link{
text-decoration: none;
color: black;
margin-top: 1rem;
}
&-signin{
text-transform: uppercase;
font-weight: 300;
font-size: 11px;
line-height: 2;
letter-spacing: 0.5px;
}
}
}
</style>

162
src/components/Settings.vue Normal file
View File

@@ -0,0 +1,162 @@
<template>
<section class="profile">
<demo-login-modal/>
<div class="profile__content" v-if="userLoggedIn">
<section class='settings'>
<h3 class='settings__header'>Plex account</h3>
<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">
<div class="form__group">
<svg class="form__group__input-icon"><use xlink:href="#iconEmail"></use></svg>
<input class="form__group-input" type="text" ref="plex_username" placeholder="Plex username"/>
</div>
<div class="form__group">
<svg class="form__group__input-icon"><use xlink:href="#iconKeyhole"></use></svg>
<input class="form__group-input" type="password" ref="plex_password" placeholder="Repeat new password">
</div>
</form>
<div class="plex">
<button type="button" class="button" @click="authenticatePlex">Link plex account</button>
</div>
<hr class='setting__divider'>
<h3 class='settings__header'>Change password</h3>
<form class="form">
<div class="form__group">
<svg class="form__group__input-icon"><use xlink:href="#iconKeyhole"></use></svg>
<input class="form__group-input" type="password" ref="password" placeholder="New password"/>
</div>
<div class="form__group">
<svg class="form__group__input-icon"><use xlink:href="#iconKeyhole"></use></svg>
<input class="form__group-input" type="password" ref="password_re" placeholder="Repeat new password">
</div>
<div class="form__group">
<button type="button" class="button" @click="$modal.show('demo-login')">Change password</button>
</div>
</form>
<hr class='setting__divider'>
</section>
</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 axios from 'axios'
import storage from '../storage.js'
import DemoLoginModal from './demo.vue'
// import CreatedLists from './CreatedLists.vue'
export default {
components: { DemoLoginModal },
data(){
return{
userLoggedIn: '',
}
},
methods: {
authenticatePlex() {
let username = this.$refs.plex_username.value;
let password = this.$refs.plex_password.value;
console.log(username, password)
axios({
method: 'POST',
url: `https://plex.tv/users/sign_in.json?user[login]=${username}&user[password]=${password}`,
headers: {
'Content-Type': 'application/json',
'X-Plex-Platform': 'Linux',
'X-Plex-Version': 'v2.0.24',
'X-Plex-Platform-Version': '4.13.0-36-generic',
'X-Plex-Device-Name': 'Tautulli',
'X-Plex-Client-Identifier': 'f9e0748ec84440dd8d0e759ab598326c'
},
})
.then((resp) => {
let data = resp.data;
console.log('response from plex:', data.user)
})
.catch((error) => {
console.log('error: ', error)
})
}
},
created(){
document.title = 'Settings' + storage.pageTitlePostfix;
storage.backTitle = document.title;
if(!localStorage.getItem('token')){
this.userLoggedIn = false;
} else {
this.userLoggedIn = true;
}
}
}
</script>
<style lang="scss" scoped>
@import "./src/scss/variables";
@import "./src/scss/media-queries";
a {
text-decoration: none;
}
.plex {
margin-top: 20px;
}
.form__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: 35px;
&__header {
margin: 0;
line-height: 16px;
color: $c-dark;
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 rgba(8, 28, 36, 0.05);
margin-top: 30px;
margin-bottom: 70px;
margin-left: 20px;
width: 96%;
text-align: left;
}
span {
font-weight: 200;
size: 16px;
}
}
</style>

122
src/components/Signin.vue Normal file
View File

@@ -0,0 +1,122 @@
<template>
<section class="profile">
<div class="profile__content">
<header class="profile__header">
<h2 class="profile__title">Register new user</h2>
</header>
<form class="form">
<div class="form__buffer"></div>
<div>
<div class="form__group">
<svg class="form__group__input-icon">
<use xlink:href="#iconEmail"></use>
</svg>
<input class="form__group-input" type="username" ref="username" placeholder="Username" >
</div>
<div class="form__group">
<svg class="form__group__input-icon">
<use xlink:href="#iconKeyhole"></use>
</svg>
<input class="form__group-input" type="password" ref="password" placeholder="Password" v-on:keyup.enter="signin">
</div>
<transition name="message-fade">
<div class="message" :class="messageClass" v-if="showMessage">
<span class="message-text">{{ messageText }}</span>
<span class="message-dismiss" @click="showMessage=false">X</span>
</div>
</transition>
<div class="form__group">
<button type="button" class="button" v-on:click="signin">Sign in</button>
</div>
</div>
</form>
<div class="form__group">
<router-link class="form__group-link" :to="{name: 'register'}" exact title="Sign in here">
<span class="form__group-signin">Don't have a user? Register here</span>
</router-link>
</div>
<!-- <created-lists></created-lists> -->
</div>
</section>
</template>
<script>
import axios from 'axios'
import storage from '../storage.js'
import MoviesList from './MoviesList.vue'
// import CreatedLists from './CreatedLists.vue'
export default {
components: { MoviesList },
data(){
return{
userLoggedIn: '',
userName: '',
showMessage: false,
messageClass: 'message-success',
messageText: 'hello world'
}
},
methods: {
signin(){
let username = this.$refs.username.value;
let password = this.$refs.password.value;
axios.post(`https://api.kevinmidboe.com/api/v1/user/login`, {
username: username,
password: password
})
.then(function (resp){
let data = resp.data;
if (data.success){
localStorage.setItem('token', data.token);
localStorage.setItem('username', username);
localStorage.setItem('admin', data.admin);
this.userLoggedIn = true;
eventHub.$emit('setUserStatus');
this.$router.push({ name: 'profile' })
}
}.bind(this))
.catch(function (error){
if (error.message.endsWith('401'))
this.msg('Incorrect username or password ', 'warning')
else
this.msg(error.message, 'warning')
}.bind(this));
},
msg(text, status){
if (status === 'warning')
this.messageClass = 'message-warning';
else if (status === 'success')
this.messageClass = 'message-success';
else
this.messageClass = 'message-info';
this.messageText = text;
this.showMessage = true;
// setTimeout(() => this.showMessage = false, 3500);
},
toggleView(){
this.register = false;
},
},
created(){
document.title = 'Sign in' + storage.pageTitlePostfix;
storage.backTitle = document.title;
if (this.userLoggedIn == true) {
this.$router.push({ name: 'profile' })
}
},
mounted(){
// this.$refs.email.focus();
}
}
</script>
<style lang="scss">
</style>

View File

@@ -0,0 +1,148 @@
<template>
<section>
<div v-if="listLoaded">
<div v-if="torrents.length">
<data-tablee
:rows="torrents"
:cols="cols"
empty="-"
>
<span
class="data-tablee-icon"
slot="sort-icon"
slot-scope="{ sortment, sorted, arrow }"
>
{{ sorted ? arrow + ' ' + (sortment === 'ascending' ? 'ASC' : 'DESC') : '' }}
</span>
<template
slot="row"
slot-scope="{ row, index }"
>
<td class="data-tablee-cell -content data-tablee-text" v-bind:title="row.name" v-if="!renderName">{{ row.name.slice(0, 50) }}</td>
<td class="data-tablee-cell -content data-tablee-text" v-on:click="showInfo(row.name)">{{ row.seed }}</td>
<td class="data-tablee-cell -content data-tablee-text" v-on:click="showInfo(row.name)">{{ row.size }}</td>
<td class="data-tablee-cell -content data-tablee-text magnet">
<button type='button' class="button" @click="sendTorrent(row.magnet, row.name)">Add</button>
</td>
</template>
</data-tablee>
</div>
<section v-if="!torrents.length" class="">
<div class="not-found__content">
<h2 class="not-found__title">{{ errorMessage }}</h2>
</div>
</section>
</div>
<i v-if="!listLoaded" class="torrentloader"></i>
</section>
</template>
<script>
import axios from 'axios'
import numeral from 'numeral'
import storage from '../storage.js'
// import testTorrents from './torrents.json';
let tablet = window.innerWidth < 768 ? true : false;
export default {
props: ['query', 'tmdb_id', 'tmdb_type'],
beforeRouteLeave (to, from, next) {
if(from.name == 'search'){
eventHub.$emit('setSearchQuery', true);
}
next();
},
data() {
return {
torrents: [],
listLoaded: false,
errorMessage: '',
renderName: tablet,
cols: [
{ label: 'Name', field: 'name', sort: true, hidden: tablet },
{ label: 'Seeders', field: 'seed', sort: (a, b) => parseInt(a) - parseInt(b) },
{ label: 'Size', field: 'size', sort: (a, b) => this.sortableSize(a) - this.sortableSize(b) },
{ label: 'Add', align: 'center' }
],
}
},
methods: {
fetchTorrents(){
axios.get(`https://api.kevinmidboe.com/api/v1/pirate/search?query=${this.query}&filter=all&page=${this.currentPage}`, {
headers: {authorization: storage.token},
})
.then(resp => {
let data = resp.data;
this.torrents = data.results;
this.listLoaded = true;
})
.catch(e => {
const error = e.toString()
this.errorMessage = error.indexOf('401') != -1 ? 'Permission denied' : 'Nothing found';
this.listLoaded = true;
});
},
sendTorrent(magnet, name){
axios.post(`https://api.kevinmidboe.com/api/v1/pirate/add`, {
magnet: magnet, name: name, tmdb_id: this.tmdb_id }, { headers: {authorization: storage.token}
})
.catch((resp) => { console.log('error:', resp.data) })
.then((resp) => { console.log('addTorrent resp: ', resp) })
},
showInfo(text){
alert(text)
},
sortableSize(string) {
const UNITS = ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const [numStr, unit] = string.split(' ');
if (UNITS.indexOf(unit) === -1)
return string
const exponent = UNITS.indexOf(unit) * 3
return numStr * (Math.pow(10, exponent))
},
},
created(){
this.fetchTorrents();
},
}
</script>
<style src="../scss/vue-data-tablee.css"></style>
<style lang="scss" scoped>
@import "./src/scss/variables";
@import "./src/scss/media-queries";
.magnet{
text-align: center;
}
.add{
padding: 3px 15px 3px 15px;
&:hover, &:active{
background: $c-dark;
color: $c-white;
}
}
.torrentloader{
animation: load 1s linear infinite;
border: 2px solid $c-dark;
border-radius: 50%;
display: block;
height: 30px;
left: 50%;
margin: 0 auto;
width: 30px;
&:after {
border: 5px solid $c-green;
border-radius: 50%;
content: '';
left: 10px;
position: absolute;
top: 16px;
}
}
@keyframes load {
100% { transform: rotate(360deg); }
}
</style>

221
src/components/demo.vue Normal file
View File

@@ -0,0 +1,221 @@
<template>
<modal name="demo-login" transition="pop-out" :width="modalWidth" :height="400">
<div class="box">
<div class="partition" id="partition-register">
<div class="partition-title">CREATE ACCOUNT</div>
<div class="partition-form">
<form autocomplete="false">
<div class="autocomplete-fix">
<input type="password">
</div>
<input id="n-username" type="text" placeholder="Username">
<input id="n-password1" type="password" placeholder="Password">
<input id="n-password2" type="password" placeholder="Retype password">
</form>
<div style="margin-top: 42px">
</div>
<div class="button-set">
<button id="goto-signin-btn">Sign In</button>
<button id="register-btn">Register</button>
</div>
<button class="large-btn github-btn">connect with <span>github</span></button>
<button class="large-btn facebook-btn">connect with <span>facebook</span></button>
</div>
</div>
</div>
</modal>
</template>
<script>
const MODAL_WIDTH = 370
export default {
name: 'DemoLoginModal',
data () {
return {
modalWidth: MODAL_WIDTH
}
},
created () {
this.modalWidth = window.innerWidth < MODAL_WIDTH
? MODAL_WIDTH / 2
: MODAL_WIDTH
}
}
</script>
<style lang="scss">
$background_color: #404142;
$github_color: #DBA226;
$facebook_color: #3880FF;
.box {
background: white;
overflow: hidden;
width: 100%;
height: 400px;
border-radius: 2px;
box-sizing: border-box;
box-shadow: 0 0 40px black;
color: #8b8c8d;
font-size: 0;
.box-part {
display: inline-block;
position: relative;
vertical-align: top;
box-sizing: border-box;
height: 100%;
&#bp-right {
background: url("/static/panorama.jpg") no-repeat top left;
border-left: 1px solid #eee;
}
}
.box-messages {
position: absolute;
left: 0;
bottom: 0;
width: 100%;
}
.box-error-message {
position: relative;
overflow: hidden;
box-sizing: border-box;
height: 0;
line-height: 32px;
padding: 0 12px;
text-align: center;
width: 100%;
font-size: 11px;
color: white;
background: #F38181;
}
.partition {
width: 100%;
height: 100%;
.partition-title {
box-sizing: border-box;
padding: 30px;
width: 100%;
text-align: center;
letter-spacing: 1px;
font-size: 20px;
font-weight: 300;
}
.partition-form {
padding: 0 20px;
box-sizing: border-box;
}
}
input[type=password],
input[type=text] {
display: block;
box-sizing: border-box;
margin-bottom: 4px;
width: 100%;
font-size: 12px;
line-height: 2;
border: 0;
border-bottom: 1px solid #DDDEDF;
padding: 4px 8px;
font-family: inherit;
transition: 0.5s all;
outline: none;
}
button {
background: white;
border-radius: 4px;
box-sizing: border-box;
padding: 10px;
letter-spacing: 1px;
font-family: "Open Sans", sans-serif;
font-weight: 400;
min-width: 140px;
margin-top: 8px;
color: #8b8c8d;
cursor: pointer;
border: 1px solid #DDDEDF;
text-transform: uppercase;
transition: 0.1s all;
font-size: 10px;
outline: none;
&:hover {
border-color: mix(#DDDEDF, black, 90%);
color: mix(#8b8c8d, black, 80%);
}
}
.large-btn {
width: 100%;
background: white;
span {
font-weight: 600;
}
&:hover {
color: white !important;
}
}
.button-set {
margin-bottom: 8px;
}
#register-btn,
#signin-btn {
margin-left: 8px;
}
.facebook-btn {
border-color: $facebook_color;
color: $facebook_color;
&:hover {
border-color: $facebook_color;
background: $facebook_color;
}
}
.github-btn {
border-color: $github_color;
color: $github_color;
&:hover {
border-color: $github_color;
background: $github_color;
}
}
.autocomplete-fix {
position: absolute;
visibility: hidden;
overflow: hidden;
opacity: 0;
width: 0;
height: 0;
left: 0;
top: 0;
}
}
.pop-out-enter-active,
.pop-out-leave-active {
transition: all 0.5s;
}
.pop-out-enter,
.pop-out-leave-active {
opacity: 0;
transform: translateY(24px);
}
</style>

1895
src/components/torrents.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -3,12 +3,16 @@ import VueRouter from 'vue-router'
import axios from 'axios'
import router from './routes'
import DataTablee from 'vue-data-tablee'
import VModal from 'vue-js-modal'
import App from './App.vue'
window.eventHub = new Vue();
Vue.use(VueRouter, axios)
Vue.use(DataTablee)
Vue.use(VModal, { dialog: true })
new Vue({
el: '#app',

View File

@@ -10,11 +10,18 @@ let routes = [
},
{
name: 'home-category',
path: '/movies/:category',
path: '/list/:category',
components: {
'list-router-view': require('./components/MoviesList.vue')
}
},
{
name: 'request',
path: '/request/all',
components: {
'request-router-view': require('./components/MoviesList.vue')
}
},
{
name: 'search',
path: '/search/:query',
@@ -22,6 +29,13 @@ let routes = [
'search-router-view': require('./components/MoviesList.vue')
}
},
{
name: 'user-requests',
path: '/profile/requests',
components: {
'user-requests-router-view': require('./components/MoviesList.vue')
}
},
{
name: 'movie',
path: '/movie/:id',
@@ -29,13 +43,41 @@ let routes = [
'page-router-view': require('./components/MoviePage.vue')
},
beforeEnter: (to, from, next) => {
if(history.state && history.state.popup){
eventHub.$emit('openMoviePopup', to.params.id, false);
if(history.state && history.state.popup && from.name){
eventHub.$emit('openMoviePopup', to.params.id, 'movie', false);
return;
}
next();
}
},
{
name: 'show',
path: '/show/:id',
components: {
'page-router-view': require('./components/MoviePage.vue')
},
beforeEnter: (to, from, next) => {
if(history.state && history.state.popup && from.name){
eventHub.$emit('openMoviePopup', to.params.id, 'show', false);
return;
}
next();
}
},
{
name: 'register',
path: '/register',
components: {
'search-router-view': require('./components/Register.vue')
}
},
{
name: 'signin',
path: '/signin',
components: {
'search-router-view': require('./components/Signin.vue')
}
},
{
name: 'profile',
path: '/profile',
@@ -43,6 +85,13 @@ let routes = [
'search-router-view': require('./components/Profile.vue')
}
},
{
name: 'settings',
path: '/profile/settings',
components: {
'search-router-view': require('./components/Settings.vue')
}
},
{
name: '404',
path: '/404',
@@ -52,12 +101,17 @@ let routes = [
},
{
path: '*',
redirect: '/404'
redirect: '/'
},
{
path: '/request',
redirect: '/'
}
];
const router = new VueRouter({
mode: 'history',
// mode: 'history',
base: '/',
routes,
linkActiveClass: 'is-active'
});

View File

@@ -3,3 +3,7 @@ $c-green: #01d277;
$c-dark: #081c24;
$c-white: #ffffff;
$c-light: #f8f8f8;
$c-green-light: #dff0d9;
$c-green-dark: #3e7549;
$c-red-light: #f2dede;
$c-red-dark: #b75b91;

View File

@@ -0,0 +1,68 @@
.data-tablee {
overflow: hidden;
border: 1px solid #eaedef;
width: 100%;
border-radius: 5px;
border-spacing: 0; }
.data-tablee-cell {
position: relative;
min-height: calc(27px + 4px);
padding: 10px;
border-top: 1px solid #eaedef; }
.data-tablee-row:first-child > .data-tablee-cell {
border-top: 0; }
.data-tablee-cell::before {
position: absolute;
left: 0;
top: 50%;
display: block;
width: 1px;
height: 27px;
background-color: #eaedef;
transform: translateY(-50%);
content: ''; }
.data-tablee-cell:first-child::before {
content: none; }
.data-tablee-cell.-right {
text-align: right; }
.data-tablee-cell.-left {
text-align: left; }
.data-tablee-cell.-center {
text-align: center; }
.data-tablee-cell.-clickable {
cursor: pointer; }
.data-tablee-text {
font-size: 13px;
font-weight: 400;
color: #5e6684; }
.data-tablee-cell.-header {
background-color: #fdfdfd; }
.data-tablee-cell.-header > .data-tablee-text,
.data-tablee-cell.-header > .data-tablee-icon {
display: inline-block;
font-size: 12px;
font-weight: 400;
text-transform: uppercase;
color: #bec0d3; }
.data-tablee-cell.-header > .data-tablee-icon {
opacity: 0;
transition: opacity .3s ease, transform .3s ease; }
.data-tablee-cell.-header.-sortable {
cursor: pointer; }
.data-tablee-cell.-header.-sortable > .data-tablee-icon {
opacity: .2; }
.data-tablee-cell.-header.-sortable:hover > .data-tablee-icon {
opacity: .8; }
.data-tablee-cell.-header.-sortable:active > .data-tablee-icon {
transition: transform .1s ease;
transform: scale(1.5); }
.data-tablee-cell.-header.-sortable.-right {
padding-right: 6px; }
.data-tablee-cell.-header.-sorting > .data-tablee-icon {
opacity: 1; }
.data-tablee-text {
line-height: 1; }

View File

@@ -1,34 +1,49 @@
let storage = {
apiKey: 'a70dbfe19b800809dfdd3e89e8532c9e',
sessionId: localStorage.getItem('session_id') || null,
userId: localStorage.getItem('user_id') || null,
token: localStorage.getItem('token') || null,
username: localStorage.getItem('username') || null,
admin: localStorage.getItem('admin') || null,
pageTitlePostfix: ' — ' + document.title,
listTypes: [
{
title: 'Popular Movies',
shortTitle: 'Popular',
query: 'popular',
type: 'collection',
isCategory: true
title: 'Your Requests',
shortTitle: 'User Requests',
query: 'user-requests',
name: 'user-requests',
type: 'user-requests',
isProfileContent: true
// isCategory: true,
},
{
title: 'Top Rated Movies',
shortTitle: 'Top Rated',
query: 'top_rated',
type: 'collection',
isCategory: true
title: 'Requested Movies & Shows',
shortTitle: 'Requested',
query: 'requests',
name: 'home-category',
type: 'requests', // Maybe change to separate group
isCategory: true,
isProfileContent: true
},
{
title: 'Upcoming Movies',
shortTitle: 'Upcoming',
query: 'upcoming',
name: 'home-category',
type: 'collection',
isCategory: true
},
{
title: 'Now Playing Movies',
shortTitle: 'Now Playing',
query: 'now_playing',
query: 'nowplaying',
name: 'home-category',
type: 'collection',
isCategory: true
},
{
title: 'Popular Movies',
shortTitle: 'Popular',
query: 'popular',
name: 'home-category',
type: 'collection',
isCategory: true
},

View File

@@ -29,7 +29,8 @@ module.exports = {
test: /\.(png|jpg|gif|svg)$/,
loader: 'file-loader',
options: {
name: '[name].[ext]?[hash]'
name: '[name].[ext]'
// name: '[name].[ext]?[hash]'
}
}
]
@@ -49,7 +50,7 @@ module.exports = {
performance: {
hints: false
},
devtool: '#eval-source-map'
// devtool: '#eval-source-map'
}
if (process.env.NODE_ENV === 'production') {

5058
yarn.lock Normal file

File diff suppressed because it is too large Load Diff