Merge pull request #154 from anultravioletaurora/59-improve-onboarding-experience

This commit is contained in:
Violet Caulfield
2025-02-20 08:23:42 -06:00
committed by GitHub
23 changed files with 154 additions and 122 deletions

View File

@@ -120,7 +120,6 @@ export default class Client {
}
private removeCredentials() {
this.library = undefined;
this.library = undefined;
this.server = undefined;
this.user = undefined;

View File

@@ -22,7 +22,7 @@ export default function Artists({
QueryKeys.RecentlyPlayedArtists ? useQuery({
queryKey: [QueryKeys.RecentlyPlayedArtists],
queryFn: () => fetchRecentlyPlayedArtists()
queryFn: () => fetchRecentlyPlayedArtists(20)
}) :
useQuery({

View File

@@ -7,6 +7,4 @@ export const cardDimensions = {
width: 150,
height: 150
}
}
export const horizontalCardLimit = 20
}

View File

@@ -23,8 +23,6 @@ export default function BlurhashedImage({
borderRadius
} : BlurhashLoadingProps) : React.JSX.Element {
console.debug(`Rendering image`);
const { data: image, isSuccess } = useQuery({
queryKey: [
QueryKeys.ItemImage,
@@ -34,6 +32,7 @@ export default function BlurhashedImage({
Math.ceil(height ?? width / 100) * 100 // So these keys need to match
],
queryFn: () => fetchItemImage(item.AlbumId ? item.AlbumId : item.Id!, type ?? ImageType.Primary, width, height ?? width),
staleTime: (1000 * 60 * 60) * 4 // 4 hours
});
const blurhash = !isEmpty(item.ImageBlurHashes)

View File

@@ -2,7 +2,6 @@ import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models/base-item
import React from "react";
import { FlatList, FlatListProps, ListRenderItem } from "react-native";
import IconCard from "../helpers/icon-card";
import { horizontalCardLimit } from "../component.config";
interface HorizontalCardListProps extends FlatListProps<BaseItemDto> {
squared?: boolean | undefined;
@@ -21,7 +20,6 @@ interface HorizontalCardListProps extends FlatListProps<BaseItemDto> {
* @returns
*/
export default function HorizontalCardList({
cutoff = horizontalCardLimit,
onSeeMore,
squared = false,
...props
@@ -30,7 +28,7 @@ export default function HorizontalCardList({
return (
<FlatList
horizontal
data={(props.data as Array<BaseItemDto> | undefined)?.slice(0, cutoff - 1) ?? undefined}
data={props.data}
renderItem={props.renderItem}
ListFooterComponent={() => {
return props.data ? (
@@ -47,6 +45,9 @@ export default function HorizontalCardList({
) : undefined}
}
removeClippedSubviews
style={{
overflow: "hidden"
}}
/>
)
}

View File

@@ -1,6 +1,5 @@
import { SizeTokens, XStack, Separator, Switch, ColorTokens, Theme } from "tamagui";
import { SizeTokens, XStack, Separator, Switch, Theme, styled, getToken } from "tamagui";
import { Label } from "./text";
import { Colors } from "react-native/Libraries/NewAppScreen";
interface SwitchWithLabelProps {
onCheckedChange: (value: boolean) => void,
@@ -8,9 +7,13 @@ interface SwitchWithLabelProps {
checked: boolean;
label: string;
width?: number | undefined;
backgroundColor?: ColorTokens;
}
const JellifySliderThumb = styled(Switch.Thumb, {
borderColor: getToken("$color.amethyst"),
backgroundColor: getToken("$color.purpleDark")
})
export function SwitchWithLabel(props: SwitchWithLabelProps) {
const id = `switch-${props.size.toString().slice(1)}-${props.checked ?? ''}}`
return (
@@ -22,18 +25,17 @@ export function SwitchWithLabel(props: SwitchWithLabelProps) {
>
{props.label}
</Label>
<Theme name={"inverted_purple"}>
<Separator minHeight={20} vertical />
<Switch
id={id}
size={props.size}
checked={props.checked}
onCheckedChange={(checked: boolean) => props.onCheckedChange(checked)}
backgroundColor={props.backgroundColor ?? Colors.Primary}
>
<Switch.Thumb animation="bouncy" />
backgroundColor={props.checked ? getToken("$color.telemagenta") : getToken("$color.purpleGray")}
borderColor={getToken("$color.purpleDark")}
>
<JellifySliderThumb animation="bouncy" />
</Switch>
</Theme>
</XStack>
)
}

View File

@@ -3,6 +3,7 @@ import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { useQuery } from "@tanstack/react-query";
import { QueryKeys } from "../../enums/query-keys";
import { fetchRecentlyPlayed, fetchRecentlyPlayedArtists } from "../../api/queries/functions/recents";
import { queryClient } from "../../constants/query-client";
interface HomeContext {
refreshing: boolean;
@@ -25,6 +26,14 @@ const HomeContextInitializer = () => {
const onRefresh = async () => {
setRefreshing(true);
queryClient.invalidateQueries({
queryKey: [QueryKeys.RecentlyPlayedArtists, 20]
});
queryClient.invalidateQueries({
queryKey: [QueryKeys.RecentlyPlayed, 20]
});
await Promise.all([
refetchRecentTracks(),

View File

@@ -1,5 +1,5 @@
import { FlatList } from "react-native";
import { SafeAreaView, useSafeAreaFrame } from "react-native-safe-area-context";
import { useSafeAreaFrame } from "react-native-safe-area-context";
import Categories from "./categories";
import IconCard from "../../components/Global/helpers/icon-card";
import { StackParamList } from "../../components/types";
@@ -17,23 +17,21 @@ export default function Library({
const { width } = useSafeAreaFrame();
return (
<SafeAreaView style={{ flex: 1 }} edges={["top", "right", "left"]}>
<FlatList
contentInsetAdjustmentBehavior="automatic"
data={Categories}
numColumns={2}
renderItem={({ index, item }) =>
<IconCard
name={item.iconName}
caption={item.name}
width={width / 2.1}
onPress={() => {
navigation.navigate(item.name, item.params)
}}
largeIcon
/>
}
/>
</SafeAreaView>
<FlatList
contentInsetAdjustmentBehavior="automatic"
data={Categories}
numColumns={2}
renderItem={({ index, item }) =>
<IconCard
name={item.iconName}
caption={item.name}
width={width / 2.1}
onPress={() => {
navigation.navigate(item.name, item.params)
}}
largeIcon
/>
}
/>
)
}

View File

@@ -79,7 +79,7 @@ export default function ServerAddress({
return (
<SafeAreaView>
<H2 marginVertical={"$7"} marginHorizontal={"$2"}>Connect to Jellyfin</H2>
<XStack marginBottom={"$3"}>
<XStack marginHorizontal={"$2"} marginBottom={"$3"}>
<SwitchWithLabel
checked={useHttps}
onCheckedChange={(checked) => setUseHttps(checked)}

View File

@@ -0,0 +1,18 @@
import { QueryKeys } from "../../enums/query-keys";
interface CategoryRoute {
name: any; // ¯\_(ツ)_/¯
iconName: string;
params?: {
query: QueryKeys
};
};
const Categories : CategoryRoute[] = [
{ name: "Account", iconName: "account-key-outline" },
{ name: "Server", iconName: "server-network" },
{ name: "Playback", iconName: "disc-player" },
{ name: "Labs", iconName: "flask-outline" },
];
export default Categories;

View File

@@ -1,12 +1,12 @@
import React from "react";
import { SafeAreaView } from "react-native";
import { ListItem, ScrollView, Separator, YGroup } from "tamagui";
import { ScrollView } from "tamagui";
import SignOut from "./helpers/sign-out";
import ServerDetails from "./helpers/server-details";
import LibraryDetails from "./helpers/library-details";
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
import { StackParamList } from "../types";
import { useSafeAreaFrame } from "react-native-safe-area-context";
import { FlatList } from "react-native";
import IconCard from "../Global/helpers/icon-card";
import Categories from "./categories";
export default function Root({
navigation
@@ -17,43 +17,23 @@ export default function Root({
const { width } = useSafeAreaFrame();
return (
<ScrollView
<FlatList
contentInsetAdjustmentBehavior="automatic"
removeClippedSubviews
>
<YGroup
alignSelf="center"
bordered
width={width / 1.5}
>
<YGroup.Item>
<ListItem
hoverTheme
pressTheme
title="Account Details"
subTitle="Everything is about you, man"
data={Categories}
numColumns={2}
renderItem={({ index, item }) =>
<IconCard
name={item.iconName}
caption={item.name}
width={width / 2.1}
onPress={() => {
navigation.navigate("AccountDetails")
navigation.navigate(item.name, item.params)
}}
largeIcon
/>
</YGroup.Item>
<YGroup.Item>
<ListItem
hoverTheme
pressTheme
title="Developer Tools"
subTitle="Nerds rule!"
onPress={() => {
navigation.navigate("DevTools");
}}
/>
</YGroup.Item>
</YGroup>
}
ListFooterComponent={(<SignOut />)}
/>
<ServerDetails />
<Separator marginVertical={15} />
<LibraryDetails />
<SignOut />
</ScrollView>
)
}

View File

@@ -1,27 +0,0 @@
import React from "react";
import { XStack, YStack } from "tamagui";
import Icon from "../../Global/helpers/icon";
import { H5, Text } from "../../../components/Global/helpers/text";
import Client from "../../../api/client";
export default function ServerDetails() : React.JSX.Element {
return (
<YStack>
<YStack>
<H5>Access Token</H5>
<XStack>
<Icon name="hand-coin-outline" />
<Text>{Client.api!.accessToken}</Text>
</XStack>
</YStack>
<YStack>
<H5>Jellyfin Server</H5>
<XStack>
<Icon name="server-network" />
<Text>{Client.api!.basePath}</Text>
</XStack>
</YStack>
</YStack>
)
}

View File

@@ -1,8 +1,8 @@
import React from "react";
import Button from "../../Global/helpers/button";
import { stop } from "react-native-track-player/lib/src/trackPlayer";
import Client from "../../../api/client";
import { useJellifyContext } from "../../../components/provider";
import TrackPlayer from "react-native-track-player";
export default function SignOut(): React.JSX.Element {
@@ -10,9 +10,9 @@ export default function SignOut(): React.JSX.Element {
return (
<Button onPress={() => {
stop();
setLoggedIn(false);
Client.signOut();
TrackPlayer.reset();
}}>
Sign Out
</Button>

View File

@@ -1,8 +1,8 @@
import { StackParamList } from "../../../components/types"
import { StackParamList } from "../../types"
import { NativeStackNavigationProp } from "@react-navigation/native-stack"
import DevTools from "../helpers/dev-tools"
export default function DevToolsScreen({
export default function Labs({
navigation
} : {
navigation: NativeStackNavigationProp<StackParamList>

View File

@@ -0,0 +1,7 @@
import { View } from "tamagui";
export default function PlaybackDetails(): React.JSX.Element {
return (
<View />
)
}

View File

@@ -0,0 +1,27 @@
import React from "react";
import Client from "../../../api/client";
import { Text } from "react-native";
import { YStack, XStack } from "tamagui";
import { H5 } from "../../../components/Global/helpers/text";
import Icon from "../../../components/Global/helpers/icon";
export default function ServerDetails() : React.JSX.Element {
return (
<YStack>
{ Client.api && (
<YStack>
<H5>Access Token</H5>
<XStack>
<Icon name="hand-coin-outline" />
<Text>{Client.api!.accessToken}</Text>
</XStack>
<H5>Jellyfin Server</H5>
<XStack>
<Icon name="server-network" />
<Text>{Client.api!.basePath}</Text>
</XStack>
</YStack>
)}
</YStack>
)
}

View File

@@ -2,9 +2,11 @@ import React from "react";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import Root from "./component";
import AccountDetails from "./screens/account-details";
import DevToolsScreen from "./screens/dev-tools";
import Labs from "./screens/labs";
import DetailsScreen from "../ItemDetail/screen";
import { StackParamList } from "../types";
import PlaybackDetails from "./screens/playback-details";
import ServerDetails from "./screens/server-details";
export const SettingsStack = createNativeStackNavigator<StackParamList>();
@@ -24,7 +26,7 @@ export default function Settings(): React.JSX.Element {
/>
<SettingsStack.Screen
name="AccountDetails"
name="Account"
component={AccountDetails}
options={{
title: "Account",
@@ -36,8 +38,18 @@ export default function Settings(): React.JSX.Element {
/>
<SettingsStack.Screen
name="DevTools"
component={DevToolsScreen}
name="Server"
component={ServerDetails}
/>
<SettingsStack.Screen
name="Playback"
component={PlaybackDetails}
/>
<SettingsStack.Screen
name="Labs"
component={Labs}
options={{
headerLargeTitle: true,
headerLargeTitleStyle: {

View File

@@ -6,6 +6,7 @@ import { QueryKeys } from "../../enums/query-keys";
import { fetchRecentlyPlayed } from "../../api/queries/functions/recents";
import { fetchFavoriteTracks } from "../../api/queries/functions/favorites";
import { useQuery } from "@tanstack/react-query";
import { Separator } from "tamagui";
export default function TracksScreen({
route,
@@ -14,13 +15,14 @@ export default function TracksScreen({
const { data: tracks, refetch, isPending } = useQuery({
queryKey: [route.params.query],
queryFn: () => route.params.query === QueryKeys.RecentlyPlayed
? fetchRecentlyPlayed()
? fetchRecentlyPlayed(20)
: fetchFavoriteTracks()
});
return (
<FlatList
contentInsetAdjustmentBehavior="automatic"
ItemSeparatorComponent={() => <Separator />}
numColumns={1}
data={tracks}
refreshControl={

View File

@@ -6,15 +6,17 @@ import Library from "./Library/stack";
import Settings from "./Settings/stack";
import { Discover } from "./Discover/stack";
import { Miniplayer } from "./Player/mini-player";
import { getTokens, Separator } from "tamagui";
import { getToken, getTokens, Separator } from "tamagui";
import { usePlayerContext } from "../player/provider";
import SearchStack from "./Search/stack";
import LibraryStack from "./Library/stack";
import { useColorScheme } from "react-native";
const Tab = createBottomTabNavigator();
export function Tabs() : React.JSX.Element {
const isDarkMode = useColorScheme() === 'dark'
const { nowPlaying } = usePlayerContext();
return (
@@ -23,7 +25,7 @@ export function Tabs() : React.JSX.Element {
screenOptions={{
animation: 'shift',
tabBarActiveTintColor: getTokens().color.telemagenta.val,
tabBarInactiveTintColor: getTokens().color.amethyst.val
tabBarInactiveTintColor: isDarkMode ? getToken("$color.amethyst") : getToken("$color.purpleGray")
}}
tabBar={(props) => (
<>

12
components/types.d.ts vendored
View File

@@ -47,8 +47,10 @@ export type StackParamList = {
Search: undefined;
Settings: undefined;
AccountDetails: undefined;
DevTools: undefined;
Account: undefined;
Server: undefined;
Playback: undefined;
Labs: undefined;
Tabs: {
screen: string;
@@ -113,8 +115,10 @@ export type GenresProps = NativeStackScreenProps<StackParamList, "Genres">;
export type DetailsProps = NativeStackScreenProps<StackParamList, "Details">;
export type AccountDetailsProps = NativeStackScreenProps<StackParamList, "AccountDetails">;
export type AccountDetailsProps = NativeStackScreenProps<StackParamList, "Account">;
export type ServerDetailsProps = NativeStackScreenProps<StackParamList, "Server">;
export type PlaybackDetailsProps = NativeStackScreenProps<StackParamList, "Playback">;
export type LabsProps = NativeStackScreenProps<StackParamList, 'Labs'>;
export type DevToolsProps = NativeStackScreenProps<StackParamList, 'DevTools'>;
export type useState<T> = [T, React.Dispatch<T>];

View File

@@ -4,7 +4,7 @@ export const queryClient = new QueryClient({
defaultOptions: {
queries: {
gcTime: Infinity, // disable
staleTime: (1000 * 60) * 60 // 1 hour, users can manually refresh stuff too!
staleTime: (1000 * 60 * 60) * 1 // 1 hour, users can manually refresh stuff too!
}
}
});

View File

@@ -11,7 +11,7 @@ import { enableFreeze, enableScreens } from "react-native-screens";
Client.instance;
// Enable React Navigation freeze for detaching inactive screens
enableFreeze();
// enableFreeze();
AppRegistry.registerComponent(appName, () => App);
AppRegistry.registerComponent('RNCarPlayScene', () => App);

View File

@@ -44,13 +44,14 @@ const jellifyConfig = createTamagui({
},
light: {
background: tokens.color.white,
backgroundActive: tokens.color.amethyst,
borderColor: tokens.color.purpleGray,
color: tokens.color.purpleDark
},
light_inverted_purple: {
color: tokens.color.amethyst,
borderColor: tokens.color.purpleGray,
background: tokens.color.amethyst
color: tokens.color.purpleDark,
borderColor: tokens.color.purpleDark,
background: tokens.color.purpleGray
}
}
});