Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion packages/app/src/App/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ const PANELS = {

const AppContent = (props) => {
const {isAuthenticated, isLoading, logout, serverUrl, serverName, api, user, hasMultipleServers, accessToken, connectionState, revalidateSession} = useAuth();
const {settings, activeTheme} = useSettings();
const {settings, activeTheme, syncFromServer} = useSettings();
const {streamNotification, dismissStreamNotification, adminMessage, dismissAdminMessage} = useSeerr();
const themeMusic = useThemeMusic();
const {openDialog: openSyncPlay, closeDialog: closeSyncPlay, isDialogOpen: syncPlayDialogOpen, playQueueItem, clearPlayQueueItem, isInGroup: isSyncPlayInGroup, setNewQueue: syncPlaySetNewQueue, displayMessage: syncPlayMessage, clearDisplayMessage: clearSyncPlayMessage} = useSyncPlay();
Expand Down Expand Up @@ -219,6 +219,14 @@ const AppContent = (props) => {
setPinCodeError('');
}, [isAuthenticated, settings.pinCodeProtection, user?.Id]);

useEffect(() => {
if (isAuthenticated && serverUrl && accessToken && settings.useMoonfinPlugin) {
syncFromServer(serverUrl, accessToken).catch((err) => {
console.warn('[App] Initial settings sync failed:', err.message);
});
}
}, [isAuthenticated, serverUrl, accessToken, settings.useMoonfinPlugin, syncFromServer]);

useEffect(() => {
if (!isPinGateActive) return;
const timer = setTimeout(() => {
Expand Down
49 changes: 38 additions & 11 deletions packages/app/src/components/MediaCard/MediaCard.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,20 @@ const MediaCard = ({item, serverUrl, cardType = 'portrait', onSelect, onFocusIte
const imageType = settings.homeRowsImageType || 'poster';
const providerIds = item.ProviderIds || {};

if (item.Type === 'Genre' && item._representative) {
const rep = item._representative;
const repServerUrl = itemServerUrl;
if (imageType === 'thumb' && rep.ImageTags?.Thumb) {
return getImageUrl(repServerUrl, rep.Id, 'Thumb', {maxWidth: 400, quality: 80});
}
if (imageType === 'backdrop' && rep.BackdropImageTags?.length > 0) {
return getImageUrl(repServerUrl, rep.Id, 'Backdrop', {maxWidth: 400, quality: 80});
}
if (rep.ImageTags?.Primary) {
return getImageUrl(repServerUrl, rep.Id, 'Primary', {maxHeight: 300, quality: 80});
}
}

if (isLandscape && item.Type === 'Episode') {
if (settings.useSeriesThumbnails && item.SeriesId && item.SeriesPrimaryImageTag) {
return getImageUrl(itemServerUrl, item.SeriesId, 'Primary', {maxHeight: 300, quality: 80});
Expand Down Expand Up @@ -98,7 +112,7 @@ const MediaCard = ({item, serverUrl, cardType = 'portrait', onSelect, onFocusIte
}

return null;
}, [isLandscape, item.Type, item.ImageTags?.Primary, item.ImageTags?.Thumb, item.ImageTags?.Logo, item.Id, item.ParentThumbItemId, item.ParentBackdropItemId, item.BackdropImageTags, item.ParentLogoItemId, item.AlbumId, item.AlbumPrimaryImageTag, item.SeriesId, item.SeriesPrimaryImageTag, item.ProviderIds, item._externalPosterUrl, itemServerUrl, settings.homeRowsImageType, settings.useSeriesThumbnails]);
}, [isLandscape, item.Type, item.ImageTags?.Primary, item.ImageTags?.Thumb, item.ImageTags?.Logo, item.Id, item.ParentThumbItemId, item.ParentBackdropItemId, item.BackdropImageTags, item.ParentLogoItemId, item.AlbumId, item.AlbumPrimaryImageTag, item.SeriesId, item.SeriesPrimaryImageTag, item.ProviderIds, item._externalPosterUrl, itemServerUrl, settings.homeRowsImageType, settings.useSeriesThumbnails, item._representative]);

const handleClick = useCallback(() => {
onSelect?.(item);
Expand Down Expand Up @@ -155,15 +169,23 @@ const MediaCard = ({item, serverUrl, cardType = 'portrait', onSelect, onFocusIte
<SpottableDiv className={cardClass} onClick={handleClick} onFocus={handleFocus} style={sizeStyle} spotlightId={spotlightId} onSpotlightLeft={onSpotlightLeft} onSpotlightRight={onSpotlightRight}>
<div className={css.imageContainer}>
{imageUrl ? (
<img
className={css.image}
src={imageUrl}
alt={item.Name}
loading={eagerLoad ? 'eager' : 'lazy'}
width={cardWidth}
height={cardHeight}
style={imgSizeStyle}
/>
<>
<img
className={css.image}
src={imageUrl}
alt={item.Name}
loading={eagerLoad ? 'eager' : 'lazy'}
width={cardWidth}
height={cardHeight}
style={imgSizeStyle}
/>
{(item?.Type === 'Genre' || item?.Type === 'MusicGenre') && (
<>
<div className={css.genreOverlay} />
<div className={css.genreTitle}>{item.Name?.toUpperCase()}</div>
</>
)}
</>
) : (
<div className={css.placeholder} style={imgSizeStyle}>{item.Name?.[0]}</div>
)}
Expand All @@ -174,7 +196,7 @@ const MediaCard = ({item, serverUrl, cardType = 'portrait', onSelect, onFocusIte
</div>
)}

{showServerBadge && item._serverName && (
{(showServerBadge || item._external) && item._serverName && (
<div className={css.serverBadge}>{item._serverName}</div>
)}

Expand Down Expand Up @@ -203,6 +225,11 @@ const MediaCard = ({item, serverUrl, cardType = 'portrait', onSelect, onFocusIte
<div className={css.title}>{displayTitle}</div>
<div className={css.episodeInfo}>{musicInfo}</div>
</>
) : item.Subtitle ? (
<>
<div className={css.title}>{displayTitle}</div>
<div className={css.episodeInfo}>{item.Subtitle}</div>
</>
) : (
<div className={css.title}>{displayTitle}</div>
)}
Expand Down
32 changes: 32 additions & 0 deletions packages/app/src/components/MediaCard/MediaCard.module.less
Original file line number Diff line number Diff line change
Expand Up @@ -226,4 +226,36 @@
.watchedBadge {
@import '../../styles/mixins.less';
.watched-badge();
}

.genreOverlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.45);
z-index: 1;
border-radius: var(--theme-card-radius, 20px);
}

.genreTitle {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
padding: 12px;
z-index: 2;
color: rgba(255, 255, 255, 0.9);
font-size: 26px;
font-weight: 700;
letter-spacing: 0.15em;
text-align: center;
text-transform: uppercase;
pointer-events: none;
word-break: break-word;
}
95 changes: 80 additions & 15 deletions packages/app/src/components/MediaCard/ModernMediaCard.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ const getGenreNames = (item) => {
const getMetadataLine = (item) => {
if (!item) return '';
const parts = [];
if (item.UserRating) parts.push(item.UserRating);
if (item.ProductionYear) parts.push(String(item.ProductionYear));
const genres = getGenreNames(item).slice(0, 3);
if (genres.length) parts.push(genres.join(' • '));
Expand Down Expand Up @@ -113,20 +114,66 @@ const ModernMediaCard = ({
if (episodeImage) return episodeImage;
}

if (item.Type === 'Movie' || item.Type === 'Series') {
if (item.Type === 'Genre' && item._representative) {
const rep = item._representative;
const repServerUrl = itemServerUrl;
if (isFocused) {
if (rep.ImageTags?.Thumb) {
return getImageUrl(repServerUrl, rep.Id, 'Thumb', {maxWidth: 600, quality: 80});
}
if (rep.BackdropImageTags?.length > 0) {
return getImageUrl(repServerUrl, rep.Id, 'Backdrop', {maxWidth: 600, quality: 80});
}
}
if (rep.ImageTags?.Primary) {
return getImageUrl(repServerUrl, rep.Id, 'Primary', {maxHeight: 360, quality: 80});
}
}

if (item.Type === 'Movie' || item.Type === 'Series' || item.Type === 'BoxSet') {
const imageType = settings.homeRowsImageType || 'poster';
if (isFocused) {
if (item.ImageTags?.Thumb) {
return getImageUrl(itemServerUrl, item.Id, 'Thumb', {maxWidth: 600, quality: 80});
}
if (item.BackdropImageTags?.length > 0) {
return getImageUrl(itemServerUrl, item.Id, 'Backdrop', {maxWidth: 600, quality: 80});
}
if (item._externalBackdropUrl) {
return toAbsoluteImageUrl(item._externalBackdropUrl, itemServerUrl);
}
}

if (imageType === 'thumb' && item.ImageTags?.Thumb) {
return getImageUrl(itemServerUrl, item.Id, 'Thumb', {maxWidth: 600, quality: 80});
}
if (imageType === 'backdrop' && item.BackdropImageTags?.length > 0) {
return getImageUrl(itemServerUrl, item.Id, 'Backdrop', {maxWidth: 600, quality: 80});
}

if (item.ImageTags?.Primary) {
return getImageUrl(itemServerUrl, item.Id, 'Primary', {maxHeight: 360, quality: 80});
}
}

if (item.Type === 'CollectionFolder' || item.isLibraryTile) {
if (isFocused) {
if (item.ImageTags?.Thumb) {
return getImageUrl(itemServerUrl, item.Id, 'Thumb', {maxWidth: 600, quality: 80});
}
if (item.BackdropImageTags?.length > 0) {
return getImageUrl(itemServerUrl, item.Id, 'Backdrop', {maxWidth: 600, quality: 80});
}
}

if (item.ImageTags?.Primary) {
return getImageUrl(itemServerUrl, item.Id, 'Primary', {maxHeight: 360, quality: 80});
}
if (item.ImageTags?.Thumb) {
return getImageUrl(itemServerUrl, item.Id, 'Thumb', {maxWidth: 600, quality: 80});
}
}

if (item.Type === 'Audio' && item.AlbumId && item.AlbumPrimaryImageTag) {
return getImageUrl(itemServerUrl, item.AlbumId, 'Primary', {maxHeight: 360, quality: 80});
}
Expand All @@ -153,7 +200,7 @@ const ModernMediaCard = ({
return toAbsoluteImageUrl(externalPoster, itemServerUrl);
}
return null;
}, [item, itemServerUrl, isFocused, settings.useSeriesThumbnails]);
}, [item, itemServerUrl, isFocused, settings.useSeriesThumbnails, settings.homeRowsImageType]);

const handleClick = useCallback(() => {
onSelect?.(item);
Expand Down Expand Up @@ -185,16 +232,24 @@ const ModernMediaCard = ({
const overviewText = useMemo(() => {
if (!shouldShowOverview) return '';
const rawOverview = typeof item?.Overview === 'string' ? item.Overview.trim() : '';
if (item?._external && !rawOverview) return '';
return rawOverview || $L('No description available.');
}, [item?.Overview, shouldShowOverview]);
}, [item?.Overview, shouldShowOverview, item?._external]);

const sizeMultiplier = POSTER_SIZE_MULTIPLIERS[settings.homeRowsPosterSize] || 1;
const imageHeight = Math.round(360 * sizeMultiplier);
const isSquareItem = item?.Type === 'MusicAlbum' || item?.Type === 'Audio';
const cardWidth = isSquareItem ? imageHeight : Math.round((imageHeight * 2) / 3);
const expandedWidthFactor = platform === 'tizen' ? 16 / 9 : 1.65;
const expandedWidth = Math.max(cardWidth, Math.round(imageHeight * expandedWidthFactor));
const canRenderExpanded = !isSquareItem && Boolean(metadata || item?.CommunityRating || shouldShowOverview);
const canRenderExpanded = !isSquareItem && (
Boolean(metadata || item?.CommunityRating || (shouldShowOverview && overviewText)) ||
item?.Type === 'Genre' ||
item?.Type === 'CollectionFolder' ||
item?.isLibraryTile ||
item?._external === true ||
item?._seerr === true
);

const cardClassName = [
css.card,
Expand Down Expand Up @@ -222,15 +277,23 @@ const ModernMediaCard = ({
>
<div className={css.imageContainer}>
{imageUrl ? (
<img
className={css.image}
src={imageUrl}
alt={item?.Name}
loading={eagerLoad ? 'eager' : 'lazy'}
width={cardWidth}
height={imageHeight}
style={{height: `${imageHeight}px`}}
/>
<>
<img
className={css.image}
src={imageUrl}
alt={item?.Name}
loading={eagerLoad ? 'eager' : 'lazy'}
width={cardWidth}
height={imageHeight}
style={{height: `${imageHeight}px`}}
/>
{(item?.Type === 'Genre' || item?.Type === 'MusicGenre') && (
<>
<div className={css.genreOverlay} />
<div className={css.genreTitle}>{item?.Name?.toUpperCase()}</div>
</>
)}
</>
) : (
<div className={css.placeholder} style={{height: `${imageHeight}px`}}>
{item?.Type === 'Person' ? (
Expand All @@ -249,7 +312,7 @@ const ModernMediaCard = ({
</div>
)}

{showServerBadge && item?._serverName && (
{(showServerBadge || item?._external) && item?._serverName && (
<div className={css.serverBadge}>{item._serverName}</div>
)}

Expand All @@ -259,7 +322,9 @@ const ModernMediaCard = ({
</div>

<div className={css.title}>{displayTitle}</div>
{episodeLabel && <div className={css.secondaryTitle}>{episodeLabel}</div>}
{(episodeLabel || item?.Subtitle) && (
<div className={css.secondaryTitle}>{episodeLabel || item.Subtitle}</div>
)}

{isFocused && canRenderExpanded && (
<div className={css.extendedSection}>
Expand Down
32 changes: 32 additions & 0 deletions packages/app/src/components/MediaCard/ModernMediaCard.module.less
Original file line number Diff line number Diff line change
Expand Up @@ -237,3 +237,35 @@
max-width: min(var(--modern-card-expanded-width), calc(100vw - 260px));
min-height: 120px;
}

.genreOverlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.45);
z-index: 1;
border-radius: var(--theme-card-radius, 20px);
}

.genreTitle {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
padding: 12px;
z-index: 2;
color: rgba(255, 255, 255, 0.9);
font-size: 26px;
font-weight: 700;
letter-spacing: 0.15em;
text-align: center;
text-transform: uppercase;
pointer-events: none;
word-break: break-word;
}
2 changes: 1 addition & 1 deletion packages/app/src/components/MediaRow/MediaRow.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ const MediaRow = ({

return (
<MediaCard
key={`${keyPrefix}-${item.Id}`}
key={`${keyPrefix}-${item.Id}-${index}`}
item={item}
serverUrl={serverUrl}
cardType={cardType}
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/components/MediaRow/ModernMediaRow.js
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ const ModernMediaRow = ({
const isLast = index === items.length - 1;
return (
<ModernMediaCard
key={`${keyPrefix}-${item.Id}`}
key={`${keyPrefix}-${item.Id}-${index}`}
item={item}
serverUrl={serverUrl}
onSelect={handleSelect}
Expand Down
Loading
Loading