diff --git a/app/components/CoverArt.tsx b/app/components/CoverArt.tsx index 7d9a624..cca919b 100644 --- a/app/components/CoverArt.tsx +++ b/app/components/CoverArt.tsx @@ -1,6 +1,5 @@ -import { artistInfoAtomFamily, useCoverArtUri } from '@app/state/music' +import { useArtistInfo, useCoverArtUri } from '@app/hooks/music' import colors from '@app/styles/colors' -import { useAtomValue } from 'jotai/utils' import React, { useEffect, useState } from 'react' import { ActivityIndicator, StyleSheet, View, ViewStyle } from 'react-native' import FastImage, { ImageStyle } from 'react-native-fast-image' @@ -32,9 +31,23 @@ type CoverArtImageProps = BaseImageProps & CoverArtProp type CoverArtProps = BaseProps & CoverArtProp & Partial -const ArtistIdImageLoaded = React.memo( +const ArtistImageFallback: React.FC<{ + enableLoading: () => void +}> = ({ enableLoading }) => { + useEffect(() => { + enableLoading() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + return <> +} + +const ArtistImage = React.memo( ({ artistId, imageSize, style, imageStyle, resizeMode, enableLoading, disableLoading, fallbackError }) => { - const artistInfo = useAtomValue(artistInfoAtomFamily(artistId)) + const artistInfo = useArtistInfo(artistId) + + if (!artistInfo) { + return + } const uri = imageSize === 'thumbnail' ? artistInfo?.smallImageUrl : artistInfo?.largeImageUrl @@ -51,24 +64,6 @@ const ArtistIdImageLoaded = React.memo( }, ) -const ArtistIdImageFallback: React.FC<{ - enableLoading: () => void -}> = ({ enableLoading }) => { - useEffect(() => { - enableLoading() - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) - return <> -} - -const ArtistIdImage = React.memo(props => { - return ( - }> - - - ) -}) - const CoverArtImage = React.memo( ({ coverArt, imageSize, style, imageStyle, resizeMode, enableLoading, disableLoading, fallbackError }) => { const coverArtUri = useCoverArtUri() @@ -108,7 +103,7 @@ const CoverArt: React.FC = ({ coverArt, artistId, resizeMode, ima let ImageComponent if (artistId) { ImageComponent = ( - }>(({ songs, typeName, queueName, style }) => { const [downloaded, setDownloaded] = useState(false) const setQueue = useSetQueue() diff --git a/app/hooks/music.ts b/app/hooks/music.ts new file mode 100644 index 0000000..919d1b8 --- /dev/null +++ b/app/hooks/music.ts @@ -0,0 +1,228 @@ +import { + mapAlbumID3toAlbumListItem, + mapArtistID3toArtist, + mapChildToSong, + mapPlaylistListItem, +} from '@app/models/music' +import { + albumListAtom, + albumListUpdatingAtom, + artistsAtom, + artistsUpdatingAtom, + homeListsUpdatingAtom, + homeListsWriteAtom, + playlistsAtom, + playlistsUpdatingAtom, + searchResultsAtom, + searchResultsUpdatingAtom, +} from '@app/state/music' +import { selectSettings } from '@app/state/settings' +import { Store, useStore } from '@app/state/store' +import { SubsonicApiClient } from '@app/subsonic/api' +import { GetAlbumList2Type, GetCoverArtParams } from '@app/subsonic/params' +import { useAtom } from 'jotai' +import { useUpdateAtom } from 'jotai/utils' +import { useCallback } from 'react' + +const selectors = { + fetchArtistInfo: (state: Store) => state.fetchArtistInfo, + fetchAlbum: (state: Store) => state.fetchAlbum, + fetchPlaylist: (state: Store) => state.fetchPlaylist, +} + +export const useArtistInfo = (id: string) => { + const server = useStore(selectSettings.activeServer) + const artistInfo = useStore(useCallback((state: Store) => state.artistInfo[id], [id])) + const fetchArtistInfo = useStore(selectors.fetchArtistInfo) + + if (server && !artistInfo) { + fetchArtistInfo(server, id) + } + + return artistInfo +} + +export const useAlbumWithSongs = (id: string) => { + const server = useStore(selectSettings.activeServer) + const album = useStore(useCallback((state: Store) => state.albums[id], [id])) + const fetchAlbum = useStore(selectors.fetchAlbum) + + if (server && !album) { + fetchAlbum(server, id) + } + + return album +} + +export const usePlaylistWithSongs = (id: string) => { + const server = useStore(selectSettings.activeServer) + const playlist = useStore(useCallback((state: Store) => state.playlists[id], [id])) + const fetchPlaylist = useStore(selectors.fetchPlaylist) + + if (server && !playlist) { + fetchPlaylist(server, id) + } + + return playlist +} + +export const useUpdateArtists = () => { + const server = useStore(selectSettings.activeServer) + const [updating, setUpdating] = useAtom(artistsUpdatingAtom) + const setArtists = useUpdateAtom(artistsAtom) + + if (!server) { + return () => Promise.resolve() + } + + return async () => { + if (updating) { + return + } + setUpdating(true) + + const client = new SubsonicApiClient(server) + + try { + const response = await client.getArtists() + setArtists(response.data.artists.map(mapArtistID3toArtist)) + } finally { + setUpdating(false) + } + } +} + +export const useUpdateHomeLists = () => { + const server = useStore(selectSettings.activeServer) + const types = useStore(selectSettings.homeLists) + const updateHomeList = useUpdateAtom(homeListsWriteAtom) + const [updating, setUpdating] = useAtom(homeListsUpdatingAtom) + + if (!server) { + return async () => {} + } + + return async () => { + if (updating) { + return + } + setUpdating(true) + + const client = new SubsonicApiClient(server) + + try { + const promises: Promise[] = [] + for (const type of types) { + promises.push( + client.getAlbumList2({ type: type as GetAlbumList2Type, size: 20 }).then(response => { + updateHomeList({ type, albums: response.data.albums.map(mapAlbumID3toAlbumListItem) }) + }), + ) + } + await Promise.all(promises) + } finally { + setUpdating(false) + } + } +} + +export const useUpdateSearchResults = () => { + const server = useStore(selectSettings.activeServer) + const updateList = useUpdateAtom(searchResultsAtom) + const [updating, setUpdating] = useAtom(searchResultsUpdatingAtom) + + if (!server) { + return async () => {} + } + + return async (query: string) => { + if (updating || query.length < 2) { + return + } + setUpdating(true) + + const client = new SubsonicApiClient(server) + + try { + const response = await client.search3({ query }) + updateList({ + artists: response.data.artists.map(mapArtistID3toArtist), + albums: response.data.albums.map(mapAlbumID3toAlbumListItem), + songs: response.data.songs.map(a => mapChildToSong(a, client)), + }) + } finally { + setUpdating(false) + } + } +} + +export const useUpdatePlaylists = () => { + const server = useStore(selectSettings.activeServer) + const updateList = useUpdateAtom(playlistsAtom) + const [updating, setUpdating] = useAtom(playlistsUpdatingAtom) + + if (!server) { + return async () => {} + } + + return async () => { + if (updating) { + return + } + setUpdating(true) + + const client = new SubsonicApiClient(server) + + try { + const response = await client.getPlaylists() + updateList(response.data.playlists.map(mapPlaylistListItem)) + } finally { + setUpdating(false) + } + } +} + +export const useUpdateAlbumList = () => { + const server = useStore(selectSettings.activeServer) + const updateList = useUpdateAtom(albumListAtom) + const [updating, setUpdating] = useAtom(albumListUpdatingAtom) + + if (!server) { + return async () => {} + } + + return async () => { + if (updating) { + return + } + setUpdating(true) + + const client = new SubsonicApiClient(server) + + try { + const response = await client.getAlbumList2({ type: 'alphabeticalByArtist', size: 500 }) + updateList(response.data.albums.map(mapAlbumID3toAlbumListItem)) + } finally { + setUpdating(false) + } + } +} + +export const useCoverArtUri = () => { + const server = useStore(selectSettings.activeServer) + + if (!server) { + return () => undefined + } + + const client = new SubsonicApiClient(server) + + return (coverArt?: string, size: 'thumbnail' | 'original' = 'thumbnail') => { + const params: GetCoverArtParams = { id: coverArt || '-1' } + if (size === 'thumbnail') { + params.size = '256' + } + + return client.getCoverArtUri(params) + } +} diff --git a/app/state/server.ts b/app/hooks/server.ts similarity index 68% rename from app/state/server.ts rename to app/hooks/server.ts index e6a5af7..9f13ad3 100644 --- a/app/state/server.ts +++ b/app/hooks/server.ts @@ -1,12 +1,13 @@ -import { useAtom } from 'jotai' -import { useAtomValue, useUpdateAtom } from 'jotai/utils' +import { albumListAtom, artistsAtom, homeListsAtom, playlistsAtom, searchResultsAtom } from '@app/state/music' +import { selectSettings } from '@app/state/settings' +import { useStore } from '@app/state/store' +import { useReset } from '@app/state/trackplayer' +import { useUpdateAtom } from 'jotai/utils' import { useEffect } from 'react' -import { albumListAtom, artistsAtom, homeListsAtom, playlistsAtom, searchResultsAtom } from './music' -import { activeServerAtom } from './settings' -import { useReset } from './trackplayer' -export const useSetActiveServer = () => { - const [activeServer, setActiveServer] = useAtom(activeServerAtom) +export const useSwitchActiveServer = () => { + const activeServer = useStore(selectSettings.activeServer) + const setActiveServer = useStore(selectSettings.setActiveServer) const setArtists = useUpdateAtom(artistsAtom) const setHomeLists = useUpdateAtom(homeListsAtom) const setSearchResults = useUpdateAtom(searchResultsAtom) @@ -32,7 +33,7 @@ export const useSetActiveServer = () => { } export const useActiveListRefresh = (list: unknown[], update: () => void) => { - const activeServer = useAtomValue(activeServerAtom) + const activeServer = useStore(selectSettings.activeServer) useEffect(() => { if (list.length === 0) { @@ -43,7 +44,7 @@ export const useActiveListRefresh = (list: unknown[], update: () => void) => { } export const useActiveServerRefresh = (update: () => void) => { - const activeServer = useAtomValue(activeServerAtom) + const activeServer = useStore(selectSettings.activeServer) useEffect(() => { if (activeServer) { diff --git a/app/models/music.ts b/app/models/music.ts index 609971c..c2ecd35 100644 --- a/app/models/music.ts +++ b/app/models/music.ts @@ -1,3 +1,14 @@ +import { SubsonicApiClient } from '@app/subsonic/api' +import { + AlbumID3Element, + ArtistID3Element, + ArtistInfo2Element, + ChildElement, + PlaylistElement, + PlaylistWithSongsElement, +} from '@app/subsonic/elements' +import { GetArtistResponse } from '@app/subsonic/responses' + export interface Artist { itemType: 'artist' id: string @@ -98,3 +109,96 @@ export type DownloadedPlaylist = { songs: string[] name: string } + +export function mapArtistID3toArtist(artist: ArtistID3Element): Artist { + return { + itemType: 'artist', + id: artist.id, + name: artist.name, + starred: artist.starred, + coverArt: artist.coverArt, + } +} + +export function mapArtistInfo( + artistResponse: GetArtistResponse, + info: ArtistInfo2Element, + topSongs: ChildElement[], + client: SubsonicApiClient, +): ArtistInfo { + const { artist, albums } = artistResponse + + const mappedAlbums = albums.map(mapAlbumID3toAlbum) + + return { + ...mapArtistID3toArtist(artist), + albums: mappedAlbums, + smallImageUrl: info.smallImageUrl, + mediumImageUrl: info.mediumImageUrl, + largeImageUrl: info.largeImageUrl, + topSongs: topSongs.map(s => mapChildToSong(s, client)).slice(0, 5), + } +} + +export function mapAlbumID3toAlbumListItem(album: AlbumID3Element): AlbumListItem { + return { + itemType: 'album', + id: album.id, + name: album.name, + artist: album.artist, + starred: album.starred, + coverArt: album.coverArt, + } +} + +export function mapAlbumID3toAlbum(album: AlbumID3Element): Album { + return { + ...mapAlbumID3toAlbumListItem(album), + coverArt: album.coverArt, + year: album.year, + } +} + +export function mapChildToSong(child: ChildElement, client: SubsonicApiClient): Song { + return { + itemType: 'song', + id: child.id, + album: child.album, + artist: child.artist, + title: child.title, + track: child.track, + duration: child.duration, + starred: child.starred, + coverArt: child.coverArt, + streamUri: client.streamUri({ id: child.id }), + } +} + +export function mapAlbumID3WithSongstoAlbunWithSongs( + album: AlbumID3Element, + songs: ChildElement[], + client: SubsonicApiClient, +): AlbumWithSongs { + return { + ...mapAlbumID3toAlbum(album), + songs: songs.map(s => mapChildToSong(s, client)), + } +} + +export function mapPlaylistListItem(playlist: PlaylistElement): PlaylistListItem { + return { + itemType: 'playlist', + id: playlist.id, + name: playlist.name, + comment: playlist.comment, + coverArt: playlist.coverArt, + } +} + +export function mapPlaylistWithSongs(playlist: PlaylistWithSongsElement, client: SubsonicApiClient): PlaylistWithSongs { + return { + ...mapPlaylistListItem(playlist), + songs: playlist.songs.map(s => mapChildToSong(s, client)), + coverArt: playlist.coverArt, + } +} diff --git a/app/navigation/BottomTabNavigator.tsx b/app/navigation/BottomTabNavigator.tsx index bc67b88..5b4d44e 100644 --- a/app/navigation/BottomTabNavigator.tsx +++ b/app/navigation/BottomTabNavigator.tsx @@ -1,9 +1,8 @@ import BottomTabBar from '@app/navigation/BottomTabBar' import LibraryTopTabNavigator from '@app/navigation/LibraryTopTabNavigator' -import AlbumView from '@app/screens/AlbumView' +import SongListView from '@app/screens/SongListView' import ArtistView from '@app/screens/ArtistView' import Home from '@app/screens/Home' -import PlaylistView from '@app/screens/PlaylistView' import Search from '@app/screens/Search' import ServerView from '@app/screens/ServerView' import SettingsView from '@app/screens/Settings' @@ -30,7 +29,7 @@ type AlbumScreenProps = { } const AlbumScreen: React.FC = ({ route }) => ( - + ) type ArtistScreenNavigationProp = NativeStackNavigationProp @@ -52,7 +51,7 @@ type PlaylistScreenProps = { } const PlaylistScreen: React.FC = ({ route }) => ( - + ) const styles = StyleSheet.create({ diff --git a/app/navigation/RootNavigator.tsx b/app/navigation/RootNavigator.tsx index 31e1cd3..4e56b28 100644 --- a/app/navigation/RootNavigator.tsx +++ b/app/navigation/RootNavigator.tsx @@ -1,5 +1,5 @@ import BottomTabNavigator from '@app/navigation/BottomTabNavigator' -import NowPlayingLayout from '@app/screens/NowPlayingLayout' +import NowPlayingView from '@app/screens/NowPlayingView' import colors from '@app/styles/colors' import { DarkTheme, NavigationContainer } from '@react-navigation/native' import React from 'react' @@ -32,7 +32,7 @@ const RootNavigator = () => ( }} initialRouteName="main"> - + ) diff --git a/app/screens/AlbumView.tsx b/app/screens/AlbumView.tsx deleted file mode 100644 index cf0aa89..0000000 --- a/app/screens/AlbumView.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import CoverArt from '@app/components/CoverArt' -import GradientBackground from '@app/components/GradientBackground' -import ImageGradientScrollView from '@app/components/ImageGradientScrollView' -import ListPlayerControls from '@app/components/ListPlayerControls' -import NothingHere from '@app/components/NothingHere' -import ListItem from '@app/components/ListItem' -import { albumAtomFamily, useCoverArtUri } from '@app/state/music' -import { useSetQueue } from '@app/state/trackplayer' -import colors from '@app/styles/colors' -import font from '@app/styles/font' -import { useNavigation } from '@react-navigation/native' -import { useAtomValue } from 'jotai/utils' -import React, { useEffect } from 'react' -import { ActivityIndicator, StyleSheet, Text, View } from 'react-native' - -const AlbumDetails: React.FC<{ - id: string -}> = ({ id }) => { - const album = useAtomValue(albumAtomFamily(id)) - const coverArtUri = useCoverArtUri() - const setQueue = useSetQueue() - - if (!album) { - return <> - } - - const Songs = () => ( - <> - - - {album.songs - .sort((a, b) => { - if (b.track && a.track) { - return a.track - b.track - } else { - return a.title.localeCompare(b.title) - } - }) - .map((s, i) => ( - setQueue(album.songs, album.name, i)} /> - ))} - - - ) - - return ( - - - - {album.name} - - {album.artist} - {album.year ? ` • ${album.year}` : ''} - - {album.songs.length > 0 ? : } - - - ) -} - -const AlbumViewFallback = () => ( - - - -) - -const AlbumView: React.FC<{ - id: string - title: string -}> = ({ id, title }) => { - const navigation = useNavigation() - - useEffect(() => { - navigation.setOptions({ title }) - }) - - return ( - }> - - - ) -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - }, - content: { - alignItems: 'center', - paddingTop: 10, - paddingHorizontal: 20, - }, - title: { - fontSize: 24, - fontFamily: font.bold, - color: colors.text.primary, - marginTop: 12, - textAlign: 'center', - }, - subtitle: { - fontFamily: font.regular, - color: colors.text.secondary, - fontSize: 14, - marginTop: 4, - marginBottom: 20, - textAlign: 'center', - }, - cover: { - height: 220, - width: 220, - }, - songs: { - marginTop: 26, - marginBottom: 30, - width: '100%', - }, - fallback: { - alignItems: 'center', - paddingTop: 100, - }, -}) - -export default React.memo(AlbumView) diff --git a/app/screens/ArtistView.tsx b/app/screens/ArtistView.tsx index 8b75598..a3c2449 100644 --- a/app/screens/ArtistView.tsx +++ b/app/screens/ArtistView.tsx @@ -1,16 +1,15 @@ import CoverArt from '@app/components/CoverArt' import GradientScrollView from '@app/components/GradientScrollView' import Header from '@app/components/Header' -import PressableOpacity from '@app/components/PressableOpacity' import ListItem from '@app/components/ListItem' -import { Album } from '@app/models/music' -import { artistInfoAtomFamily } from '@app/state/music' +import PressableOpacity from '@app/components/PressableOpacity' +import { useArtistInfo } from '@app/hooks/music' +import { Album, Song } from '@app/models/music' import { useSetQueue } from '@app/state/trackplayer' import colors from '@app/styles/colors' import font from '@app/styles/font' import { useLayout } from '@react-native-community/hooks' import { useNavigation } from '@react-navigation/native' -import { useAtomValue } from 'jotai/utils' import React, { useEffect } from 'react' import { StyleSheet, Text, View } from 'react-native' import FastImage from 'react-native-fast-image' @@ -33,9 +32,30 @@ const AlbumItem = React.memo<{ ) }) -const ArtistDetails: React.FC<{ id: string }> = ({ id }) => { - const artist = useAtomValue(artistInfoAtomFamily(id)) +const TopSongs = React.memo<{ + songs: Song[] + name: string +}>(({ songs, name }) => { const setQueue = useSetQueue() + + return ( + <> +
Top Songs
+ {songs.map((s, i) => ( + setQueue(songs, `Top Songs: ${name}`, i)} + /> + ))} + + ) +}) + +const ArtistDetails: React.FC<{ id: string }> = ({ id }) => { + const artist = useArtistInfo(id) const albumsLayout = useLayout() const coverLayout = useLayout() @@ -45,21 +65,6 @@ const ArtistDetails: React.FC<{ id: string }> = ({ id }) => { return <> } - const TopSongs = () => ( - <> -
Top Songs
- {artist.topSongs.map((s, i) => ( - setQueue(artist.topSongs, `Top Songs: ${artist.name}`, i)} - /> - ))} - - ) - return ( = ({ id }) => { {artist.name} - {artist.topSongs.length > 0 ? : <>} + {artist.topSongs.length > 0 ? : <>}
Albums
{artist.albums.map(a => ( @@ -89,22 +94,18 @@ const ArtistDetails: React.FC<{ id: string }> = ({ id }) => { ) } -const ArtistView: React.FC<{ +const ArtistView = React.memo<{ id: string title: string -}> = ({ id, title }) => { +}>(({ id, title }) => { const navigation = useNavigation() useEffect(() => { navigation.setOptions({ title }) }) - return ( - Loading...}> - - - ) -} + return +}) const artistCoverHeight = 280 @@ -164,4 +165,4 @@ const styles = StyleSheet.create({ }, }) -export default React.memo(ArtistView) +export default ArtistView diff --git a/app/screens/ArtistsList.tsx b/app/screens/ArtistsList.tsx deleted file mode 100644 index 1e3e874..0000000 --- a/app/screens/ArtistsList.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import React from 'react' -import { FlatList, Text, View } from 'react-native' -import { useAtomValue } from 'jotai/utils' -import { Artist } from '@app/models/music' -import { artistsAtom } from '@app/state/music' - -const ArtistItem: React.FC<{ item: Artist }> = ({ item }) => ( - - {item.id} - - {item.name} - - -) - -const List = () => { - const artists = useAtomValue(artistsAtom) - - const renderItem: React.FC<{ item: Artist }> = ({ item }) => - - return item.id} /> -} - -const ArtistsList = () => ( - - Loading...}> - - - -) - -export default ArtistsList diff --git a/app/screens/Home.tsx b/app/screens/Home.tsx index 8a044f3..189dd19 100644 --- a/app/screens/Home.tsx +++ b/app/screens/Home.tsx @@ -3,10 +3,12 @@ import GradientScrollView from '@app/components/GradientScrollView' import Header from '@app/components/Header' import NothingHere from '@app/components/NothingHere' import PressableOpacity from '@app/components/PressableOpacity' +import { useUpdateHomeLists } from '@app/hooks/music' +import { useActiveServerRefresh } from '@app/hooks/server' import { AlbumListItem } from '@app/models/music' -import { homeListsAtom, homeListsUpdatingAtom, useUpdateHomeLists } from '@app/state/music' -import { useActiveServerRefresh } from '@app/state/server' -import { homeListTypesAtom } from '@app/state/settings' +import { homeListsAtom, homeListsUpdatingAtom } from '@app/state/music' +import { selectSettings } from '@app/state/settings' +import { useStore } from '@app/state/store' import colors from '@app/styles/colors' import font from '@app/styles/font' import { GetAlbumListType } from '@app/subsonic/params' @@ -75,7 +77,7 @@ const Category = React.memo<{ }) const Home = () => { - const types = useAtomValue(homeListTypesAtom) + const types = useStore(selectSettings.homeLists) const lists = useAtomValue(homeListsAtom) const updating = useAtomValue(homeListsUpdatingAtom) const update = useUpdateHomeLists() diff --git a/app/screens/LibraryAlbums.tsx b/app/screens/LibraryAlbums.tsx index da38b4d..5050f0e 100644 --- a/app/screens/LibraryAlbums.tsx +++ b/app/screens/LibraryAlbums.tsx @@ -1,9 +1,10 @@ import CoverArt from '@app/components/CoverArt' import GradientFlatList from '@app/components/GradientFlatList' import PressableOpacity from '@app/components/PressableOpacity' +import { useUpdateAlbumList } from '@app/hooks/music' +import { useActiveListRefresh } from '@app/hooks/server' import { Album } from '@app/models/music' -import { albumListAtom, albumListUpdatingAtom, useUpdateAlbumList } from '@app/state/music' -import { useActiveListRefresh } from '@app/state/server' +import { albumListAtom, albumListUpdatingAtom } from '@app/state/music' import colors from '@app/styles/colors' import font from '@app/styles/font' import { useNavigation } from '@react-navigation/native' @@ -87,12 +88,6 @@ const AlbumsList = () => { ) } -const AlbumsTab = () => ( - Loading...}> - - -) - const styles = StyleSheet.create({ listContent: { minHeight: '100%', @@ -123,4 +118,4 @@ const styles = StyleSheet.create({ }, }) -export default React.memo(AlbumsTab) +export default React.memo(AlbumsList) diff --git a/app/screens/LibraryArtists.tsx b/app/screens/LibraryArtists.tsx index 4e0f6c8..5143231 100644 --- a/app/screens/LibraryArtists.tsx +++ b/app/screens/LibraryArtists.tsx @@ -1,8 +1,9 @@ import GradientFlatList from '@app/components/GradientFlatList' import ListItem from '@app/components/ListItem' +import { useUpdateArtists } from '@app/hooks/music' +import { useActiveListRefresh } from '@app/hooks/server' import { Artist } from '@app/models/music' -import { artistsAtom, artistsUpdatingAtom, useUpdateArtists } from '@app/state/music' -import { useActiveListRefresh } from '@app/state/server' +import { artistsAtom, artistsUpdatingAtom } from '@app/state/music' import { useAtomValue } from 'jotai/utils' import React from 'react' import { StyleSheet } from 'react-native' diff --git a/app/screens/LibraryPlaylists.tsx b/app/screens/LibraryPlaylists.tsx index ad8d2c9..3a13ef4 100644 --- a/app/screens/LibraryPlaylists.tsx +++ b/app/screens/LibraryPlaylists.tsx @@ -1,8 +1,9 @@ import GradientFlatList from '@app/components/GradientFlatList' import ListItem from '@app/components/ListItem' +import { useUpdatePlaylists } from '@app/hooks/music' +import { useActiveListRefresh } from '@app/hooks/server' import { PlaylistListItem } from '@app/models/music' -import { playlistsAtom, playlistsUpdatingAtom, useUpdatePlaylists } from '@app/state/music' -import { useActiveListRefresh } from '@app/state/server' +import { playlistsAtom, playlistsUpdatingAtom } from '@app/state/music' import { useAtomValue } from 'jotai/utils' import React from 'react' import { StyleSheet } from 'react-native' diff --git a/app/screens/NowPlayingLayout.tsx b/app/screens/NowPlayingView.tsx similarity index 98% rename from app/screens/NowPlayingLayout.tsx rename to app/screens/NowPlayingView.tsx index 0557b65..b6af544 100644 --- a/app/screens/NowPlayingLayout.tsx +++ b/app/screens/NowPlayingView.tsx @@ -303,7 +303,7 @@ type RootStackParamList = { } type NowPlayingProps = NativeStackScreenProps -const NowPlayingLayout: React.FC = ({ navigation }) => { +const NowPlayingView: React.FC = ({ navigation }) => { const track = useAtomValue(currentTrackAtom) const back = useCallback(() => { @@ -354,4 +354,4 @@ const styles = StyleSheet.create({ }, }) -export default NowPlayingLayout +export default NowPlayingView diff --git a/app/screens/PlaylistView.tsx b/app/screens/PlaylistView.tsx deleted file mode 100644 index bb755b7..0000000 --- a/app/screens/PlaylistView.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import CoverArt from '@app/components/CoverArt' -import GradientBackground from '@app/components/GradientBackground' -import ImageGradientScrollView from '@app/components/ImageGradientScrollView' -import ListPlayerControls from '@app/components/ListPlayerControls' -import NothingHere from '@app/components/NothingHere' -import ListItem from '@app/components/ListItem' -import { playlistAtomFamily, useCoverArtUri } from '@app/state/music' -import { useSetQueue } from '@app/state/trackplayer' -import colors from '@app/styles/colors' -import font from '@app/styles/font' -import { useNavigation } from '@react-navigation/native' -import { useAtomValue } from 'jotai/utils' -import React, { useEffect } from 'react' -import { ActivityIndicator, StyleSheet, Text, View } from 'react-native' - -const PlaylistDetails: React.FC<{ - id: string -}> = ({ id }) => { - const playlist = useAtomValue(playlistAtomFamily(id)) - const setQueue = useSetQueue() - const coverArtUri = useCoverArtUri() - - if (!playlist) { - return <> - } - - const Songs = () => ( - <> - - - {playlist.songs.map((s, i) => ( - setQueue(playlist.songs, playlist.name, i)} - /> - ))} - - - ) - - return ( - - - - {playlist.name} - {playlist.comment ? {playlist.comment} : <>} - {playlist.songs.length > 0 ? : } - - - ) -} - -const PlaylistViewFallback = () => ( - - - -) - -const PlaylistView: React.FC<{ - id: string - title: string -}> = ({ id, title }) => { - const navigation = useNavigation() - - useEffect(() => { - navigation.setOptions({ title }) - }) - - return ( - }> - - - ) -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - }, - content: { - alignItems: 'center', - paddingTop: 10, - paddingHorizontal: 10, - }, - title: { - fontSize: 24, - fontFamily: font.bold, - color: colors.text.primary, - marginTop: 12, - textAlign: 'center', - }, - subtitle: { - fontFamily: font.regular, - color: colors.text.secondary, - fontSize: 14, - marginTop: 4, - textAlign: 'center', - }, - cover: { - height: 160, - width: 160, - }, - songsContainer: { - width: '100%', - marginTop: 18, - alignItems: 'center', - }, - controls: { - marginTop: 20, - }, - songs: { - marginTop: 26, - marginBottom: 30, - width: '100%', - }, - fallback: { - alignItems: 'center', - paddingTop: 100, - }, - nothingContainer: { - height: 400, - backgroundColor: 'green', - justifyContent: 'center', - alignItems: 'center', - }, -}) - -export default React.memo(PlaylistView) diff --git a/app/screens/Search.tsx b/app/screens/Search.tsx index 11af06e..927946e 100644 --- a/app/screens/Search.tsx +++ b/app/screens/Search.tsx @@ -2,8 +2,9 @@ import GradientScrollView from '@app/components/GradientScrollView' import Header from '@app/components/Header' import ListItem from '@app/components/ListItem' import NothingHere from '@app/components/NothingHere' +import { useUpdateSearchResults } from '@app/hooks/music' import { ListableItem, SearchResults, Song } from '@app/models/music' -import { searchResultsAtom, searchResultsUpdatingAtom, useUpdateSearchResults } from '@app/state/music' +import { searchResultsAtom, searchResultsUpdatingAtom } from '@app/state/music' import { useSetQueue } from '@app/state/trackplayer' import colors from '@app/styles/colors' import font from '@app/styles/font' diff --git a/app/screens/ServerView.tsx b/app/screens/ServerView.tsx index a2d3805..086f2d8 100644 --- a/app/screens/ServerView.tsx +++ b/app/screens/ServerView.tsx @@ -2,11 +2,11 @@ import Button from '@app/components/Button' import GradientScrollView from '@app/components/GradientScrollView' import SettingsItem from '@app/components/SettingsItem' import { Server } from '@app/models/settings' -import { activeServerAtom, serversAtom } from '@app/state/settings' +import { selectSettings } from '@app/state/settings' +import { useStore } from '@app/state/store' import colors from '@app/styles/colors' import font from '@app/styles/font' import { useNavigation } from '@react-navigation/native' -import { useAtom } from 'jotai' import md5 from 'md5' import React, { useCallback, useState } from 'react' import { StyleSheet, Text, TextInput, View } from 'react-native' @@ -23,8 +23,10 @@ const ServerView: React.FC<{ id?: string }> = ({ id }) => { const navigation = useNavigation() - const [activeServer, setActiveServer] = useAtom(activeServerAtom) - const [servers, setServers] = useAtom(serversAtom) + const activeServer = useStore(selectSettings.activeServer) + const setActiveServer = useStore(selectSettings.setActiveServer) + const servers = useStore(selectSettings.servers) + const setServers = useStore(selectSettings.setServers) const server = id ? servers.find(s => s.id === id) : undefined const [address, setAddress] = useState(server?.address || '') diff --git a/app/screens/Settings.tsx b/app/screens/Settings.tsx index fe1be6b..ba5ba3e 100644 --- a/app/screens/Settings.tsx +++ b/app/screens/Settings.tsx @@ -3,12 +3,12 @@ import GradientScrollView from '@app/components/GradientScrollView' import Header from '@app/components/Header' import PressableOpacity from '@app/components/PressableOpacity' import SettingsItem from '@app/components/SettingsItem' +import { useSwitchActiveServer } from '@app/hooks/server' import { Server } from '@app/models/settings' -import { useSetActiveServer } from '@app/state/server' -import { activeServerAtom, appSettingsAtom } from '@app/state/settings' +import { selectSettings } from '@app/state/settings' +import { useStore } from '@app/state/store' import colors from '@app/styles/colors' import { useNavigation } from '@react-navigation/core' -import { useAtomValue } from 'jotai/utils' import React, { useCallback } from 'react' import { StatusBar, StyleSheet, View } from 'react-native' import Icon from 'react-native-vector-icons/MaterialCommunityIcons' @@ -16,13 +16,13 @@ import Icon from 'react-native-vector-icons/MaterialCommunityIcons' const ServerItem = React.memo<{ server: Server }>(({ server }) => { - const activeServer = useAtomValue(activeServerAtom) - const setActiveServer = useSetActiveServer() + const activeServer = useStore(selectSettings.activeServer) + const switchActiveServer = useSwitchActiveServer() const navigation = useNavigation() const setActive = useCallback(() => { - setActiveServer(server.id) - }, [server.id, setActiveServer]) + switchActiveServer(server.id) + }, [server.id, switchActiveServer]) return ( { - const settings = useAtomValue(appSettingsAtom) + const servers = useStore(selectSettings.servers) const navigation = useNavigation() return (
Servers
- {settings.servers.map(s => ( + {servers.map(s => ( ))} -