mirror of
https://github.com/KevinMidboe/seasoned.git
synced 2026-04-24 16:53:37 +00:00
* include credentials on login fetch requests, allows set header response * Add theme composable and utility improvements - Create useTheme composable for centralized theme management - Update main.ts to use useTheme for initialization - Generalize getCookie utility in user module - Add utility functions for data formatting * Add Plex integration composables and icons - Create usePlexAuth composable for Plex OAuth flow - Create usePlexApi composable for Plex API interactions - Create useRandomWords composable for password generation - Add Plex-related icons (IconPlex, IconServer, IconSync) - Add Plex helper utilities - Update API with Plex-related endpoints * Add storage management components for data & privacy section - Create StorageManager component for browser storage overview - Create StorageSectionBrowser for localStorage/sessionStorage/cookies - Create StorageSectionServer for server-side data (mock) - Create ExportSection for data export functionality - Refactor DataExport component with modular sections - Add storage icons (IconCookie, IconDatabase, IconTimer) - Implement collapsible sections with visual indicators - Add colored borders per storage type - Display item counts and total size in headers * Add theme, password, and security settings components - Create ThemePreferences with visual theme selector - Create PasswordGenerator with passphrase and random modes - Create SecuritySettings wrapper for password management - Update ChangePassword to work with new layout - Implement improved slider UX with visual feedback - Add theme preview cards with gradients - Standardize component styling and typography * Add Plex settings and authentication components - Create PlexSettings component for Plex account management - Create PlexAuthButton with improved OAuth flow - Create PlexServerInfo for server details display - Use icon components instead of inline SVGs - Add sync and unlink functionality - Implement user-friendly authentication flow * Redesign settings page with two-column layout and ProfileHero - Create ProfileHero component with avatar and user info - Create RequestHistory component for Plex requests (placeholder) - Redesign SettingsPage with modern two-column grid layout - Add shared-settings.scss for consistent styling - Organize sections: Appearance, Security, Integrations, Data & Privacy - Implement responsive mobile layout - Standardize typography (h2: 1.5rem, 700 weight) - Add compact modifier for tighter sections
231 lines
5.2 KiB
Vue
231 lines
5.2 KiB
Vue
<template>
|
|
<a
|
|
v-if="item.plexUrl"
|
|
:href="item.plexUrl"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
class="plex-library-item"
|
|
>
|
|
<figure :class="`item-poster ${item.type}`">
|
|
<img
|
|
v-if="item.poster"
|
|
:src="item.poster"
|
|
:alt="item.title"
|
|
class="poster-image"
|
|
@error="handleImageError"
|
|
/>
|
|
<div v-else class="poster-fallback">
|
|
<component :is="fallbackIconComponent" />
|
|
</div>
|
|
</figure>
|
|
|
|
<div class="item-details">
|
|
<p class="item-title">{{ item.title }}</p>
|
|
<div class="item-meta">
|
|
<span v-if="item.year" class="item-year">{{ item.year }}</span>
|
|
<span v-if="item.rating" class="item-rating">
|
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor">
|
|
<polygon
|
|
points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"
|
|
/>
|
|
</svg>
|
|
{{ item.rating }}
|
|
</span>
|
|
</div>
|
|
<div v-if="showExtras" class="item-extras">
|
|
<span v-if="item.artist">{{ item.artist }}</span>
|
|
<span v-if="item.episodes">{{ item.episodes }} episodes</span>
|
|
<span v-if="item.tracks">{{ item.tracks }} tracks</span>
|
|
</div>
|
|
</div>
|
|
</a>
|
|
<div v-else class="plex-library-item plex-library-item--no-link">
|
|
<figure class="item-poster">
|
|
<img
|
|
v-if="item.poster"
|
|
:src="item.poster"
|
|
:alt="item.title"
|
|
class="poster-image"
|
|
@error="handleImageError"
|
|
/>
|
|
<div v-else class="poster-fallback">
|
|
<component :is="fallbackIconComponent" />
|
|
</div>
|
|
</figure>
|
|
|
|
<div class="item-details">
|
|
<p class="item-title">{{ item.title }}</p>
|
|
<div class="item-meta">
|
|
<span v-if="item.year" class="item-year">{{ item.year }}</span>
|
|
<span v-if="item.rating" class="item-rating">
|
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor">
|
|
<polygon
|
|
points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"
|
|
/>
|
|
</svg>
|
|
{{ item.rating }}
|
|
</span>
|
|
</div>
|
|
<div v-if="showExtras" class="item-extras">
|
|
<span v-if="item.artist">{{ item.artist }}</span>
|
|
<span v-if="item.episodes">{{ item.episodes }} episodes</span>
|
|
<span v-if="item.tracks">{{ item.tracks }} tracks</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed } from "vue";
|
|
import IconMovie from "@/icons/IconMovie.vue";
|
|
import IconShow from "@/icons/IconShow.vue";
|
|
import IconMusic from "@/icons/IconMusic.vue";
|
|
|
|
interface LibraryItem {
|
|
title: string;
|
|
poster?: string;
|
|
fallbackIcon?: string;
|
|
year?: number;
|
|
rating?: number;
|
|
artist?: string;
|
|
episodes?: number;
|
|
tracks?: number;
|
|
plexUrl?: string | null;
|
|
}
|
|
|
|
interface Props {
|
|
item: LibraryItem;
|
|
showExtras?: boolean;
|
|
}
|
|
|
|
const props = defineProps<Props>();
|
|
|
|
const fallbackIconComponent = computed(() => {
|
|
if (props.item.fallbackIcon === "🎬") return IconMovie;
|
|
if (props.item.fallbackIcon === "📺") return IconShow;
|
|
if (props.item.fallbackIcon === "🎵") return IconMusic;
|
|
return IconMovie; // Default fallback
|
|
});
|
|
|
|
function handleImageError(event: Event) {
|
|
const target = event.target as HTMLImageElement;
|
|
target.style.display = "none";
|
|
}
|
|
</script>
|
|
|
|
<style style="scss" scoped>
|
|
.plex-library-item {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
cursor: pointer;
|
|
transition: transform 0.2s;
|
|
text-decoration: none;
|
|
color: inherit;
|
|
}
|
|
|
|
.plex-library-item:hover {
|
|
transform: translateY(-4px);
|
|
}
|
|
|
|
.plex-library-item--no-link {
|
|
cursor: default;
|
|
}
|
|
|
|
.plex-library-item--no-link:hover {
|
|
transform: none;
|
|
}
|
|
|
|
.item-poster {
|
|
position: relative;
|
|
width: 100%;
|
|
aspect-ratio: 2 / 3;
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
background: #333;
|
|
margin: 0;
|
|
|
|
&.music {
|
|
aspect-ratio: 1/1;
|
|
}
|
|
}
|
|
|
|
.poster-image {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
}
|
|
|
|
.poster-fallback {
|
|
width: 100%;
|
|
height: 100%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: linear-gradient(135deg, #333 0%, #222 100%);
|
|
padding: 20%;
|
|
|
|
svg {
|
|
width: 100%;
|
|
height: 100%;
|
|
fill: #666;
|
|
}
|
|
}
|
|
|
|
.item-details {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
}
|
|
|
|
.item-title {
|
|
margin: 0;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
color: #fff;
|
|
line-height: 1.3;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 2;
|
|
-webkit-box-orient: vertical;
|
|
}
|
|
|
|
.item-meta {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
font-size: 12px;
|
|
color: #888;
|
|
}
|
|
|
|
.item-year {
|
|
color: #aaa;
|
|
}
|
|
|
|
.item-rating {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
color: #fbbf24;
|
|
}
|
|
|
|
.item-extras {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
font-size: 11px;
|
|
color: #888;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.item-title {
|
|
font-size: 13px;
|
|
}
|
|
|
|
.item-meta {
|
|
font-size: 11px;
|
|
}
|
|
}
|
|
</style>
|