mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-06-06 14:08:30 +01:00
fix: refactor buttons
This commit is contained in:
@@ -10,6 +10,7 @@ import GoogleCast, {
|
|||||||
useMediaStatus,
|
useMediaStatus,
|
||||||
useRemoteMediaClient,
|
useRemoteMediaClient,
|
||||||
} from "react-native-google-cast";
|
} from "react-native-google-cast";
|
||||||
|
import { RoundButton } from "./RoundButton";
|
||||||
|
|
||||||
interface Props extends ViewProps {
|
interface Props extends ViewProps {
|
||||||
width?: number;
|
width?: number;
|
||||||
@@ -53,51 +54,30 @@ export const Chromecast: React.FC<Props> = ({
|
|||||||
|
|
||||||
if (background === "transparent")
|
if (background === "transparent")
|
||||||
return (
|
return (
|
||||||
<>
|
<RoundButton
|
||||||
<TouchableOpacity
|
size="large"
|
||||||
onPress={() => {
|
className="mr-2"
|
||||||
if (mediaStatus?.currentItemId) CastContext.showExpandedControls();
|
background={false}
|
||||||
else CastContext.showCastDialog();
|
|
||||||
}}
|
|
||||||
className="rounded-full h-10 w-10 flex items-center justify-center b"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<Feather name="cast" size={22} color={"white"} />
|
|
||||||
</TouchableOpacity>
|
|
||||||
<AndroidCastButton />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (Platform.OS === "android")
|
|
||||||
return (
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
if (mediaStatus?.currentItemId) CastContext.showExpandedControls();
|
if (mediaStatus?.currentItemId) CastContext.showExpandedControls();
|
||||||
else CastContext.showCastDialog();
|
else CastContext.showCastDialog();
|
||||||
}}
|
}}
|
||||||
className="rounded-full h-10 w-10 flex items-center justify-center bg-neutral-800/80"
|
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<Feather name="cast" size={22} color={"white"} />
|
<Feather name="cast" size={22} color={"white"} />
|
||||||
</TouchableOpacity>
|
</RoundButton>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<RoundButton
|
||||||
|
size="large"
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
if (mediaStatus?.currentItemId) CastContext.showExpandedControls();
|
if (mediaStatus?.currentItemId) CastContext.showExpandedControls();
|
||||||
else CastContext.showCastDialog();
|
else CastContext.showCastDialog();
|
||||||
}}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<BlurView
|
<Feather name="cast" size={22} color={"white"} />
|
||||||
intensity={100}
|
</RoundButton>
|
||||||
className="rounded-full overflow-hidden h-10 aspect-square flex items-center justify-center"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<Feather name="cast" size={22} color={"white"} />
|
|
||||||
</BlurView>
|
|
||||||
<AndroidCastButton />
|
|
||||||
</TouchableOpacity>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import {
|
|||||||
import { Href, router, useFocusEffect } from "expo-router";
|
import { Href, router, useFocusEffect } from "expo-router";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import React, { useCallback, useMemo, useRef, useState } from "react";
|
import React, { useCallback, useMemo, useRef, useState } from "react";
|
||||||
import { Alert, TouchableOpacity, View, ViewProps } from "react-native";
|
import { Alert, View, ViewProps } from "react-native";
|
||||||
import { toast } from "sonner-native";
|
import { toast } from "sonner-native";
|
||||||
import { AudioTrackSelector } from "./AudioTrackSelector";
|
import { AudioTrackSelector } from "./AudioTrackSelector";
|
||||||
import { Bitrate, BitrateSelector } from "./BitrateSelector";
|
import { Bitrate, BitrateSelector } from "./BitrateSelector";
|
||||||
@@ -30,6 +30,7 @@ import { Text } from "./common/Text";
|
|||||||
import { Loader } from "./Loader";
|
import { Loader } from "./Loader";
|
||||||
import { MediaSourceSelector } from "./MediaSourceSelector";
|
import { MediaSourceSelector } from "./MediaSourceSelector";
|
||||||
import ProgressCircle from "./ProgressCircle";
|
import ProgressCircle from "./ProgressCircle";
|
||||||
|
import { RoundButton } from "./RoundButton";
|
||||||
import { SubtitleTrackSelector } from "./SubtitleTrackSelector";
|
import { SubtitleTrackSelector } from "./SubtitleTrackSelector";
|
||||||
|
|
||||||
interface DownloadProps extends ViewProps {
|
interface DownloadProps extends ViewProps {
|
||||||
@@ -38,6 +39,7 @@ interface DownloadProps extends ViewProps {
|
|||||||
DownloadedIconComponent: () => React.ReactElement;
|
DownloadedIconComponent: () => React.ReactElement;
|
||||||
title?: string;
|
title?: string;
|
||||||
subtitle?: string;
|
subtitle?: string;
|
||||||
|
size?: "default" | "large";
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DownloadItems: React.FC<DownloadProps> = ({
|
export const DownloadItems: React.FC<DownloadProps> = ({
|
||||||
@@ -46,6 +48,7 @@ export const DownloadItems: React.FC<DownloadProps> = ({
|
|||||||
DownloadedIconComponent,
|
DownloadedIconComponent,
|
||||||
title = "Download",
|
title = "Download",
|
||||||
subtitle = "",
|
subtitle = "",
|
||||||
|
size = "default",
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const [api] = useAtom(apiAtom);
|
const [api] = useAtom(apiAtom);
|
||||||
@@ -75,9 +78,6 @@ export const DownloadItems: React.FC<DownloadProps> = ({
|
|||||||
[settings]
|
[settings]
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
|
||||||
* Bottom sheet
|
|
||||||
*/
|
|
||||||
const bottomSheetModalRef = useRef<BottomSheetModal>(null);
|
const bottomSheetModalRef = useRef<BottomSheetModal>(null);
|
||||||
|
|
||||||
const handlePresentModalPress = useCallback(() => {
|
const handlePresentModalPress = useCallback(() => {
|
||||||
@@ -92,7 +92,6 @@ export const DownloadItems: React.FC<DownloadProps> = ({
|
|||||||
|
|
||||||
const itemIds = useMemo(() => items.map((i) => i.Id), [items]);
|
const itemIds = useMemo(() => items.map((i) => i.Id), [items]);
|
||||||
|
|
||||||
// Get a list of all items that are not downloaded - based on the items passed in as props
|
|
||||||
const itemsNotDownloaded = useMemo(
|
const itemsNotDownloaded = useMemo(
|
||||||
() =>
|
() =>
|
||||||
items.filter((i) => !downloadedFiles?.some((f) => f.item.Id === i.Id)),
|
items.filter((i) => !downloadedFiles?.some((f) => f.item.Id === i.Id)),
|
||||||
@@ -125,9 +124,6 @@ export const DownloadItems: React.FC<DownloadProps> = ({
|
|||||||
itemsNotDownloaded.every((p) => queue.some((q) => p.Id == q.item.Id))
|
itemsNotDownloaded.every((p) => queue.some((q) => p.Id == q.item.Id))
|
||||||
);
|
);
|
||||||
}, [queue, itemsNotDownloaded]);
|
}, [queue, itemsNotDownloaded]);
|
||||||
// endregion computed
|
|
||||||
|
|
||||||
// region helper functions
|
|
||||||
const navigateToDownloads = () => router.push("/downloads");
|
const navigateToDownloads = () => router.push("/downloads");
|
||||||
|
|
||||||
const onDownloadedPress = () => {
|
const onDownloadedPress = () => {
|
||||||
@@ -172,17 +168,12 @@ export const DownloadItems: React.FC<DownloadProps> = ({
|
|||||||
itemsNotDownloaded,
|
itemsNotDownloaded,
|
||||||
usingOptimizedServer,
|
usingOptimizedServer,
|
||||||
userCanDownload,
|
userCanDownload,
|
||||||
|
|
||||||
// Need to be reference at the time async lambda is created for initiateDownload
|
|
||||||
maxBitrate,
|
maxBitrate,
|
||||||
selectedMediaSource,
|
selectedMediaSource,
|
||||||
selectedAudioStream,
|
selectedAudioStream,
|
||||||
selectedSubtitleStream,
|
selectedSubtitleStream,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
/**
|
|
||||||
* Start download
|
|
||||||
*/
|
|
||||||
const initiateDownload = useCallback(
|
const initiateDownload = useCallback(
|
||||||
async (...items: BaseItemDto[]) => {
|
async (...items: BaseItemDto[]) => {
|
||||||
if (
|
if (
|
||||||
@@ -265,9 +256,6 @@ export const DownloadItems: React.FC<DownloadProps> = ({
|
|||||||
),
|
),
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
// endregion helper functions
|
|
||||||
|
|
||||||
// Allow to select & set settings for single download
|
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
if (!settings) return;
|
if (!settings) return;
|
||||||
@@ -275,7 +263,6 @@ export const DownloadItems: React.FC<DownloadProps> = ({
|
|||||||
const { bitrate, mediaSource, audioIndex, subtitleIndex } =
|
const { bitrate, mediaSource, audioIndex, subtitleIndex } =
|
||||||
getDefaultPlaySettings(items[0], settings);
|
getDefaultPlaySettings(items[0], settings);
|
||||||
|
|
||||||
// 4. Set states
|
|
||||||
setSelectedMediaSource(mediaSource ?? undefined);
|
setSelectedMediaSource(mediaSource ?? undefined);
|
||||||
setSelectedAudioStream(audioIndex ?? 0);
|
setSelectedAudioStream(audioIndex ?? 0);
|
||||||
setSelectedSubtitleStream(subtitleIndex ?? -1);
|
setSelectedSubtitleStream(subtitleIndex ?? -1);
|
||||||
@@ -283,40 +270,47 @@ export const DownloadItems: React.FC<DownloadProps> = ({
|
|||||||
}, [items, itemsNotDownloaded, settings])
|
}, [items, itemsNotDownloaded, settings])
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
const renderButtonContent = () => {
|
||||||
<View
|
if (processes && itemsProcesses.length > 0) {
|
||||||
className="bg-neutral-800/80 rounded-full h-9 w-9 flex items-center justify-center"
|
return progress === 0 ? (
|
||||||
{...props}
|
<Loader />
|
||||||
>
|
|
||||||
{processes && itemsProcesses.length > 0 ? (
|
|
||||||
<TouchableOpacity onPress={navigateToDownloads}>
|
|
||||||
{progress === 0 ? (
|
|
||||||
<Loader />
|
|
||||||
) : (
|
|
||||||
<View className="-rotate-45">
|
|
||||||
<ProgressCircle
|
|
||||||
size={24}
|
|
||||||
fill={progress}
|
|
||||||
width={4}
|
|
||||||
tintColor="#9334E9"
|
|
||||||
backgroundColor="#bdc3c7"
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</TouchableOpacity>
|
|
||||||
) : itemsQueued ? (
|
|
||||||
<TouchableOpacity onPress={navigateToDownloads}>
|
|
||||||
<Ionicons name="hourglass" size={24} color="white" />
|
|
||||||
</TouchableOpacity>
|
|
||||||
) : allItemsDownloaded ? (
|
|
||||||
<TouchableOpacity onPress={onDownloadedPress}>
|
|
||||||
{DownloadedIconComponent()}
|
|
||||||
</TouchableOpacity>
|
|
||||||
) : (
|
) : (
|
||||||
<TouchableOpacity onPress={handlePresentModalPress}>
|
<View className="-rotate-45">
|
||||||
{MissingDownloadIconComponent()}
|
<ProgressCircle
|
||||||
</TouchableOpacity>
|
size={24}
|
||||||
)}
|
fill={progress}
|
||||||
|
width={4}
|
||||||
|
tintColor="#9334E9"
|
||||||
|
backgroundColor="#bdc3c7"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
} else if (itemsQueued) {
|
||||||
|
return <Ionicons name="hourglass" size={24} color="white" />;
|
||||||
|
} else if (allItemsDownloaded) {
|
||||||
|
return <DownloadedIconComponent />;
|
||||||
|
} else {
|
||||||
|
return <MissingDownloadIconComponent />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onButtonPress = () => {
|
||||||
|
if (processes && itemsProcesses.length > 0) {
|
||||||
|
navigateToDownloads();
|
||||||
|
} else if (itemsQueued) {
|
||||||
|
navigateToDownloads();
|
||||||
|
} else if (allItemsDownloaded) {
|
||||||
|
onDownloadedPress();
|
||||||
|
} else {
|
||||||
|
handlePresentModalPress();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View {...props}>
|
||||||
|
<RoundButton size={size} onPress={onButtonPress}>
|
||||||
|
{renderButtonContent()}
|
||||||
|
</RoundButton>
|
||||||
<BottomSheetModal
|
<BottomSheetModal
|
||||||
ref={bottomSheetModalRef}
|
ref={bottomSheetModalRef}
|
||||||
enableDynamicSizing
|
enableDynamicSizing
|
||||||
@@ -390,11 +384,13 @@ export const DownloadItems: React.FC<DownloadProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DownloadSingleItem: React.FC<{ item: BaseItemDto }> = ({
|
export const DownloadSingleItem: React.FC<{
|
||||||
item,
|
size?: "default" | "large";
|
||||||
}) => {
|
item: BaseItemDto;
|
||||||
|
}> = ({ item, size = "default" }) => {
|
||||||
return (
|
return (
|
||||||
<DownloadItems
|
<DownloadItems
|
||||||
|
size={size}
|
||||||
title="Download Episode"
|
title="Download Episode"
|
||||||
subtitle={item.Name!}
|
subtitle={item.Name!}
|
||||||
items={[item]}
|
items={[item]}
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo(
|
|||||||
<Chromecast background="blur" width={22} height={22} />
|
<Chromecast background="blur" width={22} height={22} />
|
||||||
{item.Type !== "Program" && (
|
{item.Type !== "Program" && (
|
||||||
<View className="flex flex-row items-center space-x-2">
|
<View className="flex flex-row items-center space-x-2">
|
||||||
<DownloadSingleItem item={item} />
|
<DownloadSingleItem item={item} size="large" />
|
||||||
<PlayedStatus item={item} />
|
<PlayedStatus item={item} />
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
@@ -286,8 +286,6 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo(
|
|||||||
<SimilarItems itemId={item.Id} />
|
<SimilarItems itemId={item.Id} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<View className="h-16"></View>
|
|
||||||
</View>
|
</View>
|
||||||
</ParallaxScrollView>
|
</ParallaxScrollView>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import * as Haptics from "expo-haptics";
|
|||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { TouchableOpacity, View, ViewProps } from "react-native";
|
import { TouchableOpacity, View, ViewProps } from "react-native";
|
||||||
|
import { RoundButton } from "./RoundButton";
|
||||||
|
|
||||||
interface Props extends ViewProps {
|
interface Props extends ViewProps {
|
||||||
item: BaseItemDto;
|
item: BaseItemDto;
|
||||||
@@ -46,44 +47,35 @@ export const PlayedStatus: React.FC<Props> = ({ item, ...props }) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handlePress = async (played: boolean) => {
|
||||||
|
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||||
|
if (played) {
|
||||||
|
await markAsNotPlayed({
|
||||||
|
api: api,
|
||||||
|
itemId: item?.Id,
|
||||||
|
userId: user?.Id,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await markAsPlayed({
|
||||||
|
api: api,
|
||||||
|
item: item,
|
||||||
|
userId: user?.Id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
invalidateQueries();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View {...props}>
|
||||||
className=" bg-neutral-800/80 rounded-full h-10 w-10 flex items-center justify-center"
|
<RoundButton
|
||||||
{...props}
|
icon={
|
||||||
>
|
item.UserData?.Played
|
||||||
{item.UserData?.Played ? (
|
? "checkmark-circle"
|
||||||
<TouchableOpacity
|
: "checkmark-circle-outline"
|
||||||
onPress={async () => {
|
}
|
||||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
onPress={() => handlePress(item.UserData?.Played || false)}
|
||||||
await markAsNotPlayed({
|
size="large"
|
||||||
api: api,
|
/>
|
||||||
itemId: item?.Id,
|
|
||||||
userId: user?.Id,
|
|
||||||
});
|
|
||||||
invalidateQueries();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<View className="rounded h-10 aspect-square flex items-center justify-center">
|
|
||||||
<Ionicons name="checkmark-circle" size={24} color="white" />
|
|
||||||
</View>
|
|
||||||
</TouchableOpacity>
|
|
||||||
) : (
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={async () => {
|
|
||||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
|
||||||
await markAsPlayed({
|
|
||||||
api: api,
|
|
||||||
item: item,
|
|
||||||
userId: user?.Id,
|
|
||||||
});
|
|
||||||
invalidateQueries();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<View className="rounded h-10 aspect-square flex items-center justify-center">
|
|
||||||
<Ionicons name="checkmark-circle-outline" size={24} color="white" />
|
|
||||||
</View>
|
|
||||||
</TouchableOpacity>
|
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
81
components/RoundButton.tsx
Normal file
81
components/RoundButton.tsx
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
|
import { BlurView } from "expo-blur";
|
||||||
|
import { PropsWithChildren } from "react";
|
||||||
|
import {
|
||||||
|
Platform,
|
||||||
|
TouchableOpacity,
|
||||||
|
TouchableOpacityProps,
|
||||||
|
} from "react-native";
|
||||||
|
|
||||||
|
interface Props extends TouchableOpacityProps {
|
||||||
|
onPress: () => void;
|
||||||
|
icon?: keyof typeof Ionicons.glyphMap;
|
||||||
|
background?: boolean;
|
||||||
|
size?: "default" | "large";
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RoundButton: React.FC<PropsWithChildren<Props>> = ({
|
||||||
|
background = true,
|
||||||
|
icon,
|
||||||
|
onPress,
|
||||||
|
children,
|
||||||
|
size = "default",
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const buttonSize = size === "large" ? "h-10 w-10" : "h-9 w-9";
|
||||||
|
|
||||||
|
if (background === false)
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={onPress}
|
||||||
|
className={`rounded-full ${buttonSize} flex items-center justify-center`}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{icon ? (
|
||||||
|
<Ionicons
|
||||||
|
name={icon}
|
||||||
|
size={size === "large" ? 22 : 18}
|
||||||
|
color={"white"}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{children ? children : null}
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (Platform.OS === "android")
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={onPress}
|
||||||
|
className={`rounded-full ${buttonSize} flex items-center justify-center bg-neutral-800/80`}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{icon ? (
|
||||||
|
<Ionicons
|
||||||
|
name={icon}
|
||||||
|
size={size === "large" ? 22 : 18}
|
||||||
|
color={"white"}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{children ? children : null}
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TouchableOpacity onPress={onPress} {...props}>
|
||||||
|
<BlurView
|
||||||
|
intensity={90}
|
||||||
|
className={`rounded-full overflow-hidden ${buttonSize} flex items-center justify-center`}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{icon ? (
|
||||||
|
<Ionicons
|
||||||
|
name={icon}
|
||||||
|
size={size === "large" ? 22 : 18}
|
||||||
|
color={"white"}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{children ? children : null}
|
||||||
|
</BlurView>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
};
|
||||||
0
svenska_kyrkan.sql
Normal file
0
svenska_kyrkan.sql
Normal file
Reference in New Issue
Block a user