Kaynağa Gözat

[web] Filter routing

Innokentiy Enikeev 4 yıl önce
ebeveyn
işleme
9f157acb32

+ 11 - 8
web/src/AlbumList.js

@@ -3,6 +3,7 @@ import InfiniteScroll from 'react-infinite-scroller';
 import {useEffect} from 'react';
 import {getAlbums, request} from './Api';
 import Album from './Album';
+import {CATEGORIES} from './Filters';
 
 function _albumProps(albums, index, props) {
   if (index === 0) {
@@ -19,15 +20,17 @@ function _albumProps(albums, index, props) {
 
 
 export function fetchAlbums(filters, page, dispatch) {
-  const {filter, categories, latest} = filters;
+  const {filter, alpha} = filters;
   const limit  = 15;
-  const offset = page * limit;
-  const params = {filter, offset, limit:(limit+1)};
-  for (const [type, value] of Object.entries(categories)) {
-    if (value)
-      params[type] = value;
+  const offset = (page || 0) * limit;
+  const params = {offset, limit:(limit+1)};
+  if (filter) params.filter = filter;
+  if (filters.album) params.album = filters.album;
+  for (const {cat} of CATEGORIES) {
+    if (filters[cat])
+      params[cat] = filters[cat];
   }
-  if (latest) params['latest'] = 1;
+  if (!alpha) params['latest'] = 1;
   const {url, options} = getAlbums(params);
 
   request(url, options)
@@ -42,7 +45,7 @@ export function fetchAlbums(filters, page, dispatch) {
 
 export default function AlbumList(props) {
   const {albums, error, filters, dispatch, playerDispatch, scrollRef} = props;
-  const title = (!props.noTitle && (filters.latest ? 'Latest' : 'Alphabetical'));
+  const title = (!props.noTitle && (filters.alpha ? 'Alphabetical' : 'Latest'));
 
   useEffect(()=>{
     fetchAlbums(filters, albums.page, dispatch);

+ 1 - 4
web/src/AlbumPage.js

@@ -7,7 +7,6 @@ import {fetchAlbums} from './AlbumList';
 import {fetchTracks} from './Album';
 
 function pageReducer(state, action) {
-  console.log(action);
   switch (action.type) {
   case 'LOAD_ALBUMS':
     if (!action.payload.items || !action.payload.items.length)
@@ -32,9 +31,7 @@ export default function AlbumPage({dispatch, scrollRef}) {
     if (album)
       fetchTracks(album.id, 'SHOW_ALBUM', dispatchPage)
     else
-      fetchAlbums({categories: {artist: artistName,
-                                album: albumName},
-                   filter: ''}, 0, dispatchPage);
+      fetchAlbums({artist: artistName, album: albumName}, 0, dispatchPage);
   }, [artistName, albumName, album, dispatch]);
 
   return (

+ 1 - 2
web/src/ArtistPage.js

@@ -8,8 +8,7 @@ import {browserReducer, ALBUMS_EMPTY} from './Browser';
 export default function ArtistPage({dispatch, scrollRef}) {
   const { route } = useRouteNode('artist');
   const artist = unslugify(route.params.artist);
-  const INIT = {albums: ALBUMS_EMPTY, error: false, filters: {categories: {artist},
-                                                              filter:''}};
+  const INIT = {albums: ALBUMS_EMPTY, error: false, filters: {artist, alpha:true}};
   const [state, dispatchBrowser] = useReducer(browserReducer, INIT);
 
   return (

+ 14 - 16
web/src/Browser.js

@@ -1,5 +1,6 @@
-import {useReducer} from 'react';
-
+import {useReducer, useEffect} from 'react';
+import {useRoute} from 'react-router5'
+import { shouldUpdateNode } from 'router5-transition-path'
 import Filters from './Filters';
 import AlbumList from './AlbumList';
 
@@ -10,19 +11,9 @@ export function browserReducer(state, action) {
     const {more} = action.payload;
     const items = state.albums.items.concat(action.payload.items);
     return {...state, albums: {...state.albums, items, more}}}
-  case 'SET_CATEGORY': {
-    const categories = {...state.filters.categories, [action.payload.cat]: action.payload.value};
-    const filters = {...state.filters, categories};
-    const albums = ALBUMS_EMPTY;
-    return {...state, filters, albums}}
-  case 'SET_FILTER': {
-    const filters = {...state.filters, filter:action.payload};
+  case 'SET_FILTERS': {
     const albums = ALBUMS_EMPTY;
-    return {...state, filters, albums}}
-  case 'SET_LATEST': {
-    const filters = {...state.filters, latest:action.payload};
-    const albums = ALBUMS_EMPTY;
-    return {...state, filters, albums}}
+    return {...state, albums, filters: action.payload}}
   case 'SET_PAGE': {
     const albums = {...state.albums, page: action.payload, more: false};
     return {...state, albums}}
@@ -31,12 +22,19 @@ export function browserReducer(state, action) {
 }
 
 export default function Browser({dispatch, scrollRef}) {
-  const INIT = {albums: ALBUMS_EMPTY, error: false, filters: {categories: {}, filter:'', latest: true}};
+  const { router } = useRoute();
+  const { params } = router.getState();
+  const INIT = {albums: ALBUMS_EMPTY, error: false, filters:params};
   const [state, dispatchBrowser] = useReducer(browserReducer, INIT);
 
+  useEffect(() => router.subscribe(({ route, previousRoute }) => {
+    const shouldUpdate = shouldUpdateNode("browser")(route, previousRoute);
+    if (shouldUpdate) dispatchBrowser({type:'SET_FILTERS', payload: route.params});
+  }), [])
+
   return (
     <>
-      <Filters {...state.filters} dispatch={dispatchBrowser} />
+      <Filters {...state.filters} />
       <AlbumList {...state} dispatch={dispatchBrowser} playerDispatch={dispatch} scrollRef={scrollRef} />
     </>
   );

+ 27 - 22
web/src/Filters.js

@@ -1,15 +1,16 @@
-import {useRef} from 'react';
+import { useRoute } from 'react-router5'
+import { useRef } from 'react';
 import AsyncSelect from 'react-select/async';
 import {getCategory, request} from './Api';
 import './Filters.css';
 
-const CATEGORIES = [{cat: 'artist', title: 'Artists'},
-                    {cat: 'year', title: 'Years'},
-                    {cat: 'genre', title: 'Genres'},
-                    {cat: 'publisher', title: 'Labels'},
-                    {cat: 'country', title: 'Countries'},
-                    {cat: 'type', title: 'Album types'},
-                    {cat: 'status', title: 'Album statuses'}];
+export const CATEGORIES = [{cat: 'artist', title: 'Artists'},
+                           {cat: 'year', title: 'Years'},
+                           {cat: 'genre', title: 'Genres'},
+                           {cat: 'publisher', title: 'Labels'},
+                           {cat: 'country', title: 'Countries'},
+                           {cat: 'type', title: 'Album types'},
+                           {cat: 'status', title: 'Album statuses'}];
 
 function handleLoadOptions(cat, q) {
   const pageSize = 10000
@@ -20,18 +21,20 @@ function handleLoadOptions(cat, q) {
   return request(url, options)
 }
 
-function handleFilterChange(e, timeout, dispatch) {
-  if (timeout.current) {
-    clearInterval(timeout.current);
-  }
-  const filter = e.target.value;
-  timeout.current = setTimeout(() => {
-    dispatch({type: 'SET_FILTER', payload: filter});
-  }, 500);
+function handleCategoryChange(router, cat, value) {
+  const params = router.getState().params;
+  router.navigate('browser', {...params, [cat]:(value || null)});
 }
 
-export default function Filters({categories, filter, latest, dispatch}) {
+function handleFilterChange(router, timeout, value) {
+  if (timeout.current) clearInterval(timeout.current);
+  timeout.current = setTimeout(() => handleCategoryChange(router, 'filter', value), 500);
+}
+
+export default function Filters() {
   const timeout = useRef();
+  const { router } = useRoute();
+  const { params } = router.getState();
 
   return (
     <div className="Filters">
@@ -41,11 +44,12 @@ export default function Filters({categories, filter, latest, dispatch}) {
           <AsyncSelect
             isClearable
             cacheOptions
+            defaultValue={params[cat] && {item: params[cat]}}
             defaultOptions
             loadOptions={(q) => handleLoadOptions(cat, q)}
-            getOptionLabel={(i)=>`${i.item} (${i.count})`}
+            getOptionLabel={(i)=>i.count ? `${i.item} (${i.count})` : i.item}
             getOptionValue={(i)=>i.item}
-            onChange={(e)=>dispatch({type: 'SET_CATEGORY', payload: {cat: cat, value: (e && e.item)}})}
+            onChange={(e)=>handleCategoryChange(router, cat, (e && e.item))}
             />
         </div>
       ))}
@@ -53,15 +57,16 @@ export default function Filters({categories, filter, latest, dispatch}) {
       <h3>Latest</h3>
           <input className="Latest"
              type="checkbox"
-             checked={latest}
-             onChange={(e) => dispatch({type: 'SET_LATEST', payload: e.target.checked})} />
+             checked={ !params.alpha }
+             onChange={(e) => handleCategoryChange(router, 'alpha', !e.target.checked)} />
       </div>
       <div>
         <h3>Search</h3>
         <input className="Filter"
           type="text"
           placeholder="Search albums"
-    onChange={(e)=>handleFilterChange(e, timeout, dispatch)} />
+          defaultValue={params.filter}
+          onChange={(e)=>handleFilterChange(router, timeout, e.target.value)} />
       </div>
     </div>
   );

+ 2 - 2
web/src/Main.js

@@ -12,9 +12,9 @@ export default function Main({dispatch}) {
   if (route.name === 'browser')
     content = <Browser dispatch={dispatch} scrollRef={scrollRef} />
   else if (route.name === 'artist')
-    content = <ArtistPage dispatch={dispatch} scrollRef={scrollRef}  />
+    content = <ArtistPage dispatch={dispatch} scrollRef={scrollRef} />
   else if (route.name === 'album')
-    content = <AlbumPage dispatch={dispatch} scrollRef={scrollRef}  />
+    content = <AlbumPage dispatch={dispatch} scrollRef={scrollRef} />
 
   return (
     <main ref={scrollRef}>{ content }</main>

+ 4 - 3
web/src/routes.js

@@ -1,6 +1,7 @@
 const routes= [
-    { name: 'browser', path: '/' },
-    { name: 'artist', path: '/:artist<[^/?#\\[\\]]+>' },
-    { name: 'album', path: '/:artist<[^/?#\\[\\]]+>/:album<[^/?#\\[\\]]+>' }
+  { name: 'browser', path: '/?page&artist&year&genre&label&country&type&alpha&status&filter',
+    queryParamsMode: 'loose', nullFormat: 'hidden'},
+  { name: 'artist', path: '/:artist<[^/?#\\[\\]]+>' },
+  { name: 'album', path: '/:artist<[^/?#\\[\\]]+>/:album<[^/?#\\[\\]]+>' }
 ]
 export default routes;