From 290a62588ee4fb274a9416d99e7e5f5c7a2a7d4c Mon Sep 17 00:00:00 2001 From: austinried <4966622+austinried@users.noreply.github.com> Date: Sun, 22 Aug 2021 11:48:22 +0900 Subject: [PATCH] added album list filtering --- app/App.tsx | 2 +- app/components/FilterButton.tsx | 99 +++++++++++++++++++++++++++++++++ app/models/settings.ts | 18 +++++- app/screens/LibraryAlbums.tsx | 33 ++++++++++- app/state/music.ts | 38 +++++++++++-- app/state/settings.ts | 31 +++++++++-- app/subsonic/params.ts | 33 +++++------ 7 files changed, 226 insertions(+), 28 deletions(-) create mode 100644 app/components/FilterButton.tsx diff --git a/app/App.tsx b/app/App.tsx index 817c984..0c834e7 100644 --- a/app/App.tsx +++ b/app/App.tsx @@ -15,7 +15,7 @@ const Debug = () => { } const App = () => ( - + diff --git a/app/components/FilterButton.tsx b/app/components/FilterButton.tsx new file mode 100644 index 0000000..a8e2ad5 --- /dev/null +++ b/app/components/FilterButton.tsx @@ -0,0 +1,99 @@ +import colors from '@app/styles/colors' +import font from '@app/styles/font' +import React from 'react' +import { Text, StyleSheet } from 'react-native' +import { MenuOption, Menu, MenuTrigger, MenuOptions } from 'react-native-popup-menu' +import PressableOpacity from './PressableOpacity' +import Icon from 'react-native-vector-icons/MaterialCommunityIcons' + +export type OptionData = { + value: string + text: string +} + +const Option = React.memo<{ + text: string + value: string + selected?: boolean +}>(({ text, value, selected }) => ( + + {text} + {selected ? ( + + ) : ( + + )} + +)) + +const FilterButton = React.memo<{ + value?: string + data: OptionData[] + onSelect?: (selection: string) => void +}>(({ value, data, onSelect }) => { + return ( + + + + + + {data.map(o => ( + + + ) +}) + +const styles = StyleSheet.create({ + filterOuterWrapper: { + position: 'absolute', + bottom: 20, + right: 20, + }, + filterWrapper: {}, + filter: { + borderRadius: 32, + width: 50, + height: 50, + elevation: 5, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: colors.accent, + }, + filterIcon: { + // top: 4, + }, + optionsWrapper: { + maxWidth: 145, + }, + optionsContainer: { + backgroundColor: 'rgba(45, 45, 45, 0.95)', + maxWidth: 145, + }, + option: { + flexDirection: 'row', + paddingHorizontal: 12, + paddingVertical: 8, + justifyContent: 'center', + alignItems: 'center', + }, + optionText: { + color: colors.text.primary, + fontFamily: font.semiBold, + fontSize: 16, + flex: 1, + }, +}) + +export default FilterButton diff --git a/app/models/settings.ts b/app/models/settings.ts index a0917b0..4dc4b57 100644 --- a/app/models/settings.ts +++ b/app/models/settings.ts @@ -1,3 +1,5 @@ +import { GetAlbumList2Type } from '@app/subsonic/params' + export interface Server { id: string address: string @@ -6,10 +8,22 @@ export interface Server { salt: string } +export interface FilterSettings { + type: GetAlbumList2Type + fromYear: number + toYear: number + genre: string +} + export interface AppSettings { servers: Server[] - home: { - lists: string[] + screens: { + home: { + lists: string[] + } + library: { + albums: FilterSettings + } } activeServer?: string scrobble: boolean diff --git a/app/screens/LibraryAlbums.tsx b/app/screens/LibraryAlbums.tsx index 7cf94c5..ead4504 100644 --- a/app/screens/LibraryAlbums.tsx +++ b/app/screens/LibraryAlbums.tsx @@ -1,14 +1,17 @@ import { AlbumContextPressable } from '@app/components/ContextMenu' import CoverArt from '@app/components/CoverArt' +import FilterButton, { OptionData } from '@app/components/FilterButton' import GradientFlatList from '@app/components/GradientFlatList' import { useFetchPaginatedList } from '@app/hooks/list' import { Album, AlbumListItem } from '@app/models/music' import { selectMusic } 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 { GetAlbumList2Type } from '@app/subsonic/params' import { useNavigation } from '@react-navigation/native' -import React from 'react' +import React, { useEffect } from 'react' import { StyleSheet, Text, useWindowDimensions, View } from 'react-native' import FastImage from 'react-native-fast-image' @@ -47,15 +50,33 @@ const AlbumListRenderItem: React.FC<{ item: { album: Album; size: number; height: number } }> = ({ item }) => +const filterOptions: OptionData[] = [ + { text: 'By name', value: 'alphabeticalByName' }, + { text: 'By artist', value: 'alphabeticalByArtist' }, + { text: 'Newest', value: 'newest' }, + { text: 'Frequent', value: 'frequent' }, + { text: 'Recent', value: 'recent' }, + { text: 'Starred', value: 'starred' }, + { text: 'Random', value: 'random' }, + // { text: 'By year...', value: 'byYear' }, + // { text: 'By genre...', value: 'byGenre' }, +] + const AlbumsList = () => { const fetchAlbums = useStore(selectMusic.fetchAlbums) const { list, refreshing, refresh, fetchNextPage } = useFetchPaginatedList(fetchAlbums, 300) + const filter = useStore(selectSettings.libraryAlbumFilter) + const setFilter = useStore(selectSettings.setLibraryAlbumFilter) const layout = useWindowDimensions() const size = layout.width / 3 - styles.itemWrapper.marginHorizontal * 2 const height = size + 36 + useEffect(() => { + refresh() + }, [refresh, filter]) + return ( { onEndReached={fetchNextPage} onEndReachedThreshold={6} /> + { + setFilter({ + ...filter, + type: selection as GetAlbumList2Type, + }) + }} + /> ) } diff --git a/app/state/music.ts b/app/state/music.ts index 09a677f..5ea4863 100644 --- a/app/state/music.ts +++ b/app/state/music.ts @@ -10,7 +10,7 @@ import { StarrableItemType, } from '@app/models/music' import { Store } from '@app/state/store' -import { GetAlbumList2Type, Search3Params, StarParams } from '@app/subsonic/params' +import { GetAlbumList2Params, GetAlbumList2TypeBase, Search3Params, StarParams } from '@app/subsonic/params' import produce from 'immer' import { GetState, SetState } from 'zustand' @@ -218,7 +218,37 @@ export const createMusicSlice = (set: SetState, get: GetState): Mu } try { - const response = await client.getAlbumList2({ type: 'alphabeticalByArtist', size, offset }) + const filter = get().settings.screens.library.albums + + let params: GetAlbumList2Params + switch (filter.type) { + case 'byYear': + params = { + size, + offset, + type: filter.type, + fromYear: filter.fromYear, + toYear: filter.toYear, + } + break + case 'byGenre': + params = { + size, + offset, + type: filter.type, + genre: filter.genre, + } + break + default: + params = { + size, + offset, + type: filter.type, + } + break + } + + const response = await client.getAlbumList2(params) const albums = response.data.albums.map(get().mapAlbumID3toAlbumListItem) set( @@ -294,13 +324,13 @@ export const createMusicSlice = (set: SetState, get: GetState): Mu } set({ homeListsUpdating: true }) - const types = get().settings.home.lists + const types = get().settings.screens.home.lists try { const promises: Promise[] = [] for (const type of types) { promises.push( - client.getAlbumList2({ type: type as GetAlbumList2Type, size: 20 }).then(response => { + client.getAlbumList2({ type: type as GetAlbumList2TypeBase, size: 20 }).then(response => { const list = response.data.albums.map(get().mapAlbumID3toAlbumListItem) set( produce(state => { diff --git a/app/state/settings.ts b/app/state/settings.ts index add45e5..2a5cefb 100644 --- a/app/state/settings.ts +++ b/app/state/settings.ts @@ -1,4 +1,4 @@ -import { AppSettings, Server } from '@app/models/settings' +import { AppSettings, FilterSettings, Server } from '@app/models/settings' import { Store } from '@app/state/store' import { SubsonicApiClient } from '@app/subsonic/api' import produce from 'immer' @@ -22,6 +22,8 @@ export type SettingsSlice = { setMaxBuffer: (maxBuffer: number) => void pingServer: (server?: Server) => Promise + + setLibraryAlbumFilter: (filter: FilterSettings) => void } export const selectSettings = { @@ -35,7 +37,7 @@ export const selectSettings = { removeServer: (state: SettingsSlice) => state.removeServer, updateServer: (state: SettingsSlice) => state.updateServer, - homeLists: (state: SettingsSlice) => state.settings.home.lists, + homeLists: (state: SettingsSlice) => state.settings.screens.home.lists, scrobble: (state: SettingsSlice) => state.settings.scrobble, setScrobble: (state: SettingsSlice) => state.setScrobble, @@ -54,13 +56,26 @@ export const selectSettings = { setMaxBuffer: (state: SettingsSlice) => state.setMaxBuffer, pingServer: (state: SettingsSlice) => state.pingServer, + + setLibraryAlbumFilter: (state: SettingsSlice) => state.setLibraryAlbumFilter, + libraryAlbumFilter: (state: SettingsSlice) => state.settings.screens.library.albums, } export const createSettingsSlice = (set: SetState, get: GetState): SettingsSlice => ({ settings: { servers: [], - home: { - lists: ['recent', 'random', 'frequent', 'starred'], + screens: { + home: { + lists: ['recent', 'random', 'frequent', 'starred'], + }, + library: { + albums: { + type: 'alphabeticalByArtist', + fromYear: 1, + toYear: 9999, + genre: '', + }, + }, }, scrobble: false, estimateContentLength: true, @@ -226,6 +241,14 @@ export const createSettingsSlice = (set: SetState, get: GetState): return false } }, + + setLibraryAlbumFilter: filter => { + set( + produce(state => { + state.settings.screens.library.albums = filter + }), + ) + }, }) function replaceIndex(array: T[], index: number, replacement: T): T[] { diff --git a/app/subsonic/params.ts b/app/subsonic/params.ts index 6a20cf2..ed2d773 100644 --- a/app/subsonic/params.ts +++ b/app/subsonic/params.ts @@ -36,7 +36,7 @@ export type GetTopSongsParams = { // Album/song lists // -export type GetAlbumList2Type = +export type GetAlbumList2TypeBase = | 'random' | 'newest' | 'frequent' @@ -44,34 +44,35 @@ export type GetAlbumList2Type = | 'starred' | 'alphabeticalByName' | 'alphabeticalByArtist' -export type GetAlbumListType = GetAlbumList2Type | ' highest' +export type GetAlbumListTypeBase = GetAlbumList2TypeBase | ' highest' -export type GetAlbumList2TypeByYear = { - type: 'byYear' - fromYear: string - toYear: string +type GetAlbumListBase = { + size?: number + offset?: number + musicFolderId?: string } -export type GetAlbumList2TypeByGenre = { +export type GetAlbumList2TypeByYear = GetAlbumListBase & { + type: 'byYear' + fromYear: number + toYear: number +} + +export type GetAlbumList2TypeByGenre = GetAlbumListBase & { type: 'byGenre' genre: string } export type GetAlbumList2Params = - | { - type: GetAlbumList2Type - size?: number - offset?: number - fromYear?: string - toYear?: string - genre?: string - musicFolderId?: string - } + | (GetAlbumListBase & { type: GetAlbumList2TypeBase }) | GetAlbumList2TypeByYear | GetAlbumList2TypeByGenre export type GetAlbumListParams = GetAlbumList2Params +export type GetAlbumList2Type = GetAlbumList2TypeBase | 'byYear' | 'byGenre' +export type GetAlbumListType = GetAlbumListTypeBase | 'byYear' | 'byGenre' + // // Playlists //