mirror of
https://github.com/Jellify-Music/App.git
synced 2026-01-30 06:58:48 -06:00
Merge pull request #154 from anultravioletaurora/59-improve-onboarding-experience
This commit is contained in:
@@ -120,7 +120,6 @@ export default class Client {
|
||||
}
|
||||
|
||||
private removeCredentials() {
|
||||
this.library = undefined;
|
||||
this.library = undefined;
|
||||
this.server = undefined;
|
||||
this.user = undefined;
|
||||
|
||||
@@ -22,7 +22,7 @@ export default function Artists({
|
||||
|
||||
QueryKeys.RecentlyPlayedArtists ? useQuery({
|
||||
queryKey: [QueryKeys.RecentlyPlayedArtists],
|
||||
queryFn: () => fetchRecentlyPlayedArtists()
|
||||
queryFn: () => fetchRecentlyPlayedArtists(20)
|
||||
}) :
|
||||
|
||||
useQuery({
|
||||
|
||||
@@ -7,6 +7,4 @@ export const cardDimensions = {
|
||||
width: 150,
|
||||
height: 150
|
||||
}
|
||||
}
|
||||
|
||||
export const horizontalCardLimit = 20
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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)}
|
||||
|
||||
18
components/Settings/categories.ts
Normal file
18
components/Settings/categories.ts
Normal 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;
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
7
components/Settings/screens/playback-details.tsx
Normal file
7
components/Settings/screens/playback-details.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import { View } from "tamagui";
|
||||
|
||||
export default function PlaybackDetails(): React.JSX.Element {
|
||||
return (
|
||||
<View />
|
||||
)
|
||||
}
|
||||
27
components/Settings/screens/server-details.tsx
Normal file
27
components/Settings/screens/server-details.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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: {
|
||||
|
||||
@@ -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={
|
||||
|
||||
@@ -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
12
components/types.d.ts
vendored
@@ -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>];
|
||||
@@ -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!
|
||||
}
|
||||
}
|
||||
});
|
||||
2
index.js
2
index.js
@@ -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);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user