This commit is contained in:
Fredrik Burmester
2024-10-03 07:37:37 +02:00
parent 60981504fc
commit b21a1cd18e
15 changed files with 137 additions and 107 deletions

View File

@@ -354,6 +354,7 @@ export default function index() {
paddingLeft: insets.left,
paddingRight: insets.right,
paddingTop: 8,
paddingBottom: 8,
rowGap: 8,
}}
style={{

View File

@@ -10,6 +10,7 @@ import { useQuery } from "@tanstack/react-query";
import { Image } from "expo-image";
import { useLocalSearchParams } from "expo-router";
import { useAtom } from "jotai";
import React from "react";
import { useEffect, useMemo } from "react";
import { View } from "react-native";

BIN
bun.lockb

Binary file not shown.

View File

@@ -9,13 +9,14 @@ import React from "react";
type ContinueWatchingPosterProps = {
item: BaseItemDto;
width?: number;
useEpisodePoster?: boolean;
size?: "small" | "normal";
};
const ContinueWatchingPoster: React.FC<ContinueWatchingPosterProps> = ({
item,
useEpisodePoster = false,
size = "normal",
}) => {
const [api] = useAtom(apiAtom);
@@ -51,7 +52,12 @@ const ContinueWatchingPoster: React.FC<ContinueWatchingPosterProps> = ({
);
return (
<View className="relative w-44 aspect-video rounded-lg overflow-hidden border border-neutral-800">
<View
className={`
relative w-44 aspect-video rounded-lg overflow-hidden border border-neutral-800
${size === "small" ? "w-32" : "w-44"}
`}
>
<Image
key={item.Id}
id={item.Id}

View File

@@ -10,7 +10,7 @@ type ItemCardProps = {
export const ItemCardText: React.FC<ItemCardProps> = ({ item }) => {
return (
<View className="mt-2 flex flex-col h-12">
<View className="mt-2 flex flex-col">
{item.Type === "Episode" ? (
<>
<Text numberOfLines={2} className="">

View File

@@ -125,6 +125,7 @@ export const ItemContent: React.FC<{ id: string }> = React.memo(({ id }) => {
});
const [localItem, setLocalItem] = useState(item);
useImageColors(item);
useEffect(() => {
if (item) {
@@ -234,18 +235,6 @@ export const ItemContent: React.FC<{ id: string }> = React.memo(({ id }) => {
});
const logoUrl = useMemo(() => getLogoImageUrlById({ api, item }), [item]);
const themeImageColorSource = useMemo(() => {
if (!api || !item) return;
return getItemImage({
item,
api,
variant: "Primary",
quality: 80,
width: 300,
});
}, [api, item]);
useImageColors(themeImageColorSource?.uri);
const loading = useMemo(() => {
return Boolean(isLoading || isFetching || (logoUrl && loadingLogo));
@@ -274,7 +263,6 @@ export const ItemContent: React.FC<{ id: string }> = React.memo(({ id }) => {
<Animated.View style={[animatedStyle, { flex: 1 }]}>
{localItem && (
<ItemImage
useThemeColor
variant={
localItem.Type === "Movie" && logoUrl
? "Backdrop"
@@ -357,7 +345,7 @@ export const ItemContent: React.FC<{ id: string }> = React.memo(({ id }) => {
<SeasonEpisodesCarousel item={item} loading={loading} />
)}
<OverviewText text={item?.Overview} className="px-4 mb-4" />
<OverviewText text={item?.Overview} className="px-4 my-4" />
<CastAndCrew item={item} className="mb-4" loading={loading} />

View File

@@ -4,6 +4,7 @@ import { MoviesTitleHeader } from "./movies/MoviesTitleHeader";
import { Ratings } from "./Ratings";
import { EpisodeTitleHeader } from "./series/EpisodeTitleHeader";
import { GenreTags } from "./GenreTags";
import React from "react";
interface Props extends ViewProps {
item?: BaseItemDto | null;

View File

@@ -22,7 +22,6 @@ interface Props extends ImageProps {
| "Thumb";
quality?: number;
width?: number;
useThemeColor?: boolean;
onError?: () => void;
}
@@ -31,7 +30,6 @@ export const ItemImage: React.FC<Props> = ({
variant = "Primary",
quality = 90,
width = 1000,
useThemeColor = false,
onError,
...props
}) => {

View File

@@ -51,10 +51,24 @@ export const ScrollingCollectionList: React.FC<Props> = ({
`}
>
{[1, 2, 3].map((i) => (
<View className="w-44 mb-2">
<View className="bg-neutral-800 h-24 w-full rounded-md mb-2"></View>
<View className="bg-neutral-800 h-4 w-full rounded-md mb-2"></View>
<View className="bg-neutral-800 h-4 w-1/2 rounded-md"></View>
<View className="w-44" key={i}>
<View className="bg-neutral-900 h-24 w-full rounded-md mb-1"></View>
<View className="rounded-md overflow-hidden mb-1 self-start">
<Text
className="text-neutral-900 bg-neutral-900 rounded-md"
numberOfLines={1}
>
Nisi mollit voluptate amet.
</Text>
</View>
<View className="rounded-md overflow-hidden self-start mb-1">
<Text
className="text-neutral-900 bg-neutral-900 text-xs rounded-md "
numberOfLines={1}
>
Lorem ipsum
</Text>
</View>
</View>
))}
</View>

View File

@@ -198,11 +198,11 @@ export const SeasonPicker: React.FC<Props> = ({ item, initialSeasonIndex }) => {
key={e.Id}
className="flex flex-col mb-4"
>
<View className="flex flex-row items-center mb-2">
<View className="w-32 aspect-video overflow-hidden mr-2">
<View className="flex flex-row items-start mb-2">
<View className="mr-2">
<ContinueWatchingPoster
size="small"
item={e}
width={128}
useEpisodePoster
/>
</View>
@@ -217,7 +217,7 @@ export const SeasonPicker: React.FC<Props> = ({ item, initialSeasonIndex }) => {
{runtimeTicksToSeconds(e.RunTimeTicks)}
</Text>
</View>
<View className="self-start ml-auto">
<View className="self-start ml-auto -mt-0.5">
<DownloadItem item={e} />
</View>
</View>

View File

@@ -50,41 +50,32 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
const queryClient = useQueryClient();
const { data: optimizeServerStatistics } = useQuery({
queryKey: ["optimize-server", settings?.optimizedVersionsServerUrl],
queryFn: async () =>
getStatistics({
url: settings?.optimizedVersionsServerUrl,
authHeader: api?.accessToken,
deviceId: await getOrSetDeviceId(),
}),
refetchInterval: 1000,
staleTime: 0,
enabled:
!!settings?.optimizedVersionsServerUrl &&
settings.optimizedVersionsServerUrl.length > 0,
});
/********************
* Background task
*******************/
useEffect(() => {
checkStatusAsync();
}, []);
const checkStatusAsync = async () => {
await BackgroundFetch.getStatusAsync();
await TaskManager.isTaskRegisteredAsync(BACKGROUND_FETCH_TASK);
return await TaskManager.isTaskRegisteredAsync(BACKGROUND_FETCH_TASK);
};
useEffect(() => {
if (settings?.autoDownload) {
registerBackgroundFetchAsync();
} else {
unregisterBackgroundFetchAsync();
}
(async () => {
const registered = await checkStatusAsync();
checkStatusAsync();
if (settings?.autoDownload === true && !registered) {
registerBackgroundFetchAsync();
toast.success("Background downlodas enabled");
} else if (settings?.autoDownload === false && registered) {
unregisterBackgroundFetchAsync();
toast.info("Background downloads disabled");
} else if (settings?.autoDownload === true && registered) {
// Don't to anything
} else if (settings?.autoDownload === false && !registered) {
// Don't to anything
} else {
updateSettings({ autoDownload: false });
}
})();
}, [settings?.autoDownload]);
/**********************
*********************/
@@ -593,14 +584,6 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
<Text className="font-semibold">
Optimized versions server
</Text>
<View
className={`
w-3 h-3 rounded-full
${
optimizeServerStatistics ? "bg-green-600" : "bg-red-600"
}
`}
></View>
</View>
<Text className="text-xs opacity-50">
Set the URL for the optimized versions server for downloads.
@@ -620,8 +603,7 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
<Button
color="purple"
className="h-12 mt-2"
onPress={() => {
toast.info("Saved");
onPress={async () => {
updateSettings({
optimizedVersionsServerUrl:
optimizedVersionsServerUrl.length === 0
@@ -630,6 +612,14 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
? optimizedVersionsServerUrl
: optimizedVersionsServerUrl + "/",
});
const res = await getStatistics({
url: settings?.optimizedVersionsServerUrl,
authHeader: api?.accessToken,
deviceId: await getOrSetDeviceId(),
});
if (res) {
toast.success("Connected");
} else toast.error("Could not connect");
}}
>
Save

View File

@@ -1,46 +1,95 @@
import { itemThemeColorAtom } from "@/utils/atoms/primaryColor";
import { apiAtom } from "@/providers/JellyfinProvider";
import {
adjustToNearBlack,
calculateTextColor,
isCloseToBlack,
itemThemeColorAtom,
} from "@/utils/atoms/primaryColor";
import { getItemImage } from "@/utils/getItemImage";
import { storage } from "@/utils/mmkv";
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
import { useAtom } from "jotai";
import { useEffect } from "react";
import { useEffect, useMemo } from "react";
import { getColors } from "react-native-image-colors";
export const useImageColors = (
uri: string | undefined | null,
disabled = false
) => {
/**
* Custom hook to extract and manage image colors for a given item.
*
* @param item - The BaseItemDto object representing the item.
* @param disabled - A boolean flag to disable color extraction.
*
*/
export const useImageColors = (item?: BaseItemDto | null, disabled = false) => {
const [api] = useAtom(apiAtom);
const [, setPrimaryColor] = useAtom(itemThemeColorAtom);
const source = useMemo(() => {
if (!api || !item) return;
return getItemImage({
item,
api,
variant: "Primary",
quality: 80,
width: 300,
});
}, [api, item]);
useEffect(() => {
if (disabled) return;
if (uri) {
getColors(uri, {
if (source?.uri) {
// Check if colors are already cached in storage
const _primary = storage.getString(`${source.uri}-primary`);
const _text = storage.getString(`${source.uri}-text`);
// If colors are cached, use them and exit
if (_primary && _text) {
console.info("[useImageColors] Using cached colors for performance.");
setPrimaryColor({
primary: _primary,
text: _text,
});
return;
}
// Extract colors from the image
getColors(source.uri, {
fallback: "#fff",
cache: true,
key: uri,
key: source.uri,
})
.then((colors) => {
let primary: string = "#fff";
let average: string = "#fff";
let secondary: string = "#fff";
let text: string = "#000";
// Select the appropriate color based on the platform
if (colors.platform === "android") {
primary = colors.dominant;
average = colors.average;
secondary = colors.muted;
} else if (colors.platform === "ios") {
primary = colors.primary;
secondary = colors.secondary;
average = colors.background;
}
// Adjust the primary color if it's too close to black
if (primary && isCloseToBlack(primary)) {
primary = adjustToNearBlack(primary);
}
// Calculate the text color based on the primary color
if (primary) text = calculateTextColor(primary);
setPrimaryColor({
primary,
secondary,
average,
text,
});
// Cache the colors in storage
if (source.uri && primary) {
storage.set(`${source.uri}-primary`, primary);
storage.set(`${source.uri}-text`, text);
}
})
.catch((error) => {
console.error("Error getting colors", error);
});
}
}, [uri, setPrimaryColor, disabled]);
}, [source?.uri, setPrimaryColor, disabled]);
};

View File

@@ -71,6 +71,7 @@
"react-native-image-colors": "^2.4.0",
"react-native-ios-context-menu": "^2.5.1",
"react-native-ios-utilities": "^4.4.5",
"react-native-mmkv": "^3.0.2",
"react-native-reanimated": "~3.10.1",
"react-native-reanimated-carousel": "4.0.0-canary.15",
"react-native-safe-area-context": "4.10.5",

View File

@@ -2,12 +2,10 @@ import { atom, useAtom } from "jotai";
interface ThemeColors {
primary: string;
secondary: string;
average: string;
text: string;
}
const calculateTextColor = (backgroundColor: string): string => {
export const calculateTextColor = (backgroundColor: string): string => {
// Convert hex to RGB
const r = parseInt(backgroundColor.slice(1, 3), 16);
const g = parseInt(backgroundColor.slice(3, 5), 16);
@@ -48,7 +46,7 @@ const calculateRelativeLuminance = (rgb: number[]): number => {
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
};
const isCloseToBlack = (color: string): boolean => {
export const isCloseToBlack = (color: string): boolean => {
const r = parseInt(color.slice(1, 3), 16);
const g = parseInt(color.slice(3, 5), 16);
const b = parseInt(color.slice(5, 7), 16);
@@ -57,33 +55,13 @@ const isCloseToBlack = (color: string): boolean => {
return r < 20 && g < 20 && b < 20;
};
const adjustToNearBlack = (color: string): string => {
export const adjustToNearBlack = (color: string): string => {
return "#212121"; // A very dark gray, almost black
};
const baseThemeColorAtom = atom<ThemeColors>({
export const itemThemeColorAtom = atom<ThemeColors>({
primary: "#FFFFFF",
secondary: "#000000",
average: "#888888",
text: "#000000",
});
export const itemThemeColorAtom = atom(
(get) => get(baseThemeColorAtom),
(get, set, update: Partial<ThemeColors>) => {
const currentColors = get(baseThemeColorAtom);
let newColors = { ...currentColors, ...update };
// Adjust primary color if it's too close to black
if (newColors.primary && isCloseToBlack(newColors.primary)) {
newColors.primary = adjustToNearBlack(newColors.primary);
}
// Recalculate text color if primary color changes
if (update.primary) newColors.text = calculateTextColor(newColors.primary);
set(baseThemeColorAtom, newColors);
}
);
export const useItemThemeColor = () => useAtom(itemThemeColorAtom);

3
utils/mmkv.ts Normal file
View File

@@ -0,0 +1,3 @@
import { MMKV } from "react-native-mmkv";
export const storage = new MMKV();