mirror of
https://github.com/Jellify-Music/App.git
synced 2026-05-13 06:48:43 -05:00
Merge pull request #124 from anultravioletaurora/59-improve-onboarding-experience
Initial Improvements
This commit is contained in:
@@ -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} />
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
+25
-4
@@ -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>
|
||||
|
||||
+35
-13
@@ -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>
|
||||
);
|
||||
}
|
||||
+3
-3
@@ -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" />
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
Vendored
+16
-2
@@ -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'>;
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -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"
|
||||
]
|
||||
|
||||
Generated
+751
-751
File diff suppressed because it is too large
Load Diff
+6
-6
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user