Resolved ALL eslint issues for project
This commit is contained in:
@@ -1,17 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<!-- Header and hamburger navigation -->
|
<!-- Header and hamburger navigation -->
|
||||||
<NavigationHeader class="header"></NavigationHeader>
|
<NavigationHeader class="header" />
|
||||||
|
|
||||||
<div class="navigation-icons-gutter desktop-only">
|
<div class="navigation-icons-gutter desktop-only">
|
||||||
<NavigationIcons />
|
<NavigationIcons />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Display the component assigned to the given route (default: home) -->
|
<!-- Display the component assigned to the given route (default: home) -->
|
||||||
<router-view
|
<router-view :key="router.currentRoute.value.path" class="content" />
|
||||||
class="content"
|
|
||||||
:key="router.currentRoute.value.path"
|
|
||||||
></router-view>
|
|
||||||
|
|
||||||
<!-- Popup that will show above existing rendered content -->
|
<!-- Popup that will show above existing rendered content -->
|
||||||
<popup />
|
<popup />
|
||||||
|
|||||||
135
src/api.ts
135
src/api.ts
@@ -1,34 +1,25 @@
|
|||||||
import config from "./config";
|
import config from "./config";
|
||||||
import { IList, IMediaCredits, IPersonCredits } from "./interfaces/IList";
|
import { IList, IMediaCredits, IPersonCredits } from "./interfaces/IList";
|
||||||
|
|
||||||
let { SEASONED_URL, ELASTIC_URL, ELASTIC_INDEX } = config;
|
const { ELASTIC_URL, ELASTIC_INDEX } = config;
|
||||||
if (!SEASONED_URL) {
|
|
||||||
SEASONED_URL = window.location.origin;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO
|
let { SEASONED_URL } = config;
|
||||||
// - Move autorization token and errors here?
|
if (!SEASONED_URL) SEASONED_URL = window.location.origin;
|
||||||
|
|
||||||
const checkStatusAndReturnJson = response => {
|
|
||||||
if (!response.ok) {
|
|
||||||
throw response;
|
|
||||||
}
|
|
||||||
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.
|
||||||
* @param {number} id
|
* @param {number} id
|
||||||
* @param {boolean} [credits=false] Include credits
|
|
||||||
* @returns {object} Tmdb response
|
* @returns {object} Tmdb response
|
||||||
*/
|
*/
|
||||||
const getMovie = (
|
const getMovie = (
|
||||||
id,
|
id,
|
||||||
checkExistance = false,
|
{
|
||||||
credits = false,
|
checkExistance,
|
||||||
release_dates = false
|
credits,
|
||||||
|
releaseDates
|
||||||
|
}: { checkExistance: boolean; credits: boolean; releaseDates?: boolean }
|
||||||
) => {
|
) => {
|
||||||
const url = new URL("/api/v2/movie", SEASONED_URL);
|
const url = new URL("/api/v2/movie", SEASONED_URL);
|
||||||
url.pathname = `${url.pathname}/${id.toString()}`;
|
url.pathname = `${url.pathname}/${id.toString()}`;
|
||||||
@@ -38,14 +29,14 @@ const getMovie = (
|
|||||||
if (credits) {
|
if (credits) {
|
||||||
url.searchParams.append("credits", "true");
|
url.searchParams.append("credits", "true");
|
||||||
}
|
}
|
||||||
if (release_dates) {
|
if (releaseDates) {
|
||||||
url.searchParams.append("release_dates", "true");
|
url.searchParams.append("release_dates", "true");
|
||||||
}
|
}
|
||||||
|
|
||||||
return fetch(url.href)
|
return fetch(url.href)
|
||||||
.then(resp => resp.json())
|
.then(resp => resp.json())
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error(`api error getting movie: ${id}`);
|
console.error(`api error getting movie: ${id}`); // eslint-disable-line no-console
|
||||||
throw error;
|
throw error;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -56,7 +47,14 @@ const getMovie = (
|
|||||||
* @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,
|
||||||
|
credits,
|
||||||
|
releaseDates
|
||||||
|
}: { checkExistance: boolean; credits: boolean; releaseDates?: boolean }
|
||||||
|
) => {
|
||||||
const url = new URL("/api/v2/show", SEASONED_URL);
|
const url = new URL("/api/v2/show", SEASONED_URL);
|
||||||
url.pathname = `${url.pathname}/${id.toString()}`;
|
url.pathname = `${url.pathname}/${id.toString()}`;
|
||||||
if (checkExistance) {
|
if (checkExistance) {
|
||||||
@@ -65,19 +63,18 @@ const getShow = (id, checkExistance = false, credits = false) => {
|
|||||||
if (credits) {
|
if (credits) {
|
||||||
url.searchParams.append("credits", "true");
|
url.searchParams.append("credits", "true");
|
||||||
}
|
}
|
||||||
|
if (releaseDates) {
|
||||||
|
url.searchParams.append("release_dates", "true");
|
||||||
|
}
|
||||||
|
|
||||||
return fetch(url.href)
|
return fetch(url.href)
|
||||||
.then(resp => resp.json())
|
.then(resp => resp.json())
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error(`api error getting show: ${id}`);
|
console.error(`api error getting show: ${id}`); // eslint-disable-line no-console
|
||||||
throw error;
|
throw error;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
function delay(ms) {
|
|
||||||
return new Promise(resolve => setTimeout(resolve, ms));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches tmdb person by id. Can optionally include cast credits in result object.
|
* Fetches tmdb person by id. Can optionally include cast credits in result object.
|
||||||
* @param {number} id
|
* @param {number} id
|
||||||
@@ -94,7 +91,7 @@ const getPerson = (id, credits = false) => {
|
|||||||
return fetch(url.href)
|
return fetch(url.href)
|
||||||
.then(resp => resp.json())
|
.then(resp => resp.json())
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error(`api error getting person: ${id}`);
|
console.error(`api error getting person: ${id}`); // eslint-disable-line no-console
|
||||||
throw error;
|
throw error;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -111,7 +108,7 @@ const getMovieCredits = (id: number): Promise<IMediaCredits> => {
|
|||||||
return fetch(url.href)
|
return fetch(url.href)
|
||||||
.then(resp => resp.json())
|
.then(resp => resp.json())
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error(`api error getting movie: ${id}`);
|
console.error(`api error getting movie: ${id}`); // eslint-disable-line no-console
|
||||||
throw error;
|
throw error;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -128,7 +125,7 @@ const getShowCredits = (id: number): Promise<IMediaCredits> => {
|
|||||||
return fetch(url.href)
|
return fetch(url.href)
|
||||||
.then(resp => resp.json())
|
.then(resp => resp.json())
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error(`api error getting show: ${id}`);
|
console.error(`api error getting show: ${id}`); // eslint-disable-line no-console
|
||||||
throw error;
|
throw error;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -145,7 +142,7 @@ const getPersonCredits = (id: number): Promise<IPersonCredits> => {
|
|||||||
return fetch(url.href)
|
return fetch(url.href)
|
||||||
.then(resp => resp.json())
|
.then(resp => resp.json())
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error(`api error getting person: ${id}`);
|
console.error(`api error getting person: ${id}`); // eslint-disable-line no-console
|
||||||
throw error;
|
throw error;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -156,15 +153,12 @@ const getPersonCredits = (id: number): Promise<IPersonCredits> => {
|
|||||||
* @param {number} [page=1]
|
* @param {number} [page=1]
|
||||||
* @returns {object} Tmdb list response
|
* @returns {object} Tmdb list response
|
||||||
*/
|
*/
|
||||||
const getTmdbMovieListByName = (
|
const getTmdbMovieListByName = (name: string, page = 1): Promise<IList> => {
|
||||||
name: string,
|
const url = new URL(`/api/v2/movie/${name}`, SEASONED_URL);
|
||||||
page: number = 1
|
|
||||||
): Promise<IList> => {
|
|
||||||
const url = new URL("/api/v2/movie/" + name, SEASONED_URL);
|
|
||||||
url.searchParams.append("page", page.toString());
|
url.searchParams.append("page", page.toString());
|
||||||
|
|
||||||
return fetch(url.href).then(resp => resp.json());
|
return fetch(url.href).then(resp => 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 }) // eslint-disable-line no-console
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -172,12 +166,12 @@ const getTmdbMovieListByName = (
|
|||||||
* @param {number} [page=1]
|
* @param {number} [page=1]
|
||||||
* @returns {object} Request response
|
* @returns {object} Request response
|
||||||
*/
|
*/
|
||||||
const getRequests = (page: number = 1) => {
|
const getRequests = (page = 1) => {
|
||||||
const url = new URL("/api/v2/request", SEASONED_URL);
|
const url = new URL("/api/v2/request", SEASONED_URL);
|
||||||
url.searchParams.append("page", page.toString());
|
url.searchParams.append("page", page.toString());
|
||||||
|
|
||||||
return fetch(url.href).then(resp => resp.json());
|
return fetch(url.href).then(resp => 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 }) // eslint-disable-line no-console
|
||||||
};
|
};
|
||||||
|
|
||||||
const getUserRequests = (page = 1) => {
|
const getUserRequests = (page = 1) => {
|
||||||
@@ -206,7 +200,7 @@ const searchTmdb = (query, page = 1, adult = false, mediaType = null) => {
|
|||||||
return fetch(url.href)
|
return fetch(url.href)
|
||||||
.then(resp => resp.json())
|
.then(resp => resp.json())
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error(`api error searching: ${query}, page: ${page}`);
|
console.error(`api error searching: ${query}, page: ${page}`); // eslint-disable-line no-console
|
||||||
throw error;
|
throw error;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -226,7 +220,7 @@ const searchTorrents = query => {
|
|||||||
return fetch(url.href)
|
return fetch(url.href)
|
||||||
.then(resp => resp.json())
|
.then(resp => resp.json())
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error(`api error searching torrents: ${query}`);
|
console.error(`api error searching torrents: ${query}`); // eslint-disable-line no-console
|
||||||
throw error;
|
throw error;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -235,26 +229,26 @@ const searchTorrents = query => {
|
|||||||
* Add magnet to download queue.
|
* Add magnet to download queue.
|
||||||
* @param {string} magnet Magnet link
|
* @param {string} magnet Magnet link
|
||||||
* @param {boolean} name Name of torrent
|
* @param {boolean} name Name of torrent
|
||||||
* @param {boolean} tmdb_id
|
* @param {boolean} tmdbId
|
||||||
* @returns {object} Success/Failure response
|
* @returns {object} Success/Failure response
|
||||||
*/
|
*/
|
||||||
const addMagnet = (magnet: string, name: string, tmdb_id: number | null) => {
|
const addMagnet = (magnet: string, name: string, tmdbId: number | null) => {
|
||||||
const url = new URL("/api/v1/pirate/add", SEASONED_URL);
|
const url = new URL("/api/v1/pirate/add", SEASONED_URL);
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
magnet: magnet,
|
magnet,
|
||||||
name: name,
|
name,
|
||||||
tmdb_id: tmdb_id
|
tmdb_id: tmdbId
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
return fetch(url.href, options)
|
return fetch(url.href, options)
|
||||||
.then(resp => resp.json())
|
.then(resp => resp.json())
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error(`api error adding magnet: ${name} ${error}`);
|
console.error(`api error adding magnet: ${name} ${error}`); // eslint-disable-line no-console
|
||||||
throw error;
|
throw error;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -280,7 +274,7 @@ const request = (id, type) => {
|
|||||||
return fetch(url.href, options)
|
return fetch(url.href, options)
|
||||||
.then(resp => resp.json())
|
.then(resp => resp.json())
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error(`api error requesting: ${id}, type: ${type}`);
|
console.error(`api error requesting: ${id}, type: ${type}`); // eslint-disable-line no-console
|
||||||
throw error;
|
throw error;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -298,16 +292,13 @@ const getRequestStatus = (id, type = undefined) => {
|
|||||||
|
|
||||||
return fetch(url.href)
|
return fetch(url.href)
|
||||||
.then(resp => {
|
.then(resp => {
|
||||||
const status = resp.status;
|
const { status } = resp;
|
||||||
if (status === 200) {
|
if (status === 200) return true;
|
||||||
return true;
|
|
||||||
} else if (status === 404) {
|
const errorMessage = `api error getting request status for id ${id} and type ${type}`;
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(errorMessage);
|
||||||
return false;
|
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));
|
||||||
};
|
};
|
||||||
@@ -341,10 +332,10 @@ const register = (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(
|
const errorMessage =
|
||||||
"Unexpected error occured before receiving response. Error:",
|
"Unexpected error occured before receiving response. Error:";
|
||||||
error
|
// eslint-disable-next-line no-console
|
||||||
);
|
console.error(errorMessage, error);
|
||||||
// TODO log to sentry the issue here
|
// TODO log to sentry the issue here
|
||||||
throw error;
|
throw error;
|
||||||
});
|
});
|
||||||
@@ -359,10 +350,11 @@ const login = (username, password, throwError = false) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return fetch(url.href, options).then(resp => {
|
return fetch(url.href, options).then(resp => {
|
||||||
if (resp.status == 200) return resp.json();
|
if (resp.status === 200) return resp.json();
|
||||||
|
|
||||||
if (throwError) throw resp;
|
if (throwError) throw resp;
|
||||||
else console.error("Error occured when trying to sign in.\nError:", resp);
|
console.error("Error occured when trying to sign in.\nError:", resp); // eslint-disable-line no-console
|
||||||
|
return Promise.reject(resp);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -371,10 +363,11 @@ const logout = (throwError = false) => {
|
|||||||
const options = { method: "POST" };
|
const options = { method: "POST" };
|
||||||
|
|
||||||
return fetch(url.href, options).then(resp => {
|
return fetch(url.href, options).then(resp => {
|
||||||
if (resp.status == 200) return resp.json();
|
if (resp.status === 200) return resp.json();
|
||||||
|
|
||||||
if (throwError) throw resp;
|
if (throwError) throw resp;
|
||||||
else console.error("Error occured when trying to log out.\nError:", resp);
|
console.error("Error occured when trying to log out.\nError:", resp); // eslint-disable-line no-console
|
||||||
|
return Promise.reject(resp);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -384,7 +377,7 @@ const getSettings = () => {
|
|||||||
return fetch(url.href)
|
return fetch(url.href)
|
||||||
.then(resp => resp.json())
|
.then(resp => resp.json())
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.log("api error getting user settings");
|
console.log("api error getting user settings"); // eslint-disable-line no-console
|
||||||
throw error;
|
throw error;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -401,7 +394,7 @@ const updateSettings = settings => {
|
|||||||
return fetch(url.href, options)
|
return fetch(url.href, options)
|
||||||
.then(resp => resp.json())
|
.then(resp => resp.json())
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.log("api error updating user settings");
|
console.log("api error updating user settings"); // eslint-disable-line no-console
|
||||||
throw error;
|
throw error;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -421,7 +414,7 @@ const linkPlexAccount = (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(`api error linking plex account: ${username}`);
|
console.error(`api error linking plex account: ${username}`); // eslint-disable-line no-console
|
||||||
throw error;
|
throw error;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -437,7 +430,7 @@ const unlinkPlexAccount = () => {
|
|||||||
return fetch(url.href, options)
|
return fetch(url.href, options)
|
||||||
.then(resp => resp.json())
|
.then(resp => resp.json())
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error(`api error unlinking your plex account`);
|
console.error(`api error unlinking your plex account`); // eslint-disable-line no-console
|
||||||
throw error;
|
throw error;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -445,13 +438,13 @@ const unlinkPlexAccount = () => {
|
|||||||
// - - - User graphs - - -
|
// - - - User graphs - - -
|
||||||
|
|
||||||
const fetchGraphData = (urlPath, days, chartType) => {
|
const fetchGraphData = (urlPath, days, chartType) => {
|
||||||
const url = new URL("/api/v1/user/" + urlPath, SEASONED_URL);
|
const url = new URL(`/api/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);
|
||||||
|
|
||||||
return fetch(url.href).then(resp => {
|
return fetch(url.href).then(resp => {
|
||||||
if (!resp.ok) {
|
if (!resp.ok) {
|
||||||
console.log("DAMN WE FAILED!", resp);
|
console.log("DAMN WE FAILED!", resp); // eslint-disable-line no-console
|
||||||
throw Error(resp.statusText);
|
throw Error(resp.statusText);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -467,7 +460,7 @@ const getEmoji = () => {
|
|||||||
return fetch(url.href)
|
return fetch(url.href)
|
||||||
.then(resp => resp.json())
|
.then(resp => resp.json())
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.log("api error getting emoji");
|
console.log("api error getting emoji"); // eslint-disable-line no-console
|
||||||
throw error;
|
throw error;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -515,7 +508,7 @@ const elasticSearchMoviesAndShows = (query, count = 22) => {
|
|||||||
return fetch(url.href, options)
|
return fetch(url.href, options)
|
||||||
.then(resp => resp.json())
|
.then(resp => resp.json())
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.log(`api error searching elasticsearch: ${query}`);
|
console.log(`api error searching elasticsearch: ${query}`); // eslint-disable-line no-console
|
||||||
throw error;
|
throw error;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
<ol class="persons">
|
<ol class="persons">
|
||||||
<CastListItem
|
<CastListItem
|
||||||
v-for="credit in cast"
|
v-for="credit in cast"
|
||||||
:creditItem="credit"
|
|
||||||
:key="credit.id"
|
:key="credit.id"
|
||||||
|
:credit-item="credit"
|
||||||
/>
|
/>
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<li class="card">
|
<li class="card">
|
||||||
<a @click="openCastItem">
|
<a @click="openCastItem" @keydown.enter="openCastItem">
|
||||||
<img :src="pictureUrl" />
|
<img :src="pictureUrl" alt="Movie or person poster image" />
|
||||||
<p class="name">{{ creditItem.name || creditItem.title }}</p>
|
<p class="name">{{ creditItem.name || creditItem.title }}</p>
|
||||||
<p class="meta">{{ creditItem.character || creditItem.year }}</p>
|
<p class="meta">{{ creditItem.character || creditItem.year }}</p>
|
||||||
</a>
|
</a>
|
||||||
@@ -25,7 +25,8 @@
|
|||||||
|
|
||||||
if ("profile_path" in props.creditItem && props.creditItem.profile_path) {
|
if ("profile_path" in props.creditItem && props.creditItem.profile_path) {
|
||||||
return baseUrl + props.creditItem.profile_path;
|
return baseUrl + props.creditItem.profile_path;
|
||||||
} else if ("poster" in props.creditItem && props.creditItem.poster) {
|
}
|
||||||
|
if ("poster" in props.creditItem && props.creditItem.poster) {
|
||||||
return baseUrl + props.creditItem.poster;
|
return baseUrl + props.creditItem.poster;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, defineProps, onMounted, watch } from "vue";
|
import { ref, defineProps, onMounted, watch } from "vue";
|
||||||
import {
|
import {
|
||||||
Chart,
|
Chart,
|
||||||
LineElement,
|
LineElement,
|
||||||
@@ -19,6 +19,11 @@
|
|||||||
ChartType
|
ChartType
|
||||||
} from "chart.js";
|
} from "chart.js";
|
||||||
|
|
||||||
|
import type { Ref } from "vue";
|
||||||
|
import { convertSecondsToHumanReadable } from "../utils";
|
||||||
|
import { GraphValueTypes } from "../interfaces/IGraph";
|
||||||
|
import type { IGraphDataset, IGraphData } from "../interfaces/IGraph";
|
||||||
|
|
||||||
Chart.register(
|
Chart.register(
|
||||||
LineElement,
|
LineElement,
|
||||||
BarElement,
|
BarElement,
|
||||||
@@ -31,23 +36,6 @@
|
|||||||
Title,
|
Title,
|
||||||
Tooltip
|
Tooltip
|
||||||
);
|
);
|
||||||
import {} from "chart.js";
|
|
||||||
import type { Ref } from "vue";
|
|
||||||
|
|
||||||
enum GraphValueTypes {
|
|
||||||
Number = "number",
|
|
||||||
Time = "time"
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IGraphDataset {
|
|
||||||
name: string;
|
|
||||||
data: Array<number>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IGraphData {
|
|
||||||
labels: Array<string>;
|
|
||||||
series: Array<IGraphDataset>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
name?: string;
|
name?: string;
|
||||||
@@ -69,8 +57,10 @@
|
|||||||
const graphCanvas: Ref<HTMLCanvasElement> = ref(null);
|
const graphCanvas: Ref<HTMLCanvasElement> = ref(null);
|
||||||
let graphInstance = null;
|
let graphInstance = null;
|
||||||
|
|
||||||
|
/* eslint-disable no-use-before-define */
|
||||||
onMounted(() => generateGraph());
|
onMounted(() => generateGraph());
|
||||||
watch(() => props.data, generateGraph);
|
watch(() => props.data, generateGraph);
|
||||||
|
/* eslint-enable no-use-before-define */
|
||||||
|
|
||||||
const graphTemplates = [
|
const graphTemplates = [
|
||||||
{
|
{
|
||||||
@@ -92,9 +82,9 @@
|
|||||||
tension: 0.4
|
tension: 0.4
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
const gridColor = getComputedStyle(document.documentElement).getPropertyValue(
|
// const gridColor = getComputedStyle(document.documentElement).getPropertyValue(
|
||||||
"--text-color-5"
|
// "--text-color-5"
|
||||||
);
|
// );
|
||||||
|
|
||||||
function hydrateGraphLineOptions(dataset: IGraphDataset, index: number) {
|
function hydrateGraphLineOptions(dataset: IGraphDataset, index: number) {
|
||||||
return {
|
return {
|
||||||
@@ -105,6 +95,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function removeEmptyDataset(dataset: IGraphDataset) {
|
function removeEmptyDataset(dataset: IGraphDataset) {
|
||||||
|
/* eslint-disable-next-line no-unneeded-ternary */
|
||||||
return dataset.data.every(point => point === 0) ? false : true;
|
return dataset.data.every(point => point === 0) ? false : true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,7 +134,7 @@
|
|||||||
yAxes: {
|
yAxes: {
|
||||||
stacked: props.stacked,
|
stacked: props.stacked,
|
||||||
ticks: {
|
ticks: {
|
||||||
callback: (value, index, values) => {
|
callback: value => {
|
||||||
if (props.graphValueType === GraphValueTypes.Time) {
|
if (props.graphValueType === GraphValueTypes.Time) {
|
||||||
return convertSecondsToHumanReadable(value);
|
return convertSecondsToHumanReadable(value);
|
||||||
}
|
}
|
||||||
@@ -174,40 +165,6 @@
|
|||||||
options: graphOptions
|
options: graphOptions
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function convertSecondsToHumanReadable(value, values = null) {
|
|
||||||
const highestValue = values ? values[0] : value;
|
|
||||||
|
|
||||||
// minutes
|
|
||||||
if (highestValue < 3600) {
|
|
||||||
const minutes = Math.floor(value / 60);
|
|
||||||
|
|
||||||
value = `${minutes} m`;
|
|
||||||
}
|
|
||||||
// hours and minutes
|
|
||||||
else if (highestValue > 3600 && highestValue < 86400) {
|
|
||||||
const hours = Math.floor(value / 3600);
|
|
||||||
const minutes = Math.floor((value % 3600) / 60);
|
|
||||||
|
|
||||||
value = hours != 0 ? `${hours} h ${minutes} m` : `${minutes} m`;
|
|
||||||
}
|
|
||||||
// days and hours
|
|
||||||
else if (highestValue > 86400 && highestValue < 31557600) {
|
|
||||||
const days = Math.floor(value / 86400);
|
|
||||||
const hours = Math.floor((value % 86400) / 3600);
|
|
||||||
|
|
||||||
value = days != 0 ? `${days} d ${hours} h` : `${hours} h`;
|
|
||||||
}
|
|
||||||
// years and days
|
|
||||||
else if (highestValue > 31557600) {
|
|
||||||
const years = Math.floor(value / 31557600);
|
|
||||||
const days = Math.floor((value % 31557600) / 86400);
|
|
||||||
|
|
||||||
value = years != 0 ? `${years} y ${days} d` : `${days} d`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped></style>
|
<style lang="scss" scoped></style>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<header ref="headerElement" :class="{ expanded, noselect: true }">
|
<header ref="headerElement" :class="{ expanded, noselect: true }">
|
||||||
<img :src="bannerImage" ref="imageElement" />
|
<img ref="imageElement" :src="bannerImage" alt="Page banner image" />
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1 class="title">Request movies or tv shows</h1>
|
<h1 class="title">Request movies or tv shows</h1>
|
||||||
<strong class="subtitle"
|
<strong class="subtitle"
|
||||||
@@ -8,7 +8,13 @@
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="expand-icon" @click="expand" @mouseover="upgradeImage">
|
<div
|
||||||
|
class="expand-icon"
|
||||||
|
@click="expand"
|
||||||
|
@keydown.enter="expand"
|
||||||
|
@mouseover="upgradeImage"
|
||||||
|
@focus="focus"
|
||||||
|
>
|
||||||
<IconExpand v-if="!expanded" />
|
<IconExpand v-if="!expanded" />
|
||||||
<IconShrink v-else />
|
<IconShrink v-else />
|
||||||
</div>
|
</div>
|
||||||
@@ -16,7 +22,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted } from "vue";
|
import { ref } from "vue";
|
||||||
import IconExpand from "@/icons/IconExpand.vue";
|
import IconExpand from "@/icons/IconExpand.vue";
|
||||||
import IconShrink from "@/icons/IconShrink.vue";
|
import IconShrink from "@/icons/IconShrink.vue";
|
||||||
import type { Ref } from "vue";
|
import type { Ref } from "vue";
|
||||||
@@ -35,9 +41,7 @@
|
|||||||
const headerElement: Ref<HTMLElement> = ref(null);
|
const headerElement: Ref<HTMLElement> = ref(null);
|
||||||
const imageElement: Ref<HTMLImageElement> = ref(null);
|
const imageElement: Ref<HTMLImageElement> = ref(null);
|
||||||
const defaultHeaderHeight: Ref<string> = ref();
|
const defaultHeaderHeight: Ref<string> = ref();
|
||||||
const disableProxy = true;
|
// const disableProxy = true;
|
||||||
|
|
||||||
bannerImage.value = randomImage();
|
|
||||||
|
|
||||||
function expand() {
|
function expand() {
|
||||||
expanded.value = !expanded.value;
|
expanded.value = !expanded.value;
|
||||||
@@ -53,11 +57,17 @@
|
|||||||
headerElement.value.style.setProperty("--header-height", height);
|
headerElement.value.style.setProperty("--header-height", height);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function focus(event: FocusEvent) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
function randomImage(): string {
|
function randomImage(): string {
|
||||||
const image = images[Math.floor(Math.random() * images?.length)];
|
const image = images[Math.floor(Math.random() * images.length)];
|
||||||
return ASSET_URL + image;
|
return ASSET_URL + image;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bannerImage.value = randomImage();
|
||||||
|
|
||||||
// function sliceToHeaderSize(url: string): string {
|
// function sliceToHeaderSize(url: string): string {
|
||||||
// let width = headerElement.value?.getBoundingClientRect()?.width || 1349;
|
// let width = headerElement.value?.getBoundingClientRect()?.width || 1349;
|
||||||
// let height = headerElement.value?.getBoundingClientRect()?.height || 261;
|
// let height = headerElement.value?.getBoundingClientRect()?.height || 261;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="isOpen" class="movie-popup" @click="close">
|
<div v-if="isOpen" class="movie-popup" @click="close" @keydown.enter="close">
|
||||||
<div class="movie-popup__box" @click.stop>
|
<div class="movie-popup__box" @click.stop>
|
||||||
<person v-if="type === 'person'" :id="id" type="person" />
|
<person v-if="type === 'person'" :id="id" type="person" />
|
||||||
<movie v-else :id="id" :type="type"></movie>
|
<movie v-else :id="id" :type="type"></movie>
|
||||||
@@ -14,8 +14,8 @@
|
|||||||
import { useStore } from "vuex";
|
import { useStore } from "vuex";
|
||||||
import Movie from "@/components/popup/Movie.vue";
|
import Movie from "@/components/popup/Movie.vue";
|
||||||
import Person from "@/components/popup/Person.vue";
|
import Person from "@/components/popup/Person.vue";
|
||||||
import { MediaTypes } from "../interfaces/IList";
|
|
||||||
import type { Ref } from "vue";
|
import type { Ref } from "vue";
|
||||||
|
import { MediaTypes } from "../interfaces/IList";
|
||||||
|
|
||||||
interface URLQueryParameters {
|
interface URLQueryParameters {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -34,14 +34,16 @@
|
|||||||
id.value = state.popup.id;
|
id.value = state.popup.id;
|
||||||
type.value = state.popup.type;
|
type.value = state.popup.type;
|
||||||
|
|
||||||
isOpen.value
|
if (isOpen.value) {
|
||||||
? document.getElementsByTagName("body")[0].classList.add("no-scroll")
|
document.getElementsByTagName("body")[0].classList.add("no-scroll");
|
||||||
: document.getElementsByTagName("body")[0].classList.remove("no-scroll");
|
} else {
|
||||||
|
document.getElementsByTagName("body")[0].classList.remove("no-scroll");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function getFromURLQuery(): URLQueryParameters {
|
function getFromURLQuery(): URLQueryParameters {
|
||||||
let id: number;
|
let _id: number;
|
||||||
let type: MediaTypes;
|
let _type: MediaTypes;
|
||||||
|
|
||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
params.forEach((value, key) => {
|
params.forEach((value, key) => {
|
||||||
@@ -55,16 +57,16 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
id = Number(params.get(key));
|
_id = Number(params.get(key));
|
||||||
type = MediaTypes[key];
|
_type = MediaTypes[key];
|
||||||
});
|
});
|
||||||
|
|
||||||
return { id, type };
|
return { id: _id, type: _type };
|
||||||
}
|
}
|
||||||
|
|
||||||
function open(id: Number, type: string) {
|
function open(_id: number, _type: string) {
|
||||||
if (!id || !type) return;
|
if (!_id || !_type) return;
|
||||||
store.dispatch("popup/open", { id, type });
|
store.dispatch("popup/open", { id: _id, type: _type });
|
||||||
}
|
}
|
||||||
|
|
||||||
function close() {
|
function close() {
|
||||||
@@ -79,8 +81,8 @@
|
|||||||
window.addEventListener("keyup", checkEventForEscapeKey);
|
window.addEventListener("keyup", checkEventForEscapeKey);
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const { id, type } = getFromURLQuery();
|
const query = getFromURLQuery();
|
||||||
open(id, type);
|
open(query?.id, query?.type);
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<results-list-item
|
<results-list-item
|
||||||
v-for="(result, index) in results"
|
v-for="(result, index) in results"
|
||||||
:key="generateResultKey(index, `${result.type}-${result.id}`)"
|
:key="generateResultKey(index, `${result.type}-${result.id}`)"
|
||||||
:listItem="result"
|
:list-item="result"
|
||||||
/>
|
/>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
@@ -23,11 +23,11 @@
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
results: Array<ListResults>;
|
results: Array<ListResults>;
|
||||||
shortList?: Boolean;
|
shortList?: boolean;
|
||||||
loading?: Boolean;
|
loading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<Props>();
|
defineProps<Props>();
|
||||||
|
|
||||||
function generateResultKey(index: string | number | symbol, value: string) {
|
function generateResultKey(index: string | number | symbol, value: string) {
|
||||||
return `${String(index)}-${value}`;
|
return `${String(index)}-${value}`;
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<li class="movie-item" ref="list-item">
|
<li ref="list-item" class="movie-item">
|
||||||
<figure
|
<figure
|
||||||
ref="posterElement"
|
ref="posterElement"
|
||||||
class="movie-item__poster"
|
class="movie-item__poster"
|
||||||
@click="openMoviePopup"
|
@click="openMoviePopup"
|
||||||
|
@keydown.enter="openMoviePopup"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
class="movie-item__img"
|
class="movie-item__img"
|
||||||
@@ -36,9 +37,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, defineProps, onMounted } from "vue";
|
import { ref, computed, defineProps, onMounted } from "vue";
|
||||||
import { useStore } from "vuex";
|
import { useStore } from "vuex";
|
||||||
import { buildImageProxyUrl } from "../utils";
|
|
||||||
import type { Ref } from "vue";
|
import type { Ref } from "vue";
|
||||||
import type { IMovie, IShow, IPerson, IRequest } from "../interfaces/IList";
|
import type { IMovie, IShow, IPerson } from "../interfaces/IList";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
listItem: IMovie | IShow | IPerson;
|
listItem: IMovie | IShow | IPerson;
|
||||||
@@ -53,19 +53,31 @@
|
|||||||
const posterElement: Ref<HTMLElement> = ref(null);
|
const posterElement: Ref<HTMLElement> = ref(null);
|
||||||
const observed: Ref<boolean> = ref(false);
|
const observed: Ref<boolean> = ref(false);
|
||||||
|
|
||||||
poster.value = props.listItem?.poster
|
if (props.listItem?.poster) {
|
||||||
? IMAGE_BASE_URL + props.listItem?.poster
|
poster.value = IMAGE_BASE_URL + props.listItem.poster;
|
||||||
: IMAGE_FALLBACK;
|
} else {
|
||||||
|
poster.value = IMAGE_FALLBACK;
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(observePosterAndSetImageSource);
|
const posterAltText = computed(() => {
|
||||||
|
const type = props.listItem.type || "";
|
||||||
|
let title = "";
|
||||||
|
|
||||||
|
if ("name" in props.listItem) title = props.listItem.name;
|
||||||
|
else if ("title" in props.listItem) title = props.listItem.title;
|
||||||
|
|
||||||
|
return props.listItem.poster
|
||||||
|
? `Poster for ${type} ${title}`
|
||||||
|
: `Missing image for ${type} ${title}`;
|
||||||
|
});
|
||||||
|
|
||||||
function observePosterAndSetImageSource() {
|
function observePosterAndSetImageSource() {
|
||||||
const imageElement = posterElement.value.getElementsByTagName("img")[0];
|
const imageElement = posterElement.value.getElementsByTagName("img")[0];
|
||||||
if (imageElement == null) return;
|
if (imageElement == null) return;
|
||||||
|
|
||||||
const imageObserver = new IntersectionObserver((entries, imgObserver) => {
|
const imageObserver = new IntersectionObserver(entries => {
|
||||||
entries.forEach(entry => {
|
entries.forEach(entry => {
|
||||||
if (entry.isIntersecting && observed.value == false) {
|
if (entry.isIntersecting && observed.value === false) {
|
||||||
const lazyImage = entry.target as HTMLImageElement;
|
const lazyImage = entry.target as HTMLImageElement;
|
||||||
lazyImage.src = lazyImage.dataset.src;
|
lazyImage.src = lazyImage.dataset.src;
|
||||||
posterElement.value.classList.add("is-loaded");
|
posterElement.value.classList.add("is-loaded");
|
||||||
@@ -77,30 +89,20 @@
|
|||||||
imageObserver.observe(imageElement);
|
imageObserver.observe(imageElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMounted(observePosterAndSetImageSource);
|
||||||
|
|
||||||
function openMoviePopup() {
|
function openMoviePopup() {
|
||||||
store.dispatch("popup/open", { ...props.listItem });
|
store.dispatch("popup/open", { ...props.listItem });
|
||||||
}
|
}
|
||||||
|
|
||||||
const posterAltText = computed(() => {
|
// const imageSize = computed(() => {
|
||||||
const type = props.listItem.type || "";
|
// if (!posterElement.value) return;
|
||||||
let title: string = "";
|
// const { height, width } = posterElement.value.getBoundingClientRect();
|
||||||
|
// return {
|
||||||
if ("name" in props.listItem) title = props.listItem.name;
|
// height: Math.ceil(height),
|
||||||
else if ("title" in props.listItem) title = props.listItem.title;
|
// width: Math.ceil(width)
|
||||||
|
// };
|
||||||
return props.listItem.poster
|
// });
|
||||||
? `Poster for ${type} ${title}`
|
|
||||||
: `Missing image for ${type} ${title}`;
|
|
||||||
});
|
|
||||||
|
|
||||||
const imageSize = computed(() => {
|
|
||||||
if (!posterElement.value) return;
|
|
||||||
const { height, width } = posterElement.value.getBoundingClientRect();
|
|
||||||
return {
|
|
||||||
height: Math.ceil(height),
|
|
||||||
width: Math.ceil(width)
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// import img from "../directives/v-image";
|
// import img from "../directives/v-image";
|
||||||
// directives: {
|
// directives: {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
v-if="!loadedPages.includes(1) && loading == false"
|
v-if="!loadedPages.includes(1) && loading == false"
|
||||||
class="button-container"
|
class="button-container"
|
||||||
>
|
>
|
||||||
<seasoned-button @click="loadLess" class="load-button" :fullWidth="true"
|
<seasoned-button class="load-button" :full-width="true" @click="loadLess"
|
||||||
>load previous</seasoned-button
|
>load previous</seasoned-button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
@@ -16,10 +16,10 @@
|
|||||||
|
|
||||||
<div ref="loadMoreButton" class="button-container">
|
<div ref="loadMoreButton" class="button-container">
|
||||||
<seasoned-button
|
<seasoned-button
|
||||||
class="load-button"
|
|
||||||
v-if="!loading && !shortList && page != totalPages && results.length"
|
v-if="!loading && !shortList && page != totalPages && results.length"
|
||||||
|
class="load-button"
|
||||||
|
:full-width="true"
|
||||||
@click="loadMore"
|
@click="loadMore"
|
||||||
:fullWidth="true"
|
|
||||||
>load more</seasoned-button
|
>load more</seasoned-button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
@@ -28,12 +28,10 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineProps, ref, computed, onMounted } from "vue";
|
import { defineProps, ref, computed, onMounted } from "vue";
|
||||||
import { useStore } from "vuex";
|
|
||||||
import PageHeader from "@/components/PageHeader.vue";
|
import PageHeader from "@/components/PageHeader.vue";
|
||||||
import ResultsList from "@/components/ResultsList.vue";
|
import ResultsList from "@/components/ResultsList.vue";
|
||||||
import SeasonedButton from "@/components/ui/SeasonedButton.vue";
|
import SeasonedButton from "@/components/ui/SeasonedButton.vue";
|
||||||
import Loader from "@/components/ui/Loader.vue";
|
import Loader from "@/components/ui/Loader.vue";
|
||||||
import { getTmdbMovieListByName } from "../api";
|
|
||||||
import type { Ref } from "vue";
|
import type { Ref } from "vue";
|
||||||
import type { IList, ListResults } from "../interfaces/IList";
|
import type { IList, ListResults } from "../interfaces/IList";
|
||||||
import type ISection from "../interfaces/ISection";
|
import type ISection from "../interfaces/ISection";
|
||||||
@@ -44,7 +42,6 @@
|
|||||||
shortList?: boolean;
|
shortList?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const store = useStore();
|
|
||||||
const props = defineProps<Props>();
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
const results: Ref<ListResults> = ref([]);
|
const results: Ref<ListResults> = ref([]);
|
||||||
@@ -54,12 +51,23 @@
|
|||||||
const totalPages: Ref<number> = ref(0);
|
const totalPages: Ref<number> = ref(0);
|
||||||
const loading: Ref<boolean> = ref(true);
|
const loading: Ref<boolean> = ref(true);
|
||||||
const autoLoad: Ref<boolean> = ref(false);
|
const autoLoad: Ref<boolean> = ref(false);
|
||||||
const observer: Ref<any> = ref(null);
|
const observer: Ref<IntersectionObserver> = ref(null);
|
||||||
const resultSection = ref(null);
|
const resultSection = ref(null);
|
||||||
const loadMoreButton = ref(null);
|
const loadMoreButton = ref(null);
|
||||||
|
|
||||||
page.value = getPageFromUrl() || page.value;
|
function pageCountString(_page: number, _totalPages: number) {
|
||||||
if (results.value?.length === 0) getListResults();
|
return `Page ${_page} of ${_totalPages}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resultCountString(_results: ListResults, _totalResults: number) {
|
||||||
|
const loadedResults = _results.length;
|
||||||
|
const __totalResults = _totalResults < 10000 ? _totalResults : "∞";
|
||||||
|
return `${loadedResults} of ${__totalResults} results`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setLoading(state: boolean) {
|
||||||
|
loading.value = state;
|
||||||
|
}
|
||||||
|
|
||||||
const info = computed(() => {
|
const info = computed(() => {
|
||||||
if (results.value.length === 0) return [null, null];
|
if (results.value.length === 0) return [null, null];
|
||||||
@@ -69,25 +77,30 @@
|
|||||||
return [pageCount, resultCount];
|
return [pageCount, resultCount];
|
||||||
});
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
if (!props?.shortList) setupAutoloadObserver();
|
|
||||||
});
|
|
||||||
|
|
||||||
function pageCountString(page: Number, totalPages: Number) {
|
|
||||||
return `Page ${page} of ${totalPages}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resultCountString(results: ListResults, totalResults: number) {
|
|
||||||
const loadedResults = results.length;
|
|
||||||
const _totalResults = totalResults < 10000 ? totalResults : "∞";
|
|
||||||
return `${loadedResults} of ${_totalResults} results`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPageFromUrl() {
|
function getPageFromUrl() {
|
||||||
const page = new URLSearchParams(window.location.search).get("page");
|
const _page = new URLSearchParams(window.location.search).get("page");
|
||||||
if (!page) return null;
|
if (!_page) return null;
|
||||||
|
|
||||||
return Number(page);
|
return Number(_page);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateQueryParams() {
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
if (params.has("page")) {
|
||||||
|
params.set("page", page.value?.toString());
|
||||||
|
} else if (page.value > 1) {
|
||||||
|
params.append("page", page.value?.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
window.history.replaceState(
|
||||||
|
{},
|
||||||
|
"search",
|
||||||
|
`${window.location.protocol}//${window.location.hostname}${
|
||||||
|
window.location.port ? `:${window.location.port}` : ""
|
||||||
|
}${window.location.pathname}${
|
||||||
|
params.toString().length ? `?${params}` : ""
|
||||||
|
}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getListResults(front = false) {
|
function getListResults(front = false) {
|
||||||
@@ -105,7 +118,7 @@
|
|||||||
totalResults.value = listResponse.total_results;
|
totalResults.value = listResponse.total_results;
|
||||||
})
|
})
|
||||||
.then(updateQueryParams)
|
.then(updateQueryParams)
|
||||||
.finally(() => (loading.value = false));
|
.finally(() => setLoading(false));
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadMore() {
|
function loadMore() {
|
||||||
@@ -114,9 +127,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
let maxPage = [...loadedPages.value].slice(-1)[0];
|
const maxPage = [...loadedPages.value].slice(-1)[0];
|
||||||
|
|
||||||
if (maxPage == NaN) return;
|
if (Number.isNaN(maxPage)) return;
|
||||||
page.value = maxPage + 1;
|
page.value = maxPage + 1;
|
||||||
getListResults();
|
getListResults();
|
||||||
}
|
}
|
||||||
@@ -130,25 +143,6 @@
|
|||||||
getListResults(true);
|
getListResults(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateQueryParams() {
|
|
||||||
let params = new URLSearchParams(window.location.search);
|
|
||||||
if (params.has("page")) {
|
|
||||||
params.set("page", page.value?.toString());
|
|
||||||
} else if (page.value > 1) {
|
|
||||||
params.append("page", page.value?.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
window.history.replaceState(
|
|
||||||
{},
|
|
||||||
"search",
|
|
||||||
`${window.location.protocol}//${window.location.hostname}${
|
|
||||||
window.location.port ? `:${window.location.port}` : ""
|
|
||||||
}${window.location.pathname}${
|
|
||||||
params.toString().length ? `?${params}` : ""
|
|
||||||
}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleButtonIntersection(entries) {
|
function handleButtonIntersection(entries) {
|
||||||
entries.map(entry =>
|
entries.map(entry =>
|
||||||
entry.isIntersecting && autoLoad.value ? loadMore() : null
|
entry.isIntersecting && autoLoad.value ? loadMore() : null
|
||||||
@@ -165,14 +159,12 @@
|
|||||||
observer.value.observe(loadMoreButton.value);
|
observer.value.observe(loadMoreButton.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// created() {
|
page.value = getPageFromUrl() || page.value;
|
||||||
// if (!this.shortList) {
|
if (results.value?.length === 0) getListResults();
|
||||||
// store.dispatch(
|
onMounted(() => {
|
||||||
// "documentTitle/updateTitle",
|
if (!props?.shortList) setupAutoloadObserver();
|
||||||
// `${this.$router.history.current.name} ${this.title}`
|
});
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// beforeDestroy() {
|
// beforeDestroy() {
|
||||||
// this.observer = undefined;
|
// this.observer = undefined;
|
||||||
// }
|
// }
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<transition name="shut">
|
<transition name="shut">
|
||||||
<ul class="dropdown">
|
<ul class="dropdown">
|
||||||
|
<!-- eslint-disable-next-line vuejs-accessibility/click-events-have-key-events -->
|
||||||
<li
|
<li
|
||||||
v-for="result in searchResults"
|
v-for="(result, _index) in searchResults"
|
||||||
:key="`${result.index}-${result.title}-${result.type}`"
|
:key="`${_index}-${result.title}-${result.type}`"
|
||||||
|
:class="`result di-${_index} ${_index === index ? 'active' : ''}`"
|
||||||
@click="openPopup(result)"
|
@click="openPopup(result)"
|
||||||
:class="`result di-${result.index} ${
|
|
||||||
result.index === index ? 'active' : ''
|
|
||||||
}`"
|
|
||||||
>
|
>
|
||||||
<IconMovie v-if="result.type == 'movie'" class="type-icon" />
|
<IconMovie v-if="result.type == 'movie'" class="type-icon" />
|
||||||
<IconShow v-if="result.type == 'show'" class="type-icon" />
|
<IconShow v-if="result.type == 'show'" class="type-icon" />
|
||||||
@@ -29,36 +28,38 @@
|
|||||||
import { useStore } from "vuex";
|
import { useStore } from "vuex";
|
||||||
import IconMovie from "@/icons/IconMovie.vue";
|
import IconMovie from "@/icons/IconMovie.vue";
|
||||||
import IconShow from "@/icons/IconShow.vue";
|
import IconShow from "@/icons/IconShow.vue";
|
||||||
import IconPerson from "@/icons/IconPerson.vue";
|
|
||||||
import { elasticSearchMoviesAndShows } from "../../api";
|
|
||||||
import type { Ref } from "vue";
|
import type { Ref } from "vue";
|
||||||
|
import { elasticSearchMoviesAndShows } from "../../api";
|
||||||
|
import { MediaTypes } from "../../interfaces/IList";
|
||||||
|
import { Index } from "../../interfaces/IAutocompleteSearch";
|
||||||
|
import type {
|
||||||
|
IAutocompleteResult,
|
||||||
|
IAutocompleteSearchResults
|
||||||
|
} from "../../interfaces/IAutocompleteSearch";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
query?: string;
|
query?: string;
|
||||||
index?: Number;
|
index?: number;
|
||||||
results?: Array<any>;
|
results?: Array<IAutocompleteResult>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Emit {
|
interface Emit {
|
||||||
(e: "update:results", value: Array<any>);
|
(e: "update:results", value: Array<IAutocompleteResult>);
|
||||||
}
|
}
|
||||||
|
|
||||||
const numberOfResults: number = 10;
|
const numberOfResults = 10;
|
||||||
const props = defineProps<Props>();
|
const props = defineProps<Props>();
|
||||||
const emit = defineEmits<Emit>();
|
const emit = defineEmits<Emit>();
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
|
|
||||||
const searchResults: Ref<Array<any>> = ref([]);
|
const searchResults: Ref<Array<IAutocompleteResult>> = ref([]);
|
||||||
const keyboardNavigationIndex: Ref<number> = ref(0);
|
const keyboardNavigationIndex: Ref<number> = ref(0);
|
||||||
|
|
||||||
// on load functions
|
|
||||||
fetchAutocompleteResults();
|
|
||||||
// end on load functions
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.query,
|
() => props.query,
|
||||||
newQuery => {
|
newQuery => {
|
||||||
if (newQuery?.length > 0) fetchAutocompleteResults();
|
if (newQuery?.length > 0)
|
||||||
|
fetchAutocompleteResults(); /* eslint-disable-line no-use-before-define */
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -68,6 +69,53 @@
|
|||||||
store.dispatch("popup/open", { ...result });
|
store.dispatch("popup/open", { ...result });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function removeDuplicates(_searchResults) {
|
||||||
|
const filteredResults = [];
|
||||||
|
_searchResults.forEach(result => {
|
||||||
|
if (result === undefined) return;
|
||||||
|
const numberOfDuplicates = filteredResults.filter(
|
||||||
|
filterItem => filterItem.id === result.id
|
||||||
|
);
|
||||||
|
if (numberOfDuplicates.length >= 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
filteredResults.push(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
return filteredResults;
|
||||||
|
}
|
||||||
|
|
||||||
|
function elasticIndexToMediaType(index: Index): MediaTypes {
|
||||||
|
if (index === Index.Movies) return MediaTypes.Movie;
|
||||||
|
if (index === Index.Shows) return MediaTypes.Show;
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseElasticResponse(elasticResponse: IAutocompleteSearchResults) {
|
||||||
|
const data = elasticResponse.hits.hits;
|
||||||
|
|
||||||
|
const results: Array<IAutocompleteResult> = [];
|
||||||
|
|
||||||
|
data.forEach(item => {
|
||||||
|
if (!Object.values(Index).includes(item._index)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
results.push({
|
||||||
|
title: item._source?.original_name || item._source.original_title,
|
||||||
|
id: item._source.id,
|
||||||
|
adult: item._source.adult,
|
||||||
|
type: elasticIndexToMediaType(item._index)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return removeDuplicates(results).map((el, index) => {
|
||||||
|
return { ...el, index };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function fetchAutocompleteResults() {
|
function fetchAutocompleteResults() {
|
||||||
keyboardNavigationIndex.value = 0;
|
keyboardNavigationIndex.value = 0;
|
||||||
searchResults.value = [];
|
searchResults.value = [];
|
||||||
@@ -80,44 +128,9 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseElasticResponse(elasticResponse: any) {
|
// on load functions
|
||||||
const data = elasticResponse.hits.hits;
|
fetchAutocompleteResults();
|
||||||
|
// end on load functions
|
||||||
let results = data.map(item => {
|
|
||||||
let index = null;
|
|
||||||
if (item._source.log.file.path.includes("movie")) index = "movie";
|
|
||||||
if (item._source.log.file.path.includes("series")) index = "show";
|
|
||||||
|
|
||||||
if (index === "movie" || index === "show") {
|
|
||||||
return {
|
|
||||||
title: item._source.original_name || item._source.original_title,
|
|
||||||
id: item._source.id,
|
|
||||||
adult: item._source.adult,
|
|
||||||
type: index
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return removeDuplicates(results).map((el, index) => {
|
|
||||||
return { ...el, index };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeDuplicates(searchResults) {
|
|
||||||
let filteredResults = [];
|
|
||||||
searchResults.map(result => {
|
|
||||||
if (result === undefined) return;
|
|
||||||
const numberOfDuplicates = filteredResults.filter(
|
|
||||||
filterItem => filterItem.id == result.id
|
|
||||||
);
|
|
||||||
if (numberOfDuplicates.length >= 1) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
filteredResults.push(result);
|
|
||||||
});
|
|
||||||
|
|
||||||
return filteredResults;
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<nav>
|
<nav>
|
||||||
|
<!-- eslint-disable-next-line vuejs-accessibility/anchor-has-content -->
|
||||||
<a v-if="isHome" class="nav__logo" href="/">
|
<a v-if="isHome" class="nav__logo" href="/">
|
||||||
<TmdbLogo class="logo" />
|
<TmdbLogo class="logo" />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<router-link v-else class="nav__logo" to="/" exact>
|
<router-link v-else class="nav__logo" to="/" exact>
|
||||||
<TmdbLogo class="logo" />
|
<TmdbLogo class="logo" />
|
||||||
</router-link>
|
</router-link>
|
||||||
@@ -13,7 +15,7 @@
|
|||||||
<NavigationIcon class="desktop-only" :route="profileRoute" />
|
<NavigationIcon class="desktop-only" :route="profileRoute" />
|
||||||
|
|
||||||
<!-- <div class="navigation-icons-grid mobile-only" :class="{ open: isOpen }"> -->
|
<!-- <div class="navigation-icons-grid mobile-only" :class="{ open: isOpen }"> -->
|
||||||
<div class="navigation-icons-grid mobile-only" v-if="isOpen">
|
<div v-if="isOpen" class="navigation-icons-grid mobile-only">
|
||||||
<NavigationIcons>
|
<NavigationIcons>
|
||||||
<NavigationIcon :route="profileRoute" />
|
<NavigationIcon :route="profileRoute" />
|
||||||
</NavigationIcons>
|
</NavigationIcons>
|
||||||
@@ -22,8 +24,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, defineProps, PropType } from "vue";
|
import { computed } from "vue";
|
||||||
import type { App } from "vue";
|
|
||||||
import { useStore } from "vuex";
|
import { useStore } from "vuex";
|
||||||
import { useRoute } from "vue-router";
|
import { useRoute } from "vue-router";
|
||||||
import SearchInput from "@/components/header/SearchInput.vue";
|
import SearchInput from "@/components/header/SearchInput.vue";
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<router-link
|
<router-link
|
||||||
:to="{ path: route?.route }"
|
|
||||||
:key="route?.title"
|
|
||||||
v-if="route?.requiresAuth == undefined || (route?.requiresAuth && loggedIn)"
|
v-if="route?.requiresAuth == undefined || (route?.requiresAuth && loggedIn)"
|
||||||
|
:key="route?.title"
|
||||||
|
:to="{ path: route?.route }"
|
||||||
>
|
>
|
||||||
<li class="navigation-link" :class="{ active: route.route == active }">
|
<li class="navigation-link" :class="{ active: route.route == active }">
|
||||||
<component class="navigation-icon" :is="route.icon"></component>
|
<component :is="route.icon" class="navigation-icon"></component>
|
||||||
<span>{{ route.title }}</span>
|
<span>{{ route.title }}</span>
|
||||||
</li>
|
</li>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<ul class="navigation-icons">
|
<ul class="navigation-icons">
|
||||||
<NavigationIcon
|
<NavigationIcon
|
||||||
v-for="route in routes"
|
v-for="_route in routes"
|
||||||
:key="route.route"
|
:key="_route.route"
|
||||||
:route="route"
|
:route="_route"
|
||||||
:active="activeRoute"
|
:active="activeRoute"
|
||||||
/>
|
/>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
@@ -66,7 +66,11 @@
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
watch(route, () => (activeRoute.value = window?.location?.pathname));
|
function setActiveRoute(_route: string) {
|
||||||
|
activeRoute.value = _route;
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(route, () => setActiveRoute(window?.location?.pathname || ""));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -3,15 +3,16 @@
|
|||||||
<div class="search" :class="{ active: inputIsActive }">
|
<div class="search" :class="{ active: inputIsActive }">
|
||||||
<IconSearch class="search-icon" tabindex="-1" />
|
<IconSearch class="search-icon" tabindex="-1" />
|
||||||
|
|
||||||
|
<!-- eslint-disable-next-line vuejs-accessibility/form-control-has-label -->
|
||||||
<input
|
<input
|
||||||
ref="inputElement"
|
ref="inputElement"
|
||||||
|
v-model="query"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search for movie or show"
|
placeholder="Search for movie or show"
|
||||||
aria-label="Search input for finding a movie or show"
|
aria-label="Search input for finding a movie or show"
|
||||||
autocorrect="off"
|
autocorrect="off"
|
||||||
autocapitalize="off"
|
autocapitalize="off"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
v-model="query"
|
|
||||||
@input="handleInput"
|
@input="handleInput"
|
||||||
@click="focus"
|
@click="focus"
|
||||||
@keydown.escape="handleEscape"
|
@keydown.escape="handleEscape"
|
||||||
@@ -23,20 +24,20 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<IconClose
|
<IconClose
|
||||||
|
v-if="query && query.length"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
aria-label="button"
|
aria-label="button"
|
||||||
v-if="query && query.length"
|
class="close-icon"
|
||||||
@click="clearInput"
|
@click="clearInput"
|
||||||
@keydown.enter.stop="clearInput"
|
@keydown.enter.stop="clearInput"
|
||||||
class="close-icon"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AutocompleteDropdown
|
<AutocompleteDropdown
|
||||||
v-if="showAutocompleteResults"
|
v-if="showAutocompleteResults"
|
||||||
|
v-model:results="dropdownResults"
|
||||||
:query="query"
|
:query="query"
|
||||||
:index="dropdownIndex"
|
:index="dropdownIndex"
|
||||||
v-model:results="dropdownResults"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -44,14 +45,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from "vue";
|
import { ref, computed } from "vue";
|
||||||
import { useStore } from "vuex";
|
import { useStore } from "vuex";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter, useRoute } from "vue-router";
|
||||||
import { useRoute } from "vue-router";
|
|
||||||
import SeasonedButton from "@/components/ui/SeasonedButton.vue";
|
|
||||||
import AutocompleteDropdown from "@/components/header/AutocompleteDropdown.vue";
|
import AutocompleteDropdown from "@/components/header/AutocompleteDropdown.vue";
|
||||||
import IconSearch from "@/icons/IconSearch.vue";
|
import IconSearch from "@/icons/IconSearch.vue";
|
||||||
import IconClose from "@/icons/IconClose.vue";
|
import IconClose from "@/icons/IconClose.vue";
|
||||||
import config from "../../config";
|
|
||||||
import type { Ref } from "vue";
|
import type { Ref } from "vue";
|
||||||
|
import config from "../../config";
|
||||||
import type { MediaTypes } from "../../interfaces/IList";
|
import type { MediaTypes } from "../../interfaces/IList";
|
||||||
|
|
||||||
interface ISearchResult {
|
interface ISearchResult {
|
||||||
@@ -70,8 +69,7 @@
|
|||||||
const dropdownIndex: Ref<number> = ref(-1);
|
const dropdownIndex: Ref<number> = ref(-1);
|
||||||
const dropdownResults: Ref<ISearchResult[]> = ref([]);
|
const dropdownResults: Ref<ISearchResult[]> = ref([]);
|
||||||
const inputIsActive: Ref<boolean> = ref(false);
|
const inputIsActive: Ref<boolean> = ref(false);
|
||||||
const showAutocomplete: Ref<boolean> = ref(false);
|
const inputElement: Ref<HTMLInputElement> = ref(null);
|
||||||
const inputElement: Ref<any> = ref(null);
|
|
||||||
|
|
||||||
const isOpen = computed(() => store.getters["popup/isOpen"]);
|
const isOpen = computed(() => store.getters["popup/isOpen"]);
|
||||||
const showAutocompleteResults = computed(() => {
|
const showAutocompleteResults = computed(() => {
|
||||||
@@ -95,12 +93,12 @@
|
|||||||
|
|
||||||
function navigateDown() {
|
function navigateDown() {
|
||||||
if (dropdownIndex.value < dropdownResults.value.length - 1) {
|
if (dropdownIndex.value < dropdownResults.value.length - 1) {
|
||||||
dropdownIndex.value++;
|
dropdownIndex.value += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function navigateUp() {
|
function navigateUp() {
|
||||||
if (dropdownIndex.value > -1) dropdownIndex.value--;
|
if (dropdownIndex.value > -1) dropdownIndex.value -= 1;
|
||||||
|
|
||||||
const textLength = inputElement.value.value.length;
|
const textLength = inputElement.value.value.length;
|
||||||
|
|
||||||
@@ -122,12 +120,31 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleInput(e) {
|
function handleInput() {
|
||||||
dropdownIndex.value = -1;
|
dropdownIndex.value = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function focus() {
|
||||||
|
inputIsActive.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
inputElement.value.blur();
|
||||||
|
dropdownIndex.value = -1;
|
||||||
|
inputIsActive.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function blur() {
|
||||||
|
return setTimeout(reset, 150);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearInput() {
|
||||||
|
query.value = "";
|
||||||
|
inputElement.value.focus();
|
||||||
|
}
|
||||||
|
|
||||||
function handleSubmit() {
|
function handleSubmit() {
|
||||||
if (!query.value || query.value.length == 0) return;
|
if (!query.value || query.value.length === 0) return;
|
||||||
|
|
||||||
if (dropdownIndex.value >= 0) {
|
if (dropdownIndex.value >= 0) {
|
||||||
const resultItem = dropdownResults.value[dropdownIndex.value];
|
const resultItem = dropdownResults.value[dropdownIndex.value];
|
||||||
@@ -143,25 +160,6 @@
|
|||||||
reset();
|
reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
function focus() {
|
|
||||||
inputIsActive.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function blur(event: MouseEvent = null) {
|
|
||||||
return setTimeout(reset, 150);
|
|
||||||
}
|
|
||||||
|
|
||||||
function reset() {
|
|
||||||
inputElement.value.blur();
|
|
||||||
dropdownIndex.value = -1;
|
|
||||||
inputIsActive.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearInput(event: MouseEvent) {
|
|
||||||
query.value = "";
|
|
||||||
inputElement.value.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleEscape() {
|
function handleEscape() {
|
||||||
if (!isOpen.value) reset();
|
if (!isOpen.value) reset();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<li
|
<li
|
||||||
class="sidebar-list-element"
|
class="sidebar-list-element"
|
||||||
@click="emit('click')"
|
|
||||||
:class="{ active, disabled }"
|
:class="{ active, disabled }"
|
||||||
|
@click="emit('click')"
|
||||||
|
@keydown.enter="emit('click')"
|
||||||
>
|
>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</li>
|
</li>
|
||||||
@@ -12,8 +13,8 @@
|
|||||||
import { defineProps, defineEmits } from "vue";
|
import { defineProps, defineEmits } from "vue";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
active?: Boolean;
|
active?: boolean;
|
||||||
disabled?: Boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Emit {
|
interface Emit {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
ref="descriptionElement"
|
ref="descriptionElement"
|
||||||
class="movie-description noselect"
|
class="movie-description noselect"
|
||||||
@click="overflow ? (truncated = !truncated) : null"
|
@click="overflow ? (truncated = !truncated) : null"
|
||||||
|
@keydown.enter="overflow ? (truncated = !truncated) : null"
|
||||||
>
|
>
|
||||||
<span :class="{ truncated }">{{ description }}</span>
|
<span :class="{ truncated }">{{ description }}</span>
|
||||||
|
|
||||||
@@ -14,8 +15,8 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, defineProps, onMounted } from "vue";
|
import { ref, defineProps, onMounted } from "vue";
|
||||||
import IconArrowDown from "../../icons/IconArrowDown.vue";
|
|
||||||
import type { Ref } from "vue";
|
import type { Ref } from "vue";
|
||||||
|
import IconArrowDown from "../../icons/IconArrowDown.vue";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
description: string;
|
description: string;
|
||||||
@@ -26,7 +27,10 @@
|
|||||||
const overflow: Ref<boolean> = ref(false);
|
const overflow: Ref<boolean> = ref(false);
|
||||||
const descriptionElement: Ref<HTMLElement> = ref(null);
|
const descriptionElement: Ref<HTMLElement> = ref(null);
|
||||||
|
|
||||||
onMounted(checkDescriptionOverflowing);
|
// eslint-disable-next-line no-undef
|
||||||
|
function removeElements(elems: NodeListOf<Element>) {
|
||||||
|
elems.forEach(el => el.remove());
|
||||||
|
}
|
||||||
|
|
||||||
// The description element overflows text after 4 rows with css
|
// The description element overflows text after 4 rows with css
|
||||||
// line-clamp this takes the same text and adds to a temporary
|
// line-clamp this takes the same text and adds to a temporary
|
||||||
@@ -53,15 +57,13 @@
|
|||||||
|
|
||||||
document.body.appendChild(descriptionComparisonElement);
|
document.body.appendChild(descriptionComparisonElement);
|
||||||
const elemWithoutOverflowHeight =
|
const elemWithoutOverflowHeight =
|
||||||
descriptionComparisonElement.getBoundingClientRect()["height"];
|
descriptionComparisonElement.getBoundingClientRect().height;
|
||||||
|
|
||||||
overflow.value = elemWithoutOverflowHeight > height;
|
overflow.value = elemWithoutOverflowHeight > height;
|
||||||
removeElements(document.querySelectorAll(".dummy-non-overflow"));
|
removeElements(document.querySelectorAll(".dummy-non-overflow"));
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeElements(elems: NodeListOf<Element>) {
|
onMounted(checkDescriptionOverflowing);
|
||||||
elems.forEach(el => el.remove());
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="movie">
|
<section class="movie">
|
||||||
<!-- HEADER w/ POSTER -->
|
<!-- HEADER w/ POSTER -->
|
||||||
|
<!-- eslint-disable-next-line vuejs-accessibility/click-events-have-key-events -->
|
||||||
<header
|
<header
|
||||||
ref="backdropElement"
|
ref="backdropElement"
|
||||||
:class="compact ? 'compact' : ''"
|
:class="compact ? 'compact' : ''"
|
||||||
@@ -8,8 +9,9 @@
|
|||||||
>
|
>
|
||||||
<figure class="movie__poster">
|
<figure class="movie__poster">
|
||||||
<img
|
<img
|
||||||
class="movie-item__img is-loaded"
|
|
||||||
ref="poster-image"
|
ref="poster-image"
|
||||||
|
class="movie-item__img is-loaded"
|
||||||
|
alt="Movie poster"
|
||||||
:src="poster"
|
:src="poster"
|
||||||
/>
|
/>
|
||||||
</figure>
|
</figure>
|
||||||
@@ -25,7 +27,7 @@
|
|||||||
<div class="movie__main">
|
<div class="movie__main">
|
||||||
<div class="movie__wrap movie__wrap--main">
|
<div class="movie__wrap movie__wrap--main">
|
||||||
<!-- SIDEBAR ACTIONS -->
|
<!-- SIDEBAR ACTIONS -->
|
||||||
<div class="movie__actions" v-if="media">
|
<div v-if="media" class="movie__actions">
|
||||||
<action-button :active="media?.exists_in_plex" :disabled="true">
|
<action-button :active="media?.exists_in_plex" :disabled="true">
|
||||||
<IconThumbsUp v-if="media?.exists_in_plex" />
|
<IconThumbsUp v-if="media?.exists_in_plex" />
|
||||||
<IconThumbsDown v-else />
|
<IconThumbsDown v-else />
|
||||||
@@ -36,7 +38,7 @@
|
|||||||
}}
|
}}
|
||||||
</action-button>
|
</action-button>
|
||||||
|
|
||||||
<action-button @click="sendRequest" :active="requested">
|
<action-button :active="requested" @click="sendRequest">
|
||||||
<transition name="fade" mode="out-in">
|
<transition name="fade" mode="out-in">
|
||||||
<div v-if="!requested" key="request"><IconRequest /></div>
|
<div v-if="!requested" key="request"><IconRequest /></div>
|
||||||
<div v-else key="requested"><IconRequested /></div>
|
<div v-else key="requested"><IconRequested /></div>
|
||||||
@@ -63,8 +65,8 @@
|
|||||||
|
|
||||||
<action-button
|
<action-button
|
||||||
v-if="admin === true"
|
v-if="admin === true"
|
||||||
@click="showTorrents = !showTorrents"
|
|
||||||
:active="showTorrents"
|
:active="showTorrents"
|
||||||
|
@click="showTorrents = !showTorrents"
|
||||||
>
|
>
|
||||||
<IconBinoculars />
|
<IconBinoculars />
|
||||||
Search for torrents
|
Search for torrents
|
||||||
@@ -80,11 +82,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Loading placeholder -->
|
<!-- Loading placeholder -->
|
||||||
<div class="movie__actions text-input__loading" v-else>
|
<div v-else class="movie__actions text-input__loading">
|
||||||
<div
|
<div
|
||||||
v-for="index in admin ? Array(4) : Array(3)"
|
v-for="index in admin ? Array(4) : Array(3)"
|
||||||
class="movie__actions-link"
|
|
||||||
:key="index"
|
:key="index"
|
||||||
|
class="movie__actions-link"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="movie__actions-text text-input__loading--line"
|
class="movie__actions-text text-input__loading--line"
|
||||||
@@ -105,7 +107,7 @@
|
|||||||
:description="media.overview"
|
:description="media.overview"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="movie__details" v-if="media">
|
<div v-if="media" class="movie__details">
|
||||||
<Detail
|
<Detail
|
||||||
v-if="media.year"
|
v-if="media.year"
|
||||||
title="Release date"
|
title="Release date"
|
||||||
@@ -144,7 +146,7 @@
|
|||||||
|
|
||||||
<!-- TODO: change this classname, this is general -->
|
<!-- TODO: change this classname, this is general -->
|
||||||
|
|
||||||
<div class="movie__admin" v-if="showCast && cast?.length">
|
<div v-if="showCast && cast?.length" class="movie__admin">
|
||||||
<Detail title="cast">
|
<Detail title="cast">
|
||||||
<CastList :cast="cast" />
|
<CastList :cast="cast" />
|
||||||
</Detail>
|
</Detail>
|
||||||
@@ -156,7 +158,7 @@
|
|||||||
v-if="media && admin && showTorrents"
|
v-if="media && admin && showTorrents"
|
||||||
class="torrents"
|
class="torrents"
|
||||||
:query="media?.title"
|
:query="media?.title"
|
||||||
:tmdb_id="id"
|
:tmdb-id="id"
|
||||||
></TorrentList>
|
></TorrentList>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -181,7 +183,7 @@
|
|||||||
import ActionButton from "@/components/popup/ActionButton.vue";
|
import ActionButton from "@/components/popup/ActionButton.vue";
|
||||||
import Description from "@/components/popup/Description.vue";
|
import Description from "@/components/popup/Description.vue";
|
||||||
import LoadingPlaceholder from "@/components/ui/LoadingPlaceholder.vue";
|
import LoadingPlaceholder from "@/components/ui/LoadingPlaceholder.vue";
|
||||||
import type { Ref, ComputedRef } from "vue";
|
import type { Ref } from "vue";
|
||||||
import type {
|
import type {
|
||||||
IMovie,
|
IMovie,
|
||||||
IShow,
|
IShow,
|
||||||
@@ -194,12 +196,11 @@
|
|||||||
import {
|
import {
|
||||||
getMovie,
|
getMovie,
|
||||||
getShow,
|
getShow,
|
||||||
getPerson,
|
|
||||||
getMovieCredits,
|
getMovieCredits,
|
||||||
getShowCredits,
|
getShowCredits,
|
||||||
request,
|
request,
|
||||||
getRequestStatus,
|
getRequestStatus
|
||||||
watchLink
|
// watchLink
|
||||||
} from "../../api";
|
} from "../../api";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -222,45 +223,26 @@
|
|||||||
|
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
|
|
||||||
const loggedIn = computed(() => store.getters["user/loggedIn"]);
|
|
||||||
const admin = computed(() => store.getters["user/admin"]);
|
const admin = computed(() => store.getters["user/admin"]);
|
||||||
const plexId = computed(() => store.getters["user/plexId"]);
|
const plexId = computed(() => store.getters["user/plexId"]);
|
||||||
const poster = computed(() => computePoster());
|
|
||||||
|
const poster = computed(() => {
|
||||||
|
if (!media.value) return "/assets/placeholder.png";
|
||||||
|
if (!media.value?.poster) return "/assets/no-image.svg";
|
||||||
|
|
||||||
|
return `${ASSET_URL}${ASSET_SIZES[0]}${media.value.poster}`;
|
||||||
|
});
|
||||||
|
|
||||||
const numberOfTorrentResults = computed(() => {
|
const numberOfTorrentResults = computed(() => {
|
||||||
const count = store.getters["torrentModule/resultCount"];
|
const count = store.getters["torrentModule/resultCount"];
|
||||||
return count ? `${count} results` : null;
|
return count ? `${count} results` : null;
|
||||||
});
|
});
|
||||||
|
|
||||||
// On created functions
|
function setCast(_cast: ICast[]) {
|
||||||
fetchMedia();
|
cast.value = _cast;
|
||||||
setBackdrop();
|
|
||||||
store.dispatch("torrentModule/setResultCount", null);
|
|
||||||
// End on create functions
|
|
||||||
|
|
||||||
function fetchMedia() {
|
|
||||||
if (!props.id || !props.type) {
|
|
||||||
console.error("Unable to fetch media, requires id & type");
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
function setRequested(status: boolean) {
|
||||||
let apiFunction: Function;
|
requested.value = status;
|
||||||
let parameters: object;
|
|
||||||
|
|
||||||
if (props.type === MediaTypes.Movie) {
|
|
||||||
apiFunction = getMovie;
|
|
||||||
parameters = { checkExistance: true, credits: false };
|
|
||||||
} else if (props.type === MediaTypes.Show) {
|
|
||||||
apiFunction = getShow;
|
|
||||||
parameters = { checkExistance: true, credits: false };
|
|
||||||
}
|
|
||||||
|
|
||||||
apiFunction(props.id, { ...parameters })
|
|
||||||
.then(setAndReturnMedia)
|
|
||||||
.then(media => getCredits(props.type))
|
|
||||||
.then(credits => (cast.value = credits?.cast))
|
|
||||||
.then(() => getRequestStatus(props.id, props.type))
|
|
||||||
.then(requestStatus => (requested.value = requestStatus || false));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCredits(
|
function getCredits(
|
||||||
@@ -268,7 +250,8 @@
|
|||||||
): Promise<IMediaCredits> {
|
): Promise<IMediaCredits> {
|
||||||
if (type === MediaTypes.Movie) {
|
if (type === MediaTypes.Movie) {
|
||||||
return getMovieCredits(props.id);
|
return getMovieCredits(props.id);
|
||||||
} else if (type === MediaTypes.Show) {
|
}
|
||||||
|
if (type === MediaTypes.Show) {
|
||||||
return getShowCredits(props.id);
|
return getShowCredits(props.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -280,35 +263,64 @@
|
|||||||
return _media;
|
return _media;
|
||||||
}
|
}
|
||||||
|
|
||||||
const computePoster = () => {
|
function fetchMedia() {
|
||||||
if (!media.value) return "/assets/placeholder.png";
|
if (!props.id || !props.type) {
|
||||||
else if (!media.value?.poster) return "/assets/no-image.svg";
|
console.error("Unable to fetch media, requires id & type"); // eslint-disable-line no-console
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
return `${ASSET_URL}${ASSET_SIZES[0]}${media.value.poster}`;
|
let apiFunction: typeof getMovie;
|
||||||
|
let parameters: {
|
||||||
|
checkExistance: boolean;
|
||||||
|
credits: boolean;
|
||||||
|
releaseDates?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
function setBackdrop() {
|
if (props.type === MediaTypes.Movie) {
|
||||||
if (!media.value?.backdrop || !backdropElement.value?.style) return "";
|
apiFunction = getMovie;
|
||||||
|
parameters = { checkExistance: true, credits: false };
|
||||||
|
} else if (props.type === MediaTypes.Show) {
|
||||||
|
apiFunction = getShow;
|
||||||
|
parameters = { checkExistance: true, credits: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
apiFunction(props.id, { ...parameters })
|
||||||
|
.then(setAndReturnMedia)
|
||||||
|
.then(() => getCredits(props.type))
|
||||||
|
.then(credits => setCast(credits?.cast || []))
|
||||||
|
.then(() => getRequestStatus(props.id, props.type))
|
||||||
|
.then(requestStatus => setRequested(requestStatus || false));
|
||||||
|
}
|
||||||
|
|
||||||
|
function setBackdrop(): void {
|
||||||
|
if (!media.value?.backdrop || !backdropElement.value?.style) return;
|
||||||
|
|
||||||
const backdropURL = `${ASSET_URL}${ASSET_SIZES[1]}${media.value.backdrop}`;
|
const backdropURL = `${ASSET_URL}${ASSET_SIZES[1]}${media.value.backdrop}`;
|
||||||
backdropElement.value.style.backgroundImage = `url(${backdropURL})`;
|
backdropElement.value.style.backgroundImage = `url(${backdropURL})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendRequest() {
|
function sendRequest() {
|
||||||
request(props.id, props.type).then(
|
request(props.id, props.type).then(resp =>
|
||||||
resp => (requested.value = resp?.success || false)
|
setRequested(resp?.success || false)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function openInPlex() {
|
function openInPlex(): boolean {
|
||||||
return;
|
// watchLink()
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function openTmdb() {
|
function openTmdb() {
|
||||||
const tmdbType = props.type === MediaTypes.Show ? "tv" : props.type;
|
const tmdbType = props.type === MediaTypes.Show ? "tv" : props.type;
|
||||||
const tmdbURL = "https://www.themoviedb.org/" + tmdbType + "/" + props.id;
|
const tmdbURL = `https://www.themoviedb.org/${tmdbType}/${props.id}`;
|
||||||
window.location.href = tmdbURL;
|
window.location.href = tmdbURL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// On created functions
|
||||||
|
fetchMedia();
|
||||||
|
setBackdrop();
|
||||||
|
store.dispatch("torrentModule/setResultCount", null);
|
||||||
|
// End on create functions
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -7,10 +7,10 @@
|
|||||||
</h1>
|
</h1>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<loading-placeholder :count="1" />
|
<loading-placeholder :count="1" />
|
||||||
<loading-placeholder :count="1" lineClass="short" :top="3.5" />
|
<loading-placeholder :count="1" line-class="short" :top="3.5" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span class="known-for" v-if="person && person['known_for_department']">
|
<span v-if="person && person['known_for_department']" class="known-for">
|
||||||
{{
|
{{
|
||||||
person.known_for_department === "Acting"
|
person.known_for_department === "Acting"
|
||||||
? "Actor"
|
? "Actor"
|
||||||
@@ -21,8 +21,9 @@
|
|||||||
|
|
||||||
<figure class="person__poster">
|
<figure class="person__poster">
|
||||||
<img
|
<img
|
||||||
class="person-item__img is-loaded"
|
|
||||||
ref="poster-image"
|
ref="poster-image"
|
||||||
|
class="person-item__img is-loaded"
|
||||||
|
:alt="`Image of ${person.name}`"
|
||||||
:src="poster"
|
:src="poster"
|
||||||
/>
|
/>
|
||||||
</figure>
|
</figure>
|
||||||
@@ -30,9 +31,9 @@
|
|||||||
|
|
||||||
<div v-if="loading">
|
<div v-if="loading">
|
||||||
<loading-placeholder :count="6" />
|
<loading-placeholder :count="6" />
|
||||||
<loading-placeholder lineClass="short" :top="3" />
|
<loading-placeholder line-class="short" :top="3" />
|
||||||
<loading-placeholder :count="6" lineClass="fullwidth" />
|
<loading-placeholder :count="6" line-class="fullwidth" />
|
||||||
<loading-placeholder lineClass="short" :top="4.5" />
|
<loading-placeholder line-class="short" :top="4.5" />
|
||||||
<loading-placeholder />
|
<loading-placeholder />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -50,17 +51,17 @@
|
|||||||
</Detail>
|
</Detail>
|
||||||
|
|
||||||
<Detail
|
<Detail
|
||||||
|
v-if="creditedShows.length"
|
||||||
title="movies"
|
title="movies"
|
||||||
:detail="`Credited in ${creditedMovies.length} movies`"
|
:detail="`Credited in ${creditedMovies.length} movies`"
|
||||||
v-if="creditedShows.length"
|
|
||||||
>
|
>
|
||||||
<CastList :cast="creditedMovies" />
|
<CastList :cast="creditedMovies" />
|
||||||
</Detail>
|
</Detail>
|
||||||
|
|
||||||
<Detail
|
<Detail
|
||||||
|
v-if="creditedShows.length"
|
||||||
title="shows"
|
title="shows"
|
||||||
:detail="`Credited in ${creditedShows.length} shows`"
|
:detail="`Credited in ${creditedShows.length} shows`"
|
||||||
v-if="creditedShows.length"
|
|
||||||
>
|
>
|
||||||
<CastList :cast="creditedShows" />
|
<CastList :cast="creditedShows" />
|
||||||
</Detail>
|
</Detail>
|
||||||
@@ -70,17 +71,15 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, defineProps } from "vue";
|
import { ref, computed, defineProps } from "vue";
|
||||||
import img from "@/directives/v-image.vue";
|
|
||||||
import CastList from "@/components/CastList.vue";
|
import CastList from "@/components/CastList.vue";
|
||||||
import Detail from "@/components/popup/Detail.vue";
|
import Detail from "@/components/popup/Detail.vue";
|
||||||
import Description from "@/components/popup/Description.vue";
|
import Description from "@/components/popup/Description.vue";
|
||||||
import LoadingPlaceholder from "@/components/ui/LoadingPlaceholder.vue";
|
import LoadingPlaceholder from "@/components/ui/LoadingPlaceholder.vue";
|
||||||
import { getPerson, getPersonCredits } from "../../api";
|
|
||||||
import type { Ref, ComputedRef } from "vue";
|
import type { Ref, ComputedRef } from "vue";
|
||||||
|
import { getPerson, getPersonCredits } from "../../api";
|
||||||
import type {
|
import type {
|
||||||
IPerson,
|
IPerson,
|
||||||
IPersonCredits,
|
IPersonCredits,
|
||||||
ICast,
|
|
||||||
IMovie,
|
IMovie,
|
||||||
IShow
|
IShow
|
||||||
} from "../../interfaces/IList";
|
} from "../../interfaces/IList";
|
||||||
@@ -100,38 +99,39 @@
|
|||||||
const creditedMovies: Ref<Array<IMovie | IShow>> = ref([]);
|
const creditedMovies: Ref<Array<IMovie | IShow>> = ref([]);
|
||||||
const creditedShows: Ref<Array<IMovie | IShow>> = ref([]);
|
const creditedShows: Ref<Array<IMovie | IShow>> = ref([]);
|
||||||
|
|
||||||
const poster: ComputedRef<string> = computed(() => computePoster());
|
const poster: ComputedRef<string> = computed(() => {
|
||||||
|
if (!person.value) return "/assets/placeholder.png";
|
||||||
|
if (!person.value?.poster) return "/assets/no-image.svg";
|
||||||
|
|
||||||
|
return `${ASSET_URL}${ASSET_SIZES[0]}${person.value.poster}`;
|
||||||
|
});
|
||||||
|
|
||||||
const age: ComputedRef<string> = computed(() => {
|
const age: ComputedRef<string> = computed(() => {
|
||||||
if (!person.value?.birthday) return;
|
if (!person.value?.birthday) return "";
|
||||||
|
|
||||||
const today = new Date().getFullYear();
|
const today = new Date().getFullYear();
|
||||||
const birthYear = new Date(person.value.birthday).getFullYear();
|
const birthYear = new Date(person.value.birthday).getFullYear();
|
||||||
return `${today - birthYear} years old`;
|
return `${today - birthYear} years old`;
|
||||||
});
|
});
|
||||||
|
|
||||||
// On create functions
|
function setCredits(_credits: IPersonCredits) {
|
||||||
fetchPerson();
|
credits.value = _credits;
|
||||||
//
|
|
||||||
|
|
||||||
function fetchPerson() {
|
|
||||||
if (!props.id) {
|
|
||||||
console.error("Unable to fetch person, missing id!");
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getPerson(props.id)
|
function setPerson(_person: IPerson) {
|
||||||
.then(_person => (person.value = _person))
|
person.value = _person;
|
||||||
.then(() => getPersonCredits(person.value?.id))
|
|
||||||
.then(_credits => (credits.value = _credits))
|
|
||||||
.then(() => personCreditedFrom(credits.value?.cast));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function sortPopularity(a: IMovie | IShow, b: IMovie | IShow): number {
|
function sortPopularity(a: IMovie | IShow, b: IMovie | IShow): number {
|
||||||
return a.popularity < b.popularity ? 1 : -1;
|
return a.popularity < b.popularity ? 1 : -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
function alreadyExists(item: IMovie | IShow, pos: number, self: any[]) {
|
function alreadyExists(
|
||||||
const names = self.map(item => item.title);
|
item: IMovie | IShow,
|
||||||
|
pos: number,
|
||||||
|
self: Array<IMovie | IShow>
|
||||||
|
) {
|
||||||
|
const names = self.map(_item => _item.title);
|
||||||
return names.indexOf(item.title) === pos;
|
return names.indexOf(item.title) === pos;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,12 +147,21 @@
|
|||||||
.sort(sortPopularity);
|
.sort(sortPopularity);
|
||||||
}
|
}
|
||||||
|
|
||||||
const computePoster = () => {
|
function fetchPerson() {
|
||||||
if (!person.value) return "/assets/placeholder.png";
|
if (!props.id) {
|
||||||
else if (!person.value?.poster) return "/assets/no-image.svg";
|
console.error("Unable to fetch person, missing id!"); // eslint-disable-line no-console
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
return `${ASSET_URL}${ASSET_SIZES[0]}${person.value.poster}`;
|
getPerson(props.id)
|
||||||
};
|
.then(setPerson)
|
||||||
|
.then(() => getPersonCredits(person.value?.id))
|
||||||
|
.then(setCredits)
|
||||||
|
.then(() => personCreditedFrom(credits.value?.cast));
|
||||||
|
}
|
||||||
|
|
||||||
|
// On create functions
|
||||||
|
fetchPerson();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -3,24 +3,24 @@
|
|||||||
<h3 class="settings__header">Change password</h3>
|
<h3 class="settings__header">Change password</h3>
|
||||||
<form class="form">
|
<form class="form">
|
||||||
<seasoned-input
|
<seasoned-input
|
||||||
|
v-model="oldPassword"
|
||||||
placeholder="old password"
|
placeholder="old password"
|
||||||
icon="Keyhole"
|
icon="Keyhole"
|
||||||
type="password"
|
type="password"
|
||||||
v-model="oldPassword"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<seasoned-input
|
<seasoned-input
|
||||||
|
v-model="newPassword"
|
||||||
placeholder="new password"
|
placeholder="new password"
|
||||||
icon="Keyhole"
|
icon="Keyhole"
|
||||||
type="password"
|
type="password"
|
||||||
v-model="newPassword"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<seasoned-input
|
<seasoned-input
|
||||||
|
v-model="newPasswordRepeat"
|
||||||
placeholder="repeat new password"
|
placeholder="repeat new password"
|
||||||
icon="Keyhole"
|
icon="Keyhole"
|
||||||
type="password"
|
type="password"
|
||||||
v-model="newPasswordRepeat"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<seasoned-button @click="changePassword">change password</seasoned-button>
|
<seasoned-button @click="changePassword">change password</seasoned-button>
|
||||||
@@ -34,24 +34,20 @@
|
|||||||
import SeasonedInput from "@/components/ui/SeasonedInput.vue";
|
import SeasonedInput from "@/components/ui/SeasonedInput.vue";
|
||||||
import SeasonedButton from "@/components/ui/SeasonedButton.vue";
|
import SeasonedButton from "@/components/ui/SeasonedButton.vue";
|
||||||
import SeasonedMessages from "@/components/ui/SeasonedMessages.vue";
|
import SeasonedMessages from "@/components/ui/SeasonedMessages.vue";
|
||||||
import { ErrorMessageTypes } from "../../interfaces/IErrorMessage";
|
|
||||||
import type { Ref } from "vue";
|
import type { Ref } from "vue";
|
||||||
|
import { ErrorMessageTypes } from "../../interfaces/IErrorMessage";
|
||||||
import type { IErrorMessage } from "../../interfaces/IErrorMessage";
|
import type { IErrorMessage } from "../../interfaces/IErrorMessage";
|
||||||
|
|
||||||
interface ResetPasswordPayload {
|
// interface ResetPasswordPayload {
|
||||||
old_password: string;
|
// old_password: string;
|
||||||
new_password: string;
|
// new_password: string;
|
||||||
}
|
// }
|
||||||
|
|
||||||
const oldPassword: Ref<string> = ref("");
|
const oldPassword: Ref<string> = ref("");
|
||||||
const newPassword: Ref<string> = ref("");
|
const newPassword: Ref<string> = ref("");
|
||||||
const newPasswordRepeat: Ref<string> = ref("");
|
const newPasswordRepeat: Ref<string> = ref("");
|
||||||
const messages: Ref<IErrorMessage[]> = ref([]);
|
const messages: Ref<IErrorMessage[]> = ref([]);
|
||||||
|
|
||||||
function clearMessages() {
|
|
||||||
messages.value = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
function addWarningMessage(message: string, title?: string) {
|
function addWarningMessage(message: string, title?: string) {
|
||||||
messages.value.push({
|
messages.value.push({
|
||||||
message,
|
message,
|
||||||
@@ -64,12 +60,12 @@
|
|||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (!oldPassword.value || oldPassword?.value?.length === 0) {
|
if (!oldPassword.value || oldPassword?.value?.length === 0) {
|
||||||
addWarningMessage("Missing old password!", "Validation error");
|
addWarningMessage("Missing old password!", "Validation error");
|
||||||
return reject();
|
reject();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!newPassword.value || newPassword?.value?.length === 0) {
|
if (!newPassword.value || newPassword?.value?.length === 0) {
|
||||||
addWarningMessage("Missing new password!", "Validation error");
|
addWarningMessage("Missing new password!", "Validation error");
|
||||||
return reject();
|
reject();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newPassword.value !== newPasswordRepeat.value) {
|
if (newPassword.value !== newPasswordRepeat.value) {
|
||||||
@@ -77,7 +73,7 @@
|
|||||||
"Password and password repeat do not match!",
|
"Password and password repeat do not match!",
|
||||||
"Validation error"
|
"Validation error"
|
||||||
);
|
);
|
||||||
return reject();
|
reject();
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve(true);
|
resolve(true);
|
||||||
@@ -89,15 +85,14 @@
|
|||||||
try {
|
try {
|
||||||
validate();
|
validate();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("not valid!");
|
console.log("not valid!"); // eslint-disable-line no-console
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const body: ResetPasswordPayload = {
|
// const body: ResetPasswordPayload = {
|
||||||
old_password: oldPassword.value,
|
// old_password: oldPassword.value,
|
||||||
new_password: newPassword.value
|
// new_password: newPassword.value
|
||||||
};
|
// };
|
||||||
const options = {};
|
// const options = {};
|
||||||
// fetch()
|
// fetch()
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -10,14 +10,14 @@
|
|||||||
|
|
||||||
<form class="form">
|
<form class="form">
|
||||||
<seasoned-input
|
<seasoned-input
|
||||||
|
v-model="username"
|
||||||
placeholder="plex username"
|
placeholder="plex username"
|
||||||
type="email"
|
type="email"
|
||||||
v-model="username"
|
|
||||||
/>
|
/>
|
||||||
<seasoned-input
|
<seasoned-input
|
||||||
|
v-model="password"
|
||||||
placeholder="plex password"
|
placeholder="plex password"
|
||||||
type="password"
|
type="password"
|
||||||
v-model="password"
|
|
||||||
@enter="authenticatePlex"
|
@enter="authenticatePlex"
|
||||||
>
|
>
|
||||||
</seasoned-input>
|
</seasoned-input>
|
||||||
@@ -48,9 +48,9 @@
|
|||||||
import seasonedInput from "@/components/ui/SeasonedInput.vue";
|
import seasonedInput from "@/components/ui/SeasonedInput.vue";
|
||||||
import SeasonedButton from "@/components/ui/SeasonedButton.vue";
|
import SeasonedButton from "@/components/ui/SeasonedButton.vue";
|
||||||
import SeasonedMessages from "@/components/ui/SeasonedMessages.vue";
|
import SeasonedMessages from "@/components/ui/SeasonedMessages.vue";
|
||||||
|
import type { Ref, ComputedRef } from "vue";
|
||||||
import { linkPlexAccount, unlinkPlexAccount } from "../../api";
|
import { linkPlexAccount, unlinkPlexAccount } from "../../api";
|
||||||
import { ErrorMessageTypes } from "../../interfaces/IErrorMessage";
|
import { ErrorMessageTypes } from "../../interfaces/IErrorMessage";
|
||||||
import type { Ref, ComputedRef } from "vue";
|
|
||||||
import type { IErrorMessage } from "../../interfaces/IErrorMessage";
|
import type { IErrorMessage } from "../../interfaces/IErrorMessage";
|
||||||
|
|
||||||
interface Emit {
|
interface Emit {
|
||||||
@@ -64,20 +64,11 @@
|
|||||||
const store = useStore();
|
const store = useStore();
|
||||||
const emit = defineEmits<Emit>();
|
const emit = defineEmits<Emit>();
|
||||||
|
|
||||||
const loggedIn: ComputedRef<boolean> = computed(
|
|
||||||
() => store.getters["user/loggedIn"]
|
|
||||||
);
|
|
||||||
const plexId: ComputedRef<boolean> = computed(
|
const plexId: ComputedRef<boolean> = computed(
|
||||||
() => store.getters["user/plexId"]
|
() => store.getters["user/plexId"]
|
||||||
);
|
);
|
||||||
const settings: ComputedRef<boolean> = computed(
|
|
||||||
() => store.getters["user/settings"]
|
|
||||||
);
|
|
||||||
|
|
||||||
async function authenticatePlex() {
|
async function authenticatePlex() {
|
||||||
let username = this.plexUsername;
|
|
||||||
let password = this.plexPassword;
|
|
||||||
|
|
||||||
const { success, message } = await linkPlexAccount(
|
const { success, message } = await linkPlexAccount(
|
||||||
username.value,
|
username.value,
|
||||||
password.value
|
password.value
|
||||||
@@ -92,7 +83,7 @@
|
|||||||
messages.value.push({
|
messages.value.push({
|
||||||
type: success ? ErrorMessageTypes.Success : ErrorMessageTypes.Error,
|
type: success ? ErrorMessageTypes.Success : ErrorMessageTypes.Error,
|
||||||
title: success ? "Authenticated with plex" : "Something went wrong",
|
title: success ? "Authenticated with plex" : "Something went wrong",
|
||||||
message: message
|
message
|
||||||
} as IErrorMessage);
|
} as IErrorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="container" v-if="query?.length">
|
<div v-if="query?.length" class="container">
|
||||||
<h2 class="torrent-header-text">
|
<h2 class="torrent-header-text">
|
||||||
Searching for: <span class="query">{{ query }}</span>
|
Searching for: <span class="query">{{ query }}</span>
|
||||||
</h2>
|
</h2>
|
||||||
@@ -22,22 +22,19 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, inject, defineProps } from "vue";
|
import { ref, inject, defineProps } from "vue";
|
||||||
import { useStore } from "vuex";
|
import { useStore } from "vuex";
|
||||||
import { sortableSize } from "../../utils";
|
|
||||||
import { searchTorrents, addMagnet } from "../../api";
|
|
||||||
|
|
||||||
import Loader from "@/components/ui/Loader.vue";
|
import Loader from "@/components/ui/Loader.vue";
|
||||||
import TorrentTable from "@/components/torrent/TorrentTable.vue";
|
import TorrentTable from "@/components/torrent/TorrentTable.vue";
|
||||||
import type { Ref } from "vue";
|
import type { Ref } from "vue";
|
||||||
|
import { searchTorrents, addMagnet } from "../../api";
|
||||||
import type ITorrent from "../../interfaces/ITorrent";
|
import type ITorrent from "../../interfaces/ITorrent";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
query: string;
|
query: string;
|
||||||
tmdb_id?: number;
|
tmdbId?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const loading: Ref<boolean> = ref(true);
|
const loading: Ref<boolean> = ref(true);
|
||||||
const torrents: Ref<ITorrent[]> = ref([]);
|
const torrents: Ref<ITorrent[]> = ref([]);
|
||||||
const release_types: Ref<string[]> = ref(["all"]);
|
|
||||||
|
|
||||||
const props = defineProps<Props>();
|
const props = defineProps<Props>();
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
@@ -47,15 +44,12 @@
|
|||||||
error;
|
error;
|
||||||
} = inject("notifications");
|
} = inject("notifications");
|
||||||
|
|
||||||
fetchTorrents();
|
function setTorrents(_torrents: ITorrent[]) {
|
||||||
|
torrents.value = _torrents || [];
|
||||||
|
}
|
||||||
|
|
||||||
function fetchTorrents() {
|
function setLoading(state: boolean) {
|
||||||
loading.value = true;
|
loading.value = state;
|
||||||
|
|
||||||
searchTorrents(props.query)
|
|
||||||
.then(torrentResponse => (torrents.value = torrentResponse?.results))
|
|
||||||
.then(() => updateResultCountDisplay())
|
|
||||||
.finally(() => (loading.value = false));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateResultCountDisplay() {
|
function updateResultCountDisplay() {
|
||||||
@@ -66,6 +60,15 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fetchTorrents() {
|
||||||
|
loading.value = true;
|
||||||
|
|
||||||
|
searchTorrents(props.query)
|
||||||
|
.then(torrentResponse => setTorrents(torrentResponse?.results))
|
||||||
|
.then(() => updateResultCountDisplay())
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
}
|
||||||
|
|
||||||
function addTorrent(torrent: ITorrent) {
|
function addTorrent(torrent: ITorrent) {
|
||||||
const { name, magnet } = torrent;
|
const { name, magnet } = torrent;
|
||||||
|
|
||||||
@@ -75,16 +78,15 @@
|
|||||||
timeout: 3000
|
timeout: 3000
|
||||||
});
|
});
|
||||||
|
|
||||||
addMagnet(magnet, name, props.tmdb_id)
|
addMagnet(magnet, name, props.tmdbId)
|
||||||
.then(resp => {
|
.then(() => {
|
||||||
notifications.success({
|
notifications.success({
|
||||||
title: "Torrent added 🎉",
|
title: "Torrent added 🎉",
|
||||||
description: props.query,
|
description: props.query,
|
||||||
timeout: 3000
|
timeout: 3000
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(resp => {
|
.catch(() => {
|
||||||
console.log("Error while adding torrent:", resp?.data);
|
|
||||||
notifications.error({
|
notifications.error({
|
||||||
title: "Failed to add torrent 🙅♀️",
|
title: "Failed to add torrent 🙅♀️",
|
||||||
description: "Check console for more info",
|
description: "Check console for more info",
|
||||||
@@ -92,6 +94,8 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fetchTorrents();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
<th
|
<th
|
||||||
v-for="column in columns"
|
v-for="column in columns"
|
||||||
:key="column"
|
:key="column"
|
||||||
@click="sortTable(column)"
|
|
||||||
:class="column === selectedColumn ? 'active' : null"
|
:class="column === selectedColumn ? 'active' : null"
|
||||||
|
@click="sortTable(column)"
|
||||||
>
|
>
|
||||||
{{ column }}
|
{{ column }}
|
||||||
<span v-if="prevCol === column && direction">↑</span>
|
<span v-if="prevCol === column && direction">↑</span>
|
||||||
@@ -18,13 +18,32 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr
|
<tr
|
||||||
v-for="torrent in torrents"
|
v-for="torrent in torrents"
|
||||||
class="table__content"
|
|
||||||
:key="torrent.magnet"
|
:key="torrent.magnet"
|
||||||
|
class="table__content"
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
@click="expand($event, torrent.name)"
|
||||||
|
@keydown.enter="expand($event, torrent.name)"
|
||||||
|
>
|
||||||
|
{{ torrent.name }}
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
@click="expand($event, torrent.name)"
|
||||||
|
@keydown.enter="expand($event, torrent.name)"
|
||||||
|
>
|
||||||
|
{{ torrent.seed }}
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
@click="expand($event, torrent.name)"
|
||||||
|
@keydown.enter="expand($event, torrent.name)"
|
||||||
|
>
|
||||||
|
{{ torrent.size }}
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
class="download"
|
||||||
|
@click="() => emit('magnet', torrent)"
|
||||||
|
@keydown.enter="() => emit('magnet', torrent)"
|
||||||
>
|
>
|
||||||
<td @click="expand($event, torrent.name)">{{ torrent.name }}</td>
|
|
||||||
<td @click="expand($event, torrent.name)">{{ torrent.seed }}</td>
|
|
||||||
<td @click="expand($event, torrent.name)">{{ torrent.size }}</td>
|
|
||||||
<td @click="() => emit('magnet', torrent)" class="download">
|
|
||||||
<IconMagnet />
|
<IconMagnet />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -35,8 +54,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, defineProps, defineEmits } from "vue";
|
import { ref, defineProps, defineEmits } from "vue";
|
||||||
import IconMagnet from "@/icons/IconMagnet.vue";
|
import IconMagnet from "@/icons/IconMagnet.vue";
|
||||||
import { sortableSize } from "../../utils";
|
|
||||||
import type { Ref } from "vue";
|
import type { Ref } from "vue";
|
||||||
|
import { sortableSize } from "../../utils";
|
||||||
import type ITorrent from "../../interfaces/ITorrent";
|
import type ITorrent from "../../interfaces/ITorrent";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -87,18 +106,6 @@
|
|||||||
tableRow.insertAdjacentElement("afterend", expandedRow);
|
tableRow.insertAdjacentElement("afterend", expandedRow);
|
||||||
}
|
}
|
||||||
|
|
||||||
function sortTable(col, sameDirection = false) {
|
|
||||||
if (prevCol.value === col && sameDirection === false) {
|
|
||||||
direction.value = !direction.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (col === "name") sortName();
|
|
||||||
else if (col === "seed") sortSeed();
|
|
||||||
else if (col === "size") sortSize();
|
|
||||||
|
|
||||||
prevCol.value = col;
|
|
||||||
}
|
|
||||||
|
|
||||||
function sortName() {
|
function sortName() {
|
||||||
const torrentsCopy = [...torrents.value];
|
const torrentsCopy = [...torrents.value];
|
||||||
if (direction.value) {
|
if (direction.value) {
|
||||||
@@ -112,11 +119,11 @@
|
|||||||
const torrentsCopy = [...torrents.value];
|
const torrentsCopy = [...torrents.value];
|
||||||
if (direction.value) {
|
if (direction.value) {
|
||||||
torrents.value = torrentsCopy.sort(
|
torrents.value = torrentsCopy.sort(
|
||||||
(a, b) => parseInt(a.seed) - parseInt(b.seed)
|
(a, b) => parseInt(a.seed, 10) - parseInt(b.seed, 10)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
torrents.value = torrentsCopy.sort(
|
torrents.value = torrentsCopy.sort(
|
||||||
(a, b) => parseInt(b.seed) - parseInt(a.seed)
|
(a, b) => parseInt(b.seed, 10) - parseInt(a.seed, 10)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -133,6 +140,18 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sortTable(col, sameDirection = false) {
|
||||||
|
if (prevCol.value === col && sameDirection === false) {
|
||||||
|
direction.value = !direction.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (col === "name") sortName();
|
||||||
|
else if (col === "seed") sortSeed();
|
||||||
|
else if (col === "size") sortSize();
|
||||||
|
|
||||||
|
prevCol.value = col;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -2,13 +2,15 @@
|
|||||||
<div>
|
<div>
|
||||||
<torrent-search-results
|
<torrent-search-results
|
||||||
:query="query"
|
:query="query"
|
||||||
:tmdb_id="tmdb_id"
|
:tmdb-id="tmdbId"
|
||||||
:class="{ truncated: truncated }"
|
:class="{ truncated: truncated }"
|
||||||
><div
|
><div
|
||||||
v-if="truncated"
|
v-if="truncated"
|
||||||
class="load-more"
|
class="load-more"
|
||||||
|
tabindex="0"
|
||||||
role="button"
|
role="button"
|
||||||
@click="truncated = false"
|
@click="truncated = false"
|
||||||
|
@keydown.enter="truncated = false"
|
||||||
>
|
>
|
||||||
<icon-arrow-down />
|
<icon-arrow-down />
|
||||||
</div>
|
</div>
|
||||||
@@ -32,7 +34,7 @@
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
query: string;
|
query: string;
|
||||||
tmdb_id?: number;
|
tmdbId?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<Props>();
|
const props = defineProps<Props>();
|
||||||
|
|||||||
@@ -1,13 +1,23 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="darkToggle">
|
<div class="darkToggle">
|
||||||
<span @click="toggleDarkmode">{{ darkmodeToggleIcon }}</span>
|
<span @click="toggleDarkmode" @keydown.enter="toggleDarkmode">{{
|
||||||
|
darkmodeToggleIcon
|
||||||
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from "vue";
|
import { ref, computed } from "vue";
|
||||||
|
|
||||||
let darkmode = ref(systemDarkModeEnabled());
|
function systemDarkModeEnabled() {
|
||||||
|
const computedStyle = window.getComputedStyle(document.body);
|
||||||
|
if (computedStyle?.colorScheme != null) {
|
||||||
|
return computedStyle.colorScheme.includes("dark");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const darkmode = ref(systemDarkModeEnabled());
|
||||||
const darkmodeToggleIcon = computed(() => {
|
const darkmodeToggleIcon = computed(() => {
|
||||||
return darkmode.value ? "🌝" : "🌚";
|
return darkmode.value ? "🌝" : "🌚";
|
||||||
});
|
});
|
||||||
@@ -16,14 +26,6 @@
|
|||||||
darkmode.value = !darkmode.value;
|
darkmode.value = !darkmode.value;
|
||||||
document.body.className = darkmode.value ? "dark" : "light";
|
document.body.className = darkmode.value ? "dark" : "light";
|
||||||
}
|
}
|
||||||
|
|
||||||
function systemDarkModeEnabled() {
|
|
||||||
const computedStyle = window.getComputedStyle(document.body);
|
|
||||||
if (computedStyle["colorScheme"] != null) {
|
|
||||||
return computedStyle.colorScheme.includes("dark");
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
<div
|
<div
|
||||||
class="nav__hamburger"
|
class="nav__hamburger"
|
||||||
:class="{ open: isOpen }"
|
:class="{ open: isOpen }"
|
||||||
|
tabindex="0"
|
||||||
@click="toggle"
|
@click="toggle"
|
||||||
@keydown.enter="toggle"
|
@keydown.enter="toggle"
|
||||||
tabindex="0"
|
|
||||||
>
|
>
|
||||||
<div v-for="(_, index) in 3" :key="index" class="bar"></div>
|
<div v-for="(_, index) in 3" :key="index" class="bar"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div :class="`loader type-${type}`">
|
<div :class="`loader type-${type || LoaderHeightType.Page}`">
|
||||||
<i class="loader--icon">
|
<i class="loader--icon">
|
||||||
<i class="loader--icon-spinner" />
|
<i class="loader--icon-spinner" />
|
||||||
</i>
|
</i>
|
||||||
@@ -13,17 +13,13 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineProps } from "vue";
|
import { defineProps } from "vue";
|
||||||
|
import LoaderHeightType from "../../interfaces/ILoader";
|
||||||
enum LoaderHeightType {
|
|
||||||
Page = "page",
|
|
||||||
Section = "section"
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
type?: LoaderHeightType;
|
type?: LoaderHeightType;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { type = LoaderHeightType.Page } = defineProps<Props>();
|
defineProps<Props>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="text-input__loading" :style="`margin-top: ${top}rem`">
|
<div class="text-input__loading" :style="`margin-top: ${top || 0}rem`">
|
||||||
<div
|
<div
|
||||||
class="text-input__loading--line"
|
v-for="l in Array(count || 1)"
|
||||||
:class="lineClass"
|
|
||||||
v-for="l in Array(count)"
|
|
||||||
:key="l"
|
:key="l"
|
||||||
|
class="text-input__loading--line"
|
||||||
|
:class="lineClass || ''"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -13,12 +13,12 @@
|
|||||||
import { defineProps } from "vue";
|
import { defineProps } from "vue";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
count?: Number;
|
count?: number;
|
||||||
lineClass?: String;
|
lineClass?: string;
|
||||||
top?: Number;
|
top?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { count = 1, lineClass = "", top = 0 } = defineProps<Props>();
|
defineProps<Props>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -1,27 +1,26 @@
|
|||||||
<template>
|
<template>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@click="emit('click')"
|
|
||||||
:class="{ active: active, fullwidth: fullWidth }"
|
:class="{ active: active, fullwidth: fullWidth }"
|
||||||
|
@click="emit('click')"
|
||||||
>
|
>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, defineProps, defineEmits } from "vue";
|
import { defineProps, defineEmits } from "vue";
|
||||||
import type { Ref } from "vue";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
active?: Boolean;
|
active?: boolean;
|
||||||
fullWidth?: Boolean;
|
fullWidth?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Emit {
|
interface Emit {
|
||||||
(e: "click");
|
(e: "click");
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<Props>();
|
defineProps<Props>();
|
||||||
const emit = defineEmits<Emit>();
|
const emit = defineEmits<Emit>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,10 @@
|
|||||||
<div class="group" :class="{ completed: modelValue, focus }">
|
<div class="group" :class="{ completed: modelValue, focus }">
|
||||||
<component :is="inputIcon" v-if="inputIcon" />
|
<component :is="inputIcon" v-if="inputIcon" />
|
||||||
|
|
||||||
|
<!-- eslint-disable-next-line vuejs-accessibility/form-control-has-label -->
|
||||||
<input
|
<input
|
||||||
class="input"
|
class="input"
|
||||||
:type="toggledType || type"
|
:type="toggledType || type || 'text'"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
:value="modelValue"
|
:value="modelValue"
|
||||||
@input="handleInput"
|
@input="handleInput"
|
||||||
@@ -15,10 +16,10 @@
|
|||||||
|
|
||||||
<i
|
<i
|
||||||
v-if="modelValue && type === 'password'"
|
v-if="modelValue && type === 'password'"
|
||||||
@click="toggleShowPassword"
|
|
||||||
@keydown.enter="toggleShowPassword"
|
|
||||||
class="show noselect"
|
class="show noselect"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
|
@click="toggleShowPassword"
|
||||||
|
@keydown.enter="toggleShowPassword"
|
||||||
>{{ toggledType == "password" ? "show" : "hide" }}</i
|
>{{ toggledType == "password" ? "show" : "hide" }}</i
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
@@ -43,16 +44,16 @@
|
|||||||
(e: "update:modelValue", value: string);
|
(e: "update:modelValue", value: string);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { placeholder, type = "text", modelValue } = defineProps<Props>();
|
const props = defineProps<Props>();
|
||||||
const emit = defineEmits<Emit>();
|
const emit = defineEmits<Emit>();
|
||||||
|
|
||||||
const toggledType: Ref<string> = ref(type);
|
const toggledType: Ref<string> = ref(props.type);
|
||||||
const focus: Ref<boolean> = ref(false);
|
const focus: Ref<boolean> = ref(false);
|
||||||
|
|
||||||
const inputIcon = computed(() => {
|
const inputIcon = computed(() => {
|
||||||
if (type === "password") return IconKey;
|
if (props.type === "password") return IconKey;
|
||||||
if (type === "email") return IconEmail;
|
if (props.type === "email") return IconEmail;
|
||||||
if (type === "torrents") return IconBinoculars;
|
if (props.type === "torrents") return IconBinoculars;
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<transition-group name="fade">
|
<transition-group name="fade">
|
||||||
<div
|
<div
|
||||||
class="card"
|
|
||||||
v-for="(message, index) in messages"
|
v-for="(message, index) in messages"
|
||||||
:key="generateMessageKey(index, message)"
|
:key="generateMessageKey(index, message)"
|
||||||
|
class="card"
|
||||||
:class="message.type || 'warning'"
|
:class="message.type || 'warning'"
|
||||||
>
|
>
|
||||||
<span class="pinstripe"></span>
|
<span class="pinstripe"></span>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<h2 class="title">
|
<h2 class="title">
|
||||||
{{ message.title }}
|
{{ message.title || titleFromType(message.type) }}
|
||||||
</h2>
|
</h2>
|
||||||
<span v-if="message.message" class="message">{{
|
<span v-if="message.message" class="message">{{
|
||||||
message.message
|
message.message
|
||||||
@@ -27,7 +27,6 @@
|
|||||||
ErrorMessageTypes,
|
ErrorMessageTypes,
|
||||||
IErrorMessage
|
IErrorMessage
|
||||||
} from "../../interfaces/IErrorMessage";
|
} from "../../interfaces/IErrorMessage";
|
||||||
import type { Ref } from "vue";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
messages: IErrorMessage[];
|
messages: IErrorMessage[];
|
||||||
@@ -52,8 +51,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function dismiss(index: number) {
|
function dismiss(index: number) {
|
||||||
props.messages.splice(index, 1);
|
const _messages = [...props.messages];
|
||||||
emit("update:messages", [...props.messages]);
|
_messages.splice(index, 1);
|
||||||
|
emit("update:messages", _messages);
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateMessageKey(
|
function generateMessageKey(
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
v-for="option in options"
|
v-for="option in options"
|
||||||
:key="option"
|
:key="option"
|
||||||
class="toggle-button"
|
class="toggle-button"
|
||||||
@click="toggleTo(option)"
|
|
||||||
:class="selected === option ? 'selected' : null"
|
:class="selected === option ? 'selected' : null"
|
||||||
|
@click="() => toggleTo(option)"
|
||||||
>
|
>
|
||||||
{{ option }}
|
{{ option }}
|
||||||
</button>
|
</button>
|
||||||
@@ -13,8 +13,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, defineProps, defineEmits } from "vue";
|
import { defineProps, defineEmits } from "vue";
|
||||||
import type { Ref } from "vue";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
options: string[];
|
options: string[];
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
viewBox="0 0 32 32"
|
viewBox="0 0 32 32"
|
||||||
version="1.1"
|
version="1.1"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
style="transition-duration: 0s"
|
||||||
@click="$emit('click')"
|
@click="$emit('click')"
|
||||||
@keydown="event => $emit('keydown', event)"
|
@keydown="event => $emit('keydown', event)"
|
||||||
style="transition-duration: 0s"
|
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
fill="inherit"
|
fill="inherit"
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<svg
|
<svg
|
||||||
@click="$emit('click')"
|
|
||||||
version="1.1"
|
version="1.1"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 32 32"
|
viewBox="0 0 32 32"
|
||||||
|
@click="$emit('click')"
|
||||||
|
@keydown.enter="$emit('click')"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M30.229 1.771c-1.142-1.142-2.658-1.771-4.275-1.771s-3.133 0.629-4.275 1.771l-18.621 18.621c-0.158 0.158-0.275 0.358-0.337 0.575l-2.667 9.333c-0.133 0.467-0.004 0.967 0.338 1.308 0.254 0.254 0.596 0.392 0.942 0.392 0.121 0 0.246-0.017 0.367-0.050l9.333-2.667c0.217-0.063 0.417-0.179 0.575-0.337l18.621-18.621c2.358-2.362 2.358-6.196 0-8.554zM6.079 21.137l14.392-14.392 4.779 4.779-14.387 14.396-4.783-4.783zM21.413 5.804l1.058-1.058 4.779 4.779-1.058 1.058-4.779-4.779zM5.167 22.108l4.725 4.725-6.617 1.892 1.892-6.617zM28.346 8.438l-0.15 0.15-4.783-4.783 0.15-0.15c0.642-0.637 1.488-0.988 2.392-0.988s1.75 0.35 2.392 0.992c1.317 1.317 1.317 3.458 0 4.779z"
|
d="M30.229 1.771c-1.142-1.142-2.658-1.771-4.275-1.771s-3.133 0.629-4.275 1.771l-18.621 18.621c-0.158 0.158-0.275 0.358-0.337 0.575l-2.667 9.333c-0.133 0.467-0.004 0.967 0.338 1.308 0.254 0.254 0.596 0.392 0.942 0.392 0.121 0 0.246-0.017 0.367-0.050l9.333-2.667c0.217-0.063 0.417-0.179 0.575-0.337l18.621-18.621c2.358-2.362 2.358-6.196 0-8.554zM6.079 21.137l14.392-14.392 4.779 4.779-14.387 14.396-4.783-4.783zM21.413 5.804l1.058-1.058 4.779 4.779-1.058 1.058-4.779-4.779zM5.167 22.108l4.725 4.725-6.617 1.892 1.892-6.617zM28.346 8.438l-0.15 0.15-4.783-4.783 0.15-0.15c0.642-0.637 1.488-0.988 2.392-0.988s1.75 0.35 2.392 0.992c1.317 1.317 1.317 3.458 0 4.779z"
|
||||||
|
|||||||
@@ -4,18 +4,18 @@
|
|||||||
viewBox="0 0 32 32"
|
viewBox="0 0 32 32"
|
||||||
width="100%"
|
width="100%"
|
||||||
height="100%"
|
height="100%"
|
||||||
style="transition-duration: 0s;"
|
style="transition-duration: 0s"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
style="transition-duration: 0s;"
|
style="transition-duration: 0s"
|
||||||
d="M29.333 2.667h-26.667c-1.471 0-2.667 1.196-2.667 2.667v21.333c0 1.471 1.196 2.667 2.667 2.667h26.667c1.471 0 2.667-1.196 2.667-2.667v-21.333c0-1.471-1.196-2.667-2.667-2.667zM29.333 26.667h-26.667v-21.333h26.667v21.333c0.004 0 0 0 0 0z"
|
d="M29.333 2.667h-26.667c-1.471 0-2.667 1.196-2.667 2.667v21.333c0 1.471 1.196 2.667 2.667 2.667h26.667c1.471 0 2.667-1.196 2.667-2.667v-21.333c0-1.471-1.196-2.667-2.667-2.667zM29.333 26.667h-26.667v-21.333h26.667v21.333c0.004 0 0 0 0 0z"
|
||||||
></path>
|
></path>
|
||||||
<path
|
<path
|
||||||
style="transition-duration: 0s;"
|
style="transition-duration: 0s"
|
||||||
d="M11.333 17.058l-4.667 4.667v-1.725h-1.333v3.333c0 0.367 0.3 0.667 0.667 0.667h3.333v-1.333h-1.725l4.667-4.667-0.942-0.942z"
|
d="M11.333 17.058l-4.667 4.667v-1.725h-1.333v3.333c0 0.367 0.3 0.667 0.667 0.667h3.333v-1.333h-1.725l4.667-4.667-0.942-0.942z"
|
||||||
></path>
|
></path>
|
||||||
<path
|
<path
|
||||||
style="transition-duration: 0s;"
|
style="transition-duration: 0s"
|
||||||
d="M26 8h-3.333v1.333h1.725l-4.667 4.667 0.942 0.942 4.667-4.667v1.725h1.333v-3.333c0-0.367-0.3-0.667-0.667-0.667z"
|
d="M26 8h-3.333v1.333h1.725l-4.667 4.667 0.942 0.942 4.667-4.667v1.725h1.333v-3.333c0-0.367-0.3-0.667-0.667-0.667z"
|
||||||
></path>
|
></path>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
163
src/interfaces/IAutocompleteSearch.ts
Normal file
163
src/interfaces/IAutocompleteSearch.ts
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
/* eslint-disable no-use-before-define */
|
||||||
|
import { MediaTypes } from "./IList";
|
||||||
|
|
||||||
|
export interface IAutocompleteResult {
|
||||||
|
title: string;
|
||||||
|
id: number;
|
||||||
|
adult: boolean;
|
||||||
|
type: MediaTypes;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAutocompleteSearchResults {
|
||||||
|
took: number;
|
||||||
|
timed_out: boolean;
|
||||||
|
_shards: Shards;
|
||||||
|
hits: Hits;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Shards {
|
||||||
|
total: number;
|
||||||
|
successful: number;
|
||||||
|
skipped: number;
|
||||||
|
failed: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Hits {
|
||||||
|
total: Total;
|
||||||
|
max_score: null;
|
||||||
|
hits: Hit[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Hit {
|
||||||
|
_index: Index;
|
||||||
|
_type: Type;
|
||||||
|
_id: string;
|
||||||
|
_score: number;
|
||||||
|
_source: Source;
|
||||||
|
sort: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Index {
|
||||||
|
Movies = "movies",
|
||||||
|
Shows = "shows"
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Source {
|
||||||
|
tags: Tag[];
|
||||||
|
ecs: Ecs;
|
||||||
|
"@timestamp": Date;
|
||||||
|
adult: boolean;
|
||||||
|
input: Input;
|
||||||
|
host: Host;
|
||||||
|
"@version": string;
|
||||||
|
popularity: number;
|
||||||
|
log: Log;
|
||||||
|
video: boolean;
|
||||||
|
id: number;
|
||||||
|
agent: Agent;
|
||||||
|
original_title: string;
|
||||||
|
original_name?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Agent {
|
||||||
|
version: AgentVersion;
|
||||||
|
ephemeral_id: string;
|
||||||
|
id: string;
|
||||||
|
hostname: HostnameEnum;
|
||||||
|
type: AgentType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum HostnameEnum {
|
||||||
|
MACProLocal = "macPro.local"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum AgentType {
|
||||||
|
Filebeat = "filebeat"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum AgentVersion {
|
||||||
|
The700 = "7.0.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Ecs {
|
||||||
|
version: EcsVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum EcsVersion {
|
||||||
|
The100 = "1.0.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Host {
|
||||||
|
os: OS;
|
||||||
|
name: HostnameEnum;
|
||||||
|
id: ID;
|
||||||
|
hostname: HostnameEnum;
|
||||||
|
architecture: Architecture;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Architecture {
|
||||||
|
X8664 = "x86_64"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ID {
|
||||||
|
The30D157C386235739Aa1E30A9464Fa192 = "30D157C3-8623-5739-AA1E-30A9464FA192"
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OS {
|
||||||
|
version: OSVersion;
|
||||||
|
name: OSName;
|
||||||
|
build: Build;
|
||||||
|
family: Family;
|
||||||
|
platform: Family;
|
||||||
|
kernel: Kernel;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Build {
|
||||||
|
The18D109 = "18D109"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Family {
|
||||||
|
Darwin = "darwin"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Kernel {
|
||||||
|
The1820 = "18.2.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum OSName {
|
||||||
|
MACOSX = "Mac OS X"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum OSVersion {
|
||||||
|
The10143 = "10.14.3"
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Input {
|
||||||
|
type: InputType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum InputType {
|
||||||
|
Log = "log"
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Log {
|
||||||
|
offset: number;
|
||||||
|
file: File;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface File {
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Tag {
|
||||||
|
BeatsInputRawEvent = "beats_input_raw_event"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Type {
|
||||||
|
Doc = "_doc"
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Total {
|
||||||
|
value: number;
|
||||||
|
relation: string;
|
||||||
|
}
|
||||||
36
src/interfaces/IGraph.ts
Normal file
36
src/interfaces/IGraph.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
/* eslint-disable no-use-before-define */
|
||||||
|
|
||||||
|
export enum GraphTypes {
|
||||||
|
Plays = "plays",
|
||||||
|
Duration = "duration"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum GraphValueTypes {
|
||||||
|
Number = "number",
|
||||||
|
Time = "time"
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IGraphDataset {
|
||||||
|
name: string;
|
||||||
|
data: Array<number>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IGraphData {
|
||||||
|
labels: Array<string>;
|
||||||
|
series: Array<IGraphDataset>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IGraphResponse {
|
||||||
|
success: boolean;
|
||||||
|
data: Data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Data {
|
||||||
|
categories: Date[];
|
||||||
|
series: Series[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Series {
|
||||||
|
name: string;
|
||||||
|
data: number[];
|
||||||
|
}
|
||||||
@@ -1,25 +1,3 @@
|
|||||||
export interface IList {
|
|
||||||
results: ListResults;
|
|
||||||
page: number;
|
|
||||||
total_results: number;
|
|
||||||
total_pages: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IMediaCredits {
|
|
||||||
cast: Array<ICast>;
|
|
||||||
crew: Array<ICrew>;
|
|
||||||
id: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IPersonCredits {
|
|
||||||
cast: Array<IMovie | IShow>;
|
|
||||||
crew: Array<ICrew>;
|
|
||||||
id: number;
|
|
||||||
type?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ListResults = Array<IMovie | IShow | IPerson | IRequest>;
|
|
||||||
|
|
||||||
export enum MediaTypes {
|
export enum MediaTypes {
|
||||||
Movie = "movie",
|
Movie = "movie",
|
||||||
Show = "show",
|
Show = "show",
|
||||||
@@ -155,3 +133,25 @@ export interface ICrew {
|
|||||||
profile_path: string | null;
|
profile_path: string | null;
|
||||||
type: string;
|
type: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IMediaCredits {
|
||||||
|
cast: Array<ICast>;
|
||||||
|
crew: Array<ICrew>;
|
||||||
|
id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPersonCredits {
|
||||||
|
cast: Array<IMovie | IShow>;
|
||||||
|
crew: Array<ICrew>;
|
||||||
|
id: number;
|
||||||
|
type?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ListResults = Array<IMovie | IShow | IPerson | IRequest>;
|
||||||
|
|
||||||
|
export interface IList {
|
||||||
|
results: ListResults;
|
||||||
|
page: number;
|
||||||
|
total_results: number;
|
||||||
|
total_pages: number;
|
||||||
|
}
|
||||||
|
|||||||
6
src/interfaces/ILoader.ts
Normal file
6
src/interfaces/ILoader.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
enum LoaderHeightType {
|
||||||
|
Page = "page",
|
||||||
|
Section = "section"
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LoaderHeightType;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
export default interface INavigationIcon {
|
export default interface INavigationIcon {
|
||||||
title: string;
|
title: string;
|
||||||
route: string;
|
route: string;
|
||||||
icon: any;
|
icon: any; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
requiresAuth?: boolean;
|
requiresAuth?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,13 @@
|
|||||||
import type { MediaTypes } from "./IList";
|
import type { MediaTypes } from "./IList";
|
||||||
// export enum PopupTypes {
|
|
||||||
// Movie = "movie",
|
|
||||||
// Show = "show",
|
|
||||||
// Person = "person"
|
|
||||||
// }
|
|
||||||
|
|
||||||
// export interface IPopupOpen {
|
|
||||||
// id: string | number;
|
|
||||||
// type: PopupTypes;
|
|
||||||
// }
|
|
||||||
|
|
||||||
export interface IStatePopup {
|
export interface IStatePopup {
|
||||||
id: number;
|
id: number;
|
||||||
type: MediaTypes;
|
type: MediaTypes;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IPopupQuery {
|
||||||
|
movie?: number | string;
|
||||||
|
show?: number | string;
|
||||||
|
person?: number | string;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { createApp } from "vue";
|
import { createApp } from "vue";
|
||||||
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";
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const App = require("./App.vue").default;
|
const App = require("./App.vue").default;
|
||||||
|
|
||||||
store.dispatch("darkmodeModule/findAndSetDarkmodeSupported");
|
store.dispatch("darkmodeModule/findAndSetDarkmodeSupported");
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ const state: IStateDarkmode = {
|
|||||||
userChoice: undefined
|
userChoice: undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/no-shadow */
|
||||||
export default {
|
export default {
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
state,
|
state,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type IStateDocumentTitle from "../interfaces/IStateDocumentTitle";
|
|||||||
const capitalize = (string: string) => {
|
const capitalize = (string: string) => {
|
||||||
if (!string) return;
|
if (!string) return;
|
||||||
|
|
||||||
|
/* eslint-disable-next-line consistent-return */
|
||||||
return string.includes(" ")
|
return string.includes(" ")
|
||||||
? string
|
? string
|
||||||
.split(" ")
|
.split(" ")
|
||||||
@@ -25,6 +26,7 @@ const state: IStateDocumentTitle = {
|
|||||||
title: undefined
|
title: undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/no-shadow, no-return-assign */
|
||||||
export default {
|
export default {
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
state,
|
state,
|
||||||
@@ -42,10 +44,10 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
updateEmoji({ commit }, emoji: String) {
|
updateEmoji({ commit }, emoji: string) {
|
||||||
commit("SET_EMOJI", emoji);
|
commit("SET_EMOJI", emoji);
|
||||||
},
|
},
|
||||||
updateTitle({ commit }, title: String) {
|
updateTitle({ commit }, title: string) {
|
||||||
commit("SET_TITLE", title);
|
commit("SET_TITLE", title);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ const state: IStateHamburger = {
|
|||||||
open: false
|
open: false
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/no-shadow, no-return-assign */
|
||||||
export default {
|
export default {
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
state,
|
state,
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import router from "../routes";
|
|
||||||
import { MediaTypes } from "../interfaces/IList";
|
import { MediaTypes } from "../interfaces/IList";
|
||||||
import type { IStatePopup } from "../interfaces/IStatePopup";
|
import type { IStatePopup, IPopupQuery } from "../interfaces/IStatePopup";
|
||||||
|
|
||||||
|
/* eslint-disable-next-line import/no-cycle */
|
||||||
|
import router from "../routes";
|
||||||
|
|
||||||
const removeIncludedQueryParams = (params, key) => {
|
const removeIncludedQueryParams = (params, key) => {
|
||||||
if (params.has(key)) params.delete(key);
|
if (params.has(key)) params.delete(key);
|
||||||
@@ -9,11 +11,9 @@ const removeIncludedQueryParams = (params, key) => {
|
|||||||
|
|
||||||
function paramsToObject(entries) {
|
function paramsToObject(entries) {
|
||||||
const result = {};
|
const result = {};
|
||||||
for (const [key, value] of entries) {
|
return entries.forEach((key, value) => {
|
||||||
// each 'entry' is a [key, value] tupple
|
|
||||||
result[key] = value;
|
result[key] = value;
|
||||||
}
|
});
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateQueryParams = (id: number = null, type: MediaTypes = null) => {
|
const updateQueryParams = (id: number = null, type: MediaTypes = null) => {
|
||||||
@@ -38,6 +38,7 @@ const state: IStatePopup = {
|
|||||||
open: false
|
open: false
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/no-shadow */
|
||||||
export default {
|
export default {
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
state,
|
state,
|
||||||
@@ -60,7 +61,10 @@ export default {
|
|||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
open: ({ commit }, { id, type }: { id: number; type: MediaTypes }) => {
|
open: ({ commit }, { id, type }: { id: number; type: MediaTypes }) => {
|
||||||
if (!isNaN(id)) id = Number(id);
|
if (!Number.isNaN(id)) {
|
||||||
|
id = Number(id); /* eslint-disable-line no-param-reassign */
|
||||||
|
}
|
||||||
|
|
||||||
commit("SET_OPEN", { id, type });
|
commit("SET_OPEN", { id, type });
|
||||||
updateQueryParams(id, type);
|
updateQueryParams(id, type);
|
||||||
},
|
},
|
||||||
@@ -68,11 +72,11 @@ export default {
|
|||||||
commit("SET_CLOSE");
|
commit("SET_CLOSE");
|
||||||
updateQueryParams(); // reset
|
updateQueryParams(); // reset
|
||||||
},
|
},
|
||||||
resetStateFromUrlQuery: ({ commit }, query: any) => {
|
resetStateFromUrlQuery: ({ commit }, query: IPopupQuery) => {
|
||||||
let { movie, show, person } = query;
|
let { movie, show, person } = query;
|
||||||
movie = !isNaN(movie) ? Number(movie) : movie;
|
movie = !Number.isNaN(movie) ? Number(movie) : movie;
|
||||||
show = !isNaN(show) ? Number(show) : show;
|
show = !Number.isNaN(show) ? Number(show) : show;
|
||||||
person = !isNaN(person) ? Number(person) : person;
|
person = !Number.isNaN(person) ? Number(person) : person;
|
||||||
|
|
||||||
if (movie) commit("SET_OPEN", { id: movie, type: "movie" });
|
if (movie) commit("SET_OPEN", { id: movie, type: "movie" });
|
||||||
else if (show) commit("SET_OPEN", { id: show, type: "show" });
|
else if (show) commit("SET_OPEN", { id: show, type: "show" });
|
||||||
|
|||||||
@@ -6,12 +6,10 @@ const state: IStateTorrent = {
|
|||||||
resultCount: null
|
resultCount: null
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/no-shadow */
|
||||||
export default {
|
export default {
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
state: {
|
state,
|
||||||
results: [],
|
|
||||||
resultCount: null
|
|
||||||
},
|
|
||||||
getters: {
|
getters: {
|
||||||
results: (state: IStateTorrent) => {
|
results: (state: IStateTorrent) => {
|
||||||
return state.results;
|
return state.results;
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ export default {
|
|||||||
username: state => state.username,
|
username: state => state.username,
|
||||||
settings: state => state.settings,
|
settings: state => state.settings,
|
||||||
token: state => state.token,
|
token: state => state.token,
|
||||||
|
// loggedIn: state => true,
|
||||||
loggedIn: state => state && state.username !== null,
|
loggedIn: state => state && state.username !== null,
|
||||||
admin: state => state.admin,
|
admin: state => state.admin,
|
||||||
plexId: state => {
|
plexId: state => {
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="wrapper" v-if="plexId">
|
<div v-if="plexId" class="wrapper">
|
||||||
<h1>Your watch activity</h1>
|
<h1>Your watch activity</h1>
|
||||||
|
|
||||||
<div style="display: flex; flex-direction: row">
|
<div style="display: flex; flex-direction: row">
|
||||||
<label class="filter">
|
<label class="filter" for="dayinput">
|
||||||
<span>Days:</span>
|
<span>Days:</span>
|
||||||
<input
|
<input
|
||||||
class="dayinput"
|
id="dayinput"
|
||||||
v-model="days"
|
v-model="days"
|
||||||
|
class="dayinput"
|
||||||
placeholder="days"
|
placeholder="days"
|
||||||
type="number"
|
type="number"
|
||||||
pattern="[0-9]*"
|
pattern="[0-9]*"
|
||||||
@@ -15,15 +16,15 @@
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label class="filter">
|
<div class="filter">
|
||||||
<span>Data sorted by:</span>
|
<span>Data sorted by:</span>
|
||||||
<toggle-button
|
<toggle-button
|
||||||
|
v-model:selected="graphViewMode"
|
||||||
class="filter-item"
|
class="filter-item"
|
||||||
:options="[GraphTypes.Plays, GraphTypes.Duration]"
|
:options="[GraphTypes.Plays, GraphTypes.Duration]"
|
||||||
v-model:selected="graphViewMode"
|
|
||||||
@change="fetchChartData"
|
@change="fetchChartData"
|
||||||
/>
|
/>
|
||||||
</label>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="chart-section">
|
<div class="chart-section">
|
||||||
@@ -34,9 +35,9 @@
|
|||||||
:data="playsByDayData"
|
:data="playsByDayData"
|
||||||
type="line"
|
type="line"
|
||||||
:stacked="false"
|
:stacked="false"
|
||||||
:datasetDescriptionSuffix="`watch last ${days} days`"
|
:dataset-description-suffix="`watch last ${days} days`"
|
||||||
:tooltipDescriptionSuffix="selectedGraphViewMode.tooltipLabel"
|
:tooltip-description-suffix="selectedGraphViewMode.tooltipLabel"
|
||||||
:graphValueType="selectedGraphViewMode.valueType"
|
:graph-value-type="selectedGraphViewMode.valueType"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -44,13 +45,12 @@
|
|||||||
<div class="graph">
|
<div class="graph">
|
||||||
<Graph
|
<Graph
|
||||||
v-if="playsByDayofweekData"
|
v-if="playsByDayofweekData"
|
||||||
class="graph"
|
|
||||||
:data="playsByDayofweekData"
|
:data="playsByDayofweekData"
|
||||||
type="bar"
|
type="bar"
|
||||||
:stacked="true"
|
:stacked="true"
|
||||||
:datasetDescriptionSuffix="`watch last ${days} days`"
|
:dataset-description-suffix="`watch last ${days} days`"
|
||||||
:tooltipDescriptionSuffix="selectedGraphViewMode.tooltipLabel"
|
:tooltip-description-suffix="selectedGraphViewMode.tooltipLabel"
|
||||||
:graphValueType="selectedGraphViewMode.valueType"
|
:graph-value-type="selectedGraphViewMode.valueType"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -61,23 +61,18 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted } from "vue";
|
import { ref, computed } from "vue";
|
||||||
import { useStore } from "vuex";
|
import { useStore } from "vuex";
|
||||||
import Graph from "@/components/Graph.vue";
|
import Graph from "@/components/Graph.vue";
|
||||||
import ToggleButton from "@/components/ui/ToggleButton.vue";
|
import ToggleButton from "@/components/ui/ToggleButton.vue";
|
||||||
import IconStop from "@/icons/IconStop.vue";
|
import IconStop from "@/icons/IconStop.vue";
|
||||||
import { fetchGraphData } from "../api";
|
|
||||||
import type { Ref } from "vue";
|
import type { Ref } from "vue";
|
||||||
|
import { fetchGraphData } from "../api";
|
||||||
enum GraphTypes {
|
import {
|
||||||
Plays = "plays",
|
GraphTypes,
|
||||||
Duration = "duration"
|
GraphValueTypes,
|
||||||
}
|
IGraphData
|
||||||
|
} from "../interfaces/IGraph";
|
||||||
enum GraphValueTypes {
|
|
||||||
Number = "number",
|
|
||||||
Time = "time"
|
|
||||||
}
|
|
||||||
|
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
|
|
||||||
@@ -85,9 +80,6 @@
|
|||||||
const graphViewMode: Ref<GraphTypes> = ref(GraphTypes.Plays);
|
const graphViewMode: Ref<GraphTypes> = ref(GraphTypes.Plays);
|
||||||
const plexId = computed(() => store.getters["user/plexId"]);
|
const plexId = computed(() => store.getters["user/plexId"]);
|
||||||
|
|
||||||
const selectedGraphViewMode = computed(() =>
|
|
||||||
graphValueViewMode.find(viewMode => viewMode.type === graphViewMode.value)
|
|
||||||
);
|
|
||||||
const graphValueViewMode = [
|
const graphValueViewMode = [
|
||||||
{
|
{
|
||||||
type: GraphTypes.Plays,
|
type: GraphTypes.Plays,
|
||||||
@@ -101,13 +93,27 @@
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
const playsByDayData: Ref<any> = ref(null);
|
const playsByDayData: Ref<IGraphData> = ref(null);
|
||||||
const playsByDayofweekData: Ref<any> = ref(null);
|
const playsByDayofweekData: Ref<IGraphData> = ref(null);
|
||||||
fetchChartData();
|
|
||||||
|
|
||||||
function fetchChartData() {
|
const selectedGraphViewMode = computed(() =>
|
||||||
fetchPlaysByDay();
|
graphValueViewMode.find(viewMode => viewMode.type === graphViewMode.value)
|
||||||
fetchPlaysByDayOfWeek();
|
);
|
||||||
|
|
||||||
|
function convertDateStringToDayMonth(date: string): string {
|
||||||
|
if (!date.match(/[0-9]{4}-[0-9]{2}-[0-9]{2}/)) {
|
||||||
|
return date;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [, month, day] = date.split("-");
|
||||||
|
return `${day}.${month}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertDateLabels(data) {
|
||||||
|
return {
|
||||||
|
labels: data.categories.map(convertDateStringToDayMonth),
|
||||||
|
series: data.series
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchPlaysByDay() {
|
async function fetchPlaysByDay() {
|
||||||
@@ -126,21 +132,12 @@
|
|||||||
).then(data => convertDateLabels(data?.data));
|
).then(data => convertDateLabels(data?.data));
|
||||||
}
|
}
|
||||||
|
|
||||||
function convertDateLabels(data) {
|
function fetchChartData() {
|
||||||
return {
|
fetchPlaysByDay();
|
||||||
labels: data.categories.map(convertDateStringToDayMonth),
|
fetchPlaysByDayOfWeek();
|
||||||
series: data.series
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function convertDateStringToDayMonth(date: string): string {
|
fetchChartData();
|
||||||
if (!date.match(/[0-9]{4}-[0-9]{2}-[0-9]{2}/)) {
|
|
||||||
return date;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [year, month, day] = date.split("-");
|
|
||||||
return `${day}.${month}`;
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<ResultsSection :title="listName" :apiFunction="_getTmdbMovieListByName" />
|
<ResultsSection :title="listName" :api-function="_getTmdbMovieListByName" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="profile">
|
<section class="profile">
|
||||||
<div class="profile__content" v-if="loggedIn">
|
<div v-if="loggedIn" class="profile__content">
|
||||||
<header class="profile__header">
|
<header class="profile__header">
|
||||||
<h2 class="profile__title">{{ emoji }} Welcome {{ username }}</h2>
|
<h2 class="profile__title">{{ emoji }} Welcome {{ username }}</h2>
|
||||||
|
|
||||||
<div class="button--group">
|
<div class="button--group">
|
||||||
<seasoned-button @click="toggleSettings" :active="showSettings">{{
|
<seasoned-button :active="showSettings" @click="toggleSettings">{{
|
||||||
showSettings ? "hide settings" : "show settings"
|
showSettings ? "hide settings" : "show settings"
|
||||||
}}</seasoned-button>
|
}}</seasoned-button>
|
||||||
<seasoned-button @click="toggleActivity" :active="showActivity">{{
|
<seasoned-button :active="showActivity" @click="toggleActivity">{{
|
||||||
showActivity ? "hide activity" : "show activity"
|
showActivity ? "hide activity" : "show activity"
|
||||||
}}</seasoned-button>
|
}}</seasoned-button>
|
||||||
|
|
||||||
@@ -16,15 +16,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<settings v-if="showSettings" />
|
<settings-page v-if="showSettings" />
|
||||||
|
|
||||||
<activity v-if="showActivity" />
|
<activity-page v-if="showActivity" />
|
||||||
|
|
||||||
<page-header title="Your requests" :info="resultCount" />
|
<page-header title="Your requests" :info="resultCount" />
|
||||||
<results-list v-if="results" :results="results" />
|
<results-list v-if="results" :results="results" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section class="not-found" v-if="!loggedIn">
|
<section v-if="!loggedIn" class="not-found">
|
||||||
<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">
|
||||||
@@ -40,11 +40,11 @@
|
|||||||
import { useStore } from "vuex";
|
import { useStore } from "vuex";
|
||||||
import PageHeader from "@/components/PageHeader.vue";
|
import PageHeader from "@/components/PageHeader.vue";
|
||||||
import ResultsList from "@/components/ResultsList.vue";
|
import ResultsList from "@/components/ResultsList.vue";
|
||||||
import Settings from "@/pages/SettingsPage.vue";
|
import SettingsPage from "@/pages/SettingsPage.vue";
|
||||||
import Activity from "@/pages/ActivityPage.vue";
|
import ActivityPage from "@/pages/ActivityPage.vue";
|
||||||
import SeasonedButton from "@/components/ui/SeasonedButton.vue";
|
import SeasonedButton from "@/components/ui/SeasonedButton.vue";
|
||||||
import { getEmoji, getUserRequests, getSettings, logout } from "../api";
|
|
||||||
import type { Ref, ComputedRef } from "vue";
|
import type { Ref, ComputedRef } from "vue";
|
||||||
|
import { getEmoji, getUserRequests } from "../api";
|
||||||
import type { ListResults } from "../interfaces/IList";
|
import type { ListResults } from "../interfaces/IList";
|
||||||
|
|
||||||
const emoji: Ref<string> = ref("");
|
const emoji: Ref<string> = ref("");
|
||||||
@@ -57,7 +57,6 @@
|
|||||||
|
|
||||||
const loggedIn: Ref<boolean> = computed(() => store.getters["user/loggedIn"]);
|
const loggedIn: Ref<boolean> = computed(() => store.getters["user/loggedIn"]);
|
||||||
const username: Ref<string> = computed(() => store.getters["user/username"]);
|
const username: Ref<string> = computed(() => store.getters["user/username"]);
|
||||||
const settings: Ref<object> = computed(() => store.getters["user/settings"]);
|
|
||||||
|
|
||||||
const resultCount: ComputedRef<number | string> = computed(() => {
|
const resultCount: ComputedRef<number | string> = computed(() => {
|
||||||
const currentCount = results?.value?.length || 0;
|
const currentCount = results?.value?.length || 0;
|
||||||
@@ -65,6 +64,10 @@
|
|||||||
return `${currentCount} of ${totalCount} results`;
|
return `${currentCount} of ${totalCount} results`;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function setEmoji(_emoji: string) {
|
||||||
|
emoji.value = _emoji;
|
||||||
|
}
|
||||||
|
|
||||||
// Component loaded actions
|
// Component loaded actions
|
||||||
getUserRequests().then(requestResults => {
|
getUserRequests().then(requestResults => {
|
||||||
if (!requestResults?.results) return;
|
if (!requestResults?.results) return;
|
||||||
@@ -72,26 +75,12 @@
|
|||||||
totalResults.value = requestResults.total_results;
|
totalResults.value = requestResults.total_results;
|
||||||
});
|
});
|
||||||
|
|
||||||
getEmoji().then(resp => (emoji.value = resp?.emoji));
|
getEmoji().then(resp => setEmoji(resp?.emoji));
|
||||||
|
|
||||||
showSettings.value = window.location.toString().includes("settings=true");
|
showSettings.value = window.location.toString().includes("settings=true");
|
||||||
showActivity.value = window.location.toString().includes("activity=true");
|
showActivity.value = window.location.toString().includes("activity=true");
|
||||||
// Component loaded actions end
|
// Component loaded actions end
|
||||||
|
|
||||||
function toggleSettings() {
|
|
||||||
showSettings.value = !showSettings.value;
|
|
||||||
updateQueryParams("settings", showSettings.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleActivity() {
|
|
||||||
showActivity.value = !showActivity.value;
|
|
||||||
updateQueryParams("activity", showActivity.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function _logout() {
|
|
||||||
store.dispatch("user/logout");
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateQueryParams(key, value = false) {
|
function updateQueryParams(key, value = false) {
|
||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
if (params.has(key)) {
|
if (params.has(key)) {
|
||||||
@@ -112,6 +101,20 @@
|
|||||||
}`
|
}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleSettings() {
|
||||||
|
showSettings.value = !showSettings.value;
|
||||||
|
updateQueryParams("settings", showSettings.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleActivity() {
|
||||||
|
showActivity.value = !showActivity.value;
|
||||||
|
updateQueryParams("activity", showActivity.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _logout() {
|
||||||
|
store.dispatch("user/logout");
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -2,27 +2,27 @@
|
|||||||
<section>
|
<section>
|
||||||
<h1>Register new user</h1>
|
<h1>Register new user</h1>
|
||||||
|
|
||||||
<form class="form" ref="formElement">
|
<form ref="formElement" class="form">
|
||||||
<seasoned-input
|
<seasoned-input
|
||||||
|
v-model="username"
|
||||||
placeholder="username"
|
placeholder="username"
|
||||||
icon="Email"
|
icon="Email"
|
||||||
type="email"
|
type="email"
|
||||||
v-model="username"
|
|
||||||
@keydown.enter="focusOnNextElement"
|
@keydown.enter="focusOnNextElement"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<seasoned-input
|
<seasoned-input
|
||||||
|
v-model="password"
|
||||||
placeholder="password"
|
placeholder="password"
|
||||||
icon="Keyhole"
|
icon="Keyhole"
|
||||||
type="password"
|
type="password"
|
||||||
v-model="password"
|
|
||||||
@keydown.enter="focusOnNextElement"
|
@keydown.enter="focusOnNextElement"
|
||||||
/>
|
/>
|
||||||
<seasoned-input
|
<seasoned-input
|
||||||
|
v-model="passwordRepeat"
|
||||||
placeholder="repeat password"
|
placeholder="repeat password"
|
||||||
icon="Keyhole"
|
icon="Keyhole"
|
||||||
type="password"
|
type="password"
|
||||||
v-model="passwordRepeat"
|
|
||||||
@keydown.enter="submit"
|
@keydown.enter="submit"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -44,10 +44,10 @@
|
|||||||
import SeasonedButton from "@/components/ui/SeasonedButton.vue";
|
import SeasonedButton from "@/components/ui/SeasonedButton.vue";
|
||||||
import SeasonedInput from "@/components/ui/SeasonedInput.vue";
|
import SeasonedInput from "@/components/ui/SeasonedInput.vue";
|
||||||
import SeasonedMessages from "@/components/ui/SeasonedMessages.vue";
|
import SeasonedMessages from "@/components/ui/SeasonedMessages.vue";
|
||||||
|
import type { Ref } from "vue";
|
||||||
import { register } from "../api";
|
import { register } from "../api";
|
||||||
import { focusFirstFormInput, focusOnNextElement } from "../utils";
|
import { focusFirstFormInput, focusOnNextElement } from "../utils";
|
||||||
import { ErrorMessageTypes } from "../interfaces/IErrorMessage";
|
import { ErrorMessageTypes } from "../interfaces/IErrorMessage";
|
||||||
import type { Ref } from "vue";
|
|
||||||
import type { IErrorMessage } from "../interfaces/IErrorMessage";
|
import type { IErrorMessage } from "../interfaces/IErrorMessage";
|
||||||
|
|
||||||
const username: Ref<string> = ref("");
|
const username: Ref<string> = ref("");
|
||||||
@@ -85,32 +85,27 @@
|
|||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (!username.value || username?.value?.length === 0) {
|
if (!username.value || username?.value?.length === 0) {
|
||||||
addWarningMessage("Missing username", "Validation error");
|
addWarningMessage("Missing username", "Validation error");
|
||||||
return reject();
|
reject();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!password.value || password?.value?.length === 0) {
|
if (!password.value || password?.value?.length === 0) {
|
||||||
addWarningMessage("Missing password", "Validation error");
|
addWarningMessage("Missing password", "Validation error");
|
||||||
return reject();
|
reject();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (passwordRepeat.value == null || passwordRepeat.value.length == 0) {
|
if (passwordRepeat.value == null || passwordRepeat.value.length === 0) {
|
||||||
addWarningMessage("Missing repeat password", "Validation error");
|
addWarningMessage("Missing repeat password", "Validation error");
|
||||||
return reject();
|
reject();
|
||||||
}
|
}
|
||||||
if (passwordRepeat != password) {
|
if (passwordRepeat.value !== password.value) {
|
||||||
addWarningMessage("Passwords do not match", "Validation error");
|
addWarningMessage("Passwords do not match", "Validation error");
|
||||||
return reject();
|
reject();
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve(true);
|
resolve(true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function submit() {
|
|
||||||
clearMessages();
|
|
||||||
validate().then(registerUser);
|
|
||||||
}
|
|
||||||
|
|
||||||
function registerUser() {
|
function registerUser() {
|
||||||
register(username.value, password.value)
|
register(username.value, password.value)
|
||||||
.then(data => {
|
.then(data => {
|
||||||
@@ -120,15 +115,19 @@
|
|||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
if (error?.status === 401) {
|
if (error?.status === 401) {
|
||||||
return addErrorMessage(
|
addErrorMessage("Incorrect username or password", "Access denied");
|
||||||
"Incorrect username or password",
|
return null;
|
||||||
"Access denied"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addErrorMessage(error?.message, "Unexpected error");
|
addErrorMessage(error?.message, "Unexpected error");
|
||||||
|
return null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function submit() {
|
||||||
|
clearMessages();
|
||||||
|
validate().then(registerUser);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<ResultsSection title="Requests" :apiFunction="getRequests" />
|
<ResultsSection title="Requests" :api-function="getRequests" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div style="display: flex; flex-direction: row">
|
<div style="display: flex; flex-direction: row">
|
||||||
<label class="filter">
|
<div class="filter">
|
||||||
<span>Search filter:</span>
|
<span>Search filter:</span>
|
||||||
|
|
||||||
<toggle-button
|
<toggle-button
|
||||||
:options="toggleOptions"
|
|
||||||
v-model:selected="mediaType"
|
v-model:selected="mediaType"
|
||||||
|
:options="toggleOptions"
|
||||||
@change="toggleChanged"
|
@change="toggleChanged"
|
||||||
/>
|
/>
|
||||||
</label>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ResultsSection v-if="query" :title="title" :apiFunction="search" />
|
<ResultsSection v-if="query" :title="title" :api-function="search" />
|
||||||
<h1 v-else class="no-results">No query found, please search above</h1>
|
<h1 v-else class="no-results">No query found, please search above</h1>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -20,12 +20,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from "vue";
|
import { ref, computed } from "vue";
|
||||||
import { useRoute, useRouter } from "vue-router";
|
import { useRoute, useRouter } from "vue-router";
|
||||||
import { searchTmdb } from "../api";
|
|
||||||
|
|
||||||
import ResultsSection from "@/components/ResultsSection.vue";
|
import ResultsSection from "@/components/ResultsSection.vue";
|
||||||
import PageHeader from "@/components/PageHeader.vue";
|
|
||||||
import ToggleButton from "@/components/ui/ToggleButton.vue";
|
import ToggleButton from "@/components/ui/ToggleButton.vue";
|
||||||
import type { Ref } from "vue";
|
import type { Ref } from "vue";
|
||||||
|
import { searchTmdb } from "../api";
|
||||||
import { MediaTypes } from "../interfaces/IList";
|
import { MediaTypes } from "../interfaces/IList";
|
||||||
|
|
||||||
// interface ISearchParams {
|
// interface ISearchParams {
|
||||||
@@ -54,20 +53,14 @@
|
|||||||
mediaType.value = (urlQuery?.media_type as MediaTypes) || mediaType.value;
|
mediaType.value = (urlQuery?.media_type as MediaTypes) || mediaType.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
let search = (
|
const search = (
|
||||||
_page = page.value || 1,
|
_page = page.value || 1,
|
||||||
_mediaType = mediaType.value || "all"
|
_mediaType = mediaType.value || "all"
|
||||||
) => {
|
) => {
|
||||||
return searchTmdb(query.value, _page, adult.value, _mediaType);
|
return searchTmdb(query.value, _page, adult.value, _mediaType);
|
||||||
};
|
};
|
||||||
|
|
||||||
function toggleChanged() {
|
|
||||||
updateQueryParams();
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateQueryParams() {
|
function updateQueryParams() {
|
||||||
const { query, page, adult, media_type } = route.query;
|
|
||||||
|
|
||||||
router.push({
|
router.push({
|
||||||
path: "search",
|
path: "search",
|
||||||
query: {
|
query: {
|
||||||
@@ -76,6 +69,10 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleChanged() {
|
||||||
|
updateQueryParams();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -11,12 +11,28 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { inject } from "vue";
|
||||||
import { useStore } from "vuex";
|
import { useStore } from "vuex";
|
||||||
|
import { useRoute } from "vue-router";
|
||||||
import ChangePassword from "@/components/profile/ChangePassword.vue";
|
import ChangePassword from "@/components/profile/ChangePassword.vue";
|
||||||
import LinkPlexAccount from "@/components/profile/LinkPlexAccount.vue";
|
import LinkPlexAccount from "@/components/profile/LinkPlexAccount.vue";
|
||||||
import { getSettings } from "../api";
|
import { getSettings } from "../api";
|
||||||
|
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
|
const route = useRoute();
|
||||||
|
const notifications: {
|
||||||
|
error;
|
||||||
|
} = inject("notifications");
|
||||||
|
|
||||||
|
function displayWarningIfMissingPlexAccount() {
|
||||||
|
if (route.query?.missingPlexAccount === "true") {
|
||||||
|
notifications.error({
|
||||||
|
title: "Missing plex account 🧲",
|
||||||
|
description: "Link your plex account to view activity",
|
||||||
|
timeout: 10000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function reloadSettings() {
|
function reloadSettings() {
|
||||||
return getSettings().then(response => {
|
return getSettings().then(response => {
|
||||||
@@ -26,6 +42,9 @@
|
|||||||
store.dispatch("user/setSettings", settings);
|
store.dispatch("user/setSettings", settings);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Functions called on component load
|
||||||
|
displayWarningIfMissingPlexAccount();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|||||||
@@ -2,19 +2,19 @@
|
|||||||
<section>
|
<section>
|
||||||
<h1>Sign in</h1>
|
<h1>Sign in</h1>
|
||||||
|
|
||||||
<form class="form" ref="formElement">
|
<form ref="formElement" class="form">
|
||||||
<seasoned-input
|
<seasoned-input
|
||||||
|
v-model="username"
|
||||||
placeholder="username"
|
placeholder="username"
|
||||||
icon="Email"
|
icon="Email"
|
||||||
type="email"
|
type="email"
|
||||||
v-model="username"
|
|
||||||
@keydown.enter="focusOnNextElement"
|
@keydown.enter="focusOnNextElement"
|
||||||
/>
|
/>
|
||||||
<seasoned-input
|
<seasoned-input
|
||||||
|
v-model="password"
|
||||||
placeholder="password"
|
placeholder="password"
|
||||||
icon="Keyhole"
|
icon="Keyhole"
|
||||||
type="password"
|
type="password"
|
||||||
v-model="password"
|
|
||||||
@keydown.enter="submit"
|
@keydown.enter="submit"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -35,10 +35,10 @@
|
|||||||
import SeasonedInput from "@/components/ui/SeasonedInput.vue";
|
import SeasonedInput from "@/components/ui/SeasonedInput.vue";
|
||||||
import SeasonedButton from "@/components/ui/SeasonedButton.vue";
|
import SeasonedButton from "@/components/ui/SeasonedButton.vue";
|
||||||
import SeasonedMessages from "@/components/ui/SeasonedMessages.vue";
|
import SeasonedMessages from "@/components/ui/SeasonedMessages.vue";
|
||||||
|
import type { Ref } from "vue";
|
||||||
import { login } from "../api";
|
import { login } from "../api";
|
||||||
import { focusFirstFormInput, focusOnNextElement } from "../utils";
|
import { focusFirstFormInput, focusOnNextElement } from "../utils";
|
||||||
import { ErrorMessageTypes } from "../interfaces/IErrorMessage";
|
import { ErrorMessageTypes } from "../interfaces/IErrorMessage";
|
||||||
import type { Ref } from "vue";
|
|
||||||
import type { IErrorMessage } from "../interfaces/IErrorMessage";
|
import type { IErrorMessage } from "../interfaces/IErrorMessage";
|
||||||
|
|
||||||
const username: Ref<string> = ref("");
|
const username: Ref<string> = ref("");
|
||||||
@@ -75,23 +75,18 @@
|
|||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (!username.value || username?.value?.length === 0) {
|
if (!username.value || username?.value?.length === 0) {
|
||||||
addWarningMessage("Missing username", "Validation error");
|
addWarningMessage("Missing username", "Validation error");
|
||||||
return reject();
|
reject();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!password.value || password?.value?.length === 0) {
|
if (!password.value || password?.value?.length === 0) {
|
||||||
addWarningMessage("Missing password", "Validation error");
|
addWarningMessage("Missing password", "Validation error");
|
||||||
return reject();
|
reject();
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve(true);
|
resolve(true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function submit() {
|
|
||||||
clearMessages();
|
|
||||||
validate().then(signin);
|
|
||||||
}
|
|
||||||
|
|
||||||
function signin() {
|
function signin() {
|
||||||
login(username.value, password.value, true)
|
login(username.value, password.value, true)
|
||||||
.then(data => {
|
.then(data => {
|
||||||
@@ -101,15 +96,19 @@
|
|||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
if (error?.status === 401) {
|
if (error?.status === 401) {
|
||||||
return addErrorMessage(
|
addErrorMessage("Incorrect username or password", "Access denied");
|
||||||
"Incorrect username or password",
|
return null;
|
||||||
"Access denied"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addErrorMessage(error?.message, "Unexpected error");
|
addErrorMessage(error?.message, "Unexpected error");
|
||||||
|
return null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function submit() {
|
||||||
|
clearMessages();
|
||||||
|
validate().then(signin);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -7,8 +7,8 @@
|
|||||||
<seasoned-input
|
<seasoned-input
|
||||||
v-model="query"
|
v-model="query"
|
||||||
type="torrents"
|
type="torrents"
|
||||||
@keydown.enter="setTorrentQuery"
|
|
||||||
placeholder="Search torrents"
|
placeholder="Search torrents"
|
||||||
|
@keydown.enter="setTorrentQuery"
|
||||||
/>
|
/>
|
||||||
<seasoned-button @click="setTorrentQuery">Search</seasoned-button>
|
<seasoned-button @click="setTorrentQuery">Search</seasoned-button>
|
||||||
</div>
|
</div>
|
||||||
@@ -27,8 +27,8 @@
|
|||||||
import SeasonedButton from "@/components/ui/SeasonedButton.vue";
|
import SeasonedButton from "@/components/ui/SeasonedButton.vue";
|
||||||
import TorrentList from "@/components/torrent/TorrentSearchResults.vue";
|
import TorrentList from "@/components/torrent/TorrentSearchResults.vue";
|
||||||
import ActiveTorrents from "@/components/torrent/ActiveTorrents.vue";
|
import ActiveTorrents from "@/components/torrent/ActiveTorrents.vue";
|
||||||
import { getValueFromUrlQuery, setUrlQueryParameter } from "../utils";
|
|
||||||
import type { Ref } from "vue";
|
import type { Ref } from "vue";
|
||||||
|
import { getValueFromUrlQuery, setUrlQueryParameter } from "../utils";
|
||||||
|
|
||||||
const urlQuery = getValueFromUrlQuery("query");
|
const urlQuery = getValueFromUrlQuery("query");
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,24 @@
|
|||||||
|
<!-- eslint-disable vuejs-accessibility/click-events-have-key-events, vue/no-v-html -->
|
||||||
<template>
|
<template>
|
||||||
<transition name="slide">
|
<transition name="slide">
|
||||||
<div v-if="show" @click="clicked" class="toast" :class="type">
|
<div v-if="show" class="toast" :class="type || 'info'" @click="clicked">
|
||||||
<div class="toast--content">
|
<div class="toast--content">
|
||||||
<div class="toast--icon">
|
<div class="toast--icon">
|
||||||
<i v-if="image"><img class="toast--icon-image" :src="image" /></i>
|
<i v-if="image"
|
||||||
|
><img class="toast--icon-image" :src="image" alt="Toast icon"
|
||||||
|
/></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="toast--text" v-if="description">
|
<div v-if="description" class="toast--text">
|
||||||
<span class="toast--text__title">{{ title }}</span>
|
<span class="toast--text__title">{{ title }}</span>
|
||||||
<br /><span
|
<br /><span
|
||||||
class="toast--text__description"
|
class="toast--text__description"
|
||||||
v-html="description"
|
v-html="description"
|
||||||
></span>
|
></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="toast--text" v-else>
|
<div v-else class="toast--text">
|
||||||
<span class="toast--text__title-large">{{ title }}</span>
|
<span class="toast--text__title-large">{{ title }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="toast--dismiss" @click="dismiss">
|
<div class="toast--dismiss" @click="dismiss">
|
||||||
<i class="fas fa-times"></i>
|
<i class="fas fa-times"></i>
|
||||||
</div>
|
</div>
|
||||||
@@ -37,14 +41,7 @@
|
|||||||
timeout?: number;
|
timeout?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const props = defineProps<Props>();
|
||||||
type = "info",
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
image,
|
|
||||||
link,
|
|
||||||
timeout = 2000
|
|
||||||
} = defineProps<Props>();
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const show: Ref<boolean> = ref(false);
|
const show: Ref<boolean> = ref(false);
|
||||||
@@ -53,16 +50,16 @@
|
|||||||
show.value = true;
|
show.value = true;
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
console.log("Notification time is up 👋");
|
console.log("Notification time is up 👋"); // eslint-disable-line no-console
|
||||||
show.value = false;
|
show.value = false;
|
||||||
}, timeout);
|
}, props.timeout || 2000);
|
||||||
});
|
});
|
||||||
|
|
||||||
function clicked() {
|
function clicked() {
|
||||||
show.value = false;
|
show.value = false;
|
||||||
|
|
||||||
if (link) {
|
if (props.link) {
|
||||||
router.push(link);
|
router.push(props.link);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -6,11 +6,7 @@ const optionsDefaults = {
|
|||||||
data: {
|
data: {
|
||||||
type: "info",
|
type: "info",
|
||||||
show: true,
|
show: true,
|
||||||
timeout: 3000,
|
timeout: 3000
|
||||||
|
|
||||||
onCreate(created = null) {},
|
|
||||||
onEdit(editted = null) {},
|
|
||||||
onRemove(removed = null) {}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -29,8 +25,8 @@ function toast(options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
install(app, options) {
|
install(app) {
|
||||||
console.log("installing toast plugin!");
|
console.log("installing toast plugin!"); // eslint-disable-line no-console
|
||||||
|
|
||||||
function info(options) {
|
function info(options) {
|
||||||
toast({ type: "info", ...options });
|
toast({ type: "info", ...options });
|
||||||
@@ -53,6 +49,8 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const notifications = { info, success, warning, error, simple };
|
const notifications = { info, success, warning, error, simple };
|
||||||
|
|
||||||
|
/* eslint-disable-next-line no-param-reassign */
|
||||||
app.config.globalProperties.$notifications = notifications;
|
app.config.globalProperties.$notifications = notifications;
|
||||||
|
|
||||||
app.provide("notifications", notifications);
|
app.provide("notifications", notifications);
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
import { defineAsyncComponent } from "vue";
|
import { createRouter, createWebHistory } from "vue-router";
|
||||||
import {
|
import type { RouteRecordRaw, RouteLocationNormalized } from "vue-router";
|
||||||
createRouter,
|
|
||||||
createWebHistory,
|
/* eslint-disable-next-line import/no-cycle */
|
||||||
RouteRecordRaw,
|
|
||||||
NavigationGuardNext,
|
|
||||||
RouteLocationNormalized
|
|
||||||
} from "vue-router";
|
|
||||||
import store from "./store";
|
import store from "./store";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
@@ -14,16 +10,16 @@ declare global {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let routes: Array<RouteRecordRaw> = [
|
const routes: Array<RouteRecordRaw> = [
|
||||||
{
|
{
|
||||||
name: "home",
|
name: "home",
|
||||||
path: "/",
|
path: "/",
|
||||||
component: () => import("./pages/Home.vue")
|
component: () => import("./pages/HomePage.vue")
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "activity",
|
name: "activity",
|
||||||
path: "/activity",
|
path: "/activity",
|
||||||
meta: { requiresAuth: true },
|
meta: { requiresAuth: true, requiresPlexAccount: true },
|
||||||
component: () => import("./pages/ActivityPage.vue")
|
component: () => import("./pages/ActivityPage.vue")
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -80,7 +76,7 @@ let routes: Array<RouteRecordRaw> = [
|
|||||||
{
|
{
|
||||||
name: "404",
|
name: "404",
|
||||||
path: "/404",
|
path: "/404",
|
||||||
component: () => import("./pages/404.vue")
|
component: () => import("./pages/404Page.vue")
|
||||||
}
|
}
|
||||||
// {
|
// {
|
||||||
// path: "*",
|
// path: "*",
|
||||||
@@ -100,9 +96,10 @@ const router = createRouter({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const loggedIn = () => store.getters["user/loggedIn"];
|
const loggedIn = () => store.getters["user/loggedIn"];
|
||||||
const popupIsOpen = () => store.getters["popup/isOpen"];
|
const hasPlexAccount = () => store.getters["user/plexId"] !== null;
|
||||||
const hamburgerIsOpen = () => store.getters["hamburger/isOpen"];
|
const hamburgerIsOpen = () => store.getters["hamburger/isOpen"];
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
router.beforeEach(
|
router.beforeEach(
|
||||||
(to: RouteLocationNormalized, from: RouteLocationNormalized, next: any) => {
|
(to: RouteLocationNormalized, from: RouteLocationNormalized, next: any) => {
|
||||||
store.dispatch("documentTitle/updateTitle", to.name);
|
store.dispatch("documentTitle/updateTitle", to.name);
|
||||||
@@ -119,6 +116,15 @@ router.beforeEach(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (to.matched.some(record => record.meta.requiresPlexAccount)) {
|
||||||
|
if (!hasPlexAccount()) {
|
||||||
|
next({
|
||||||
|
path: "/settings",
|
||||||
|
query: { missingPlexAccount: true }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ 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 user from "./modules/user";
|
import user from "./modules/user";
|
||||||
import popup from "./modules/popup";
|
|
||||||
import hamburger from "./modules/hamburger";
|
import hamburger from "./modules/hamburger";
|
||||||
|
|
||||||
|
/* eslint-disable-next-line import/no-cycle */
|
||||||
|
import popup from "./modules/popup";
|
||||||
|
|
||||||
const store = createStore({
|
const store = createStore({
|
||||||
modules: {
|
modules: {
|
||||||
darkmodeModule,
|
darkmodeModule,
|
||||||
|
|||||||
58
src/utils.ts
58
src/utils.ts
@@ -5,17 +5,17 @@ export const sortableSize = (string: string): number => {
|
|||||||
if (UNITS.indexOf(unit) === -1) return null;
|
if (UNITS.indexOf(unit) === -1) return null;
|
||||||
|
|
||||||
const exponent = UNITS.indexOf(unit) * 3;
|
const exponent = UNITS.indexOf(unit) * 3;
|
||||||
return Number(numStr) * Math.pow(10, exponent);
|
return Number(numStr) * exponent ** 10;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const parseJwt = (token: string) => {
|
export const parseJwt = (token: string) => {
|
||||||
var base64Url = token.split(".")[1];
|
const base64Url = token.split(".")[1];
|
||||||
var base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
|
const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
|
||||||
var jsonPayload = decodeURIComponent(
|
const jsonPayload = decodeURIComponent(
|
||||||
atob(base64)
|
atob(base64)
|
||||||
.split("")
|
.split("")
|
||||||
.map(function (c) {
|
.map(c => {
|
||||||
return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2);
|
return `%${`00${c.charCodeAt(0).toString(16)}`.slice(-2)}`;
|
||||||
})
|
})
|
||||||
.join("")
|
.join("")
|
||||||
);
|
);
|
||||||
@@ -48,19 +48,15 @@ export function focusFirstFormInput(formElement: HTMLFormElement): void {
|
|||||||
|
|
||||||
export function focusOnNextElement(elementEvent: KeyboardEvent): void {
|
export function focusOnNextElement(elementEvent: KeyboardEvent): void {
|
||||||
const { target } = elementEvent;
|
const { target } = elementEvent;
|
||||||
console.log("target:", target);
|
|
||||||
if (!target) return;
|
if (!target) return;
|
||||||
|
|
||||||
const form = document.getElementsByTagName("form")[0];
|
const form = document.getElementsByTagName("form")[0];
|
||||||
console.log("form:", form);
|
|
||||||
if (!form) return;
|
if (!form) return;
|
||||||
|
|
||||||
const inputElements = form.getElementsByTagName("input");
|
const inputElements = form.getElementsByTagName("input");
|
||||||
console.log("inputElements:", inputElements);
|
|
||||||
const targetIndex = Array.from(inputElements).findIndex(
|
const targetIndex = Array.from(inputElements).findIndex(
|
||||||
element => element === target
|
element => element === target
|
||||||
);
|
);
|
||||||
console.log("targetIndex:", targetIndex);
|
|
||||||
if (targetIndex < inputElements.length) {
|
if (targetIndex < inputElements.length) {
|
||||||
inputElements[targetIndex + 1].focus();
|
inputElements[targetIndex + 1].focus();
|
||||||
}
|
}
|
||||||
@@ -68,15 +64,18 @@ export function focusOnNextElement(elementEvent: KeyboardEvent): void {
|
|||||||
|
|
||||||
export function humanMinutes(minutes) {
|
export function humanMinutes(minutes) {
|
||||||
if (minutes instanceof Array) {
|
if (minutes instanceof Array) {
|
||||||
|
/* eslint-disable-next-line prefer-destructuring, no-param-reassign */
|
||||||
minutes = minutes[0];
|
minutes = minutes[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
const hours = Math.floor(minutes / 60);
|
const hours = Math.floor(minutes / 60);
|
||||||
const minutesLeft = minutes - hours * 60;
|
const minutesLeft = minutes - hours * 60;
|
||||||
|
|
||||||
if (minutesLeft == 0) {
|
if (minutesLeft === 0) {
|
||||||
return hours > 1 ? `${hours} hours` : `${hours} hour`;
|
return hours > 1 ? `${hours} hours` : `${hours} hour`;
|
||||||
} else if (hours == 0) {
|
}
|
||||||
|
|
||||||
|
if (hours === 0) {
|
||||||
return `${minutesLeft} min`;
|
return `${minutesLeft} min`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,3 +97,38 @@ export function setUrlQueryParameter(parameter: string, value: string): void {
|
|||||||
|
|
||||||
window.history.pushState({}, "search", url);
|
window.history.pushState({}, "search", url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function convertSecondsToHumanReadable(_value, values = null) {
|
||||||
|
let value = _value;
|
||||||
|
const highestValue = values ? values[0] : value;
|
||||||
|
|
||||||
|
// minutes
|
||||||
|
if (highestValue < 3600) {
|
||||||
|
const minutes = Math.floor(value / 60);
|
||||||
|
|
||||||
|
value = `${minutes} m`;
|
||||||
|
}
|
||||||
|
// hours and minutes
|
||||||
|
else if (highestValue > 3600 && highestValue < 86400) {
|
||||||
|
const hours = Math.floor(value / 3600);
|
||||||
|
const minutes = Math.floor((value % 3600) / 60);
|
||||||
|
|
||||||
|
value = hours !== 0 ? `${hours} h ${minutes} m` : `${minutes} m`;
|
||||||
|
}
|
||||||
|
// days and hours
|
||||||
|
else if (highestValue > 86400 && highestValue < 31557600) {
|
||||||
|
const days = Math.floor(value / 86400);
|
||||||
|
const hours = Math.floor((value % 86400) / 3600);
|
||||||
|
|
||||||
|
value = days !== 0 ? `${days} d ${hours} h` : `${hours} h`;
|
||||||
|
}
|
||||||
|
// years and days
|
||||||
|
else if (highestValue > 31557600) {
|
||||||
|
const years = Math.floor(value / 31557600);
|
||||||
|
const days = Math.floor((value % 31557600) / 86400);
|
||||||
|
|
||||||
|
value = years !== 0 ? `${years} y ${days} d` : `${days} d`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|||||||
1
src/vue-shim.d.ts
vendored
1
src/vue-shim.d.ts
vendored
@@ -1,5 +1,6 @@
|
|||||||
declare module "*.vue" {
|
declare module "*.vue" {
|
||||||
import { defineComponent } from "vue";
|
import { defineComponent } from "vue";
|
||||||
|
|
||||||
const Component: ReturnType<typeof defineComponent>;
|
const Component: ReturnType<typeof defineComponent>;
|
||||||
export default Component;
|
export default Component;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user