mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-05-22 14:56:38 +01:00
Fix/tv interface android (#1576)
Signed-off-by: Lance Chant <13349722+lancechant@users.noreply.github.com>
This commit is contained in:
@@ -543,11 +543,6 @@ export default function page() {
|
||||
],
|
||||
);
|
||||
|
||||
/** Gets the initial playback position in seconds. */
|
||||
const _startPosition = useMemo(() => {
|
||||
return ticksToSeconds(getInitialPlaybackTicks());
|
||||
}, [getInitialPlaybackTicks]);
|
||||
|
||||
/** Build video source config for MPV */
|
||||
const videoSource = useMemo<MpvVideoSource | undefined>(() => {
|
||||
if (!stream?.url) return undefined;
|
||||
@@ -1104,6 +1099,13 @@ export default function page() {
|
||||
applySubtitleSettings();
|
||||
}, [isVideoLoaded, settings]);
|
||||
|
||||
// Seek to resume position after file is loaded (MPV_EVENT_FILE_LOADED)
|
||||
useEffect(() => {
|
||||
if (!tracksReady || !videoRef.current) return;
|
||||
const ticks = getInitialPlaybackTicks();
|
||||
videoRef.current?.seekTo?.(ticksToSeconds(ticks));
|
||||
}, [tracksReady, getInitialPlaybackTicks]);
|
||||
|
||||
// Apply initial playback speed when video loads
|
||||
useEffect(() => {
|
||||
if (!isVideoLoaded || !videoRef.current) return;
|
||||
|
||||
@@ -69,6 +69,7 @@ export const PlayButton: React.FC<Props> = ({
|
||||
subtitleIndex: selectedOptions.subtitleIndex?.toString() ?? "",
|
||||
mediaSourceId: selectedOptions.mediaSource?.Id ?? "",
|
||||
bitrateValue: selectedOptions.bitrate?.value?.toString() ?? "",
|
||||
playbackPosition: item.UserData?.PlaybackPositionTicks?.toString() ?? "0",
|
||||
});
|
||||
|
||||
const queryString = queryParams.toString();
|
||||
|
||||
@@ -46,7 +46,7 @@ import { updateTVDiscovery } from "@/utils/tvDiscovery/sync";
|
||||
const HORIZONTAL_PADDING = scaleSize(60);
|
||||
const TOP_PADDING = scaleSize(100);
|
||||
// Generous gap between sections for Apple TV+ aesthetic
|
||||
const SECTION_GAP = scaleSize(10);
|
||||
const SECTION_GAP = scaleSize(24);
|
||||
|
||||
type InfiniteScrollingCollectionListSection = {
|
||||
type: "InfiniteScrollingCollectionList";
|
||||
|
||||
@@ -207,7 +207,7 @@ export const TVHeroCarousel: React.FC<TVHeroCarouselProps> = ({
|
||||
const typography = useScaledTVTypography();
|
||||
const sizes = useScaledTVSizes();
|
||||
const api = useAtomValue(apiAtom);
|
||||
const _insets = useSafeAreaInsets();
|
||||
const insets = useSafeAreaInsets();
|
||||
const router = useRouter();
|
||||
|
||||
// Active item for featured display (debounced)
|
||||
@@ -381,7 +381,13 @@ export const TVHeroCarousel: React.FC<TVHeroCarouselProps> = ({
|
||||
const heroHeight = SCREEN_HEIGHT * sizes.padding.heroHeight;
|
||||
|
||||
return (
|
||||
<View style={{ height: heroHeight, width: "100%" }}>
|
||||
<View
|
||||
style={{
|
||||
height: heroHeight + insets.top,
|
||||
width: "100%",
|
||||
paddingTop: insets.top,
|
||||
}}
|
||||
>
|
||||
{/* Backdrop layers with crossfade */}
|
||||
<View
|
||||
style={{
|
||||
|
||||
@@ -438,7 +438,7 @@ export const TVPosterCard: React.FC<TVPosterCardProps> = ({
|
||||
position: "relative",
|
||||
width,
|
||||
aspectRatio,
|
||||
borderRadius: scaleSize(4),
|
||||
borderRadius: scaleSize(24),
|
||||
overflow: "hidden",
|
||||
backgroundColor: "#1a1a1a",
|
||||
borderWidth: scaleSize(2),
|
||||
|
||||
@@ -462,7 +462,19 @@ class MPVLayerRenderer(private val context: Context) : MPVLib.EventObserver {
|
||||
fun setSubtitleFontSize(size: Int) {
|
||||
MPVLib.setPropertyInt("sub-font-size", size)
|
||||
}
|
||||
|
||||
|
||||
fun setSubtitleBorderStyle(style: String) {
|
||||
MPVLib.setPropertyString("sub-border-style", style)
|
||||
}
|
||||
|
||||
fun setSubtitleBackgroundColor(color: String) {
|
||||
MPVLib.setPropertyString("sub-back-color", color)
|
||||
}
|
||||
|
||||
fun setSubtitleAssOverride(mode: String) {
|
||||
MPVLib.setPropertyString("sub-ass-override", mode)
|
||||
}
|
||||
|
||||
// MARK: - Audio Track Controls
|
||||
|
||||
fun getAudioTracks(): List<Map<String, Any>> {
|
||||
|
||||
@@ -151,6 +151,18 @@ class MpvPlayerModule : Module() {
|
||||
view.setSubtitleFontSize(size)
|
||||
}
|
||||
|
||||
AsyncFunction("setSubtitleBorderStyle") { view: MpvPlayerView, style: String ->
|
||||
view.setSubtitleBorderStyle(style)
|
||||
}
|
||||
|
||||
AsyncFunction("setSubtitleBackgroundColor") { view: MpvPlayerView, color: String ->
|
||||
view.setSubtitleBackgroundColor(color)
|
||||
}
|
||||
|
||||
AsyncFunction("setSubtitleAssOverride") { view: MpvPlayerView, mode: String ->
|
||||
view.setSubtitleAssOverride(mode)
|
||||
}
|
||||
|
||||
// Audio track functions
|
||||
AsyncFunction("getAudioTracks") { view: MpvPlayerView ->
|
||||
view.getAudioTracks()
|
||||
|
||||
@@ -271,7 +271,19 @@ class MpvPlayerView(context: Context, appContext: AppContext) : ExpoView(context
|
||||
fun setSubtitleFontSize(size: Int) {
|
||||
renderer?.setSubtitleFontSize(size)
|
||||
}
|
||||
|
||||
|
||||
fun setSubtitleBorderStyle(style: String) {
|
||||
renderer?.setSubtitleBorderStyle(style)
|
||||
}
|
||||
|
||||
fun setSubtitleBackgroundColor(color: String) {
|
||||
renderer?.setSubtitleBackgroundColor(color)
|
||||
}
|
||||
|
||||
fun setSubtitleAssOverride(mode: String) {
|
||||
renderer?.setSubtitleAssOverride(mode)
|
||||
}
|
||||
|
||||
// MARK: - Audio Track Controls
|
||||
|
||||
fun getAudioTracks(): List<Map<String, Any>> {
|
||||
|
||||
@@ -243,6 +243,7 @@ internal object TvRecommendationsPublisher {
|
||||
.setContentId(providerId)
|
||||
.setIntentUri(buildIntentUri(context, item.optString("playRoute").ifBlank { item.optString("route") }))
|
||||
.setWeight(weight)
|
||||
.setPosterArtAspectRatio(TvContractCompat.PreviewPrograms.ASPECT_RATIO_16_9)
|
||||
|
||||
item.optString("subtitle").takeIf { it.isNotBlank() }?.let {
|
||||
builder.setDescription(it)
|
||||
@@ -250,7 +251,6 @@ internal object TvRecommendationsPublisher {
|
||||
|
||||
imageUrl.takeIf { it.isNotBlank() }?.let {
|
||||
val imageUri = Uri.parse(it)
|
||||
builder.setPosterArtUri(imageUri)
|
||||
builder.setThumbnailUri(imageUri)
|
||||
}
|
||||
|
||||
|
||||
@@ -25,30 +25,60 @@ export interface TVDiscoveryPayload {
|
||||
sections: TVDiscoverySection[];
|
||||
}
|
||||
|
||||
function getTVDiscoveryImageUrl(
|
||||
function getTVDiscoveryImage(
|
||||
item: BaseItemDto,
|
||||
api: Api,
|
||||
): string | undefined {
|
||||
): { url: string } | undefined {
|
||||
const baseUrl = api.basePath;
|
||||
|
||||
if (item.Type === "Episode") {
|
||||
if (item.SeriesId && item.SeriesPrimaryImageTag) {
|
||||
return `${baseUrl}/Items/${item.SeriesId}/Images/Primary?quality=90&tag=${encodeURIComponent(item.SeriesPrimaryImageTag)}&width=500`;
|
||||
}
|
||||
|
||||
if (item.ParentPrimaryImageItemId && item.ParentPrimaryImageTag) {
|
||||
return `${baseUrl}/Items/${item.ParentPrimaryImageItemId}/Images/Primary?quality=90&tag=${encodeURIComponent(item.ParentPrimaryImageTag)}&width=500`;
|
||||
}
|
||||
// 1. Episode backdrop
|
||||
const episodeBackdrop = item.BackdropImageTags?.[0];
|
||||
if (item.Id && episodeBackdrop) {
|
||||
return {
|
||||
url:
|
||||
`${baseUrl}/Items/${item.Id}/Images/Backdrop/0` +
|
||||
`?fillWidth=1920` +
|
||||
`&fillHeight=1080` +
|
||||
`&quality=90` +
|
||||
`&tag=${encodeURIComponent(episodeBackdrop)}`,
|
||||
};
|
||||
}
|
||||
|
||||
// 2. Series backdrop
|
||||
if (item.SeriesId) {
|
||||
return {
|
||||
url:
|
||||
`${baseUrl}/Items/${item.SeriesId}/Images/Backdrop` +
|
||||
`?fillWidth=1920` +
|
||||
`&fillHeight=1080` +
|
||||
`&quality=90`,
|
||||
};
|
||||
}
|
||||
|
||||
// 3. Generic item backdrop
|
||||
const backdrop = item.BackdropImageTags?.[0];
|
||||
if (item.Id && backdrop) {
|
||||
return {
|
||||
url:
|
||||
`${baseUrl}/Items/${item.Id}/Images/Backdrop/0` +
|
||||
`?fillWidth=1920` +
|
||||
`&fillHeight=1080` +
|
||||
`&quality=90` +
|
||||
`&tag=${encodeURIComponent(backdrop)}`,
|
||||
};
|
||||
}
|
||||
|
||||
// 4. Last resort: crop poster into landscape
|
||||
const primaryTag = item.ImageTags?.Primary;
|
||||
if (item.Id && primaryTag) {
|
||||
return `${baseUrl}/Items/${item.Id}/Images/Primary?quality=90&tag=${encodeURIComponent(primaryTag)}&width=500`;
|
||||
}
|
||||
|
||||
const backdropTag = item.BackdropImageTags?.[0];
|
||||
if (item.Id && backdropTag) {
|
||||
return `${baseUrl}/Items/${item.Id}/Images/Backdrop/0?quality=90&tag=${encodeURIComponent(backdropTag)}&width=800`;
|
||||
return {
|
||||
url:
|
||||
`${baseUrl}/Items/${item.Id}/Images/Primary` +
|
||||
`?fillWidth=1920` +
|
||||
`&fillHeight=1080` +
|
||||
`&quality=90` +
|
||||
`&tag=${encodeURIComponent(primaryTag)}`,
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
@@ -98,15 +128,18 @@ function sectionFromItems(
|
||||
const payloadItems = (items || [])
|
||||
.filter((item) => item.Id && item.Name)
|
||||
.slice(0, TV_DISCOVERY_ITEM_LIMIT)
|
||||
.map((item) => ({
|
||||
id: item.Id!,
|
||||
itemType: item.Type || undefined,
|
||||
title: getTVDiscoveryTitle(item),
|
||||
subtitle: getTVDiscoverySubtitle(item),
|
||||
imageUrl: getTVDiscoveryImageUrl(item, api),
|
||||
route: `streamyfin://topshelf/item?id=${encodeURIComponent(item.Id!)}&type=${encodeURIComponent(item.Type || "")}`,
|
||||
playRoute: `streamyfin://topshelf/play?id=${encodeURIComponent(item.Id!)}`,
|
||||
}));
|
||||
.map((item) => {
|
||||
const image = getTVDiscoveryImage(item, api);
|
||||
return {
|
||||
id: item.Id!,
|
||||
itemType: item.Type || undefined,
|
||||
title: getTVDiscoveryTitle(item),
|
||||
subtitle: getTVDiscoverySubtitle(item),
|
||||
imageUrl: image?.url,
|
||||
route: `streamyfin://topshelf/item?id=${encodeURIComponent(item.Id!)}&type=${encodeURIComponent(item.Type || "")}`,
|
||||
playRoute: `streamyfin://topshelf/play?id=${encodeURIComponent(item.Id!)}`,
|
||||
};
|
||||
});
|
||||
|
||||
if (payloadItems.length === 0) return null;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user