mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-01-15 15:48:05 +00:00
fix: refactor buttons
This commit is contained in:
@@ -10,6 +10,7 @@ import GoogleCast, {
|
||||
useMediaStatus,
|
||||
useRemoteMediaClient,
|
||||
} from "react-native-google-cast";
|
||||
import { RoundButton } from "./RoundButton";
|
||||
|
||||
interface Props extends ViewProps {
|
||||
width?: number;
|
||||
@@ -53,51 +54,30 @@ export const Chromecast: React.FC<Props> = ({
|
||||
|
||||
if (background === "transparent")
|
||||
return (
|
||||
<>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
if (mediaStatus?.currentItemId) CastContext.showExpandedControls();
|
||||
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
|
||||
<RoundButton
|
||||
size="large"
|
||||
className="mr-2"
|
||||
background={false}
|
||||
onPress={() => {
|
||||
if (mediaStatus?.currentItemId) CastContext.showExpandedControls();
|
||||
else CastContext.showCastDialog();
|
||||
}}
|
||||
className="rounded-full h-10 w-10 flex items-center justify-center bg-neutral-800/80"
|
||||
{...props}
|
||||
>
|
||||
<Feather name="cast" size={22} color={"white"} />
|
||||
</TouchableOpacity>
|
||||
</RoundButton>
|
||||
);
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
<RoundButton
|
||||
size="large"
|
||||
onPress={() => {
|
||||
if (mediaStatus?.currentItemId) CastContext.showExpandedControls();
|
||||
else CastContext.showCastDialog();
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<BlurView
|
||||
intensity={100}
|
||||
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>
|
||||
<Feather name="cast" size={22} color={"white"} />
|
||||
</RoundButton>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
import { Href, router, useFocusEffect } from "expo-router";
|
||||
import { useAtom } from "jotai";
|
||||
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 { AudioTrackSelector } from "./AudioTrackSelector";
|
||||
import { Bitrate, BitrateSelector } from "./BitrateSelector";
|
||||
@@ -30,6 +30,7 @@ import { Text } from "./common/Text";
|
||||
import { Loader } from "./Loader";
|
||||
import { MediaSourceSelector } from "./MediaSourceSelector";
|
||||
import ProgressCircle from "./ProgressCircle";
|
||||
import { RoundButton } from "./RoundButton";
|
||||
import { SubtitleTrackSelector } from "./SubtitleTrackSelector";
|
||||
|
||||
interface DownloadProps extends ViewProps {
|
||||
@@ -38,6 +39,7 @@ interface DownloadProps extends ViewProps {
|
||||
DownloadedIconComponent: () => React.ReactElement;
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
size?: "default" | "large";
|
||||
}
|
||||
|
||||
export const DownloadItems: React.FC<DownloadProps> = ({
|
||||
@@ -46,6 +48,7 @@ export const DownloadItems: React.FC<DownloadProps> = ({
|
||||
DownloadedIconComponent,
|
||||
title = "Download",
|
||||
subtitle = "",
|
||||
size = "default",
|
||||
...props
|
||||
}) => {
|
||||
const [api] = useAtom(apiAtom);
|
||||
@@ -75,9 +78,6 @@ export const DownloadItems: React.FC<DownloadProps> = ({
|
||||
[settings]
|
||||
);
|
||||
|
||||
/**
|
||||
* Bottom sheet
|
||||
*/
|
||||
const bottomSheetModalRef = useRef<BottomSheetModal>(null);
|
||||
|
||||
const handlePresentModalPress = useCallback(() => {
|
||||
@@ -92,7 +92,6 @@ export const DownloadItems: React.FC<DownloadProps> = ({
|
||||
|
||||
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(
|
||||
() =>
|
||||
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))
|
||||
);
|
||||
}, [queue, itemsNotDownloaded]);
|
||||
// endregion computed
|
||||
|
||||
// region helper functions
|
||||
const navigateToDownloads = () => router.push("/downloads");
|
||||
|
||||
const onDownloadedPress = () => {
|
||||
@@ -172,17 +168,12 @@ export const DownloadItems: React.FC<DownloadProps> = ({
|
||||
itemsNotDownloaded,
|
||||
usingOptimizedServer,
|
||||
userCanDownload,
|
||||
|
||||
// Need to be reference at the time async lambda is created for initiateDownload
|
||||
maxBitrate,
|
||||
selectedMediaSource,
|
||||
selectedAudioStream,
|
||||
selectedSubtitleStream,
|
||||
]);
|
||||
|
||||
/**
|
||||
* Start download
|
||||
*/
|
||||
const initiateDownload = useCallback(
|
||||
async (...items: BaseItemDto[]) => {
|
||||
if (
|
||||
@@ -265,9 +256,6 @@ export const DownloadItems: React.FC<DownloadProps> = ({
|
||||
),
|
||||
[]
|
||||
);
|
||||
// endregion helper functions
|
||||
|
||||
// Allow to select & set settings for single download
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
if (!settings) return;
|
||||
@@ -275,7 +263,6 @@ export const DownloadItems: React.FC<DownloadProps> = ({
|
||||
const { bitrate, mediaSource, audioIndex, subtitleIndex } =
|
||||
getDefaultPlaySettings(items[0], settings);
|
||||
|
||||
// 4. Set states
|
||||
setSelectedMediaSource(mediaSource ?? undefined);
|
||||
setSelectedAudioStream(audioIndex ?? 0);
|
||||
setSelectedSubtitleStream(subtitleIndex ?? -1);
|
||||
@@ -283,40 +270,47 @@ export const DownloadItems: React.FC<DownloadProps> = ({
|
||||
}, [items, itemsNotDownloaded, settings])
|
||||
);
|
||||
|
||||
return (
|
||||
<View
|
||||
className="bg-neutral-800/80 rounded-full h-9 w-9 flex items-center justify-center"
|
||||
{...props}
|
||||
>
|
||||
{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>
|
||||
const renderButtonContent = () => {
|
||||
if (processes && itemsProcesses.length > 0) {
|
||||
return progress === 0 ? (
|
||||
<Loader />
|
||||
) : (
|
||||
<TouchableOpacity onPress={handlePresentModalPress}>
|
||||
{MissingDownloadIconComponent()}
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
<View className="-rotate-45">
|
||||
<ProgressCircle
|
||||
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
|
||||
ref={bottomSheetModalRef}
|
||||
enableDynamicSizing
|
||||
@@ -390,11 +384,13 @@ export const DownloadItems: React.FC<DownloadProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export const DownloadSingleItem: React.FC<{ item: BaseItemDto }> = ({
|
||||
item,
|
||||
}) => {
|
||||
export const DownloadSingleItem: React.FC<{
|
||||
size?: "default" | "large";
|
||||
item: BaseItemDto;
|
||||
}> = ({ item, size = "default" }) => {
|
||||
return (
|
||||
<DownloadItems
|
||||
size={size}
|
||||
title="Download Episode"
|
||||
subtitle={item.Name!}
|
||||
items={[item]}
|
||||
|
||||
@@ -90,7 +90,7 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo(
|
||||
<Chromecast background="blur" width={22} height={22} />
|
||||
{item.Type !== "Program" && (
|
||||
<View className="flex flex-row items-center space-x-2">
|
||||
<DownloadSingleItem item={item} />
|
||||
<DownloadSingleItem item={item} size="large" />
|
||||
<PlayedStatus item={item} />
|
||||
</View>
|
||||
)}
|
||||
@@ -286,8 +286,6 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo(
|
||||
<SimilarItems itemId={item.Id} />
|
||||
</>
|
||||
)}
|
||||
|
||||
<View className="h-16"></View>
|
||||
</View>
|
||||
</ParallaxScrollView>
|
||||
</View>
|
||||
|
||||
@@ -8,6 +8,7 @@ import * as Haptics from "expo-haptics";
|
||||
import { useAtom } from "jotai";
|
||||
import React from "react";
|
||||
import { TouchableOpacity, View, ViewProps } from "react-native";
|
||||
import { RoundButton } from "./RoundButton";
|
||||
|
||||
interface Props extends ViewProps {
|
||||
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 (
|
||||
<View
|
||||
className=" bg-neutral-800/80 rounded-full h-10 w-10 flex items-center justify-center"
|
||||
{...props}
|
||||
>
|
||||
{item.UserData?.Played ? (
|
||||
<TouchableOpacity
|
||||
onPress={async () => {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
await markAsNotPlayed({
|
||||
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 {...props}>
|
||||
<RoundButton
|
||||
icon={
|
||||
item.UserData?.Played
|
||||
? "checkmark-circle"
|
||||
: "checkmark-circle-outline"
|
||||
}
|
||||
onPress={() => handlePress(item.UserData?.Played || false)}
|
||||
size="large"
|
||||
/>
|
||||
</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