diff --git a/.stylelintrc b/.stylelintrc index 6fd7cfd3f..173020772 100644 --- a/.stylelintrc +++ b/.stylelintrc @@ -1,12 +1,6 @@ { - "extends": [ - "stylelint-config-standard", - "stylelint-config-css-modules" - ], - "ignoreFiles": [ - "./src/**/*.tsx", - "./src/**/*.ts" - ], + "extends": ["stylelint-config-standard", "stylelint-config-css-modules"], + "ignoreFiles": ["./src/**/*.tsx", "./src/**/*.ts"], "rules": { "no-descending-specificity": null, "string-quotes": "single", diff --git a/src/renderer/components/Header/Header.module.css b/src/renderer/components/Header/Header.module.css index 3e9073b36..5694571f5 100644 --- a/src/renderer/components/Header/Header.module.css +++ b/src/renderer/components/Header/Header.module.css @@ -15,11 +15,10 @@ border-bottom: 1px solid var(--border-color); background-color: var(--header-bg); color: var(--header-color); - padding: 0 10px; + padding: 5px; display: flex; align-items: center; justify-content: space-between; - height: 50px; flex: 0 0 auto; /* Draggable region (zone able to move the window) */ diff --git a/src/renderer/components/Playlists/Playlist.tsx b/src/renderer/components/Playlists/Playlist.tsx index 4f8b7db01..44c70d953 100644 --- a/src/renderer/components/Playlists/Playlist.tsx +++ b/src/renderer/components/Playlists/Playlist.tsx @@ -14,23 +14,25 @@ const Playlist: React.FC = () => { const params = useParams(); const playlistId = params.playlistId; - const { tracks, trackPlayingId, playerStatus, playlists, currentPlaylist } = useSelector((state: RootState) => { - const { library, player, playlists } = state; + const { tracks, trackPlayingId, playerStatus, playlists, currentPlaylist, libraryLayoutSettings } = useSelector( + (state: RootState) => { + const { library, player, playlists } = state; + const { search, tracks } = library; - const { search, tracks } = library; - const filteredTracks = filterTracks(tracks.playlist, search); + const filteredTracks = filterTracks(tracks.playlist, search); + const currentPlaylist = playlists.list.find((p) => p._id === playlistId); - const currentPlaylist = playlists.list.find((p) => p._id === playlistId); - - return { - playlists: playlists.list, - currentPlaylist, - tracks: filteredTracks, - playerStatus: player.playerStatus, - trackPlayingId: - player.queue.length > 0 && player.queueCursor !== null ? player.queue[player.queueCursor]._id : null, - }; - }); + return { + libraryLayoutSettings: library.libraryLayoutSettings, + playlists: playlists.list, + currentPlaylist, + tracks: filteredTracks, + playerStatus: player.playerStatus, + trackPlayingId: + player.queue.length > 0 && player.queueCursor !== null ? player.queue[player.queueCursor]._id : null, + }; + } + ); useEffect(() => { if (playlistId) { @@ -84,6 +86,7 @@ const Playlist: React.FC = () => { return ( void; onDragEnd?: () => void; onDrop?: (targetTrackId: string, position: 'above' | 'below') => void; + + layout: LibraryLayoutSettings; } interface State { @@ -103,7 +106,7 @@ export default class TrackRow extends React.PureComponent { }; render() { - const { track, selected, reordered, draggable } = this.props; + const { track, selected, reordered, draggable, layout } = this.props; const { reorderOver, reorderPosition } = this.state; const trackClasses = cx(styles.track, { @@ -114,6 +117,34 @@ export default class TrackRow extends React.PureComponent { [styles.isBelow]: reorderPosition === 'below', }); + const sorts: [boolean, React.ReactElement][] = [ + [ + layout.visibility.includes('title'), +
{track.title}
, + ], + [ + layout.visibility.includes('duration'), +
{parseDuration(track.duration)}
, + ], + [ + layout.visibility.includes('artist'), +
{track.artist.sort().join(', ')}
, + ], + [ + layout.visibility.includes('album'), +
{track.album}
, + ], + [ + layout.visibility.includes('genre'), +
{track.genre.join(', ')}
, + ], + ]; + + const rows: React.ReactElement[] = []; + sorts.forEach((element) => { + if (element[0]) rows.push(element[1]); + }); + return (
{
{this.props.isPlaying ? : null}
-
{track.title}
-
{parseDuration(track.duration)}
-
{track.artist.sort().join(', ')}
-
{track.album}
-
{track.genre.join(', ')}
+ {...rows}
); } diff --git a/src/renderer/components/TracksList/TracksList.tsx b/src/renderer/components/TracksList/TracksList.tsx index d1e71a69c..acbbb8d95 100644 --- a/src/renderer/components/TracksList/TracksList.tsx +++ b/src/renderer/components/TracksList/TracksList.tsx @@ -43,11 +43,13 @@ interface Props { playlists: PlaylistModel[]; currentPlaylist?: string; reorderable?: boolean; + layout: LibraryActions.LibraryLayoutSettings; onReorder?: (playlistId: string, tracksIds: string[], targetTrackId: string, position: 'above' | 'below') => void; } const TracksList: React.FC = (props) => { - const { tracks, type, trackPlayingId, reorderable, currentPlaylist, onReorder, playerStatus, playlists } = props; + const { tracks, type, trackPlayingId, reorderable, currentPlaylist, onReorder, playerStatus, playlists, layout } = + props; const [tilesScrolled, setTilesScrolled] = useState(0); const [selected, setSelected] = useState([]); @@ -492,6 +494,7 @@ const TracksList: React.FC = (props) => { return ( = (props) => { return (
- +
{trackTiles} diff --git a/src/renderer/components/TracksListHeader/TracksListHeader.module.css b/src/renderer/components/TracksListHeader/TracksListHeader.module.css index 89d439683..f3709b542 100644 --- a/src/renderer/components/TracksListHeader/TracksListHeader.module.css +++ b/src/renderer/components/TracksListHeader/TracksListHeader.module.css @@ -21,6 +21,13 @@ .cellArtist, .cellAlbum, +.cellTitle, +.cellSection, .cellGenre { width: 20%; } + +.cellSection { + width: 20%; + height: 45px; +} diff --git a/src/renderer/components/TracksListHeader/TracksListHeader.tsx b/src/renderer/components/TracksListHeader/TracksListHeader.tsx index e47812fe9..e0446b09f 100644 --- a/src/renderer/components/TracksListHeader/TracksListHeader.tsx +++ b/src/renderer/components/TracksListHeader/TracksListHeader.tsx @@ -1,16 +1,19 @@ import React from 'react'; +import { Menu } from '@electron/remote'; import { connect } from 'react-redux'; +import electron from 'electron'; +import { LibraryLayoutSettings, set_context_state } from '../../store/actions/LibraryActions'; import TracksListHeaderCell from '../TracksListHeaderCell/TracksListHeaderCell'; import { SortBy, SortOrder } from '../../../shared/types/museeks'; import { RootState } from '../../store/reducers'; import { LibrarySort } from '../../store/reducers/library'; - import styles from './TracksListHeader.module.css'; interface OwnProps { enableSort: boolean; + layout: LibraryLayoutSettings; } interface InjectedProps { @@ -19,6 +22,11 @@ interface InjectedProps { type Props = OwnProps & InjectedProps; +const LAYOUT_LISTS = ['title', 'duration', 'album', 'artist', 'genre']; +const capitalize = (str: string) => { + return str.toUpperCase()[0] + str.substring(1); +}; + class TracksListHeader extends React.Component { static getIcon = (sort: LibrarySort | undefined, sortType: SortBy) => { if (sort && sort.by === sortType) { @@ -33,42 +41,124 @@ class TracksListHeader extends React.Component { return null; }; + // questionable? + constructor(props: Props, state: LibraryLayoutSettings) { + super(props, state); + } + + showContextMenu(_e: React.MouseEvent, id: string) { + const template: electron.MenuItemConstructorOptions[] = [ + { + label: `Selected: ${capitalize(id)}`, + enabled: false, + }, + ]; + + LAYOUT_LISTS.forEach((tag) => { + template.push({ + type: 'checkbox', + label: capitalize(tag), + checked: this.props.layout.visibility.includes(tag), + click: () => { + // toggles the existence of props.layout.visibility[tag] + const visibility = this.props.layout.visibility; + if (visibility.includes(tag)) { + set_context_state({ + visibility: visibility.filter((value) => value !== tag), + }); + } else { + set_context_state({ + visibility: [...visibility, tag], + }); + } + }, + }); + }); + + const context = Menu.buildFromTemplate(template); + + context.popup({}); // Let it appear + } + render() { - const { enableSort, sort } = this.props; + const { enableSort, sort, layout } = this.props; - return ( -
- + const sorts: [boolean, React.ReactElement][] = [ + [ + layout.visibility.includes('title'), this.showContextMenu(e, 'title')} className={styles.cellTrack} title='Title' sortBy={enableSort ? SortBy.TITLE : null} icon={TracksListHeader.getIcon(sort, SortBy.TITLE)} - /> + layout={this.props.layout} + key='Title' + />, + ], + [ + layout.visibility.includes('duration'), this.showContextMenu(e, 'duration')} className={styles.cellDuration} title='Duration' sortBy={enableSort ? SortBy.DURATION : null} icon={TracksListHeader.getIcon(sort, SortBy.DURATION)} - /> + layout={this.props.layout} + key='Duration' + />, + ], + [ + layout.visibility.includes('artist'), this.showContextMenu(e, 'artist')} className={styles.cellArtist} title='Artist' sortBy={enableSort ? SortBy.ARTIST : null} icon={TracksListHeader.getIcon(sort, SortBy.ARTIST)} - /> + layout={this.props.layout} + key='Artist' + />, + ], + [ + layout.visibility.includes('album'), this.showContextMenu(e, 'album')} className={styles.cellAlbum} title='Album' sortBy={enableSort ? SortBy.ALBUM : null} icon={TracksListHeader.getIcon(sort, SortBy.ALBUM)} - /> + layout={this.props.layout} + key='Album' + />, + ], + [ + layout.visibility.includes('genre'), this.showContextMenu(e, 'genre')} className={styles.cellGenre} title='Genre' sortBy={enableSort ? SortBy.GENRE : null} icon={TracksListHeader.getIcon(sort, SortBy.GENRE)} - /> + layout={this.props.layout} + key='Genre' + />, + ], + ]; + + const headers: React.ReactElement[] = []; + sorts.forEach((element) => { + if (element[0]) headers.push(element[1]); + }); + + return ( +
this.showContextMenu(e, 'background') : undefined} + > + {' '} + + {...headers}
); } diff --git a/src/renderer/components/TracksListHeaderCell/TracksListHeaderCell.tsx b/src/renderer/components/TracksListHeaderCell/TracksListHeaderCell.tsx index e418232a2..5a83aea9a 100644 --- a/src/renderer/components/TracksListHeaderCell/TracksListHeaderCell.tsx +++ b/src/renderer/components/TracksListHeaderCell/TracksListHeaderCell.tsx @@ -12,6 +12,8 @@ interface Props { className?: string; sortBy?: SortBy | null; icon?: string | null; + layout: LibraryActions.LibraryLayoutSettings; + onContextMenu?: (event: React.MouseEvent) => void; } class TracksListHeaderCell extends React.Component { @@ -52,13 +54,17 @@ class TracksListHeaderCell extends React.Component { if (sortBy) { return ( - ); } - return
{content}
; + return ( +
+ {content} +
+ ); } } diff --git a/src/renderer/store/action-types.ts b/src/renderer/store/action-types.ts index 3c91cd06e..5ae36497f 100644 --- a/src/renderer/store/action-types.ts +++ b/src/renderer/store/action-types.ts @@ -39,6 +39,8 @@ enum ActionTypes { TOAST_ADD = 'TOAST_ADD', TOAST_REMOVE = 'TOAST_REMOVE', + SET_LIBRARY_LAYOUT = 'SET_LIBRARY_LAYOUT', + NOTIFICATION_NEW = 'NOTIFICATION_NEW', } diff --git a/src/renderer/store/actions/LibraryActions.ts b/src/renderer/store/actions/LibraryActions.ts index bea7f1f73..ce4e61501 100644 --- a/src/renderer/store/actions/LibraryActions.ts +++ b/src/renderer/store/actions/LibraryActions.ts @@ -67,6 +67,18 @@ export const sort = (sortBy: SortBy): void => { }); }; +// idk what to name or where to put this +export interface LibraryLayoutSettings { + visibility: string[]; +} + +export const set_context_state = (state: LibraryLayoutSettings): void => { + store.dispatch({ + type: types.SET_LIBRARY_LAYOUT, + payload: state, + }); +}; + const scanPlaylists = async (paths: string[]) => { return Promise.all( paths.map(async (filePath) => { diff --git a/src/renderer/store/actions/PlaylistsActions.ts b/src/renderer/store/actions/PlaylistsActions.ts index 9c0c7e9f3..e92382d49 100644 --- a/src/renderer/store/actions/PlaylistsActions.ts +++ b/src/renderer/store/actions/PlaylistsActions.ts @@ -31,6 +31,7 @@ export const load = async (_id: string): Promise => { try { const playlist = await app.db.Playlist.findOneAsync({ _id }); const tracks = await app.db.Track.findAsync({ _id: { $in: playlist.tracks } }); + store.dispatch({ type: types.PLAYLISTS_LOAD_ONE, payload: { diff --git a/src/renderer/store/reducers/library.ts b/src/renderer/store/reducers/library.ts index 1f9989840..13ade1e8c 100644 --- a/src/renderer/store/reducers/library.ts +++ b/src/renderer/store/reducers/library.ts @@ -3,6 +3,7 @@ import types from '../action-types'; import { config } from '../../lib/app'; import * as utils from '../../lib/utils'; import { Action, TrackModel, SortBy, SortOrder } from '../../../shared/types/museeks'; +import { LibraryLayoutSettings as LibraryLayoutSettings } from '../actions/LibraryActions'; export interface LibrarySort { by: SortBy; @@ -23,6 +24,7 @@ export interface LibraryState { total: number; }; highlightPlayingTrack: boolean; + libraryLayoutSettings: LibraryLayoutSettings; } const initialState: LibraryState = { @@ -39,16 +41,23 @@ const initialState: LibraryState = { total: 0, }, highlightPlayingTrack: false, + // I'm not sure if this is a good place for the default settings + // FIXME + libraryLayoutSettings: config.get('libraryLayoutSettings') || { + visibility: ['title', 'duration', 'album', 'artist', 'genre'], + }, }; export default (state = initialState, action: Action): LibraryState => { switch (action.type) { case types.LIBRARY_REFRESH: { + const prevTracks = state.tracks.playlist; + return { ...state, tracks: { library: [...action.payload.tracks], - playlist: [], + playlist: prevTracks, }, loading: false, }; @@ -82,6 +91,18 @@ export default (state = initialState, action: Action): LibraryState => { }; } + case types.SET_LIBRARY_LAYOUT: { + // const prevState = state.sort; + + config.set('libraryLayoutSettings', action.payload); + config.save(); + + return { + ...state, + libraryLayoutSettings: action.payload, + }; + } + case types.FILTER_SEARCH: { return { ...state, diff --git a/src/renderer/views/Library/Library.tsx b/src/renderer/views/Library/Library.tsx index 1b2d38756..4b2944ea7 100644 --- a/src/renderer/views/Library/Library.tsx +++ b/src/renderer/views/Library/Library.tsx @@ -77,6 +77,7 @@ const Library: React.FC = () => { return (