mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-07-25 02:57:57 +00:00
Again... Ignore
This commit is contained in:
parent
c16ab49d56
commit
763e5afef8
6 changed files with 296 additions and 64 deletions
|
|
@ -1,4 +1,4 @@
|
|||
# Generated by Django 5.0.14 on 2025-09-16 18:34
|
||||
# Generated by Django 5.2.4 on 2025-09-18 01:25
|
||||
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
|
|
@ -63,6 +63,7 @@ class Migration(migrations.Migration):
|
|||
('started_at', models.DateTimeField(blank=True, null=True)),
|
||||
('finished_at', models.DateTimeField(blank=True, null=True)),
|
||||
('total_files', models.PositiveIntegerField(default=0)),
|
||||
('processed_files', models.PositiveIntegerField(default=0)),
|
||||
('new_files', models.PositiveIntegerField(default=0)),
|
||||
('updated_files', models.PositiveIntegerField(default=0)),
|
||||
('removed_files', models.PositiveIntegerField(default=0)),
|
||||
|
|
|
|||
|
|
@ -1,18 +0,0 @@
|
|||
# Generated by Django 5.0.14 on 2025-09-17 15:54
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('media_library', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='libraryscan',
|
||||
name='processed_files',
|
||||
field=models.PositiveIntegerField(default=0),
|
||||
),
|
||||
]
|
||||
|
|
@ -12,6 +12,8 @@ services:
|
|||
- 5678:5678 # Debugging port
|
||||
volumes:
|
||||
- ../:/app
|
||||
- "D:/Server Backup/Movies:/library/Movies"
|
||||
- "D:/Server Backup/TV Shows:/library/TVShows"
|
||||
environment:
|
||||
- DISPATCHARR_ENV=dev
|
||||
- DISPATCHARR_DEBUG=true
|
||||
|
|
|
|||
11
frontend/package-lock.json
generated
11
frontend/package-lock.json
generated
|
|
@ -24,6 +24,7 @@
|
|||
"dayjs": "^1.11.13",
|
||||
"formik": "^2.4.6",
|
||||
"hls.js": "^1.5.20",
|
||||
"immer": "^10.1.1",
|
||||
"lucide-react": "^0.511.0",
|
||||
"mpegts.js": "^1.8.0",
|
||||
"react": "^19.0.0",
|
||||
|
|
@ -2177,6 +2178,16 @@
|
|||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/immer": {
|
||||
"version": "10.1.3",
|
||||
"resolved": "https://registry.npmjs.org/immer/-/immer-10.1.3.tgz",
|
||||
"integrity": "sha512-tmjF/k8QDKydUlm3mZU+tjM6zeq9/fFpPqH9SzWmBnVVKsPBg/V66qsMwb3/Bo90cgUN+ghdVBess+hPsxUyRw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/immer"
|
||||
}
|
||||
},
|
||||
"node_modules/import-fresh": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import {
|
|||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
rem,
|
||||
} from '@mantine/core';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import {
|
||||
|
|
@ -31,18 +32,26 @@ import API from '../../api';
|
|||
import useMediaLibraryStore from '../../store/mediaLibrary';
|
||||
import useVideoStore from '../../store/useVideoStore';
|
||||
|
||||
// ---- quick tuning knobs ----
|
||||
const CAST_TILE_WIDTH = 96; // was 116
|
||||
const CAST_TILE_HEIGHT = 148; // was 160
|
||||
const CAST_AVATAR_SIZE = 72; // was 80
|
||||
const CAST_GAP = 8; // was "md" (~16)
|
||||
const STACK_TIGHT = 4; // was 6
|
||||
const SECTION_STACK = 12; // was larger in some places
|
||||
// ----------------------------
|
||||
|
||||
const runtimeLabel = (runtimeMs) => {
|
||||
if (!runtimeMs) return null;
|
||||
const totalSeconds = Math.round(runtimeMs / 1000);
|
||||
const hours = Math.floor(totalSeconds / 3600);
|
||||
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
||||
if (hours) {
|
||||
return `${hours}h ${minutes}m`;
|
||||
}
|
||||
if (hours) return `${hours}h ${minutes}m`;
|
||||
return `${minutes}m`;
|
||||
};
|
||||
|
||||
const MediaDetailModal = ({ opened, onClose }) => {
|
||||
<<<<<<< Updated upstream
|
||||
const activeItem = useMediaLibraryStore((state) => state.activeItem);
|
||||
const activeItemLoading = useMediaLibraryStore((state) => state.activeItemLoading);
|
||||
const activeProgress = useMediaLibraryStore((state) => state.activeProgress);
|
||||
|
|
@ -59,6 +68,26 @@ const MediaDetailModal = ({ opened, onClose }) => {
|
|||
const [episodePlayLoadingId, setEpisodePlayLoadingId] = useState(null);
|
||||
const [episodeActionLoading, setEpisodeActionLoading] = useState({});
|
||||
|
||||
=======
|
||||
const activeItem = useMediaLibraryStore((s) => s.activeItem);
|
||||
const activeItemLoading = useMediaLibraryStore((s) => s.activeItemLoading);
|
||||
const activeProgress = useMediaLibraryStore((s) => s.activeProgress);
|
||||
const resumePrompt = useMediaLibraryStore((s) => s.resumePrompt);
|
||||
const requestResume = useMediaLibraryStore((s) => s.requestResume);
|
||||
const clearResumePrompt = useMediaLibraryStore((s) => s.clearResumePrompt);
|
||||
const setActiveProgress = useMediaLibraryStore((s) => s.setActiveProgress);
|
||||
const showVideo = useVideoStore((s) => s.showVideo);
|
||||
|
||||
const [startingPlayback, setStartingPlayback] = useState(false);
|
||||
const [resumeModalOpen, setResumeModalOpen] = useState(false);
|
||||
const [resumeMode, setResumeMode] = useState('start');
|
||||
|
||||
const [episodes, setEpisodes] = useState([]);
|
||||
const [episodesLoading, setEpisodesLoading] = useState(false);
|
||||
const [episodePlayLoadingId, setEpisodePlayLoadingId] = useState(null);
|
||||
const [episodeActionLoading, setEpisodeActionLoading] = useState({});
|
||||
|
||||
>>>>>>> Stashed changes
|
||||
const setEpisodeLoading = useCallback((episodeId, action) => {
|
||||
setEpisodeActionLoading((prev) => ({ ...prev, [episodeId]: action }));
|
||||
}, []);
|
||||
|
|
@ -100,6 +129,7 @@ const MediaDetailModal = ({ opened, onClose }) => {
|
|||
}, [activeItem]);
|
||||
|
||||
const orderedEpisodes = useMemo(() => {
|
||||
<<<<<<< Updated upstream
|
||||
if (!episodes || episodes.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
|
@ -114,6 +144,16 @@ const MediaDetailModal = ({ opened, onClose }) => {
|
|||
if (episodeA !== episodeB) {
|
||||
return episodeA - episodeB;
|
||||
}
|
||||
=======
|
||||
if (!episodes || episodes.length === 0) return [];
|
||||
return [...episodes].sort((a, b) => {
|
||||
const seasonA = a.season_number ?? 0;
|
||||
const seasonB = b.season_number ?? 0;
|
||||
if (seasonA !== seasonB) return seasonA - seasonB;
|
||||
const episodeA = a.episode_number ?? 0;
|
||||
const episodeB = b.episode_number ?? 0;
|
||||
if (episodeA !== episodeB) return episodeA - episodeB;
|
||||
>>>>>>> Stashed changes
|
||||
return (a.sort_title || '').localeCompare(b.sort_title || '');
|
||||
});
|
||||
}, [episodes]);
|
||||
|
|
@ -121,6 +161,7 @@ const MediaDetailModal = ({ opened, onClose }) => {
|
|||
const showWatchSummary = activeItem?.watch_summary || null;
|
||||
|
||||
const playbackPlan = useMemo(() => {
|
||||
<<<<<<< Updated upstream
|
||||
if (!activeItem || activeItem.item_type !== 'show') {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -163,7 +204,37 @@ const MediaDetailModal = ({ opened, onClose }) => {
|
|||
const canResume = useMemo(() => {
|
||||
if (!activeProgress || !activeProgress.position_ms || !activeProgress.duration_ms) {
|
||||
return false;
|
||||
=======
|
||||
if (!activeItem || activeItem.item_type !== 'show') return null;
|
||||
|
||||
const sorted = orderedEpisodes;
|
||||
if (!sorted || sorted.length === 0) {
|
||||
return { sorted: [], resumeEpisode: null, nextEpisode: null };
|
||||
>>>>>>> Stashed changes
|
||||
}
|
||||
|
||||
const resumeId = showWatchSummary?.resume_episode_id;
|
||||
const nextId = showWatchSummary?.next_episode_id;
|
||||
let resumeEpisode = sorted.find((ep) => ep.id === resumeId) || null;
|
||||
let nextEpisode = sorted.find((ep) => ep.id === nextId) || null;
|
||||
|
||||
if (!resumeEpisode) {
|
||||
resumeEpisode = sorted.find((ep) => ep.watch_summary?.status === 'in_progress') || null;
|
||||
}
|
||||
if (!resumeEpisode) {
|
||||
resumeEpisode = sorted.find((ep) => ep.watch_summary?.status !== 'watched') || sorted[0];
|
||||
}
|
||||
if (!nextEpisode && resumeEpisode) {
|
||||
const currentIndex = sorted.findIndex((ep) => ep.id === resumeEpisode.id);
|
||||
if (currentIndex >= 0 && currentIndex + 1 < sorted.length) {
|
||||
nextEpisode = sorted[currentIndex + 1];
|
||||
}
|
||||
}
|
||||
return { sorted, resumeEpisode, nextEpisode };
|
||||
}, [activeItem, orderedEpisodes, showWatchSummary]);
|
||||
|
||||
const canResume = useMemo(() => {
|
||||
if (!activeProgress || !activeProgress.position_ms || !activeProgress.duration_ms) return false;
|
||||
const remaining = activeProgress.duration_ms - activeProgress.position_ms;
|
||||
return remaining > activeProgress.duration_ms * 0.04;
|
||||
}, [activeProgress]);
|
||||
|
|
@ -191,6 +262,7 @@ const MediaDetailModal = ({ opened, onClose }) => {
|
|||
}, [opened, activeItem?.id, activeItem?.item_type, loadEpisodes]);
|
||||
|
||||
const castPeople = useMemo(() => {
|
||||
<<<<<<< Updated upstream
|
||||
if (!activeItem?.cast) {
|
||||
return [];
|
||||
}
|
||||
|
|
@ -211,6 +283,17 @@ const MediaDetailModal = ({ opened, onClose }) => {
|
|||
if (!name) {
|
||||
return null;
|
||||
}
|
||||
=======
|
||||
if (!activeItem?.cast) return [];
|
||||
return activeItem.cast
|
||||
.map((entry, index) => {
|
||||
if (!entry) return null;
|
||||
if (typeof entry === 'string') {
|
||||
return { key: `${entry}-${index}`, name: entry, role: null, profile: null };
|
||||
}
|
||||
const name = entry.name || entry.character || entry.job;
|
||||
if (!name) return null;
|
||||
>>>>>>> Stashed changes
|
||||
return {
|
||||
key: `${name}-${index}`,
|
||||
name,
|
||||
|
|
@ -222,6 +305,7 @@ const MediaDetailModal = ({ opened, onClose }) => {
|
|||
}, [activeItem]);
|
||||
|
||||
const crewPeople = useMemo(() => {
|
||||
<<<<<<< Updated upstream
|
||||
if (!activeItem?.crew) {
|
||||
return [];
|
||||
}
|
||||
|
|
@ -242,6 +326,17 @@ const MediaDetailModal = ({ opened, onClose }) => {
|
|||
if (!name) {
|
||||
return null;
|
||||
}
|
||||
=======
|
||||
if (!activeItem?.crew) return [];
|
||||
return activeItem.crew
|
||||
.map((entry, index) => {
|
||||
if (!entry) return null;
|
||||
if (typeof entry === 'string') {
|
||||
return { key: `${entry}-${index}`, name: entry, role: null, profile: null };
|
||||
}
|
||||
const name = entry.name || entry.job || entry.department;
|
||||
if (!name) return null;
|
||||
>>>>>>> Stashed changes
|
||||
return {
|
||||
key: `${name}-${index}`,
|
||||
name,
|
||||
|
|
@ -266,9 +361,7 @@ const MediaDetailModal = ({ opened, onClose }) => {
|
|||
setResumeMode(mode);
|
||||
setStartingPlayback(true);
|
||||
try {
|
||||
const streamInfo = await API.streamMediaItem(activeItem.id, {
|
||||
fileId,
|
||||
});
|
||||
const streamInfo = await API.streamMediaItem(activeItem.id, { fileId });
|
||||
const playbackUrl = streamInfo?.url || streamInfo?.stream_url;
|
||||
if (!playbackUrl) {
|
||||
notifications.show({
|
||||
|
|
@ -330,13 +423,31 @@ const MediaDetailModal = ({ opened, onClose }) => {
|
|||
return 'Play';
|
||||
}, [activeItem, showWatchSummary, canResume, resumePrompt, activeProgress]);
|
||||
|
||||
<<<<<<< Updated upstream
|
||||
=======
|
||||
const primaryButtonLabel = useMemo(() => {
|
||||
if (!activeItem) return 'Play';
|
||||
if (activeItem.item_type === 'show') {
|
||||
if (showWatchSummary?.status === 'in_progress') return 'Continue Watching';
|
||||
if (showWatchSummary?.status === 'watched') return 'Watch Again';
|
||||
return 'Play';
|
||||
}
|
||||
if (canResume && (resumePrompt || activeProgress)) return 'Continue Watching';
|
||||
return 'Play';
|
||||
}, [activeItem, showWatchSummary, canResume, resumePrompt, activeProgress]);
|
||||
|
||||
>>>>>>> Stashed changes
|
||||
const handlePrimaryAction = () => {
|
||||
if (!activeItem) return;
|
||||
if (activeItem.item_type === 'show') {
|
||||
const targetEpisode =
|
||||
<<<<<<< Updated upstream
|
||||
playbackPlan?.resumeEpisode ||
|
||||
playbackPlan?.nextEpisode ||
|
||||
playbackPlan?.sorted?.[0];
|
||||
=======
|
||||
playbackPlan?.resumeEpisode || playbackPlan?.nextEpisode || playbackPlan?.sorted?.[0];
|
||||
>>>>>>> Stashed changes
|
||||
if (!targetEpisode) {
|
||||
notifications.show({
|
||||
title: 'No episodes available',
|
||||
|
|
@ -345,6 +456,7 @@ const MediaDetailModal = ({ opened, onClose }) => {
|
|||
});
|
||||
return;
|
||||
}
|
||||
<<<<<<< Updated upstream
|
||||
const startIndex = playbackPlan.sorted.findIndex(
|
||||
(ep) => ep.id === targetEpisode.id
|
||||
);
|
||||
|
|
@ -352,6 +464,10 @@ const MediaDetailModal = ({ opened, onClose }) => {
|
|||
sequence: playbackPlan.sorted,
|
||||
startIndex,
|
||||
});
|
||||
=======
|
||||
const startIndex = playbackPlan.sorted.findIndex((ep) => ep.id === targetEpisode.id);
|
||||
void handlePlayEpisode(targetEpisode, { sequence: playbackPlan.sorted, startIndex });
|
||||
>>>>>>> Stashed changes
|
||||
return;
|
||||
}
|
||||
if (canResume && (resumePrompt || activeProgress)) {
|
||||
|
|
@ -380,9 +496,13 @@ const MediaDetailModal = ({ opened, onClose }) => {
|
|||
return;
|
||||
}
|
||||
|
||||
<<<<<<< Updated upstream
|
||||
const streamInfo = await API.streamMediaItem(episodeDetail.id, {
|
||||
fileId: episodeFileId,
|
||||
});
|
||||
=======
|
||||
const streamInfo = await API.streamMediaItem(episodeDetail.id, { fileId: episodeFileId });
|
||||
>>>>>>> Stashed changes
|
||||
const playbackUrl = streamInfo?.url || streamInfo?.stream_url;
|
||||
if (!playbackUrl) {
|
||||
notifications.show({
|
||||
|
|
@ -400,10 +520,14 @@ const MediaDetailModal = ({ opened, onClose }) => {
|
|||
const computedIndex = startIndex ?? episodeIds.findIndex((id) => id === episode.id);
|
||||
|
||||
const playbackSequence = episodeIds.length
|
||||
<<<<<<< Updated upstream
|
||||
? {
|
||||
episodeIds,
|
||||
currentIndex: computedIndex >= 0 ? computedIndex : 0,
|
||||
}
|
||||
=======
|
||||
? { episodeIds, currentIndex: computedIndex >= 0 ? computedIndex : 0 }
|
||||
>>>>>>> Stashed changes
|
||||
: null;
|
||||
|
||||
const episodeProgress = episodeDetail.watch_progress;
|
||||
|
|
@ -452,21 +576,33 @@ const MediaDetailModal = ({ opened, onClose }) => {
|
|||
const grouped = new Map();
|
||||
orderedEpisodes.forEach((episode) => {
|
||||
const season = episode.season_number || 1;
|
||||
<<<<<<< Updated upstream
|
||||
if (!grouped.has(season)) {
|
||||
grouped.set(season, []);
|
||||
}
|
||||
=======
|
||||
if (!grouped.has(season)) grouped.set(season, []);
|
||||
>>>>>>> Stashed changes
|
||||
grouped.get(season).push(episode);
|
||||
});
|
||||
return grouped;
|
||||
}, [orderedEpisodes]);
|
||||
|
||||
<<<<<<< Updated upstream
|
||||
const sortedSeasons = useMemo(() => {
|
||||
return Array.from(episodesBySeason.keys()).sort((a, b) => a - b);
|
||||
}, [episodesBySeason]);
|
||||
=======
|
||||
const sortedSeasons = useMemo(
|
||||
() => Array.from(episodesBySeason.keys()).sort((a, b) => a - b),
|
||||
[episodesBySeason]
|
||||
);
|
||||
>>>>>>> Stashed changes
|
||||
|
||||
const formatEpisodeCode = (episode) => {
|
||||
const season = episode.season_number || 0;
|
||||
const ep = episode.episode_number || 0;
|
||||
<<<<<<< Updated upstream
|
||||
if (!season && !ep) {
|
||||
return '';
|
||||
}
|
||||
|
|
@ -476,6 +612,11 @@ const MediaDetailModal = ({ opened, onClose }) => {
|
|||
if (!ep) {
|
||||
return `S${season.toString().padStart(2, '0')}`;
|
||||
}
|
||||
=======
|
||||
if (!season && !ep) return '';
|
||||
if (!season) return `E${ep.toString().padStart(2, '0')}`;
|
||||
if (!ep) return `S${season.toString().padStart(2, '0')}`;
|
||||
>>>>>>> Stashed changes
|
||||
return `S${season.toString().padStart(2, '0')}E${ep.toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
|
|
@ -563,10 +704,14 @@ const MediaDetailModal = ({ opened, onClose }) => {
|
|||
<ScrollArea h="70vh" offsetScrollbars>
|
||||
<Group align="flex-start" gap="xl" wrap="wrap">
|
||||
{activeItem.poster_url ? (
|
||||
<<<<<<< Updated upstream
|
||||
<Box
|
||||
w={{ base: '100%', sm: 240 }}
|
||||
style={{ flexShrink: 0, maxWidth: 260 }}
|
||||
>
|
||||
=======
|
||||
<Box w={{ base: '100%', sm: 240 }} style={{ flexShrink: 0, maxWidth: 260 }}>
|
||||
>>>>>>> Stashed changes
|
||||
<Box
|
||||
style={{
|
||||
borderRadius: 16,
|
||||
|
|
@ -589,8 +734,13 @@ const MediaDetailModal = ({ opened, onClose }) => {
|
|||
</Box>
|
||||
) : null}
|
||||
|
||||
<<<<<<< Updated upstream
|
||||
<Stack spacing="xl" style={{ flex: 1, minWidth: 0 }}>
|
||||
<Stack spacing="sm">
|
||||
=======
|
||||
<Stack spacing={SECTION_STACK} style={{ flex: 1, minWidth: 0 }}>
|
||||
<Stack spacing={STACK_TIGHT}>
|
||||
>>>>>>> Stashed changes
|
||||
<Group justify="space-between" align="center">
|
||||
<Title order={3}>{activeItem.title}</Title>
|
||||
<Group gap="xs">
|
||||
|
|
@ -625,15 +775,11 @@ const MediaDetailModal = ({ opened, onClose }) => {
|
|||
)}
|
||||
|
||||
<Group gap="lg" mt="sm">
|
||||
{runtimeLabel(
|
||||
activeItem.runtime_ms || files[0]?.duration_ms
|
||||
) && (
|
||||
{runtimeLabel(activeItem.runtime_ms || files[0]?.duration_ms) && (
|
||||
<Group gap={4} align="center">
|
||||
<Clock size={18} />
|
||||
<Text size="sm">
|
||||
{runtimeLabel(
|
||||
activeItem.runtime_ms || files[0]?.duration_ms
|
||||
)}
|
||||
{runtimeLabel(activeItem.runtime_ms || files[0]?.duration_ms)}
|
||||
</Text>
|
||||
</Group>
|
||||
)}
|
||||
|
|
@ -648,6 +794,7 @@ const MediaDetailModal = ({ opened, onClose }) => {
|
|||
)}
|
||||
</Group>
|
||||
|
||||
<<<<<<< Updated upstream
|
||||
{activeItem.item_type === 'show' &&
|
||||
showWatchSummary?.total_episodes ? (
|
||||
<Text size="sm" c="dimmed">
|
||||
|
|
@ -655,6 +802,14 @@ const MediaDetailModal = ({ opened, onClose }) => {
|
|||
{showWatchSummary.total_episodes} episodes watched
|
||||
</Text>
|
||||
) : null}
|
||||
=======
|
||||
{activeItem.item_type === 'show' && showWatchSummary?.total_episodes ? (
|
||||
<Text size="sm" c="dimmed">
|
||||
{showWatchSummary.completed_episodes || 0} of{' '}
|
||||
{showWatchSummary.total_episodes} episodes watched
|
||||
</Text>
|
||||
) : null}
|
||||
>>>>>>> Stashed changes
|
||||
|
||||
<Group gap="sm" mt="md" align="center" wrap="wrap">
|
||||
<Button
|
||||
|
|
@ -672,13 +827,11 @@ const MediaDetailModal = ({ opened, onClose }) => {
|
|||
<Text size="sm" c="dimmed">
|
||||
Resume at{' '}
|
||||
{runtimeLabel(
|
||||
resumePrompt?.position_ms ||
|
||||
activeProgress?.position_ms
|
||||
resumePrompt?.position_ms || activeProgress?.position_ms
|
||||
)}{' '}
|
||||
of{' '}
|
||||
{runtimeLabel(
|
||||
resumePrompt?.duration_ms ||
|
||||
activeProgress?.duration_ms
|
||||
resumePrompt?.duration_ms || activeProgress?.duration_ms
|
||||
)}
|
||||
</Text>
|
||||
)}
|
||||
|
|
@ -708,7 +861,11 @@ const MediaDetailModal = ({ opened, onClose }) => {
|
|||
{sortedSeasons.map((season) => {
|
||||
const seasonEpisodes = episodesBySeason.get(season) || [];
|
||||
return (
|
||||
<<<<<<< Updated upstream
|
||||
<Stack key={season} spacing={6}>
|
||||
=======
|
||||
<Stack key={season} spacing={STACK_TIGHT}>
|
||||
>>>>>>> Stashed changes
|
||||
<Group justify="space-between" align="center">
|
||||
<Group gap="xs" align="center">
|
||||
<Title order={5}>Season {season}</Title>
|
||||
|
|
@ -718,7 +875,11 @@ const MediaDetailModal = ({ opened, onClose }) => {
|
|||
</Badge>
|
||||
</Group>
|
||||
</Group>
|
||||
<<<<<<< Updated upstream
|
||||
<Stack spacing={6}>
|
||||
=======
|
||||
<Stack spacing={STACK_TIGHT}>
|
||||
>>>>>>> Stashed changes
|
||||
{seasonEpisodes.map((episode) => {
|
||||
const episodeProgress = episode.watch_progress;
|
||||
const episodeStatus = episode.watch_summary?.status;
|
||||
|
|
@ -740,9 +901,15 @@ const MediaDetailModal = ({ opened, onClose }) => {
|
|||
padding: '10px 12px',
|
||||
}}
|
||||
>
|
||||
<<<<<<< Updated upstream
|
||||
<Stack spacing={4} style={{ flex: 1, minWidth: 0 }}>
|
||||
<Group justify="space-between" align="center">
|
||||
<Text fw={600} size="sm">
|
||||
=======
|
||||
<Stack spacing={STACK_TIGHT} style={{ flex: 1, minWidth: 0 }}>
|
||||
<Group justify="space-between" align="center">
|
||||
<Text fw={600} size="sm" lineClamp={1}>
|
||||
>>>>>>> Stashed changes
|
||||
{[formatEpisodeCode(episode), episode.title]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
|
|
@ -780,12 +947,20 @@ const MediaDetailModal = ({ opened, onClose }) => {
|
|||
)}
|
||||
</Group>
|
||||
{(episode.synopsis || activeItem?.synopsis) && (
|
||||
<<<<<<< Updated upstream
|
||||
<Text size="xs" c="dimmed" lineClamp={3}>
|
||||
=======
|
||||
<Text size="xs" c="dimmed" lineClamp={2}>
|
||||
>>>>>>> Stashed changes
|
||||
{episode.synopsis || activeItem?.synopsis}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
<<<<<<< Updated upstream
|
||||
<Stack spacing={6} align="flex-end">
|
||||
=======
|
||||
<Stack spacing={STACK_TIGHT} align="flex-end">
|
||||
>>>>>>> Stashed changes
|
||||
<Group gap={6}>
|
||||
<Button
|
||||
size="xs"
|
||||
|
|
@ -840,8 +1015,13 @@ const MediaDetailModal = ({ opened, onClose }) => {
|
|||
|
||||
<Divider label="Metadata" labelPosition="center" />
|
||||
|
||||
<<<<<<< Updated upstream
|
||||
<Stack spacing="sm">
|
||||
<Group gap="sm">
|
||||
=======
|
||||
<Stack spacing={STACK_TIGHT}>
|
||||
<Group gap="xs">
|
||||
>>>>>>> Stashed changes
|
||||
{activeItem.tmdb_id && (
|
||||
<Badge
|
||||
component="a"
|
||||
|
|
@ -866,6 +1046,7 @@ const MediaDetailModal = ({ opened, onClose }) => {
|
|||
)}
|
||||
</Group>
|
||||
|
||||
<<<<<<< Updated upstream
|
||||
{castPeople.length > 0 && (
|
||||
<Stack spacing="sm">
|
||||
<Text fw={500}>Cast</Text>
|
||||
|
|
@ -880,10 +1061,36 @@ const MediaDetailModal = ({ opened, onClose }) => {
|
|||
>
|
||||
<Avatar
|
||||
size={80}
|
||||
=======
|
||||
{/* CAST */}
|
||||
{castPeople.length > 0 && (
|
||||
<Stack spacing={STACK_TIGHT}>
|
||||
<Text fw={500}>Cast</Text>
|
||||
<ScrollArea
|
||||
type="auto"
|
||||
offsetScrollbars
|
||||
styles={{ viewport: { paddingBottom: rem(6) } }}
|
||||
>
|
||||
<Group gap={CAST_GAP} wrap="nowrap">
|
||||
{castPeople.map((person) => (
|
||||
<Stack
|
||||
key={person.key}
|
||||
spacing={STACK_TIGHT}
|
||||
align="center"
|
||||
style={{
|
||||
width: rem(CAST_TILE_WIDTH),
|
||||
height: rem(CAST_TILE_HEIGHT),
|
||||
flex: `0 0 ${rem(CAST_TILE_WIDTH)}`,
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
size={CAST_AVATAR_SIZE}
|
||||
>>>>>>> Stashed changes
|
||||
radius="md"
|
||||
src={person.profile || undefined}
|
||||
alt={person.name}
|
||||
color="indigo"
|
||||
<<<<<<< Updated upstream
|
||||
>
|
||||
{!person.profile && person.name?.[0]}
|
||||
</Avatar>
|
||||
|
|
@ -892,6 +1099,17 @@ const MediaDetailModal = ({ opened, onClose }) => {
|
|||
</Text>
|
||||
{person.role && (
|
||||
<Text size="xs" c="dimmed" ta="center" lineClamp={2}>
|
||||
=======
|
||||
styles={{ image: { objectFit: 'cover' } }}
|
||||
>
|
||||
{!person.profile && person.name?.[0]}
|
||||
</Avatar>
|
||||
<Text size="xs" fw={600} ta="center" lineClamp={2} style={{ minHeight: 30 }}>
|
||||
{person.name}
|
||||
</Text>
|
||||
{person.role && (
|
||||
<Text size="xs" c="dimmed" ta="center" lineClamp={1} style={{ minHeight: 16 }}>
|
||||
>>>>>>> Stashed changes
|
||||
{person.role}
|
||||
</Text>
|
||||
)}
|
||||
|
|
@ -902,6 +1120,7 @@ const MediaDetailModal = ({ opened, onClose }) => {
|
|||
</Stack>
|
||||
)}
|
||||
|
||||
<<<<<<< Updated upstream
|
||||
{crewPeople.length > 0 && (
|
||||
<Stack spacing="sm">
|
||||
<Text fw={500}>Crew</Text>
|
||||
|
|
@ -916,10 +1135,36 @@ const MediaDetailModal = ({ opened, onClose }) => {
|
|||
>
|
||||
<Avatar
|
||||
size={80}
|
||||
=======
|
||||
{/* CREW */}
|
||||
{crewPeople.length > 0 && (
|
||||
<Stack spacing={STACK_TIGHT}>
|
||||
<Text fw={500}>Crew</Text>
|
||||
<ScrollArea
|
||||
type="auto"
|
||||
offsetScrollbars
|
||||
styles={{ viewport: { paddingBottom: rem(6) } }}
|
||||
>
|
||||
<Group gap={CAST_GAP} wrap="nowrap">
|
||||
{crewPeople.map((person) => (
|
||||
<Stack
|
||||
key={person.key}
|
||||
spacing={STACK_TIGHT}
|
||||
align="center"
|
||||
style={{
|
||||
width: rem(CAST_TILE_WIDTH),
|
||||
height: rem(CAST_TILE_HEIGHT),
|
||||
flex: `0 0 ${rem(CAST_TILE_WIDTH)}`,
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
size={CAST_AVATAR_SIZE}
|
||||
>>>>>>> Stashed changes
|
||||
radius="md"
|
||||
src={person.profile || undefined}
|
||||
alt={person.name}
|
||||
color="grape"
|
||||
<<<<<<< Updated upstream
|
||||
>
|
||||
{!person.profile && person.name?.[0]}
|
||||
</Avatar>
|
||||
|
|
@ -928,6 +1173,17 @@ const MediaDetailModal = ({ opened, onClose }) => {
|
|||
</Text>
|
||||
{person.role && (
|
||||
<Text size="xs" c="dimmed" ta="center" lineClamp={2}>
|
||||
=======
|
||||
styles={{ image: { objectFit: 'cover' } }}
|
||||
>
|
||||
{!person.profile && person.name?.[0]}
|
||||
</Avatar>
|
||||
<Text size="xs" fw={600} ta="center" lineClamp={2} style={{ minHeight: 30 }}>
|
||||
{person.name}
|
||||
</Text>
|
||||
{person.role && (
|
||||
<Text size="xs" c="dimmed" ta="center" lineClamp={1} style={{ minHeight: 16 }}>
|
||||
>>>>>>> Stashed changes
|
||||
{person.role}
|
||||
</Text>
|
||||
)}
|
||||
|
|
@ -955,15 +1211,8 @@ const MediaDetailModal = ({ opened, onClose }) => {
|
|||
>
|
||||
<Stack spacing="md">
|
||||
<Text>
|
||||
Resume from{' '}
|
||||
{runtimeLabel(
|
||||
resumePrompt?.position_ms || activeProgress?.position_ms
|
||||
)}{' '}
|
||||
of{' '}
|
||||
{runtimeLabel(
|
||||
resumePrompt?.duration_ms || activeProgress?.duration_ms
|
||||
)}
|
||||
?
|
||||
Resume from {runtimeLabel(resumePrompt?.position_ms || activeProgress?.position_ms)} of{' '}
|
||||
{runtimeLabel(resumePrompt?.duration_ms || activeProgress?.duration_ms)}?
|
||||
</Text>
|
||||
<Group justify="flex-end">
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -50,26 +50,13 @@ const SettingsPage = () => {
|
|||
const suppressWarning = useWarningsStore((s) => s.suppressWarning);
|
||||
const isWarningSuppressed = useWarningsStore((s) => s.isWarningSuppressed);
|
||||
|
||||
const {
|
||||
libraries: mediaLibraries,
|
||||
loading: librariesLoading,
|
||||
fetchLibraries: fetchMediaLibraries,
|
||||
createLibrary: createMediaLibrary,
|
||||
updateLibrary: updateMediaLibrary,
|
||||
deleteLibrary: deleteMediaLibrary,
|
||||
triggerScan: triggerLibraryScan,
|
||||
} = useLibraryStore(
|
||||
(state) => ({
|
||||
libraries: state.libraries,
|
||||
loading: state.loading,
|
||||
fetchLibraries: state.fetchLibraries,
|
||||
createLibrary: state.createLibrary,
|
||||
updateLibrary: state.updateLibrary,
|
||||
deleteLibrary: state.deleteLibrary,
|
||||
triggerScan: state.triggerScan,
|
||||
}),
|
||||
shallow
|
||||
);
|
||||
const mediaLibraries = useLibraryStore((state) => state.libraries);
|
||||
const librariesLoading = useLibraryStore((state) => state.loading);
|
||||
const fetchMediaLibraries = useLibraryStore((state) => state.fetchLibraries);
|
||||
const createMediaLibrary = useLibraryStore((state) => state.createLibrary);
|
||||
const updateMediaLibrary = useLibraryStore((state) => state.updateLibrary);
|
||||
const deleteMediaLibrary = useLibraryStore((state) => state.deleteLibrary);
|
||||
const triggerLibraryScan = useLibraryStore((state) => state.triggerScan);
|
||||
|
||||
const [accordianValue, setAccordianValue] = useState(null);
|
||||
const [networkAccessSaved, setNetworkAccessSaved] = useState(false);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue