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

Initial Improvements
This commit is contained in:
Violet Caulfield
2025-02-13 17:36:46 -06:00
committed by GitHub
16 changed files with 925 additions and 809 deletions
@@ -0,0 +1,9 @@
import { ToastViewport } from '@tamagui/toast'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
export default function SafeToastViewport() : React.JSX.Element {
const { left, top, right } = useSafeAreaInsets()
return (
<ToastViewport flexDirection="column-reverse" top={top} left={left} right={right} />
)
}
+28
View File
@@ -0,0 +1,28 @@
import {Toast as TamaguiToast, useToastState} from "@tamagui/toast"
import { YStack } from "tamagui"
export default function Toast() : React.JSX.Element | null {
const currentToast = useToastState()
if (!currentToast || currentToast.isHandledNatively) return null
return (
<TamaguiToast
key={currentToast.id}
duration={currentToast.duration}
enterStyle={{ opacity: 0, scale: 0.5, y: -25 }}
exitStyle={{ opacity: 0, scale: 1, y: -20 }}
y={0}
opacity={1}
scale={1}
animation="200ms"
viewportName={currentToast.viewportName}
>
<YStack>
<TamaguiToast.Title>{currentToast.title}</TamaguiToast.Title>
{!!currentToast.message && (
<TamaguiToast.Description>{currentToast.message}</TamaguiToast.Description>
)}
</YStack>
</TamaguiToast>
)
}
+22 -5
View File
@@ -1,15 +1,32 @@
import React from 'react';
import { Input as TamaguiInput, InputProps as TamaguiInputProps} from 'tamagui';
import { Input as TamaguiInput, InputProps as TamaguiInputProps, XStack, YStack} from 'tamagui';
interface InputProps extends TamaguiInputProps {
prependElement?: React.JSX.Element | undefined;
}
export default function Input(props: InputProps): React.JSX.Element {
return (
<TamaguiInput
{...props}
clearButtonMode="always"
/>
<XStack>
{ props.prependElement && (
<YStack
flex={1}
alignItems='center'
justifyContent='center'
>
{ props.prependElement }
</YStack>
)}
<TamaguiInput
flex={props.prependElement ? 8 : 1}
{...props}
clearButtonMode="always"
/>
</XStack>
)
}
+17 -17
View File
@@ -1,14 +1,14 @@
import _ from "lodash"
import ServerAuthentication from "./helpers/server-authentication";
import ServerAddress from "./helpers/server-address";
import _, { isUndefined } from "lodash"
import ServerAuthentication from "./screens/server-authentication";
import ServerAddress from "./screens/server-address";
import { createStackNavigator } from "@react-navigation/stack";
import ServerLibrary from "./helpers/server-library";
import ServerLibrary from "./screens/server-library";
import { useAuthenticationContext } from "./provider";
import { useEffect } from "react";
export default function Login(): React.JSX.Element {
const { user, server, triggerAuth, setTriggerAuth } = useAuthenticationContext();
const { user, server, setTriggerAuth } = useAuthenticationContext();
const Stack = createStackNavigator();
@@ -17,40 +17,40 @@ export default function Login(): React.JSX.Element {
});
return (
<Stack.Navigator screenOptions={{ headerShown: false }}>
{
(_.isUndefined(server)) ? (
<Stack.Navigator
initialRouteName={
isUndefined(server)
? "ServerAddress"
: isUndefined(user)
? "ServerAuthentication"
: "LibrarySelection"
}
screenOptions={{ headerShown: false }}
>
<Stack.Screen
name="ServerAddress"
options={{
headerShown: false,
animationTypeForReplace: triggerAuth ? 'push' : 'pop'
}}
component={ServerAddress}
/>
) : (
(_.isUndefined(user)) ? (
<Stack.Screen
name="ServerAuthentication"
options={{
headerShown: false,
animationTypeForReplace: 'push'
}}
initialParams={{ server }}
//@ts-ignore
component={ServerAuthentication}
/>
) : (
<Stack.Screen
name="LibrarySelection"
options={{
headerShown: false,
animationTypeForReplace: 'push'
}}
component={ServerLibrary}
/>
)
)
}
</Stack.Navigator>
);
}
@@ -4,7 +4,7 @@ import { useMutation } from "@tanstack/react-query";
import { JellifyServer } from "../../../types/JellifyServer";
import { Input, Spacer, Spinner, XStack, ZStack } from "tamagui";
import { SwitchWithLabel } from "../../Global/helpers/switch-with-label";
import { H1 } from "../../Global/helpers/text";
import { H2 } from "../../Global/helpers/text";
import Button from "../../Global/helpers/button";
import { http, https } from "../utils/constants";
import { JellyfinInfo } from "../../../api/info";
@@ -13,8 +13,20 @@ import { getSystemApi } from "@jellyfin/sdk/lib/utils/api/system-api";
import { SafeAreaView } from "react-native-safe-area-context";
import Client from "../../../api/client";
import { useAuthenticationContext } from "../provider";
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
import { StackParamList } from "../../../components/types";
export default function ServerAddress(): React.JSX.Element {
import * as Burnt from "burnt";
export default function ServerAddress({
navigation
}: {
navigation: NativeStackNavigationProp<StackParamList>
}): React.JSX.Element {
navigation.setOptions({
animationTypeForReplace: 'push'
})
const [useHttps, setUseHttps] = useState<boolean>(true);
const [serverAddress, setServerAddress] = useState<string | undefined>(undefined);
@@ -48,18 +60,26 @@ export default function ServerAddress(): React.JSX.Element {
Client.setPublicApiClient(server);
setServer(server);
navigation.navigate("ServerAuthentication", { server });
},
onError: async (error: Error) => {
console.error("An error occurred connecting to the Jellyfin instance", error);
Client.signOut();
setServer(undefined);
Burnt.toast({
title: "Unable to connect",
preset: "error",
// message: `Unable to connect to Jellyfin at ${useHttps ? https : http}${serverAddress}`,
});
}
});
return (
<SafeAreaView>
<H1>Connect to Jellyfin</H1>
<XStack>
<H2 marginVertical={"$7"} marginHorizontal={"$2"}>Connect to Jellyfin</H2>
<XStack marginBottom={"$3"}>
<SwitchWithLabel
checked={useHttps}
onCheckedChange={(checked) => setUseHttps(checked)}
@@ -76,6 +96,7 @@ export default function ServerAddress(): React.JSX.Element {
autoCapitalize="none"
autoCorrect={false}
flexGrow={1}
placeholder="jellyfin.org"
/>
</XStack>
@@ -2,20 +2,30 @@ import React, { useState } from "react";
import { useMutation } from "@tanstack/react-query";
import _ from "lodash";
import { JellyfinCredentials } from "../../../api/types/jellyfin-credentials";
import { Input, Spinner, YStack, ZStack } from "tamagui";
import { getToken, Spacer, Spinner, YStack, ZStack } from "tamagui";
import { useAuthenticationContext } from "../provider";
import { H1 } from "../../Global/helpers/text";
import { H2 } from "../../Global/helpers/text";
import Button from "../../Global/helpers/button";
import { SafeAreaView } from "react-native-safe-area-context";
import Client from "../../../api/client";
import { JellifyUser } from "../../../types/JellifyUser";
import { ServerAuthenticationProps } from "../../../components/types";
import Input from "../../../components/Global/helpers/input";
import Icon from "../../../components/Global/helpers/icon";
import { useToastController } from "@tamagui/toast";
import Toast from "../../../components/Global/components/toast";
export default function ServerAuthentication(): React.JSX.Element {
export default function ServerAuthentication({
route,
navigation,
}: ServerAuthenticationProps): React.JSX.Element {
const toast = useToastController()
const [username, setUsername] = useState<string | undefined>(undefined);
const [password, setPassword] = React.useState<string | undefined>(undefined);
const { setUser, server, setServer } = useAuthenticationContext();
const { setUser, setServer } = useAuthenticationContext();
const useApiMutation = useMutation({
mutationFn: async (credentials: JellyfinCredentials) => {
@@ -42,42 +52,53 @@ export default function ServerAuthentication(): React.JSX.Element {
}
Client.setUser(user);
return setUser(user);
setUser(user);
navigation.navigate("LibrarySelection", { user });
},
onError: async (error: Error) => {
console.error("An error occurred connecting to the Jellyfin instance", error);
toast.show("Sign in failed", {
});
return Promise.reject(`An error occured signing into ${Client.server!.name}`);
}
});
return (
<SafeAreaView>
<H1>
{ `Sign in to ${server?.name ?? "Jellyfin"}`}
</H1>
<H2 marginHorizontal={"$2"} marginVertical={"$7"}>
{ `Sign in to ${route.params.server.name}`}
</H2>
<Button onPress={() => {
Client.switchServer()
setServer(undefined);
navigation.push("ServerAddress");
}}>
Switch Server
</Button>
<YStack>
<YStack marginHorizontal={"$2"} alignContent="space-between">
<Input
prependElement={(<Icon small name="human-greeting-variant" color={getToken("$color.amethyst")} />)}
placeholder="Username"
value={username}
onChangeText={(value : string | undefined) => setUsername(value)}
autoCapitalize="none"
autoCorrect={false}
/>
/>
<Spacer />
<Input
prependElement={(<Icon small name="lock-outline" color={getToken("$color.amethyst")} />)}
placeholder="Password"
value={password}
onChangeText={(value : string | undefined) => setPassword(value)}
autoCapitalize="none"
autoCorrect={false}
secureTextEntry
/>
/>
</YStack>
<ZStack>
@@ -90,7 +111,7 @@ export default function ServerAuthentication(): React.JSX.Element {
onPress={() => {
if (!_.isUndefined(username)) {
console.log(`Signing in to ${server!.name}`);
console.log(`Signing in...`);
useApiMutation.mutate({ username, password });
}
}}
@@ -98,6 +119,7 @@ export default function ServerAuthentication(): React.JSX.Element {
Sign in
</Button>
</ZStack>
<Toast />
</SafeAreaView>
);
}
@@ -1,13 +1,13 @@
import React, { useEffect, useState } from "react";
import { Spinner, ToggleGroup } from "tamagui";
import { useAuthenticationContext } from "../provider";
import { H1, Label, Text } from "../../Global/helpers/text";
import { H1, H2, Label, Text } from "../../Global/helpers/text";
import Button from "../../Global/helpers/button";
import _ from "lodash";
import { useUserViews } from "../../../api/queries/libraries";
import { SafeAreaView } from "react-native-safe-area-context";
import Client from "../../../api/client";
import { useJellifyContext } from "../../../components/provider";
import { useJellifyContext } from "../../provider";
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
export default function ServerLibrary(): React.JSX.Element {
@@ -31,7 +31,7 @@ export default function ServerLibrary(): React.JSX.Element {
return (
<SafeAreaView>
<H1>Select Music Library</H1>
<H2>Select Music Library</H2>
{ isPending ? (
<Spinner size="large" />
+1 -1
View File
@@ -26,7 +26,7 @@ export default function Tracks({ navigation }: { navigation: NativeStackNavigati
showArtwork
track={track}
tracklist={tracks?.slice(index, index + 50) ?? []}
queueName="Favorite Tracks"
queue="Queue"
/>
)
+5 -3
View File
@@ -10,13 +10,14 @@ import { PlayerProvider } from "../player/provider";
import { useColorScheme } from "react-native";
import { PortalProvider } from "@tamagui/portal";
import { JellifyProvider, useJellifyContext } from "./provider";
import { ToastProvider } from "@tamagui/toast";
import { ToastProvider, ToastViewport } from "@tamagui/toast";
import SafeToastViewport from "./Global/components/toast-area-view-port";
export default function Jellify(): React.JSX.Element {
return (
<PortalProvider shouldAddRootHost>
<ToastProvider>
<ToastProvider burntOptions={{ from: 'top'}}>
<JellifyProvider>
<App />
</JellifyProvider>
@@ -38,10 +39,11 @@ function App(): React.JSX.Element {
<Navigation />
</PlayerProvider>
) : (
<JellyfinAuthenticationProvider>
<JellyfinAuthenticationProvider>
<Login />
</JellyfinAuthenticationProvider>
)}
<SafeToastViewport />
</SafeAreaProvider>
</NavigationContainer>
)
+16 -2
View File
@@ -1,9 +1,19 @@
import { QueryKeys } from "../../enums/query-keys";
import { QueryKeys } from "../enums/query-keys";
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { NativeStackScreenProps } from "@react-navigation/native-stack";
import { JellifyServer } from "../types/JellifyServer";
import { JellifyUser } from "../types/JellifyUser";
export type StackParamList = {
ServerAddress: undefined;
ServerAuthentication: {
server: JellifyServer
}
LibrarySelection: {
user: JellifyUser
}
Home: undefined;
AddPlaylist: undefined;
RecentArtists: {
@@ -59,6 +69,10 @@ export type StackParamList = {
}
}
export type ServerAddressProps = NativeStackScreenProps<StackParamList, "ServerAddress">;
export type ServerAuthenticationProps = NativeStackScreenProps<StackParamList, "ServerAuthentication">;
export type LibrarySelectionProps = NativeStackScreenProps<StackParamList, "LibrarySelection">;
export type TabProps = NativeStackScreenProps<StackParamList, 'Tabs'>;
export type PlayerProps = NativeStackScreenProps<StackParamList, 'Player'>;
+5 -2
View File
@@ -3,8 +3,11 @@ import { QueryClient } from "@tanstack/react-query";
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
gcTime: (1000 * 60 * 10), // 10 minutes
staleTime: (1000 * 60 * 10) // 10 minutes
//@ts-ignore
cacheTime: (1000 * 60 * 60) * 24, // 1 day
gcTime: (1000 * 60 * 5), // 5 minutes,
staleTime: (1000 * 60 * 5), // 5 minutes,
}
}
});
+1 -1
View File
@@ -58,7 +58,7 @@ platform :ios do
beta_app_description: "A music app for Jellyfin",
expire_previous_builds: true,
distribute_external: true,
changelog: "General Functionality, User Experience",
changelog: "General Functionality, User Experience, updated Sign in",
groups: [
"Selfhosters"
]
+751 -751
View File
File diff suppressed because it is too large Load Diff
+6 -6
View File
@@ -25,11 +25,11 @@
"@react-navigation/native": "^7.0.14",
"@react-navigation/native-stack": "^7.1.1",
"@react-navigation/stack": "^7.1.0",
"@tamagui/config": "^1.124.13",
"@tamagui/toast": "^1.124.13",
"@tanstack/query-sync-storage-persister": "^5.62.0",
"@tanstack/react-query": "^5.52.1",
"@tanstack/react-query-persist-client": "^5.62.0",
"@tamagui/config": "^1.124.17",
"@tamagui/toast": "^1.124.17",
"@tanstack/query-sync-storage-persister": "^5.66.0",
"@tanstack/react-query": "^5.66.0",
"@tanstack/react-query-persist-client": "^5.66.0",
"axios": "^1.7.9",
"burnt": "^0.12.2",
"invert-color": "^2.0.0",
@@ -54,7 +54,7 @@
"react-native-url-polyfill": "^2.0.0",
"react-native-uuid": "^2.0.3",
"react-native-vector-icons": "^10.2.0",
"tamagui": "^1.124.13",
"tamagui": "^1.124.17",
"expo": "^52.0.0"
},
"devDependencies": {
+1 -1
View File
@@ -30,7 +30,7 @@ const jellifyConfig = createTamagui({
dark: {
shadowColor: tokens.color.purple,
background: tokens.color.purpleDark,
// backgroundActive: tokens.color.amethyst,
backgroundActive: tokens.color.amethyst,
backgroundPress: tokens.color.amethyst,
backgroundFocus: tokens.color.amethyst,
backgroundHover: tokens.color.purpleGray,