Refactored user store & moved popup logic from App to store

Cleaned up bits of all the components that use these stores.

User store now focuses around keeping track of the authorization token
and the response from /login. When a sucessfull login request is made we
save our new token and username & admin data to the with login(). Since
cookies aren't implemented yet we keep track of the auth_token to make
authroized requests back to the api later.
The username and admin data from within the body of the token is saved
and only cleared on logout().
Since we haven't implemented cookies we persist storage with
localStorage. Whenever we successfully decode and save a token body we
also save the token to localStorage. This is later used by
initFromLocalStorage() to hydrate the store on first page load.

Popup module is for opening and closing the popup, and now moved away
from a inline plugin in App entry. Now handles loading from &
updating query parameters type=movie | show.
The route listens checks if open every navigation and closes popup if it
is.
This commit is contained in:
2022-01-12 22:20:12 +01:00
parent d1cbbfffd8
commit b021882013
14 changed files with 723 additions and 685 deletions

View File

@@ -46,18 +46,13 @@
<router-view class="content" :key="$route.fullPath"></router-view> <router-view class="content" :key="$route.fullPath"></router-view>
<!-- Movie popup that will show above existing rendered content --> <!-- Movie popup that will show above existing rendered content -->
<movie-popup <movie-popup></movie-popup>
v-if="moviePopupIsVisible"
:id="popupID"
:type="popupType"
></movie-popup>
<darkmode-toggle /> <darkmode-toggle />
</div> </div>
</template> </template>
<script> <script>
import Vue from "vue";
import NavigationHeader from "@/components/NavigationHeader"; import NavigationHeader from "@/components/NavigationHeader";
import NavigationIcons from "@/components/NavigationIcons"; import NavigationIcons from "@/components/NavigationIcons";
import MoviePopup from "@/components/MoviePopup"; import MoviePopup from "@/components/MoviePopup";
@@ -70,37 +65,6 @@ export default {
NavigationIcons, NavigationIcons,
MoviePopup, MoviePopup,
DarkmodeToggle 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");
}
};
const movieId = new URLSearchParams(window.location.search).get("movie");
if (movieId) {
this.$popup.open(movieId, "movie");
}
} }
}; };
</script> </script>
@@ -126,7 +90,8 @@ body {
transition: background-color 0.5s ease, color 0.5s ease; transition: background-color 0.5s ease, color 0.5s ease;
* { * {
transition: background-color 0.5s ease, color 0.5s ease; transition: background-color 0.5s ease, color 0.5s ease,
border-color 0.5s ease;
} }
&.hidden { &.hidden {
@@ -158,20 +123,6 @@ img {
.wrapper { .wrapper {
position: relative; 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;
// top: 0;
// }
// }
// router view transition // router view transition
.fade-enter-active, .fade-enter-active,

View File

@@ -1,24 +1,34 @@
import axios from 'axios' import axios from "axios";
import storage from '@/storage' import storage from "@/storage";
import config from '@/config.json' import config from "@/config.json";
import path from 'path' import path from "path";
import store from '@/store' import store from "@/store";
const SEASONED_URL = config.SEASONED_URL const token = () => store.getters["user/token"];
const ELASTIC_URL = config.ELASTIC_URL const plexId = () => store.getters["user/plexId"];
const ELASTIC_INDEX = config.ELASTIC_INDEX
const AUTHORIZATION_HEADERS = () => {
return {
Authorization: token(),
"Content-Type": "application/json"
};
};
const SEASONED_URL = config.SEASONED_URL;
const ELASTIC_URL = config.ELASTIC_URL;
const ELASTIC_INDEX = config.ELASTIC_INDEX;
// TODO // TODO
// - Move autorization token and errors here? // - Move autorization token and errors here?
const checkStatusAndReturnJson = (response) => { const checkStatusAndReturnJson = response => {
if (!response.ok) { if (!response.ok) {
throw resp throw resp;
} }
return response.json() return response.json();
} };
// - - - TMDB - - - // - - - TMDB - - -
/** /**
* Fetches tmdb movie by id. Can optionally include cast credits in result object. * Fetches tmdb movie by id. Can optionally include cast credits in result object.
@@ -26,23 +36,31 @@ const checkStatusAndReturnJson = (response) => {
* @param {boolean} [credits=false] Include credits * @param {boolean} [credits=false] Include credits
* @returns {object} Tmdb response * @returns {object} Tmdb response
*/ */
const getMovie = (id, checkExistance=false, credits=false, release_dates=false) => { const getMovie = (
const url = new URL('v2/movie', SEASONED_URL) id,
url.pathname = path.join(url.pathname, id.toString()) 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) { if (checkExistance) {
url.searchParams.append('check_existance', true) url.searchParams.append("check_existance", true);
} }
if (credits) { if (credits) {
url.searchParams.append('credits', true) url.searchParams.append("credits", true);
} }
if(release_dates) { if (release_dates) {
url.searchParams.append('release_dates', true) url.searchParams.append("release_dates", true);
} }
return fetch(url.href) return fetch(url.href, { headers: AUTHORIZATION_HEADERS() })
.then(resp => resp.json()) .then(resp => resp.json())
.catch(error => { console.error(`api error getting movie: ${id}`); throw error }) .catch(error => {
} console.error(`api error getting movie: ${id}`);
throw error;
});
};
/** /**
* Fetches tmdb show by id. Can optionally include cast credits in result object. * Fetches tmdb show by id. Can optionally include cast credits in result object.
@@ -50,20 +68,23 @@ const getMovie = (id, checkExistance=false, credits=false, release_dates=false)
* @param {boolean} [credits=false] Include credits * @param {boolean} [credits=false] Include credits
* @returns {object} Tmdb response * @returns {object} Tmdb response
*/ */
const getShow = (id, checkExistance=false, credits=false) => { const getShow = (id, checkExistance = false, credits = false) => {
const url = new URL('v2/show', SEASONED_URL) const url = new URL("v2/show", SEASONED_URL);
url.pathname = path.join(url.pathname, id.toString()) url.pathname = path.join(url.pathname, id.toString());
if (checkExistance) { if (checkExistance) {
url.searchParams.append('check_existance', true) url.searchParams.append("check_existance", true);
} }
if (credits) { if (credits) {
url.searchParams.append('credits', true) url.searchParams.append("credits", true);
} }
return fetch(url.href) return fetch(url.href, { headers: AUTHORIZATION_HEADERS() })
.then(resp => resp.json()) .then(resp => resp.json())
.catch(error => { console.error(`api error getting show: ${id}`); throw error }) .catch(error => {
} console.error(`api error getting show: ${id}`);
throw error;
});
};
/** /**
* Fetches tmdb person by id. Can optionally include cast credits in result object. * Fetches tmdb person by id. Can optionally include cast credits in result object.
@@ -71,17 +92,20 @@ const getShow = (id, checkExistance=false, credits=false) => {
* @param {boolean} [credits=false] Include credits * @param {boolean} [credits=false] Include credits
* @returns {object} Tmdb response * @returns {object} Tmdb response
*/ */
const getPerson = (id, credits=false) => { const getPerson = (id, credits = false) => {
const url = new URL('v2/person', SEASONED_URL) const url = new URL("v2/person", SEASONED_URL);
url.pathname = path.join(url.pathname, id.toString()) url.pathname = path.join(url.pathname, id.toString());
if (credits) { if (credits) {
url.searchParams.append('credits', true) url.searchParams.append("credits", true);
} }
return fetch(url.href) return fetch(url.href)
.then(resp => resp.json()) .then(resp => resp.json())
.catch(error => { console.error(`api error getting person: ${id}`); throw error }) .catch(error => {
} console.error(`api error getting person: ${id}`);
throw error;
});
};
/** /**
* Fetches tmdb list by name. * Fetches tmdb list by name.
@@ -89,41 +113,39 @@ const getPerson = (id, credits=false) => {
* @param {number} [page=1] * @param {number} [page=1]
* @returns {object} Tmdb list response * @returns {object} Tmdb list response
*/ */
const getTmdbMovieListByName = (name, page=1) => { const getTmdbMovieListByName = (name, page = 1) => {
const url = new URL('v2/movie/' + name, SEASONED_URL) const url = new URL("v2/movie/" + name, SEASONED_URL);
url.searchParams.append('page', page) url.searchParams.append("page", page);
const headers = { authorization: storage.token }
return fetch(url.href, { headers: headers }) return fetch(url.href, { headers: AUTHORIZATION_HEADERS() }).then(resp =>
.then(resp => resp.json()) resp.json()
// .catch(error => { console.error(`api error getting list: ${name}, page: ${page}`); throw error }) );
} // .catch(error => { console.error(`api error getting list: ${name}, page: ${page}`); throw error })
};
/** /**
* Fetches requested items. * Fetches requested items.
* @param {number} [page=1] * @param {number} [page=1]
* @returns {object} Request response * @returns {object} Request response
*/ */
const getRequests = (page=1) => { const getRequests = (page = 1) => {
const url = new URL('v2/request', SEASONED_URL) const url = new URL("v2/request", SEASONED_URL);
url.searchParams.append('page', page) url.searchParams.append("page", page);
const headers = { authorization: storage.token }
return fetch(url.href, { headers: headers }) return fetch(url.href, {
.then(resp => resp.json()) headers: AUTHORIZATION_HEADERS()
// .catch(error => { console.error(`api error getting list: ${name}, page: ${page}`); throw error }) }).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 getUserRequests = (page=1) => { return fetch(url.href, {
const url = new URL('v1/user/requests', SEASONED_URL) headers: AUTHORIZATION_HEADERS()
url.searchParams.append('page', page) }).then(resp => resp.json());
};
const headers = { authorization: localStorage.getItem('token') }
return fetch(url.href, { headers })
.then(resp => resp.json())
}
/** /**
* Fetches tmdb movies and shows by query. * Fetches tmdb movies and shows by query.
@@ -131,24 +153,27 @@ const getUserRequests = (page=1) => {
* @param {number} [page=1] * @param {number} [page=1]
* @returns {object} Tmdb response * @returns {object} Tmdb response
*/ */
const searchTmdb = (query, page=1, adult=false, mediaType=null) => { const searchTmdb = (query, page = 1, adult = false, mediaType = null) => {
const url = new URL('v2/search', SEASONED_URL) const url = new URL("v2/search", SEASONED_URL);
if (mediaType != null && ['movie', 'show', 'person'].includes(mediaType)) { if (mediaType != null && ["movie", "show", "person"].includes(mediaType)) {
url.pathname += `/${mediaType}` url.pathname += `/${mediaType}`;
} }
url.searchParams.append('query', query) url.searchParams.append("query", query);
url.searchParams.append('page', page) url.searchParams.append("page", page);
url.searchParams.append('adult', adult) url.searchParams.append("adult", adult);
const headers = { authorization: localStorage.getItem('token') } return fetch(url.href, {
headers: AUTHORIZATION_HEADERS()
return fetch(url.href, { headers }) })
.then(resp => resp.json()) .then(resp => resp.json())
.catch(error => { console.error(`api error searching: ${query}, page: ${page}`); throw error }) .catch(error => {
} console.error(`api error searching: ${query}, page: ${page}`);
throw error;
});
};
// - - - Torrents - - - // - - - Torrents - - -
/** /**
* Search for torrents by query * Search for torrents by query
@@ -157,15 +182,18 @@ const searchTmdb = (query, page=1, adult=false, mediaType=null) => {
* @returns {object} Torrent response * @returns {object} Torrent response
*/ */
const searchTorrents = (query, authorization_token) => { const searchTorrents = (query, authorization_token) => {
const url = new URL('/api/v1/pirate/search', SEASONED_URL) const url = new URL("/api/v1/pirate/search", SEASONED_URL);
url.searchParams.append('query', query) url.searchParams.append("query", query);
const headers = { authorization: storage.token } return fetch(url.href, {
headers: AUTHORIZATION_HEADERS()
return fetch(url.href, { headers: headers }) })
.then(resp => resp.json()) .then(resp => resp.json())
.catch(error => { console.error(`api error searching torrents: ${query}`); throw error }) .catch(error => {
} console.error(`api error searching torrents: ${query}`);
throw error;
});
};
/** /**
* Add magnet to download queue. * Add magnet to download queue.
@@ -175,28 +203,27 @@ const searchTorrents = (query, authorization_token) => {
* @returns {object} Success/Failure response * @returns {object} Success/Failure response
*/ */
const addMagnet = (magnet, name, tmdb_id) => { const addMagnet = (magnet, name, tmdb_id) => {
const url = new URL('v1/pirate/add', SEASONED_URL) const url = new URL("v1/pirate/add", SEASONED_URL);
const body = JSON.stringify({ const body = JSON.stringify({
magnet: magnet, magnet: magnet,
name: name, name: name,
tmdb_id: tmdb_id tmdb_id: tmdb_id
}) });
const headers = {
'Content-Type': 'application/json',
authorization: storage.token
}
return fetch(url.href, { return fetch(url.href, {
method: 'POST', method: "POST",
headers, headers: AUTHORIZATION_HEADERS(),
body body
}) })
.then(resp => resp.json()) .then(resp => resp.json())
.catch(error => { console.error(`api error adding magnet: ${name} ${error}`); throw error }) .catch(error => {
} console.error(`api error adding magnet: ${name} ${error}`);
throw error;
});
};
// - - - Plex/Request - - - // - - - Plex/Request - - -
/** /**
* Request a movie or show from id. If authorization token is included the user will be linked * Request a movie or show from id. If authorization token is included the user will be linked
@@ -206,28 +233,19 @@ const addMagnet = (magnet, name, tmdb_id) => {
* @param {string} [authorization_token] To identify the requesting user * @param {string} [authorization_token] To identify the requesting user
* @returns {object} Success/Failure response * @returns {object} Success/Failure response
*/ */
const request = (id, type, authorization_token=undefined) => { const request = (id, type) => {
const url = new URL('v2/request', SEASONED_URL) 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, { return fetch(url.href, {
method: 'POST', method: "POST",
headers: headers, headers: AUTHORIZATION_HEADERS(),
body: JSON.stringify(body) body: JSON.stringify({ id, type })
}) })
.then(resp => resp.json()) .then(resp => resp.json())
.catch(error => { console.error(`api error requesting: ${id}, type: ${type}`); throw error }) .catch(error => {
} console.error(`api error requesting: ${id}, type: ${type}`);
throw error;
});
};
/** /**
* Check request status by tmdb id and type * Check request status by tmdb id and type
@@ -235,186 +253,182 @@ const request = (id, type, authorization_token=undefined) => {
* @param {string} type * @param {string} type
* @returns {object} Success/Failure response * @returns {object} Success/Failure response
*/ */
const getRequestStatus = (id, type, authorization_token=undefined) => { const getRequestStatus = (id, type, authorization_token = undefined) => {
const url = new URL('v2/request', SEASONED_URL) const url = new URL("v2/request", SEASONED_URL);
url.pathname = path.join(url.pathname, id.toString()) url.pathname = path.join(url.pathname, id.toString());
url.searchParams.append('type', type) url.searchParams.append("type", type);
return fetch(url.href) return fetch(url.href, { headers: AUTHORIZATION_HEADERS() })
.then(resp => { .then(resp => {
const status = resp.status; const status = resp.status;
if (status === 200) { return true } if (status === 200) {
else if (status === 404) { return false } return true;
else { } else if (status === 404) {
console.error(`api error getting request status for id ${id} and type ${type}`) return false;
} else {
console.error(
`api error getting request status for id ${id} and type ${type}`
);
} }
}) })
.catch(err => Promise.reject(err)) .catch(err => Promise.reject(err));
} };
const watchLink = (title, year, authorization_token=undefined) => { const watchLink = (title, year) => {
const url = new URL('v1/plex/watch-link', SEASONED_URL) const url = new URL("v1/plex/watch-link", SEASONED_URL);
url.searchParams.append('title', title) url.searchParams.append("title", title);
url.searchParams.append('year', year) url.searchParams.append("year", year);
return fetch(url.href, { headers: AUTHORIZATION_HEADERS() })
.then(resp => resp.json())
.then(response => response.link);
};
const movieImages = id => {
const url = new URL(`v2/movie/${id}/images`, SEASONED_URL);
const headers = { const headers = {
'Authorization': authorization_token, "Content-Type": "application/json"
'Content-Type': 'application/json' };
}
return fetch(url.href, { headers }) return fetch(url.href, { headers }).then(resp => resp.json());
.then(resp => resp.json()) };
.then(response => response.link)
}
// - - - Seasoned user endpoints - - - // - - - Seasoned user endpoints - - -
const register = (username, password) => { const register = (username, password) => {
const url = new URL('v1/user', SEASONED_URL) const url = new URL("v1/user", SEASONED_URL);
const options = { const options = {
method: 'POST', method: "POST",
headers: { 'Content-Type': 'application/json' }, headers: AUTHORIZATION_HEADERS(),
body: JSON.stringify({ username, password }) body: JSON.stringify({ username, password })
} };
return fetch(url.href, options) return fetch(url.href, options)
.then(resp => resp.json()) .then(resp => resp.json())
.catch(error => { .catch(error => {
console.error('Unexpected error occured before receiving response. Error:', error) console.error(
"Unexpected error occured before receiving response. Error:",
error
);
// TODO log to sentry the issue here // TODO log to sentry the issue here
throw error throw error;
}) });
} };
const login = (username, password, throwError=false) => { const login = (username, password, throwError = false) => {
const url = new URL('v1/user/login', SEASONED_URL) const url = new URL("v1/user/login", SEASONED_URL);
const options = { const options = {
method: 'POST', method: "POST",
headers: { 'Content-Type': 'application/json' }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username, password }) body: JSON.stringify({ username, password })
} };
return fetch(url.href, options) return fetch(url.href, options).then(resp => {
.then(resp => { if (resp.status == 200) return resp.json();
if (resp.status == 200)
return resp.json();
if (throwError) if (throwError) throw resp;
throw resp; else console.error("Error occured when trying to sign in.\nError:", resp);
else });
console.error("Error occured when trying to sign in.\nError:", resp); };
})
}
const getSettings = () => { const getSettings = () => {
const settingsExists = (value) => { const settingsExists = value => {
if (value instanceof Object && value.hasOwnProperty('settings')) if (value instanceof Object && value.hasOwnProperty("settings"))
return value; return value;
throw "Settings does not exist in response object."; throw "Settings does not exist in response object.";
} };
const commitSettingsToStore = (response) => { const commitSettingsToStore = response => {
store.dispatch('userModule/setSettings', response.settings) store.dispatch("user/setSettings", response.settings);
return response return response;
} };
const url = new URL('v1/user/settings', SEASONED_URL) const url = new URL("v1/user/settings", SEASONED_URL);
const authorization_token = localStorage.getItem('token') return fetch(url.href, { headers: AUTHORIZATION_HEADERS() })
const headers = authorization_token ? {
'Authorization': authorization_token,
'Content-Type': 'application/json'
} : {}
return fetch(url.href, { headers })
.then(resp => resp.json()) .then(resp => resp.json())
.then(settingsExists) .then(settingsExists)
.then(commitSettingsToStore) .then(commitSettingsToStore)
.then(response => response.settings) .then(response => response.settings)
.catch(error => { console.log('api error getting user settings'); throw error }) .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'
} : {}
const updateSettings = settings => {
const url = new URL("v1/user/settings", SEASONED_URL);
return fetch(url.href, { return fetch(url.href, {
method: 'PUT', method: "PUT",
headers, headers: AUTHORIZATION_HEADERS(),
body: JSON.stringify(settings) body: JSON.stringify(settings)
}) })
.then(resp => resp.json()) .then(resp => resp.json())
.catch(error => { console.log('api error updating user settings'); throw error }) .catch(error => {
} console.log("api error updating user settings");
throw error;
});
};
// - - - Authenticate with plex - - - // - - - Authenticate with plex - - -
const linkPlexAccount = (username, password) => { const linkPlexAccount = (username, password) => {
const url = new URL('v1/user/link_plex', SEASONED_URL) const url = new URL("v1/user/link_plex", SEASONED_URL);
const body = { username, password } const body = { username, password };
const headers = {
'Content-Type': 'application/json',
authorization: storage.token
}
return fetch(url.href, { return fetch(url.href, {
method: 'POST', method: "POST",
headers, headers: AUTHORIZATION_HEADERS(),
body: JSON.stringify(body) body: JSON.stringify(body)
}) })
.then(resp => resp.json()) .then(resp => resp.json())
.catch(error => { console.error(`api error linking plex account: ${username}`); throw error }) .catch(error => {
} console.error(`api error linking plex account: ${username}`);
throw error;
});
};
const unlinkPlexAccount = (username, password) => { const unlinkPlexAccount = (username, password) => {
const url = new URL('v1/user/unlink_plex', SEASONED_URL) const url = new URL("v1/user/unlink_plex", SEASONED_URL);
const headers = {
'Content-Type': 'application/json',
authorization: storage.token
}
return fetch(url.href, { return fetch(url.href, {
method: 'POST', method: "POST",
headers headers: AUTHORIZATION_HEADERS()
}) })
.then(resp => resp.json()) .then(resp => resp.json())
.catch(error => { console.error(`api error unlinking plex account: ${username}`); throw error }) .catch(error => {
} console.error(`api error unlinking plex account: ${username}`);
throw error;
});
};
// - - - User graphs - - - // - - - User graphs - - -
const fetchChart = (urlPath, days, chartType) => { const fetchChart = (urlPath, days, chartType) => {
const url = new URL('v1/user' + urlPath, SEASONED_URL) const url = new URL("v1/user" + urlPath, SEASONED_URL);
url.searchParams.append('days', days) url.searchParams.append("days", days);
url.searchParams.append('y_axis', chartType) url.searchParams.append("y_axis", chartType);
const authorization_token = localStorage.getItem('token') return fetch(url.href, { headers: AUTHORIZATION_HEADERS() })
const headers = authorization_token ? {
'Authorization': authorization_token,
'Content-Type': 'application/json'
} : {}
return fetch(url.href, { headers })
.then(resp => resp.json()) .then(resp => resp.json())
.catch(error => { console.log('api error fetching chart'); throw error }) .catch(error => {
} console.log("api error fetching chart");
throw error;
});
};
// - - - Random emoji - - - // - - - Random emoji - - -
const getEmoji = () => { const getEmoji = () => {
const url = new URL('v1/emoji', SEASONED_URL) const url = new URL("v1/emoji", SEASONED_URL);
return fetch(url.href) return fetch(url.href)
.then(resp => resp.json()) .then(resp => resp.json())
.catch(error => { console.log('api error getting emoji'); throw error }) .catch(error => {
} console.log("api error getting emoji");
throw error;
});
};
// - - - ELASTIC SEARCH - - - // - - - ELASTIC SEARCH - - -
// This elastic index contains titles mapped to ids. Lightning search // This elastic index contains titles mapped to ids. Lightning search
@@ -426,44 +440,41 @@ const getEmoji = () => {
* @param {string} query * @param {string} query
* @returns {object} List of movies and shows matching query * @returns {object} List of movies and shows matching query
*/ */
const elasticSearchMoviesAndShows = (query) => { const elasticSearchMoviesAndShows = query => {
const url = new URL(path.join(ELASTIC_INDEX, '/_search'), ELASTIC_URL) const url = new URL(path.join(ELASTIC_INDEX, "/_search"), ELASTIC_URL);
const headers = {
'Content-Type': 'application/json'
}
const body = { const body = {
"sort" : [ sort: [{ popularity: { order: "desc" } }, "_score"],
{ "popularity" : {"order" : "desc"}}, query: {
"_score" bool: {
], should: [
"query": { {
"bool": { match_phrase_prefix: {
"should": [{ original_name: query
"match_phrase_prefix": { }
"original_name": query },
{
match_phrase_prefix: {
original_title: query
}
} }
}, ]
{
"match_phrase_prefix": {
"original_title": query
}
}]
} }
}, },
"size": 6 size: 22
} };
return fetch(url.href, { return fetch(url.href, {
method: 'POST', method: "POST",
headers: headers, headers: AUTHORIZATION_HEADERS(),
body: JSON.stringify(body) body: JSON.stringify(body)
}) })
.then(resp => resp.json()) .then(resp => resp.json())
.catch(error => { console.log(`api error searching elasticsearch: ${query}`); throw error }) .catch(error => {
} console.log(`api error searching elasticsearch: ${query}`);
throw error;
});
};
export { export {
getMovie, getMovie,
@@ -477,6 +488,7 @@ export {
addMagnet, addMagnet,
request, request,
watchLink, watchLink,
movieImages,
getRequestStatus, getRequestStatus,
linkPlexAccount, linkPlexAccount,
unlinkPlexAccount, unlinkPlexAccount,
@@ -487,4 +499,4 @@ export {
fetchChart, fetchChart,
getEmoji, getEmoji,
elasticSearchMoviesAndShows elasticSearchMoviesAndShows
} };

View File

@@ -3,26 +3,31 @@
<section class="not-found"> <section class="not-found">
<h1 class="not-found__title">Page Not Found</h1> <h1 class="not-found__title">Page Not Found</h1>
</section> </section>
<seasoned-button class="button" @click="goBack">go back to previous page</seasoned-button> <seasoned-button class="button" @click="goBack"
>go back to previous page</seasoned-button
>
</div> </div>
</template> </template>
<script> <script>
import store from '@/store' import { mapActions, mapGetters } from "vuex";
import SeasonedButton from '@/components/ui/SeasonedButton' import SeasonedButton from "@/components/ui/SeasonedButton";
export default { export default {
components: { SeasonedButton }, components: { SeasonedButton },
computed: {
...mapGetters("popup", ["isOpen"])
},
methods: { methods: {
...mapActions("popup", ["close"]),
goBack() { goBack() {
this.$router.go(-1) this.$router.go(-1);
} }
}, },
created() { created() {
if (this.$popup.isOpen == true) if (this.isOpen) this.close();
this.$popup.close()
} }
} };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -47,7 +52,7 @@ export default {
.not-found { .not-found {
display: flex; display: flex;
height: calc(100vh - var(--header-size)); height: calc(100vh - var(--header-size));
background: url('~assets/pulp-fiction.jpg') no-repeat 50% 50%; background: url("~assets/pulp-fiction.jpg") no-repeat 50% 50%;
background-size: cover; background-size: cover;
align-items: center; align-items: center;
flex-direction: column; flex-direction: column;

View File

@@ -1,64 +1,58 @@
<template> <template>
<div class="movie-popup" @click="$popup.close()"> <div v-if="isOpen" class="movie-popup" @click="close">
<div class="movie-popup__box" @click.stop> <div class="movie-popup__box" @click.stop>
<movie :id="id" :type="type"></movie> <movie :id="id" :type="type"></movie>
<button class="movie-popup__close" @click="$popup.close()"></button> <button class="movie-popup__close" @click="close"></button>
</div> </div>
<i class="loader"></i> <i class="loader"></i>
</div> </div>
</template> </template>
<script> <script>
import { mapActions, mapGetters } from "vuex";
import Movie from "./Movie"; import Movie from "./Movie";
export default { export default {
props: { components: { Movie },
id: { computed: {
type: Number, ...mapGetters("popup", ["isOpen", "id", "type"])
required: true },
}, watch: {
type: { isOpen(value) {
type: String, value
required: true ? document.getElementsByTagName("body")[0].classList.add("no-scroll")
: document
.getElementsByTagName("body")[0]
.classList.remove("no-scroll");
} }
}, },
components: { Movie },
methods: { methods: {
...mapActions("popup", ["close", "open"]),
checkEventForEscapeKey(event) { checkEventForEscapeKey(event) {
if (event.keyCode == 27) { if (event.keyCode == 27) this.close();
this.$popup.close();
}
},
updateQueryParams(id = false) {
const params = new URLSearchParams(window.location.search);
if (params.has("movie")) {
params.delete("movie");
}
if (id) {
params.append("movie", id);
}
window.history.replaceState(
{},
"search",
`${window.location.protocol}//${window.location.hostname}${
window.location.port ? `:${window.location.port}` : ""
}${window.location.pathname}${
params.toString().length ? `?${params}` : ""
}`
);
} }
}, },
created() { created() {
this.updateQueryParams(this.id); 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";
}
if (id && type) {
this.open({ id, type });
}
window.addEventListener("keyup", this.checkEventForEscapeKey); window.addEventListener("keyup", this.checkEventForEscapeKey);
document.getElementsByTagName("body")[0].classList.add("no-scroll");
}, },
beforeDestroy() { beforeDestroy() {
this.updateQueryParams();
window.removeEventListener("keyup", this.checkEventForEscapeKey); window.removeEventListener("keyup", this.checkEventForEscapeKey);
document.getElementsByTagName("body")[0].classList.remove("no-scroll");
} }
}; };
</script> </script>

View File

@@ -4,7 +4,7 @@
<img <img
class="movie-item__img" class="movie-item__img"
ref="poster-image" ref="poster-image"
@click="openMoviePopup(movie.id, movie.type)" @click="openMoviePopup"
:alt="posterAltText" :alt="posterAltText"
:data-src="poster" :data-src="poster"
src="~assets/placeholder.png" src="~assets/placeholder.png"
@@ -27,6 +27,7 @@
</template> </template>
<script> <script>
import { mapActions } from "vuex";
import img from "../directives/v-image"; import img from "../directives/v-image";
export default { export default {
@@ -101,8 +102,12 @@ export default {
imageObserver.observe(poster); imageObserver.observe(poster);
}, },
methods: { methods: {
openMoviePopup(id, type) { ...mapActions("popup", ["open"]),
this.$popup.open(id, type); openMoviePopup() {
this.open({
id: this.movie.id,
type: this.movie.type
});
} }
} }
}; };

View File

@@ -1,6 +1,6 @@
<template> <template>
<section class="profile"> <section class="profile">
<div class="profile__content" v-if="userLoggedIn"> <div class="profile__content" v-if="loggedIn">
<header class="profile__header"> <header class="profile__header">
<h2 class="profile__title">{{ emoji }} Welcome {{ username }}</h2> <h2 class="profile__title">{{ emoji }} Welcome {{ username }}</h2>
@@ -12,7 +12,7 @@
showActivity ? "hide activity" : "show activity" showActivity ? "hide activity" : "show activity"
}}</seasoned-button> }}</seasoned-button>
<seasoned-button @click="logOut">Log out</seasoned-button> <seasoned-button @click="_logout">Log out</seasoned-button>
</div> </div>
</header> </header>
@@ -24,7 +24,7 @@
<results-list v-if="results" :results="results" /> <results-list v-if="results" :results="results" />
</div> </div>
<section class="not-found" v-if="!userLoggedIn"> <section class="not-found" v-if="!loggedIn">
<div class="not-found__content"> <div class="not-found__content">
<h2 class="not-found__title">Authentication Request Failed</h2> <h2 class="not-found__title">Authentication Request Failed</h2>
<router-link :to="{ name: 'signin' }" exact title="Sign in here"> <router-link :to="{ name: 'signin' }" exact title="Sign in here">
@@ -36,21 +36,19 @@
</template> </template>
<script> <script>
import storage from "@/storage"; import { mapGetters, mapActions } from "vuex";
import store from "@/store";
import ListHeader from "@/components/ListHeader"; import ListHeader from "@/components/ListHeader";
import ResultsList from "@/components/ResultsList"; import ResultsList from "@/components/ResultsList";
import Settings from "@/components/Settings"; import Settings from "@/components/Settings";
import Activity from "@/components/ActivityPage"; import Activity from "@/components/ActivityPage";
import SeasonedButton from "@/components/ui/SeasonedButton"; import SeasonedButton from "@/components/ui/SeasonedButton";
import { getEmoji, getUserRequests } from "@/api"; import { getEmoji, getUserRequests, getSettings } from "@/api";
export default { export default {
components: { ListHeader, ResultsList, Settings, Activity, SeasonedButton }, components: { ListHeader, ResultsList, Settings, Activity, SeasonedButton },
data() { data() {
return { return {
userLoggedIn: "",
emoji: "", emoji: "",
results: undefined, results: undefined,
totalResults: undefined, totalResults: undefined,
@@ -59,16 +57,17 @@ export default {
}; };
}, },
computed: { computed: {
...mapGetters("user", ["loggedIn", "username", "settings"]),
resultCount() { resultCount() {
if (this.results === undefined) return; if (this.results === undefined) return;
const loadedResults = this.results.length; const loadedResults = this.results.length;
const totalResults = this.totalResults < 10000 ? this.totalResults : "∞"; const totalResults = this.totalResults < 10000 ? this.totalResults : "∞";
return `${loadedResults} of ${totalResults} results`; return `${loadedResults} of ${totalResults} results`;
}, }
username: () => store.getters["userModule/username"]
}, },
methods: { methods: {
...mapActions("user", ["logout", "updateSettings"]),
toggleSettings() { toggleSettings() {
this.showSettings = this.showSettings ? false : true; this.showSettings = this.showSettings ? false : true;
@@ -98,16 +97,20 @@ export default {
this.showActivity = this.showActivity == true ? false : true; this.showActivity = this.showActivity == true ? false : true;
this.updateQueryParams("activity", this.showActivity); this.updateQueryParams("activity", this.showActivity);
}, },
logOut() { _logout() {
this.$router.push("logout"); this.logout();
this.$router.push("home");
} }
}, },
created() { created() {
if (!localStorage.getItem("token")) { if (!this.settings) {
this.userLoggedIn = false; getSettings().then(resp => {
} else { const { settings } = resp;
this.userLoggedIn = true; if (settings) updateSettings(settings);
});
}
if (this.loggedIn) {
this.showSettings = window.location.toString().includes("settings=true"); this.showSettings = window.location.toString().includes("settings=true");
this.showActivity = window.location.toString().includes("activity=true"); this.showActivity = window.location.toString().includes("activity=true");

View File

@@ -1,47 +1,77 @@
<template> <template>
<section class="profile"> <section class="profile">
<div class="profile__content" v-if="userLoggedIn"> <div class="profile__content" v-if="loggedIn">
<section class='settings'> <section class="settings">
<h3 class='settings__header'>Plex account</h3> <h3 class="settings__header">Plex account</h3>
<div v-if="!hasPlexUser"> <div v-if="!plexId">
<span class="settings__info">Sign in to your plex account to get information about recently added movies and to see your watch history</span> <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"> <form class="form">
<seasoned-input placeholder="plex username" icon="Email" :value.sync="plexUsername"/> <seasoned-input
<seasoned-input placeholder="plex password" icon="Keyhole" type="password" placeholder="plex username"
:value.sync="plexPassword" @submit="authenticatePlex" /> 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> <seasoned-button @click="authenticatePlex"
>link plex account</seasoned-button
>
</form> </form>
</div> </div>
<div v-else> <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> <span class="settings__info"
<seasoned-button @click="unauthenticatePlex">un-link plex account</seasoned-button> >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> </div>
<seasoned-messages :messages.sync="messages" /> <seasoned-messages :messages.sync="messages" />
<hr class='setting__divider'> <hr class="setting__divider" />
<h3 class='settings__header'>Change password</h3> <h3 class="settings__header">Change password</h3>
<form class="form"> <form class="form">
<seasoned-input placeholder="new password" icon="Keyhole" type="password" <seasoned-input
:value.sync="newPassword" /> placeholder="new password"
icon="Keyhole"
type="password"
:value.sync="newPassword"
/>
<seasoned-input placeholder="repeat new password" icon="Keyhole" type="password" <seasoned-input
:value.sync="newPasswordRepeat" /> placeholder="repeat new password"
icon="Keyhole"
type="password"
:value.sync="newPasswordRepeat"
/>
<seasoned-button @click="changePassword">change password</seasoned-button> <seasoned-button @click="changePassword"
>change password</seasoned-button
>
</form> </form>
<hr class='setting__divider'> <hr class="setting__divider" />
</section> </section>
</div> </div>
<section class="not-found" v-else> <section class="not-found" v-else>
<div class="not-found__content"> <div class="not-found__content">
<h2 class="not-found__title">Authentication Request Failed</h2> <h2 class="not-found__title">Authentication Request Failed</h2>
<router-link :to="{name: 'signin'}" exact title="Sign in here"> <router-link :to="{ name: 'signin' }" exact title="Sign in here">
<button class="not-found__button button">Sign In</button> <button class="not-found__button button">Sign In</button>
</router-link> </router-link>
</div> </div>
@@ -50,82 +80,68 @@
</template> </template>
<script> <script>
import store from '@/store' import { mapGetters, mapState, mapActions } from "vuex";
import storage from '@/storage' import storage from "@/storage";
import SeasonedInput from '@/components/ui/SeasonedInput' import SeasonedInput from "@/components/ui/SeasonedInput";
import SeasonedButton from '@/components/ui/SeasonedButton' import SeasonedButton from "@/components/ui/SeasonedButton";
import SeasonedMessages from '@/components/ui/SeasonedMessages' import SeasonedMessages from "@/components/ui/SeasonedMessages";
import { getSettings, updateSettings, linkPlexAccount, unlinkPlexAccount } from '@/api' import { linkPlexAccount, unlinkPlexAccount, getSettings } from "@/api";
export default { export default {
components: { SeasonedInput, SeasonedButton, SeasonedMessages }, components: { SeasonedInput, SeasonedButton, SeasonedMessages },
data(){ data() {
return{ return {
userLoggedIn: '',
messages: [], messages: [],
plexUsername: null, plexUsername: null,
plexPassword: null, plexPassword: null,
newPassword: null, newPassword: null,
newPasswordRepeat: null, newPasswordRepeat: null,
emoji: null emoji: null
} };
}, },
computed: { computed: {
hasPlexUser: function() { ...mapGetters("user", ["loggedIn", "plexId", "settings"])
return this.settings && this.settings['plex_userid']
},
settings: {
get: () => {
return store.getters['userModule/settings']
},
set: function(newSettings) {
store.dispatch('userModule/setSettings', newSettings)
}
}
}, },
methods: { methods: {
setValue(l, t) { ...mapActions("user", ["login", "updateSettings"]),
this[l] = t
},
changePassword() { changePassword() {
return return;
},
created() {
if (!this.settings) {
console.log("settings does not exists.", this.settings);
getSettings().then(resp => {
const { settings } = resp;
if (settings) updateSettings(settings);
});
}
}, },
async authenticatePlex() { async authenticatePlex() {
let username = this.plexUsername let username = this.plexUsername;
let password = this.plexPassword let password = this.plexPassword;
const response = await linkPlexAccount(username, password) const { success, message } = await linkPlexAccount(username, password);
this.messages.push({ this.messages.push({
type: response.success ? 'success' : 'error', type: success ? "success" : "error",
title: response.success ? 'Authenticated with plex' : 'Something went wrong', title: success ? "Authenticated with plex" : "Something went wrong",
message: response.message message: message
}) });
if (response.success)
getSettings().then(settings => this.settings = settings)
}, },
async unauthenticatePlex() { async unauthenticatePlex() {
const response = await unlinkPlexAccount() const response = await unlinkPlexAccount();
this.messages.push({ this.messages.push({
type: response.success ? 'success' : 'error', type: response.success ? "success" : "error",
title: response.success ? 'Unlinked plex account ' : 'Something went wrong', title: response.success
? "Unlinked plex account "
: "Something went wrong",
message: response.message 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> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -133,7 +149,7 @@ export default {
@import "./src/scss/media-queries"; @import "./src/scss/media-queries";
a { a {
text-decoration: none; text-decoration: none;
} }
// DUPLICATE CODE // DUPLICATE CODE
@@ -142,22 +158,22 @@ a {
margin-top: 1rem; margin-top: 1rem;
} }
&__group{ &__group {
justify-content: unset; justify-content: unset;
&__input-icon { &__input-icon {
margin-top: 8px; margin-top: 8px;
height: 22px; height: 22px;
width: 22px; width: 22px;
} }
&-input { &-input {
padding: 10px 5px 10px 45px; padding: 10px 5px 10px 45px;
height: 40px; height: 40px;
font-size: 17px; font-size: 17px;
width: 75%; width: 75%;
@include desktop-min { @include desktop-min {
width: 400px; width: 400px;
} }
} }
} }
} }
.settings { .settings {
@@ -167,32 +183,32 @@ a {
padding: 1rem; padding: 1rem;
} }
&__header { &__header {
margin: 0; margin: 0;
line-height: 16px; line-height: 16px;
color: $text-color; color: $text-color;
font-weight: 300; font-weight: 300;
margin-bottom: 20px; margin-bottom: 20px;
text-transform: uppercase; text-transform: uppercase;
} }
&__info { &__info {
display: block; display: block;
margin-bottom: 25px; margin-bottom: 25px;
} }
hr { hr {
display: block; display: block;
height: 1px; height: 1px;
border: 0; border: 0;
border-bottom: 1px solid $text-color-50; border-bottom: 1px solid $text-color-50;
margin-top: 30px; margin-top: 30px;
margin-bottom: 70px; margin-bottom: 70px;
margin-left: 20px; margin-left: 20px;
width: 96%; width: 96%;
text-align: left; text-align: left;
} }
span { span {
font-weight: 200; font-weight: 200;
size: 16px; size: 16px;
} }
} }
</style> </style>

View File

@@ -2,88 +2,94 @@
<section> <section>
<h1>Sign in</h1> <h1>Sign in</h1>
<seasoned-input placeholder="username" <seasoned-input
icon="Email" placeholder="username"
type="email" icon="Email"
@enter="submit" type="email"
:value.sync="username" /> @enter="submit"
<seasoned-input placeholder="password" icon="Keyhole" type="password" :value.sync="password" @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> <seasoned-button @click="submit">sign in</seasoned-button>
<router-link class="link" to="/register">Don't have a user? Register here</router-link> <router-link class="link" to="/register"
>Don't have a user? Register here</router-link
>
<seasoned-messages :messages.sync="messages"></seasoned-messages> <seasoned-messages :messages.sync="messages"></seasoned-messages>
</section> </section>
</template> </template>
<script> <script>
import { login } from '@/api' import { mapActions } from "vuex";
import storage from '../storage' import { login } from "@/api";
import SeasonedInput from '@/components/ui/SeasonedInput' import storage from "../storage";
import SeasonedButton from '@/components/ui/SeasonedButton' import SeasonedInput from "@/components/ui/SeasonedInput";
import SeasonedMessages from '@/components/ui/SeasonedMessages' import SeasonedButton from "@/components/ui/SeasonedButton";
import { parseJwt } from '@/utils' import SeasonedMessages from "@/components/ui/SeasonedMessages";
export default { export default {
components: { SeasonedInput, SeasonedButton, SeasonedMessages }, components: { SeasonedInput, SeasonedButton, SeasonedMessages },
data(){ data() {
return{ return {
messages: [], messages: [],
username: null, username: null,
password: null password: null
} };
}, },
methods: { methods: {
setValue(l, t) { ...mapActions("user", ["login"]),
this[l] = t
},
submit() { submit() {
this.messages = []; this.messages = [];
let username = this.username; let { username, password } = this;
let password = this.password;
if (username == null || username.length == 0) { if (!username || username.length == 0) {
this.messages.push({ type: 'error', title: 'Missing username' }) this.messages.push({ type: "error", title: "Missing username" });
return return;
} }
if (password == null || password.length == 0) { if (!password || password.length == 0) {
this.messages.push({ type: 'error', title: 'Missing password' }) this.messages.push({ type: "error", title: "Missing password" });
return return;
} }
this.signin(username, password) this.signin(username, password);
}, },
signin(username, password) { signin(username, password) {
login(username, password, true) login(username, password, true)
.then(data => { .then(data => {
if (data.success){ if (data.success && this.login(data.token)) {
const jwtData = parseJwt(data.token) this.$router.push({ name: "profile" });
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 => { .catch(error => {
if (error.status === 401) { if (error.status === 401) {
this.messages.push({ type: 'error', title: 'Access denied', message: 'Incorrect username or password' }) this.messages.push({
} type: "error",
else { title: "Access denied",
this.messages.push({ type: 'error', title: 'Unexpected error', message: error.message }) message: "Incorrect username or password"
});
} else {
this.messages.push({
type: "error",
title: "Unexpected error",
message: error.message
});
} }
}); });
} }
}, },
created(){ created() {
document.title = 'Sign in' + storage.pageTitlePostfix; document.title = "Sign in" + storage.pageTitlePostfix;
storage.backTitle = document.title; storage.backTitle = document.title;
} }
} };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -1,28 +1,29 @@
import Vue from 'vue' import Vue from "vue";
import VueRouter from 'vue-router' import VueRouter from "vue-router";
import axios from 'axios' import axios from "axios";
import router from './routes' import router from "./routes";
import store from './store' import store from "./store";
import Toast from './plugins/Toast' import Toast from "./plugins/Toast";
import DataTablee from 'vue-data-tablee' import DataTablee from "vue-data-tablee";
import VModal from 'vue-js-modal' import VModal from "vue-js-modal";
import App from './App.vue' import App from "./App.vue";
window.eventHub = new Vue(); window.eventHub = new Vue();
Vue.use(VueRouter) Vue.use(VueRouter);
Vue.use(Toast) Vue.use(Toast);
Vue.use(DataTablee) Vue.use(DataTablee);
Vue.use(VModal, { dialog: true }) Vue.use(VModal, { dialog: true });
store.dispatch('darkmodeModule/findAndSetDarkmodeSupported') store.dispatch("darkmodeModule/findAndSetDarkmodeSupported");
store.dispatch("user/initFromLocalStorage");
new Vue({ new Vue({
el: '#app', el: "#app",
router, router,
store, store,
components: { App }, components: { App },
template: '<App />' template: "<App />"
}) });

59
src/modules/popup.js Normal file
View File

@@ -0,0 +1,59 @@
const removeIncludedQueryParams = (params, key) => {
if (params.has(key)) params.delete(key);
return params;
};
const updateQueryParams = (id = null, type = null) => {
let params = new URLSearchParams(window.location.search);
params = removeIncludedQueryParams(params, "movie");
params = removeIncludedQueryParams(params, "show");
if (id && type === "movie") {
params.append("movie", id);
}
if (id && type === "show") {
params.append("show", id);
}
let url = `${window.location.protocol}//${window.location.hostname}${
window.location.port ? `:${window.location.port}` : ""
}${window.location.pathname}${params.toString().length ? `?${params}` : ""}`;
window.history.replaceState({}, "search", url);
};
export default {
namespaced: true,
state: {
id: null,
type: null,
open: false
},
getters: {
isOpen: state => state.open,
id: state => state.id,
type: state => state.type
},
mutations: {
SET_OPEN: (state, { id, type }) => {
state.id = id;
state.type = type;
state.open = true;
},
SET_CLOSE: state => {
state.id = null;
state.type = null;
state.open = false;
}
},
actions: {
open: ({ commit }, { id, type = "movie" }) => {
commit("SET_OPEN", { id, type });
updateQueryParams(id, type);
},
close: ({ commit }) => {
commit("SET_CLOSE");
updateQueryParams(); // reset
}
}
};

103
src/modules/user.js Normal file
View File

@@ -0,0 +1,103 @@
import { refreshToken } from "@/api";
import { parseJwt } from "@/utils";
function setLocalStorageByKey(key, value) {
if (value instanceof Object || value instanceof Array) {
value = JSON.stringify(value);
}
const buff = Buffer.from(value);
const encodedValue = buff.toString("base64");
localStorage.setItem(key, encodedValue);
}
function getLocalStorageByKey(key) {
const encodedValue = localStorage.getItem(key);
if (encodedValue == null) {
return null;
}
const buff = new Buffer(encodedValue, "base64");
const value = buff.toString("utf-8");
try {
return JSON.parse(value);
} catch {
return value;
}
}
export default {
namespaced: true,
state: {
token: null,
admin: false,
settings: null,
username: null
},
getters: {
username: state => state.username,
settings: state => state.settings,
token: state => state.token,
loggedIn: state => state && state.username !== null,
admin: state => state && state.admin !== null,
plexId: state => {
if (state && state.settings && state.settings.plex_userid)
return state.settings.plex_userid;
return null;
}
},
mutations: {
SET_TOKEN: (state, token) => (state.token = token),
SET_USERNAME: (state, username) => (state.username = username),
SET_SETTINGS: (state, settings) => (state.settings = settings),
SET_ADMIN: (state, admin) => (state.admin = admin),
LOGOUT: state => {
state.token = null;
state.username = null;
state.settings = null;
state.admin = false;
localStorage.removeItem("token");
}
},
actions: {
initFromLocalStorage: async ({ dispatch }) => {
const token = getLocalStorageByKey("token");
if (token) await dispatch("setupStateFromToken", token);
const settings = getLocalStorageByKey("settings");
if (settings) await dispatch("setSettings", settings);
},
setupStateFromToken: ({ commit }, token) => {
try {
const jwtData = parseJwt(token);
const { username, admin } = jwtData;
if (!username) {
return false;
}
commit("SET_TOKEN", token);
commit("SET_USERNAME", username);
commit("SET_ADMIN", admin != undefined ? true : false);
return true;
} catch (error) {
console.log("Unable to parse JWT, failed with error:", error);
return false;
}
},
setSettings: ({ commit }, settings) => {
if (!(settings instanceof Object)) {
throw "Parameter is not a object.";
}
commit("SET_SETTINGS", settings);
setLocalStorageByKey("settings", settings);
},
logout: ({ commit }) => commit("LOGOUT"),
login: async ({ dispatch }, token) => {
const loggedIn = await dispatch("setupStateFromToken", token);
if (loggedIn) setLocalStorageByKey("token", token);
return loggedIn;
}
}
};

View File

@@ -1,112 +0,0 @@
import { getSettings } from '@/api'
function setLocalStorageByKey(key, value) {
if (value instanceof Object || value instanceof Array) {
value = JSON.stringify(value)
}
const buff = Buffer.from(value)
const encodedValue = buff.toString('base64')
localStorage.setItem(key, encodedValue)
}
function getLocalStorageByKey(key) {
const encodedValue = localStorage.getItem(key)
if (encodedValue == null) {
return undefined
}
const buff = new Buffer(encodedValue, 'base64')
const value = buff.toString('utf-8')
try {
return JSON.parse(value)
} catch {
return value
}
}
const ifMissingSettingsAndTokenExistsFetchSettings =
() => getLocalStorageByKey('token') ? getSettings() : null
export default {
namespaced: true,
state: {
admin: false,
settings: undefined,
username: undefined,
plex_userid: undefined
},
getters: {
admin: (state) => {
return state.admin
},
settings: (state, foo, bar) => {
console.log('is this called?')
const settings = state.settings || getLocalStorageByKey('settings')
if (settings instanceof Object) {
return settings
}
ifMissingSettingsAndTokenExistsFetchSettings()
return undefined
},
username: (state) => {
const settings = state.settings || getLocalStorageByKey('settings')
if (settings instanceof Object && settings.hasOwnProperty('user_name')) {
return settings.user_name
}
ifMissingSettingsAndTokenExistsFetchSettings()
return undefined
},
plex_userid: (state) => {
const settings = state.settings || getLocalStorageByKey('settings')
console.log('plex_userid from store', settings)
if (settings instanceof Object && settings.hasOwnProperty('plex_userid')) {
return settings.plex_userid
}
ifMissingSettingsAndTokenExistsFetchSettings()
return undefined
},
isPlexAuthenticated: (state) => {
const settings = state.settings || getLocalStorageByKey('settings')
if (settings == null)
return false
const hasPlexId = settings['plex_userid']
return hasPlexId != null ? true : false
}
},
mutations: {
SET_ADMIN: (state, isAdmin) => {
state.admin = isAdmin
},
SET_USERNAME: (state, username) => {
state.username = username
console.log('username')
setLocalStorageByKey('username', username)
},
SET_SETTINGS: (state, settings) => {
state.settings = settings
console.log('settings')
setLocalStorageByKey('settings', settings)
}
},
actions: {
setAdmin: ({commit}, isAdmin) => {
if (!(isAdmin instanceof Object)) {
throw "Parameter is not a boolean value."
}
commit('SET_ADMIN', isAdmin)
},
setSettings: ({commit}, settings) => {
console.log('settings input', settings)
if (!(settings instanceof Object)) {
throw "Parameter is not a object."
}
commit('SET_SETTINGS', settings)
}
}
}

View File

@@ -66,17 +66,6 @@ let routes = [
path: "/404", path: "/404",
component: resolve => require(["./components/404.vue"], resolve) component: resolve => require(["./components/404.vue"], resolve)
}, },
{
name: "logout",
path: "/logout",
component: {
template: "<div></div>",
created() {
localStorage.clear();
this.$router.push({ name: "home" });
}
}
},
{ {
path: "*", path: "*",
redirect: "/" redirect: "/"
@@ -94,8 +83,12 @@ const router = new VueRouter({
linkActiveClass: "is-active" linkActiveClass: "is-active"
}); });
const loggedIn = () => store.getters["user/loggedIn"];
const isOpen = () => store.getters["user/isOpen"];
router.beforeEach((to, from, next) => { router.beforeEach((to, from, next) => {
store.dispatch("documentTitle/updateTitle", to.name); store.dispatch("documentTitle/updateTitle", to.name);
if (isOpen()) store.dispatch("popup/close");
// Toggle mobile nav // Toggle mobile nav
if (document.querySelector(".nav__hamburger--active")) { if (document.querySelector(".nav__hamburger--active")) {
@@ -106,7 +99,7 @@ router.beforeEach((to, from, next) => {
} }
if (to.matched.some(record => record.meta.requiresAuth)) { if (to.matched.some(record => record.meta.requiresAuth)) {
if (localStorage.getItem("token") == null) { if (!loggedIn) {
next({ path: "/signin" }); next({ path: "/signin" });
} }
} }

View File

@@ -1,20 +1,22 @@
import Vue from 'vue' import Vue from "vue";
import Vuex from 'vuex' import Vuex from "vuex";
import darkmodeModule from './modules/darkmodeModule' import darkmodeModule from "./modules/darkmodeModule";
import documentTitle from './modules/documentTitle' import documentTitle from "./modules/documentTitle";
import torrentModule from './modules/torrentModule' import torrentModule from "./modules/torrentModule";
import userModule from './modules/userModule' import user from "./modules/user";
import popup from "./modules/popup";
Vue.use(Vuex) Vue.use(Vuex);
const store = new Vuex.Store({ const store = new Vuex.Store({
modules: { modules: {
darkmodeModule, darkmodeModule,
documentTitle, documentTitle,
torrentModule, torrentModule,
userModule user,
popup
} }
}) });
export default store export default store;