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 - - -
const fetchGraphData = async (
@@ -543,6 +557,7 @@ const elasticSearchMoviesAndShows = async (query: string, count = 22) => {
};
export {
API_HOSTNAME,
getMovie,
getShow,
getPerson,
@@ -559,6 +574,7 @@ export {
getRequestStatus,
linkPlexAccount,
unlinkPlexAccount,
plexRecentlyAddedInLibrary,
register,
login,
logout,

View File

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

View File

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

View File

@@ -215,7 +215,8 @@
const props = defineProps<Props>();
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 media: Ref<IMovie | IShow> = ref();
@@ -435,7 +436,7 @@
> img {
width: 100%;
border-radius: inherit;
border-radius: calc(1.6rem - 1px);
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -59,7 +59,7 @@
@include mobile-only {
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;
}
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];
}