Feat/settings page redesign (#104)

* 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
This commit is contained in:
2026-03-08 21:16:36 +01:00
committed by GitHub
parent 081240c83e
commit c309016299
39 changed files with 6232 additions and 115 deletions

176
src/utils/plexHelpers.ts Normal file
View File

@@ -0,0 +1,176 @@
export function getLibraryIcon(type: string): string {
const icons: Record<string, string> = {
movies: "🎬",
"tv shows": "📺",
music: "🎵"
};
return icons[type] || "📁";
}
export function getLibraryIconComponent(type: string): string {
const components: Record<string, string> = {
movies: "IconMovie",
"tv shows": "IconShow",
music: "IconMusic"
};
return components[type] || "IconMovie";
}
export function getLibraryTitle(type: string): string {
const titles: Record<string, string> = {
movies: "Movies",
"tv shows": "TV Shows",
music: "Music"
};
return titles[type] || type;
}
export function formatDate(dateString: string): string {
try {
const date = new Date(dateString);
return date.toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric"
});
} catch {
return dateString;
}
}
export function formatMemberSince(dateString: string): string {
try {
const date = new Date(dateString);
const now = new Date();
const years = now.getFullYear() - date.getFullYear();
if (years === 0) return "New Member";
if (years === 1) return "1 Year";
return `${years} Years`;
} catch {
return "Member";
}
}
export function processLibraryItem(
item: any,
libraryType: string,
authToken: string,
serverUrl: string,
machineIdentifier: string
) {
// Get poster/thumbnail URL
let posterUrl = null;
// For TV tv shows, prefer grandparentThumb (show poster) over thumb (episode thumbnail)
if (libraryType === "tv shows") {
if (item.grandparentThumb) {
posterUrl = `${serverUrl}${item.grandparentThumb}?X-Plex-Token=${authToken}`;
} else if (item.thumb) {
posterUrl = `${serverUrl}${item.thumb}?X-Plex-Token=${authToken}`;
}
}
// For music, prefer grandparentThumb (artist/album) over thumb
else if (libraryType === "music") {
if (item.grandparentThumb) {
posterUrl = `${serverUrl}${item.grandparentThumb}?X-Plex-Token=${authToken}`;
} else if (item.thumb) {
posterUrl = `${serverUrl}${item.thumb}?X-Plex-Token=${authToken}`;
}
}
// For movies and other types, use thumb
else if (item.thumb) {
posterUrl = `${serverUrl}${item.thumb}?X-Plex-Token=${authToken}`;
}
// Build Plex Web App URL
// Format: https://app.plex.tv/desktop/#!/server/{machineId}/details?key=%2Flibrary%2Fmetadata%2F{ratingKey}
const ratingKey = item.ratingKey || item.key;
let plexUrl = null;
if (ratingKey && machineIdentifier) {
const encodedKey = encodeURIComponent(`/library/metadata/${ratingKey}`);
plexUrl = `https://app.plex.tv/desktop/#!/server/${machineIdentifier}/details?key=${encodedKey}`;
}
// For tv shows, use grandparent data (show info) instead of episode info
const title =
libraryType === "tv shows" && item.grandparentTitle
? item.grandparentTitle
: item.title;
const year =
libraryType === "tv shows" && item.grandparentYear
? item.grandparentYear
: item.year || item.parentYear || new Date().getFullYear();
const baseItem = {
title,
year,
poster: posterUrl,
fallbackIcon: getLibraryIcon(libraryType),
rating: item.rating ? Math.round(item.rating * 10) / 10 : null,
type: libraryType,
ratingKey,
plexUrl
};
if (libraryType === "tv shows") {
return {
...baseItem,
episodes: item.leafCount || 0
};
}
if (libraryType === "music") {
return {
...baseItem,
artist: item.parentTitle || "Unknown Artist",
tracks: item.leafCount || 0
};
}
return baseItem;
}
export function calculateGenreStats(metadata: any[]) {
const genreMap = new Map<string, number>();
metadata.forEach((item: any) => {
if (item.Genre) {
item.Genre.forEach((genre: any) => {
genreMap.set(genre.tag, (genreMap.get(genre.tag) || 0) + 1);
});
}
});
return Array.from(genreMap.entries())
.sort((a, b) => b[1] - a[1])
.slice(0, 5)
.map(([name, count]) => ({ name, count }));
}
export function calculateDuration(metadata: any[], libraryType: string) {
let totalDuration = 0;
let totalEpisodes = 0;
let totalTracks = 0;
metadata.forEach((item: any) => {
if (item.duration) {
totalDuration += item.duration;
}
if (libraryType === "tv shows" && item.leafCount) {
totalEpisodes += item.leafCount;
} else if (libraryType === "music" && item.leafCount) {
totalTracks += item.leafCount;
}
});
const hours = Math.round(totalDuration / (1000 * 60 * 60));
const formattedDuration = `${hours.toLocaleString()} hours`;
return {
totalDuration: formattedDuration,
totalEpisodes,
totalTracks
};
}