Minor improvements and code cleanup across components

- Refactor PlexSettings component for better organization
- Add utility functions to utils.ts for shared logic
- Update API methods for improved error handling
- Clean up navigation icon components
- Remove unused code from CommandPalette and SeasonedButton
- Fix minor issues in Movie popup component
- Update page component imports (RegisterPage, TorrentsPage)
This commit is contained in:
2026-03-08 20:57:37 +01:00
parent 1b99399b4c
commit e69f0c52b8
10 changed files with 213 additions and 198 deletions

View File

@@ -413,6 +413,20 @@ const unlinkPlexAccount = async () => {
}); });
}; };
const plexRecentlyAddedInLibrary = async (id: number) => {
const url = new URL(`/api/v2/plex/recently_added/${id}`, API_HOSTNAME);
const options: RequestInit = {
credentials: "include"
};
return fetch(url.href, options)
.then(resp => resp.json())
.catch(error => {
console.error(`api error fetch plex recently added`); // eslint-disable-line no-console
throw error;
});
};
// - - - User graphs - - - // - - - User graphs - - -
const fetchGraphData = async ( const fetchGraphData = async (
@@ -543,6 +557,7 @@ const elasticSearchMoviesAndShows = async (query: string, count = 22) => {
}; };
export { export {
API_HOSTNAME,
getMovie, getMovie,
getShow, getShow,
getPerson, getPerson,
@@ -559,6 +574,7 @@ export {
getRequestStatus, getRequestStatus,
linkPlexAccount, linkPlexAccount,
unlinkPlexAccount, unlinkPlexAccount,
plexRecentlyAddedInLibrary,
register, register,
login, login,
logout, logout,

View File

@@ -42,8 +42,7 @@
.navigation-link { .navigation-link {
display: grid; display: grid;
place-items: center; place-items: center;
height: var(--header-size); min-height: var(--header-size);
width: var(--header-size);
list-style: none; list-style: none;
padding: 1rem 0.15rem; padding: 1rem 0.15rem;
text-align: center; text-align: center;

View File

@@ -90,18 +90,10 @@
@include desktop { @include desktop {
grid-template-rows: var(--header-size); grid-template-rows: var(--header-size);
grid-auto-flow: row;
} }
@include mobile { @include mobile {
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
} }
} }
:global(.navigation-icons > *:last-child) {
margin-top: auto;
justify-self: end;
align-self: end;
background-color: red;
}
</style> </style>

View File

@@ -215,7 +215,8 @@
const props = defineProps<Props>(); const props = defineProps<Props>();
const ASSET_URL = "https://image.tmdb.org/t/p/"; const ASSET_URL = "https://image.tmdb.org/t/p/";
const COLORS_URL = "https://colors.schleppe.cloud/colors"; // const COLORS_URL = "https://colors.schleppe.cloud/colors";
const COLORS_URL = "http://localhost:8080/colors";
const ASSET_SIZES = ["w500", "w780", "original"]; const ASSET_SIZES = ["w500", "w780", "original"];
const media: Ref<IMovie | IShow> = ref(); const media: Ref<IMovie | IShow> = ref();
@@ -435,7 +436,7 @@
> img { > img {
width: 100%; width: 100%;
border-radius: inherit; border-radius: calc(1.6rem - 1px);
} }
} }
} }

View File

