Item Card Fields

This commit is contained in:
Simon Caron
2025-01-04 14:49:56 -05:00
parent ed993d07ce
commit 894305e126
17 changed files with 92 additions and 27 deletions

View File

@@ -3,6 +3,7 @@ import { useMemo } from "react";
import { TouchableOpacity, View } from "react-native";
import * as DropdownMenu from "zeego/dropdown-menu";
import { Text } from "./common/Text";
import { useTranslation } from "react-i18next";
interface Props extends React.ComponentProps<typeof View> {
source?: MediaSourceInfo;
@@ -26,6 +27,8 @@ export const AudioTrackSelector: React.FC<Props> = ({
[audioStreams, selected]
);
const { t } = useTranslation();
return (
<View
className="flex shrink"
@@ -36,7 +39,7 @@ export const AudioTrackSelector: React.FC<Props> = ({
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<View className="flex flex-col" {...props}>
<Text className="opacity-50 mb-1 text-xs">Audio</Text>
<Text className="opacity-50 mb-1 text-xs">{t("series.audio")}</Text>
<TouchableOpacity className="bg-neutral-900 h-10 rounded-xl border-neutral-800 border px-3 py-2 flex flex-row items-center justify-between">
<Text className="" numberOfLines={1}>
{selectedAudioSteam?.DisplayTitle}

View File

@@ -2,6 +2,7 @@ import { TouchableOpacity, View } from "react-native";
import * as DropdownMenu from "zeego/dropdown-menu";
import { Text } from "./common/Text";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
export type Bitrate = {
key: string;
@@ -59,6 +60,8 @@ export const BitrateSelector: React.FC<Props> = ({
);
}, []);
const { t } = useTranslation();
return (
<View
className="flex shrink"
@@ -70,7 +73,7 @@ export const BitrateSelector: React.FC<Props> = ({
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<View className="flex flex-col" {...props}>
<Text className="opacity-50 mb-1 text-xs">Quality</Text>
<Text className="opacity-50 mb-1 text-xs">{t("series.quality")}</Text>
<TouchableOpacity className="bg-neutral-900 h-10 rounded-xl border-neutral-800 border px-3 py-2 flex flex-row items-center justify-between">
<Text style={{}} className="" numberOfLines={1}>
{BITRATES.find((b) => b.value === selected?.value)?.key}

View File

@@ -15,6 +15,7 @@ import {
BottomSheetScrollView,
} from "@gorhom/bottom-sheet";
import { Button } from "./Button";
import { useTranslation } from "react-i18next";
interface Props {
source?: MediaSourceInfo;
@@ -22,15 +23,16 @@ interface Props {
export const ItemTechnicalDetails: React.FC<Props> = ({ source, ...props }) => {
const bottomSheetModalRef = useRef<BottomSheetModal>(null);
const { t } = useTranslation();
return (
<View className="px-4 mt-2 mb-4">
<Text className="text-lg font-bold mb-4">Video</Text>
<Text className="text-lg font-bold mb-4">{t("series.video")}</Text>
<TouchableOpacity onPress={() => bottomSheetModalRef.current?.present()}>
<View className="flex flex-row space-x-2">
<VideoStreamInfo source={source} />
</View>
<Text className="text-purple-600">More details</Text>
<Text className="text-purple-600">{t("series.more_details")}</Text>
</TouchableOpacity>
<BottomSheetModal
ref={bottomSheetModalRef}
@@ -52,14 +54,14 @@ export const ItemTechnicalDetails: React.FC<Props> = ({ source, ...props }) => {
<BottomSheetScrollView>
<View className="flex flex-col space-y-2 p-4 mb-4">
<View className="">
<Text className="text-lg font-bold mb-4">Video</Text>
<Text className="text-lg font-bold mb-4">{t("series.video")}</Text>
<View className="flex flex-row space-x-2">
<VideoStreamInfo source={source} />
</View>
</View>
<View className="">
<Text className="text-lg font-bold mb-2">Audio</Text>
<Text className="text-lg font-bold mb-2">{t("series.audio")}</Text>
<AudioStreamInfo
audioStreams={
source?.MediaStreams?.filter(
@@ -70,7 +72,7 @@ export const ItemTechnicalDetails: React.FC<Props> = ({ source, ...props }) => {
</View>
<View className="">
<Text className="text-lg font-bold mb-2">Subtitles</Text>
<Text className="text-lg font-bold mb-2">{t("series.subtitles")}</Text>
<SubtitleStreamInfo
subtitleStreams={
source?.MediaStreams?.filter(

View File

@@ -11,6 +11,7 @@ import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { useQuery } from "@tanstack/react-query";
import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData";
import { useTranslation } from "react-i18next";
interface Props extends ViewProps {
actorId: string;
@@ -24,6 +25,7 @@ export const MoreMoviesWithActor: React.FC<Props> = ({
}) => {
const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom);
const { t } = useTranslation();
const { data: actor } = useQuery({
queryKey: ["actor", actorId],
@@ -76,7 +78,7 @@ export const MoreMoviesWithActor: React.FC<Props> = ({
return (
<View {...props}>
<Text className="text-lg font-bold mb-2 px-4">
More with {actor?.Name}
{t("series.more_with", {name: actor?.Name})}
</Text>
<HorizontalScroll
data={items}

View File

@@ -2,6 +2,7 @@ import { TouchableOpacity, View, ViewProps } from "react-native";
import { Text } from "@/components/common/Text";
import { tc } from "@/utils/textTools";
import { useState } from "react";
import { useTranslation } from "react-i18next";
interface Props extends ViewProps {
text?: string | null;
@@ -14,12 +15,13 @@ export const OverviewText: React.FC<Props> = ({
...props
}) => {
const [limit, setLimit] = useState(characterLimit);
const { t } = useTranslation();
if (!text) return null;
return (
<View className="flex flex-col" {...props}>
<Text className="text-lg font-bold mb-2">Overview</Text>
<Text className="text-lg font-bold mb-2">{t("series.overview")}</Text>
<TouchableOpacity
onPress={() =>
setLimit((prev) =>

View File

@@ -12,6 +12,7 @@ import { ItemCardText } from "./ItemCardText";
import { Loader } from "./Loader";
import { HorizontalScroll } from "./common/HorrizontalScroll";
import { TouchableItemRouter } from "./common/TouchableItemRouter";
import { useTranslation } from "react-i18next";
interface SimilarItemsProps extends ViewProps {
itemId?: string | null;
@@ -23,6 +24,7 @@ export const SimilarItems: React.FC<SimilarItemsProps> = ({
}) => {
const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom);
const { t } = useTranslation();
const { data: similarItems, isLoading } = useQuery<BaseItemDto[]>({
queryKey: ["similarItems", itemId],
@@ -47,12 +49,12 @@ export const SimilarItems: React.FC<SimilarItemsProps> = ({
return (
<View {...props}>
<Text className="px-4 text-lg font-bold mb-2">Similar items</Text>
<Text className="px-4 text-lg font-bold mb-2">{t("series.similar_items")}</Text>
<HorizontalScroll
data={movies}
loading={isLoading}
height={247}
noItemsText="No similar items found"
noItemsText={t("series.no_similar_items_found")}
renderItem={(item: BaseItemDto, idx: number) => (
<TouchableItemRouter
key={idx}

View File

@@ -5,6 +5,7 @@ import { Platform, TouchableOpacity, View } from "react-native";
import * as DropdownMenu from "zeego/dropdown-menu";
import { Text } from "./common/Text";
import { SubtitleHelper } from "@/utils/SubtitleHelper";
import { useTranslation } from "react-i18next";
interface Props extends React.ComponentProps<typeof View> {
source?: MediaSourceInfo;
@@ -37,6 +38,8 @@ export const SubtitleTrackSelector: React.FC<Props> = ({
if (subtitleStreams.length === 0) return null;
const { t } = useTranslation();
return (
<View
className="flex col shrink justify-start place-self-start items-start"
@@ -48,7 +51,7 @@ export const SubtitleTrackSelector: React.FC<Props> = ({
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<View className="flex flex-col " {...props}>
<Text className="opacity-50 mb-1 text-xs">Subtitle</Text>
<Text className="opacity-50 mb-1 text-xs">{t("series.subtitles")}</Text>
<TouchableOpacity className="bg-neutral-900 h-10 rounded-xl border-neutral-800 border px-3 py-2 flex flex-row items-center justify-between">
<Text className=" ">
{selectedSubtitleSteam

View File

@@ -12,6 +12,7 @@ import { HorizontalScroll } from "../common/HorrizontalScroll";
import { Text } from "../common/Text";
import Poster from "../posters/Poster";
import { itemRouter } from "../common/TouchableItemRouter";
import { useTranslation } from "react-i18next";
interface Props extends ViewProps {
item?: BaseItemDto | null;
@@ -21,6 +22,7 @@ interface Props extends ViewProps {
export const CastAndCrew: React.FC<Props> = ({ item, loading, ...props }) => {
const [api] = useAtom(apiAtom);
const segments = useSegments();
const { t } = useTranslation();
const from = segments[2];
const destinctPeople = useMemo(() => {
@@ -40,7 +42,7 @@ export const CastAndCrew: React.FC<Props> = ({ item, loading, ...props }) => {
return (
<View {...props} className="flex flex-col">
<Text className="text-lg font-bold mb-2 px-4">Cast & Crew</Text>
<Text className="text-lg font-bold mb-2 px-4">{t("series.cast_and_crew")}</Text>
<HorizontalScroll
loading={loading}
keyExtractor={(i, idx) => i.Id.toString()}

View File

@@ -8,6 +8,7 @@ import Poster from "../posters/Poster";
import { HorizontalScroll } from "../common/HorrizontalScroll";
import { Text } from "../common/Text";
import { getPrimaryImageUrlById } from "@/utils/jellyfin/image/getPrimaryImageUrlById";
import { useTranslation } from "react-i18next";
interface Props extends ViewProps {
item?: BaseItemDto | null;
@@ -15,10 +16,11 @@ interface Props extends ViewProps {
export const CurrentSeries: React.FC<Props> = ({ item, ...props }) => {
const [api] = useAtom(apiAtom);
const { t } = useTranslation();
return (
<View {...props}>
<Text className="text-lg font-bold mb-2 px-4">Series</Text>
<Text className="text-lg font-bold mb-2 px-4">{t("series.series")}</Text>
<HorizontalScroll
data={[item]}
height={247}

View File

@@ -190,7 +190,7 @@ const JellyseerrSeasons: React.FC<{
return (
<View>
<View className="flex flex-row justify-between items-end px-4">
<Text className="text-lg font-bold mb-2">Seasons</Text>
<Text className="text-lg font-bold mb-2">{t("series.seasons")}</Text>
{!allSeasonsAvailable && (
<RoundButton className="mb-2 pa-2" onPress={promptRequestAll}>
<Ionicons name="bag-add" color="white" size={26} />
@@ -210,7 +210,7 @@ const JellyseerrSeasons: React.FC<{
)}
ListHeaderComponent={() => (
<View className="flex flex-row justify-between items-end px-4">
<Text className="text-lg font-bold mb-2">Seasons</Text>
<Text className="text-lg font-bold mb-2">{t("series.seasons")}</Text>
{!allSeasonsAvailable && (
<RoundButton className="mb-2 pa-2" onPress={promptRequestAll}>
<Ionicons name="bag-add" color="white" size={26} />

View File

@@ -12,10 +12,12 @@ import ContinueWatchingPoster from "../ContinueWatchingPoster";
import { ItemCardText } from "../ItemCardText";
import { TouchableItemRouter } from "../common/TouchableItemRouter";
import { FlashList } from "@shopify/flash-list";
import { useTranslation } from "react-i18next";
export const NextUp: React.FC<{ seriesId: string }> = ({ seriesId }) => {
const [user] = useAtom(userAtom);
const [api] = useAtom(apiAtom);
const { t } = useTranslation();
const { data: items } = useQuery({
queryKey: ["nextUp", seriesId],
@@ -37,14 +39,14 @@ export const NextUp: React.FC<{ seriesId: string }> = ({ seriesId }) => {
if (!items?.length)
return (
<View className="px-4">
<Text className="text-lg font-bold mb-2">Next up</Text>
<Text className="opacity-50">No items to display</Text>
<Text className="text-lg font-bold mb-2">{t("series.next_up")}</Text>
<Text className="opacity-50">{t("series.no_items_to_display")}</Text>
</View>
);
return (
<View>
<Text className="text-lg font-bold px-4 mb-2">Next up</Text>
<Text className="text-lg font-bold px-4 mb-2">{t("series.next_up")}</Text>
<FlashList
contentContainerStyle={{ paddingLeft: 16 }}
horizontal

View File

@@ -3,6 +3,7 @@ import { useEffect, useMemo } from "react";
import { TouchableOpacity, View } from "react-native";
import * as DropdownMenu from "zeego/dropdown-menu";
import { Text } from "../common/Text";
import { t } from "i18next";
type Props = {
item: BaseItemDto;
@@ -91,7 +92,7 @@ export const SeasonDropdown: React.FC<Props> = ({
<DropdownMenu.Trigger>
<View className="flex flex-row">
<TouchableOpacity className="bg-neutral-900 rounded-2xl border-neutral-900 border px-3 py-2 flex flex-row items-center justify-between">
<Text>Season {seasonIndex}</Text>
<Text>{t("series.season")} {seasonIndex}</Text>
</TouchableOpacity>
</View>
</DropdownMenu.Trigger>
@@ -104,7 +105,7 @@ export const SeasonDropdown: React.FC<Props> = ({
collisionPadding={8}
sideOffset={8}
>
<DropdownMenu.Label>Seasons</DropdownMenu.Label>
<DropdownMenu.Label>{t("series.seasons")}</DropdownMenu.Label>
{seasons?.sort(sortByIndex).map((season: any) => (
<DropdownMenu.Item
key={season[keys.title]}

View File

@@ -17,7 +17,7 @@ import {
SeasonIndexState,
} from "@/components/series/SeasonDropdown";
import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
import { useTranslation } from "react-i18next";
type Props = {
item: BaseItemDto;
initialSeasonIndex?: number;
@@ -29,6 +29,7 @@ export const SeasonPicker: React.FC<Props> = ({ item, initialSeasonIndex }) => {
const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom);
const [seasonIndexState, setSeasonIndexState] = useAtom(seasonIndexAtom);
const { t } = useTranslation();
const seasonIndex = useMemo(
() => seasonIndexState[item.Id ?? ""],
@@ -210,7 +211,7 @@ export const SeasonPicker: React.FC<Props> = ({ item, initialSeasonIndex }) => {
{(episodes?.length || 0) === 0 ? (
<View className="flex flex-col">
<Text className="text-neutral-500">
No episodes for this season
{t("series.no_episodes_for_this_seasonz")}
</Text>
</View>
) : null}

View File

@@ -68,7 +68,7 @@ export const AudioToggles: React.FC<Props> = ({ ...props }) => {
});
}}
>
<DropdownMenu.ItemTitle>None</DropdownMenu.ItemTitle>
<DropdownMenu.ItemTitle>t("home.settings.audio.none")</DropdownMenu.ItemTitle>
</DropdownMenu.Item>
{cultures?.map((l) => (
<DropdownMenu.Item

View File

@@ -69,7 +69,7 @@ export const SubtitleToggles: React.FC<Props> = ({ ...props }) => {
});
}}
>
<DropdownMenu.ItemTitle>None</DropdownMenu.ItemTitle>
<DropdownMenu.ItemTitle>{t("home.settings.subtitles.none")}</DropdownMenu.ItemTitle>
</DropdownMenu.Item>
{cultures?.map((l) => (
<DropdownMenu.Item

View File

@@ -63,7 +63,8 @@
"audio_title": "Audio",
"set_audio_track": "Set Audio Track From Previous Item",
"audio_language": "Audio language",
"audio_hint": "Choose a default audio language."
"audio_hint": "Choose a default audio language.",
"none": "None"
},
"subtitles": {
"subtitle_title": "Subtitles",
@@ -71,7 +72,8 @@
"subtitle_mode": "Subtitle Mode",
"set_subtitle_track": "Set Subtitle Track From Previous Item",
"subtitle_size": "Subtitle Size",
"subtitle_hint": "Configure subtitle preference."
"subtitle_hint": "Configure subtitle preference.",
"none": "None"
},
"other": {
"other_title": "Other",
@@ -255,6 +257,25 @@
"message_from_server": "Message from server: {{message}}",
"video_has_finished_playing": "Video has finished playing!"
},
"series": {
"next_up": "Next up",
"no_items_to_display": "No items to display",
"cast_and_crew": "Cast & Crew",
"series": "Series",
"seasons": "Seasons",
"season": "Season",
"episodes": "Episodes",
"no_episodes_for_this_season": "No episodes for this season",
"overview": "Overview",
"more_with": "More with {{name}}",
"similar_items": "Similar items",
"no_similar_items_found": "No similar items found",
"video": "Video",
"more_details": "More details",
"quality": "Quality",
"audio": "Audio",
"subtitles": "Subtitle"
},
"jellyseerr":{
"confirm": "Confirm",
"cancel": "Cancel",

View File

@@ -255,6 +255,25 @@
"message_from_server": "Message du serveur: {{message}}",
"video_has_finished_playing": "La vidéo a fini de jouer!"
},
"series": {
"next_up": "À suivre",
"no_items_to_display": "Aucun item à afficher",
"cast_and_crew": "Distribution et équipe",
"series": "Séries",
"seasons": "Saisons",
"season": "Saison",
"episodes": "Épisodes",
"no_episodes_for_this_season": "Aucun épisode pour cette saison",
"overview": "Aperçu",
"more_with": "Plus avec {{name}}",
"similar_items": "Items similaires",
"no_similar_items_found": "Aucun item similaire trouvé",
"video": "Vidéo",
"more_details": "Plus de détails",
"quality": "Qualité",
"audio": "Audio",
"subtitles": "Sous-titres"
},
"jellyseerr":{
"confirm": "Confirmer",
"cancel": "Annuler",