@@ -2,7 +2,7 @@
<div class="plex-settings"> <div class="plex-settings">
<!-- Unconnected state --> <!-- Unconnected state -->
<PlexAuthButton <PlexAuthButton
v-if="!isPlexConnected" v-if="!showPlexInformation"
@auth-success="handleAuthSuccess" @auth-success="handleAuthSuccess"
@auth-error="handleAuthError" @auth-error="handleAuthError"
/> />
@@ -16,20 +16,20 @@
/> />
<PlexLibraryStats <PlexLibraryStats
:movies="libraryStats.movies" :movies="libraryStats?.movies"
:shows="libraryStats.shows" :shows="libraryStats?.['tv shows']"
:music="libraryStats.music" :music="libraryStats?.music"
:watchtime="libraryStats.watchtime" :watchtime="libraryStats?.watchtime || 0"
:loading="loadingLibraries" :loading="syncingLibrary"
@open-library="showLibraryDetails" @open-library="showLibraryDetails"
/> />
<PlexServerInfo <PlexServerInfo
:serverName="plexServer" :serverName="plexServer"
:lastSync="lastSync" :lastSync="lastSync"
:syncing="syncing" :syncing="syncingServer"
@sync="syncLibrary" @sync="syncLibrary"
@unlink="confirmUnlink" @unlink="() => (showUnlinkModal = true)"
/> />
</div> </div>
@@ -38,16 +38,18 @@
<!-- Unlink Confirmation Modal --> <!-- Unlink Confirmation Modal -->
<PlexUnlinkModal <PlexUnlinkModal
v-if="showConfirmModal" v-if="showUnlinkModal"
@confirm="unauthenticatePlex" @confirm="unauthenticatePlex"
@cancel="cancelUnlink" @cancel="() => (showUnlinkModal = false)"
/> />
<!-- Library Details Modal --> <!-- Library Details Modal -->
<PlexLibraryModal <PlexLibraryModal
v-if="showLibraryModal && selectedLibrary" v-if="showLibraryModal && selectedLibrary"
:libraryType="selectedLibrary" :libraryType="selectedLibrary"
:details="libraryDetails[selectedLibrary]" :details="libraryStats[selectedLibrary]"
:serverUrl="plexServerUrl"
:serverMachineId="plexMachineId"
@close="closeLibraryModal" @close="closeLibraryModal"
/> />
</div> </div>
@@ -55,7 +57,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue"; import { ref, onMounted, onUnmounted } from "vue";
import { useStore } from "vuex";
import SeasonedMessages from "@/components/ui/SeasonedMessages.vue"; import SeasonedMessages from "@/components/ui/SeasonedMessages.vue";
import PlexAuthButton from "@/components/plex/PlexAuthButton.vue"; import PlexAuthButton from "@/components/plex/PlexAuthButton.vue";
import PlexProfileCard from "@/components/plex/PlexProfileCard.vue"; import PlexProfileCard from "@/components/plex/PlexProfileCard.vue";
@@ -64,184 +65,167 @@
import PlexUnlinkModal from "@/components/plex/PlexUnlinkModal.vue"; import PlexUnlinkModal from "@/components/plex/PlexUnlinkModal.vue";
import PlexLibraryModal from "@/components/plex/PlexLibraryModal.vue"; import PlexLibraryModal from "@/components/plex/PlexLibraryModal.vue";
import { usePlexAuth } from "@/composables/usePlexAuth"; import { usePlexAuth } from "@/composables/usePlexAuth";
import { usePlexApi } from "@/composables/usePlexApi"; import {
import { usePlexLibraries } from "@/composables/usePlexLibraries"; fetchPlexServers,
fetchPlexUserData,
fetchLibraryDetails
} from "@/composables/usePlexApi";
import type { Ref } from "vue"; import type { Ref } from "vue";
import { linkPlexAccount, unlinkPlexAccount } from "../../api";
import { ErrorMessageTypes } from "../../interfaces/IErrorMessage"; import { ErrorMessageTypes } from "../../interfaces/IErrorMessage";
import type { IErrorMessage } from "../../interfaces/IErrorMessage"; import type { IErrorMessage } from "../../interfaces/IErrorMessage";
const messages: Ref<IErrorMessage[]> = ref([]); const messages: Ref<IErrorMessage[]> = ref([]);
const loading = ref(false); const syncingServer = ref(false);
const syncing = ref(false); const syncingLibrary = ref(false);
const showConfirmModal = ref(false); const showUnlinkModal = ref(false);
const plexUsername = ref<string>(""); const plexUsername = ref<string>("");
const plexUserData = ref<any>(null); const plexUserData = ref<any>(null);
const isPlexConnected = ref<boolean>(false); const showPlexInformation = ref<boolean>(false);
const hasLocalStorageData = ref<boolean>(false); const hasLocalStorageData = ref<boolean>(false);
const hasCookieData = ref<boolean>(false);
const showLibraryModal = ref<boolean>(false); const showLibraryModal = ref<boolean>(false);
const selectedLibrary = ref<string>(""); const selectedLibrary = ref<string>("");
const loadingLibraries = ref<boolean>(false);
const plexServer = ref(""); const plexServer = ref("");
const plexServerUrl = ref(""); const plexServerUrl = ref("");
const plexMachineId = ref(""); const plexMachineId = ref("");
const lastSync = ref(""); const lastSync = ref(sessionStorage.getItem("plex_library_last_sync"));
const libraryStats = ref({ const libraryStats = ref({
movies: 0, movies: 0,
shows: 0, shows: 0,
music: 0, music: 0,
watchtime: 0 watchtime: 0
}); });
const libraryDetails = ref<any>({
movies: {
total: 0,
recentlyAdded: [],
genres: [],
totalDuration: "0 hours"
},
shows: {
total: 0,
recentlyAdded: [],
genres: [],
totalEpisodes: 0,
totalDuration: "0 hours"
},
music: {
total: 0,
recentlyAdded: [],
genres: [],
totalTracks: 0
}
});
const store = useStore();
const emit = defineEmits<{ const emit = defineEmits<{
(e: "reload"): void; (e: "reload"): void;
}>(); }>();
// Composables // Composables
const { getCookie, setPlexAuthCookie, cleanup } = usePlexAuth(); const { getPlexAuthCookie, setPlexAuthCookie, cleanup } = usePlexAuth();
const {
fetchPlexUserData,
fetchPlexServers,
fetchLibrarySections,
fetchLibraryDetails
} = usePlexApi();
const { loadLibraries } = usePlexLibraries();
// ----- Connection check ----- // ----- Connection check -----
function checkPlexConnection() { function checkPlexConnection() {
const cachedData = localStorage.getItem("plex_user_data"); const authToken = getPlexAuthCookie();
const authToken = getCookie("plex_auth_token"); showPlexInformation.value = !!authToken;
const storeHasPlexUserId = store.getters["user/plexUserId"]; return showPlexInformation.value;
hasLocalStorageData.value = !!cachedData;
hasCookieData.value = !!authToken;
isPlexConnected.value = !!(cachedData || authToken || storeHasPlexUserId);
return isPlexConnected.value;
} }
// ----- Library loading ----- // ----- Library loading -----
async function fetchPlexLibraries(authToken: string) { async function loadPlexServer() {
try { // return cached value from sessionStorage if exists
loadingLibraries.value = true; const cacheKey = "plex_server_data";
const cachedData = sessionStorage.getItem(cacheKey);
if (cachedData) {
const server = JSON.parse(cachedData);
plexServer.value = server?.name;
plexServerUrl.value = server?.url;
plexMachineId.value = server?.machineIdentifier;
return;
}
// get token from cookie
const authToken = getPlexAuthCookie();
if (!authToken) return;
// make api call for data
syncingServer.value = true;
const server = await fetchPlexServers(authToken); const server = await fetchPlexServers(authToken);
if (!server) {
console.error("No Plex server found"); if (server) {
return; // set server name & id
plexServer.value = server?.name;
plexServerUrl.value = server?.url;
plexMachineId.value = server?.machineIdentifier;
// cache in sessionStorage
sessionStorage.setItem(cacheKey, JSON.stringify(server));
// set last-sync date
const now = new Date().toLocaleString();
lastSync.value = now;
sessionStorage.setItem("plex_library_last_sync", now);
} else {
console.log("unable to load plex server informmation");
} }
plexServer.value = server.name; syncingServer.value = false;
plexServerUrl.value = server.url;
plexMachineId.value = server.machineIdentifier;
lastSync.value = new Date().toLocaleString();
const sections = await fetchLibrarySections(authToken, server.url);
if (!sections || sections.length === 0) {
console.error("No library sections found");
return;
}
const result = await loadLibraries(
sections,
authToken,
server.url,
server.machineIdentifier,
plexUsername.value,
fetchLibraryDetails
);
libraryStats.value = result.stats;
libraryDetails.value = result.details;
} catch (error) {
console.error("[PlexSettings] Error fetching Plex libraries:", error);
} finally {
loadingLibraries.value = false;
}
} }
// ----- User data loading ----- // ----- User data loading -----
async function loadPlexUserData() { async function loadPlexUserData() {
checkPlexConnection(); // return cached value from sessionStorage if exists
const cachedData = localStorage.getItem("plex_user_data"); const cacheKey = "plex_user_data";
const cachedData = sessionStorage.getItem(cacheKey);
hasLocalStorageData.value = !!cachedData; hasLocalStorageData.value = !!cachedData;
if (cachedData) { if (cachedData) {
try {
plexUserData.value = JSON.parse(cachedData); plexUserData.value = JSON.parse(cachedData);
plexUsername.value = plexUserData.value.username; plexUsername.value = plexUserData.value.username;
isPlexConnected.value = true; return;
} catch (error) {
console.error("[PlexSettings] Error parsing cached Plex data:", error);
} }
}
const authToken = getCookie("plex_auth_token"); // get token from cookie
hasCookieData.value = !!authToken; const authToken = getPlexAuthCookie();
if (authToken) { if (!authToken) return;
// make api call for data
const userData = await fetchPlexUserData(authToken); const userData = await fetchPlexUserData(authToken);
if (userData) { if (userData) {
// set plex user data
plexUserData.value = userData; plexUserData.value = userData;
plexUsername.value = userData.username; plexUsername.value = userData?.username;
isPlexConnected.value = true;
} else if (!cachedData) { // cache in sessionStorage
isPlexConnected.value = false; sessionStorage.setItem(cacheKey, JSON.stringify(userData));
}
if (isPlexConnected.value) {
await fetchPlexLibraries(authToken);
}
} else { } else {
isPlexConnected.value = false; console.log("unable to load user data from plex");
} }
} }
// ----- Load plex libary details -----
async function loadPlexLibraries() {
// return cached value from sessionStorage if exists
const cacheKey = "plex_library_data";
const cachedData = sessionStorage.getItem(cacheKey);
hasLocalStorageData.value = !!cachedData;
if (cachedData) {
libraryStats.value = JSON.parse(cachedData);
return;
}
// get token from cookie
const authToken = getPlexAuthCookie();
if (!authToken) return;
// make api call for data
syncingLibrary.value = true;
const library = await fetchLibraryDetails();
if (library) {
libraryStats.value = library;
// cache in sessionStorage
sessionStorage.setItem(cacheKey, JSON.stringify(library));
} else {
console.log("unable to load plex library details");
}
syncingLibrary.value = false;
}
// ----- OAuth flow (handlers for PlexAuthButton events) ----- // ----- OAuth flow (handlers for PlexAuthButton events) -----
async function handleAuthSuccess(authToken: string) { async function handleAuthSuccess(authToken: string) {
try {
setPlexAuthCookie(authToken); setPlexAuthCookie(authToken);
const userData = await fetchPlexUserData(authToken); checkPlexConnection();
if (userData) { const success = await loadAll();
plexUserData.value = userData;
plexUsername.value = userData.username;
isPlexConnected.value = true;
}
const { success, message } = await linkPlexAccount(authToken);
if (success) { if (success) {
emit("reload");
await fetchPlexLibraries(authToken);
messages.value.push({ messages.value.push({
type: ErrorMessageTypes.Success, type: ErrorMessageTypes.Success,
title: "Authenticated with Plex", title: "Authenticated with Plex",
message: message || "Successfully connected your Plex account" message: "Successfully connected your Plex account"
} as IErrorMessage); } as IErrorMessage);
} else { } else {
messages.value.push({ console.error("[PlexSettings] Error in handleAuthSuccess:");
type: ErrorMessageTypes.Error,
title: "Authentication failed",
message: message || "Could not connect to Plex"
} as IErrorMessage);
}
} catch (error) {
console.error("[PlexSettings] Error in handleAuthSuccess:", error);
messages.value.push({ messages.value.push({
type: ErrorMessageTypes.Error, type: ErrorMessageTypes.Error,
title: "Authentication failed", title: "Authentication failed",
@@ -259,35 +243,24 @@
} }
// ----- Unlink flow ----- // ----- Unlink flow -----
function confirmUnlink() {
showConfirmModal.value = true;
}
function cancelUnlink() {
showConfirmModal.value = false;
}
async function unauthenticatePlex() { async function unauthenticatePlex() {
showConfirmModal.value = false; showUnlinkModal.value = false;
loading.value = true; sessionStorage.removeItem("plex_user_data");
const response = await unlinkPlexAccount(); sessionStorage.removeItem("plex_server_data");
if (response?.success) { sessionStorage.removeItem("plex_library_data");
localStorage.removeItem("plex_user_data"); sessionStorage.removeItem("plex_library_last_sync");
document.cookie = document.cookie =
"plex_auth_token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 UTC; SameSite=Strict"; "plex_auth_token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 UTC; SameSite=Strict";
plexUserData.value = null; plexUserData.value = null;
plexUsername.value = ""; plexUsername.value = "";
isPlexConnected.value = false; showPlexInformation.value = false;
emit("reload"); emit("reload");
}
messages.value.push({ messages.value.push({
type: response.success type: ErrorMessageTypes.Success,
? ErrorMessageTypes.Success title: "Unlinked Plex account",
: ErrorMessageTypes.Error, message: "All browser storage has been clear of plex account"
title: response.success
? "Unlinked Plex account"
: "Something went wrong",
message: response.message
} as IErrorMessage); } as IErrorMessage);
loading.value = false;
} }
// ----- Library modal ----- // ----- Library modal -----
@@ -304,39 +277,60 @@
// ----- Sync ----- // ----- Sync -----
async function syncLibrary() { async function syncLibrary() {
syncing.value = true; const authToken = getPlexAuthCookie();
const authToken = getCookie("plex_auth_token");
if (!authToken) { if (!authToken) {
messages.value.push({ messages.value.push({
type: ErrorMessageTypes.Error, type: ErrorMessageTypes.Error,
title: "Sync failed", title: "Sync failed",
message: "No authentication token found" message: "No authentication token found"
} as IErrorMessage); } as IErrorMessage);
syncing.value = false;
return; return;
} }
try {
await fetchPlexLibraries(authToken); sessionStorage.removeItem("plex_user_data");
sessionStorage.removeItem("plex_server_data");
sessionStorage.removeItem("plex_library_data");
const success = await loadAll();
if (success) {
messages.value.push({ messages.value.push({
type: ErrorMessageTypes.Success, type: ErrorMessageTypes.Success,
title: "Library synced", title: "Library synced",
message: "Your Plex library has been successfully synced" message: "Your Plex library has been successfully synced"
} as IErrorMessage); } as IErrorMessage);
} catch (error) { } else {
messages.value.push({ messages.value.push({
type: ErrorMessageTypes.Error, type: ErrorMessageTypes.Error,
title: "Sync failed", title: "Sync failed",
message: "An error occurred while syncing your library" message: "An error occurred while syncing your library"
} as IErrorMessage); } as IErrorMessage);
} finally {
syncing.value = false;
} }
} }
onMounted(() => { // ---- Helper load all ----
async function loadAll() {
let success = false;
try {
await Promise.all([
loadPlexServer(),
loadPlexUserData(),
loadPlexLibraries()
]);
success = true;
} catch (error) {
console.log("loadall error, some info might be missing");
}
checkPlexConnection(); checkPlexConnection();
loadPlexUserData(); return success;
}); }
// ---- Lifecycle functions ----
onMounted(loadAll);
onUnmounted(() => { onUnmounted(() => {
cleanup(); cleanup();
}); });

View File

@@ -139,7 +139,6 @@
import IconMovie from "@/icons/IconMovie.vue"; import IconMovie from "@/icons/IconMovie.vue";
import IconActivity from "@/icons/IconActivity.vue"; import IconActivity from "@/icons/IconActivity.vue";
import IconProfile from "@/icons/IconProfile.vue"; import IconProfile from "@/icons/IconProfile.vue";
import IconRequest from "@/icons/IconRequest.vue";
import IconInbox from "@/icons/IconInbox.vue"; import IconInbox from "@/icons/IconInbox.vue";
import IconSearch from "@/icons/IconSearch.vue"; import IconSearch from "@/icons/IconSearch.vue";
import IconEdit from "@/icons/IconEdit.vue"; import IconEdit from "@/icons/IconEdit.vue";

View File

@@ -2,7 +2,7 @@
<button <button
type="button" type="button"
:class="{ active: active, fullwidth: fullWidth }" :class="{ active: active, fullwidth: fullWidth }"
@click="emit('click')" @click="event => emit('click', event)"
> >
<slot></slot> <slot></slot>
</button> </button>
@@ -15,7 +15,7 @@
} }
interface Emit { interface Emit {
(e: "click"); (e: "click", event?: MouseEvent);
} }
defineProps<Props>(); defineProps<Props>();

View File

@@ -29,7 +29,7 @@
<seasoned-button @click="submit">Register</seasoned-button> <seasoned-button @click="submit">Register</seasoned-button>
</form> </form>
<router-link class="link" to="/signin" <router-link class="link" to="/login"
>Have a user? Sign in here</router-link >Have a user? Sign in here</router-link
> >

View File

@@ -59,7 +59,7 @@
@include mobile-only { @include mobile-only {
font-size: 1.5rem; font-size: 1.5rem;
margin: 0 0 1rem 0; margin: 1rem 0;
} }
} }

View File

@@ -129,3 +129,17 @@ export function convertSecondsToHumanReadable(_value, values = null) {
return value; return value;
} }
export function formatNumber(n: number) {
if (!n?.toString()) return n;
return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, " ");
}
export function formatBytes(bytes: number): string {
if (bytes === 0) return "0 Bytes";
const k = 1024;
const sizes = ["Bytes", "KB", "MB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i];
}