New: Favorite folders in Manual Import

(cherry picked from commit 3ddc6ac6de5c27a9aab915672321c8818dc5da48)

Closes #10630
This commit is contained in:
Mark McDowall 2024-10-26 21:57:03 -07:00 committed by Bogdan
parent 9ab3e6bab7
commit 3b9bd696fb
14 changed files with 251 additions and 93 deletions

View file

@ -1,11 +1,20 @@
import AppSectionState from 'App/State/AppSectionState'; import AppSectionState from 'App/State/AppSectionState';
import RecentFolder from 'InteractiveImport/Folder/RecentFolder';
import ImportMode from 'InteractiveImport/ImportMode'; import ImportMode from 'InteractiveImport/ImportMode';
import InteractiveImport from 'InteractiveImport/InteractiveImport'; import InteractiveImport from 'InteractiveImport/InteractiveImport';
interface FavoriteFolder {
folder: string;
}
interface RecentFolder {
folder: string;
lastUsed: string;
}
interface InteractiveImportAppState extends AppSectionState<InteractiveImport> { interface InteractiveImportAppState extends AppSectionState<InteractiveImport> {
originalItems: InteractiveImport[]; originalItems: InteractiveImport[];
importMode: ImportMode; importMode: ImportMode;
favoriteFolders: FavoriteFolder[];
recentFolders: RecentFolder[]; recentFolders: RecentFolder[];
} }

View file

@ -15,6 +15,7 @@ import {
faFileVideo as farFileVideo, faFileVideo as farFileVideo,
faFolder as farFolder, faFolder as farFolder,
faHdd as farHdd, faHdd as farHdd,
faHeart as farHeart,
faKeyboard as farKeyboard, faKeyboard as farKeyboard,
faObjectGroup as farObjectGroup, faObjectGroup as farObjectGroup,
faObjectUngroup as farObjectUngroup, faObjectUngroup as farObjectUngroup,
@ -174,6 +175,7 @@ export const GENRE = fasTheaterMasks;
export const GROUP = farObjectGroup; export const GROUP = farObjectGroup;
export const HEALTH = fasMedkit; export const HEALTH = fasMedkit;
export const HEART = fasHeart; export const HEART = fasHeart;
export const HEART_OUTLINE = farHeart;
export const HISTORY = fasHistory; export const HISTORY = fasHistory;
export const HOUSEKEEPING = fasHome; export const HOUSEKEEPING = fasHome;
export const IGNORE = fasTimesCircle; export const IGNORE = fasTimesCircle;

View file

@ -0,0 +1,5 @@
.actions {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 70px;
}

View file

@ -0,0 +1,7 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'actions': string;
}
export const cssExports: CssExports;
export default cssExports;

View file

@ -0,0 +1,48 @@
import React, { SyntheticEvent, useCallback } from 'react';
import { useDispatch } from 'react-redux';
import IconButton from 'Components/Link/IconButton';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRowButton from 'Components/Table/TableRowButton';
import { icons } from 'Helpers/Props';
import { removeFavoriteFolder } from 'Store/Actions/interactiveImportActions';
import translate from 'Utilities/String/translate';
import styles from './FavoriteFolderRow.css';
interface FavoriteFolderRowProps {
folder: string;
onPress: (folder: string) => unknown;
}
function FavoriteFolderRow({ folder, onPress }: FavoriteFolderRowProps) {
const dispatch = useDispatch();
const handlePress = useCallback(() => {
onPress(folder);
}, [folder, onPress]);
const handleRemoveFavoritePress = useCallback(
(e: SyntheticEvent) => {
e.stopPropagation();
dispatch(removeFavoriteFolder({ folder }));
},
[folder, dispatch]
);
return (
<TableRowButton onPress={handlePress}>
<TableRowCell>{folder}</TableRowCell>
<TableRowCell className={styles.actions}>
<IconButton
title={translate('FavoriteFolderRemove')}
kind="danger"
name={icons.HEART}
onPress={handleRemoveFavoritePress}
/>
</TableRowCell>
</TableRowButton>
);
}
export default FavoriteFolderRow;

View file

@ -1,7 +1,12 @@
.recentFoldersContainer { .foldersContainer {
margin-top: 15px; margin-top: 15px;
} }
.foldersTitle {
border-bottom: 1px solid var(--borderColor);
font-size: 21px;
}
.buttonsContainer { .buttonsContainer {
margin-top: 30px; margin-top: 30px;
} }

View file

@ -5,7 +5,8 @@ interface CssExports {
'buttonContainer': string; 'buttonContainer': string;
'buttonIcon': string; 'buttonIcon': string;
'buttonsContainer': string; 'buttonsContainer': string;
'recentFoldersContainer': string; 'foldersContainer': string;
'foldersTitle': string;
} }
export const cssExports: CssExports; export const cssExports: CssExports;
export default cssExports; export default cssExports;

View file

@ -1,4 +1,4 @@
import React, { useCallback, useState } from 'react'; import React, { useCallback, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import AppState from 'App/State/AppState'; import AppState from 'App/State/AppState';
@ -14,14 +14,23 @@ import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody'; import TableBody from 'Components/Table/TableBody';
import { icons, kinds, sizes } from 'Helpers/Props'; import { icons, kinds, sizes } from 'Helpers/Props';
import { executeCommand } from 'Store/Actions/commandActions'; import { executeCommand } from 'Store/Actions/commandActions';
import { import { addRecentFolder } from 'Store/Actions/interactiveImportActions';
addRecentFolder,
removeRecentFolder,
} from 'Store/Actions/interactiveImportActions';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import FavoriteFolderRow from './FavoriteFolderRow';
import RecentFolderRow from './RecentFolderRow'; import RecentFolderRow from './RecentFolderRow';
import styles from './InteractiveImportSelectFolderModalContent.css'; import styles from './InteractiveImportSelectFolderModalContent.css';
const favoriteFoldersColumns = [
{
name: 'folder',
label: () => translate('Folder'),
},
{
name: 'actions',
label: '',
},
];
const recentFoldersColumns = [ const recentFoldersColumns = [
{ {
name: 'folder', name: 'folder',
@ -49,15 +58,22 @@ function InteractiveImportSelectFolderModalContent(
const { modalTitle, onFolderSelect, onModalClose } = props; const { modalTitle, onFolderSelect, onModalClose } = props;
const [folder, setFolder] = useState(''); const [folder, setFolder] = useState('');
const dispatch = useDispatch(); const dispatch = useDispatch();
const recentFolders = useSelector( const { favoriteFolders, recentFolders } = useSelector(
createSelector( createSelector(
(state: AppState) => state.interactiveImport.recentFolders, (state: AppState) => state.interactiveImport,
(recentFolders) => { (interactiveImport) => {
return recentFolders; return {
favoriteFolders: interactiveImport.favoriteFolders,
recentFolders: interactiveImport.recentFolders,
};
} }
) )
); );
const favoriteFolderMap = useMemo(() => {
return new Map(favoriteFolders.map((f) => [f.folder, f]));
}, [favoriteFolders]);
const onPathChange = useCallback( const onPathChange = useCallback(
({ value }: { value: string }) => { ({ value }: { value: string }) => {
setFolder(value); setFolder(value);
@ -90,13 +106,6 @@ function InteractiveImportSelectFolderModalContent(
onFolderSelect(folder); onFolderSelect(folder);
}, [folder, onFolderSelect, dispatch]); }, [folder, onFolderSelect, dispatch]);
const onRemoveRecentFolderPress = useCallback(
(folderToRemove: string) => {
dispatch(removeRecentFolder({ folder: folderToRemove }));
},
[dispatch]
);
return ( return (
<ModalContent onModalClose={onModalClose}> <ModalContent onModalClose={onModalClose}>
<ModalHeader> <ModalHeader>
@ -110,8 +119,34 @@ function InteractiveImportSelectFolderModalContent(
onChange={onPathChange} onChange={onPathChange}
/> />
{favoriteFolders.length ? (
<div className={styles.foldersContainer}>
<div className={styles.foldersTitle}>
{translate('FavoriteFolders')}
</div>
<Table columns={favoriteFoldersColumns}>
<TableBody>
{favoriteFolders.map((favoriteFolder) => {
return (
<FavoriteFolderRow
key={favoriteFolder.folder}
folder={favoriteFolder.folder}
onPress={onRecentPathPress}
/>
);
})}
</TableBody>
</Table>
</div>
) : null}
{recentFolders.length ? ( {recentFolders.length ? (
<div className={styles.recentFoldersContainer}> <div className={styles.foldersContainer}>
<div className={styles.foldersTitle}>
{translate('RecentFolders')}
</div>
<Table columns={recentFoldersColumns}> <Table columns={recentFoldersColumns}>
<TableBody> <TableBody>
{recentFolders {recentFolders
@ -123,8 +158,8 @@ function InteractiveImportSelectFolderModalContent(
key={recentFolder.folder} key={recentFolder.folder}
folder={recentFolder.folder} folder={recentFolder.folder}
lastUsed={recentFolder.lastUsed} lastUsed={recentFolder.lastUsed}
isFavorite={favoriteFolderMap.has(recentFolder.folder)}
onPress={onRecentPathPress} onPress={onRecentPathPress}
onRemoveRecentFolderPress={onRemoveRecentFolderPress}
/> />
); );
})} })}

View file

@ -1,6 +0,0 @@
interface RecentFolder {
folder: string;
lastUsed: string;
}
export default RecentFolder;

View file

@ -1,5 +1,5 @@
.actions { .actions {
composes: cell from '~Components/Table/Cells/TableRowCell.css'; composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 40px; width: 70px;
} }

View file

@ -1,65 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import IconButton from 'Components/Link/IconButton';
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRowButton from 'Components/Table/TableRowButton';
import { icons } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './RecentFolderRow.css';
class RecentFolderRow extends Component {
//
// Listeners
onPress = () => {
this.props.onPress(this.props.folder);
};
onRemovePress = (event) => {
event.stopPropagation();
const {
folder,
onRemoveRecentFolderPress
} = this.props;
onRemoveRecentFolderPress(folder);
};
//
// Render
render() {
const {
folder,
lastUsed
} = this.props;
return (
<TableRowButton onPress={this.onPress}>
<TableRowCell>{folder}</TableRowCell>
<RelativeDateCell date={lastUsed} />
<TableRowCell className={styles.actions}>
<IconButton
title={translate('Remove')}
name={icons.REMOVE}
onPress={this.onRemovePress}
/>
</TableRowCell>
</TableRowButton>
);
}
}
RecentFolderRow.propTypes = {
folder: PropTypes.string.isRequired,
lastUsed: PropTypes.string.isRequired,
onPress: PropTypes.func.isRequired,
onRemoveRecentFolderPress: PropTypes.func.isRequired
};
export default RecentFolderRow;

View file

@ -0,0 +1,85 @@
import React, { SyntheticEvent, useCallback } from 'react';
import { useDispatch } from 'react-redux';
import IconButton from 'Components/Link/IconButton';
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRowButton from 'Components/Table/TableRowButton';
import { icons } from 'Helpers/Props';
import {
addFavoriteFolder,
removeFavoriteFolder,
removeRecentFolder,
} from 'Store/Actions/interactiveImportActions';
import translate from 'Utilities/String/translate';
import styles from './RecentFolderRow.css';
interface RecentFolderRowProps {
folder: string;
lastUsed: string;
isFavorite: boolean;
onPress: (folder: string) => unknown;
}
function RecentFolderRow({
folder,
lastUsed,
isFavorite,
onPress,
}: RecentFolderRowProps) {
const dispatch = useDispatch();
const handlePress = useCallback(() => {
onPress(folder);
}, [folder, onPress]);
const handleFavoritePress = useCallback(
(e: SyntheticEvent) => {
e.stopPropagation();
if (isFavorite) {
dispatch(removeFavoriteFolder({ folder }));
} else {
dispatch(addFavoriteFolder({ folder }));
}
},
[folder, isFavorite, dispatch]
);
const handleRemovePress = useCallback(
(e: SyntheticEvent) => {
e.stopPropagation();
dispatch(removeRecentFolder({ folder }));
},
[folder, dispatch]
);
return (
<TableRowButton onPress={handlePress}>
<TableRowCell>{folder}</TableRowCell>
<RelativeDateCell date={lastUsed} />
<TableRowCell className={styles.actions}>
<IconButton
title={
isFavorite
? translate('FavoriteFolderRemove')
: translate('FavoriteFolderAdd')
}
kind={isFavorite ? 'danger' : 'default'}
name={isFavorite ? icons.HEART : icons.HEART_OUTLINE}
onPress={handleFavoritePress}
/>
<IconButton
title={translate('Remove')}
name={icons.REMOVE}
onPress={handleRemovePress}
/>
</TableRowCell>
</TableRowButton>
);
}
export default RecentFolderRow;

View file

@ -3,6 +3,7 @@ import { createAction } from 'redux-actions';
import { batchActions } from 'redux-batched-actions'; import { batchActions } from 'redux-batched-actions';
import { sortDirections } from 'Helpers/Props'; import { sortDirections } from 'Helpers/Props';
import { createThunk, handleThunks } from 'Store/thunks'; import { createThunk, handleThunks } from 'Store/thunks';
import sortByProp from 'Utilities/Array/sortByProp';
import createAjaxRequest from 'Utilities/createAjaxRequest'; import createAjaxRequest from 'Utilities/createAjaxRequest';
import naturalExpansion from 'Utilities/String/naturalExpansion'; import naturalExpansion from 'Utilities/String/naturalExpansion';
import { set, update, updateItem } from './baseActions'; import { set, update, updateItem } from './baseActions';
@ -30,6 +31,7 @@ export const defaultState = {
items: [], items: [],
sortKey: 'relativePath', sortKey: 'relativePath',
sortDirection: sortDirections.ASCENDING, sortDirection: sortDirections.ASCENDING,
favoriteFolders: [],
recentFolders: [], recentFolders: [],
importMode: 'chooseImportMode', importMode: 'chooseImportMode',
sortPredicates: { sortPredicates: {
@ -58,6 +60,7 @@ export const defaultState = {
export const persistState = [ export const persistState = [
'interactiveImport.sortKey', 'interactiveImport.sortKey',
'interactiveImport.sortDirection', 'interactiveImport.sortDirection',
'interactiveImport.favoriteFolders',
'interactiveImport.recentFolders', 'interactiveImport.recentFolders',
'interactiveImport.importMode' 'interactiveImport.importMode'
]; ];
@ -73,6 +76,8 @@ export const UPDATE_INTERACTIVE_IMPORT_ITEMS = 'interactiveImport/updateInteract
export const CLEAR_INTERACTIVE_IMPORT = 'interactiveImport/clearInteractiveImport'; export const CLEAR_INTERACTIVE_IMPORT = 'interactiveImport/clearInteractiveImport';
export const ADD_RECENT_FOLDER = 'interactiveImport/addRecentFolder'; export const ADD_RECENT_FOLDER = 'interactiveImport/addRecentFolder';
export const REMOVE_RECENT_FOLDER = 'interactiveImport/removeRecentFolder'; export const REMOVE_RECENT_FOLDER = 'interactiveImport/removeRecentFolder';
export const ADD_FAVORITE_FOLDER = 'interactiveImport/addFavoriteFolder';
export const REMOVE_FAVORITE_FOLDER = 'interactiveImport/removeFavoriteFolder';
export const SET_INTERACTIVE_IMPORT_MODE = 'interactiveImport/setInteractiveImportMode'; export const SET_INTERACTIVE_IMPORT_MODE = 'interactiveImport/setInteractiveImportMode';
// //
@ -86,6 +91,8 @@ export const updateInteractiveImportItems = createAction(UPDATE_INTERACTIVE_IMPO
export const clearInteractiveImport = createAction(CLEAR_INTERACTIVE_IMPORT); export const clearInteractiveImport = createAction(CLEAR_INTERACTIVE_IMPORT);
export const addRecentFolder = createAction(ADD_RECENT_FOLDER); export const addRecentFolder = createAction(ADD_RECENT_FOLDER);
export const removeRecentFolder = createAction(REMOVE_RECENT_FOLDER); export const removeRecentFolder = createAction(REMOVE_RECENT_FOLDER);
export const addFavoriteFolder = createAction(ADD_FAVORITE_FOLDER);
export const removeFavoriteFolder = createAction(REMOVE_FAVORITE_FOLDER);
export const setInteractiveImportMode = createAction(SET_INTERACTIVE_IMPORT_MODE); export const setInteractiveImportMode = createAction(SET_INTERACTIVE_IMPORT_MODE);
// //
@ -264,9 +271,31 @@ export const reducers = createHandleActions({
return Object.assign({}, state, { recentFolders }); return Object.assign({}, state, { recentFolders });
}, },
[ADD_FAVORITE_FOLDER]: function(state, { payload }) {
const folder = payload.folder;
const favoriteFolder = { folder };
const favoriteFolders = [...state.favoriteFolders, favoriteFolder].sort(sortByProp('folder'));
return Object.assign({}, state, { favoriteFolders });
},
[REMOVE_FAVORITE_FOLDER]: function(state, { payload }) {
const folder = payload.folder;
const favoriteFolders = state.favoriteFolders.reduce((acc, item) => {
if (item.folder !== folder) {
acc.push(item);
}
return acc;
}, []);
return Object.assign({}, state, { favoriteFolders });
},
[CLEAR_INTERACTIVE_IMPORT]: function(state) { [CLEAR_INTERACTIVE_IMPORT]: function(state) {
const newState = { const newState = {
...defaultState, ...defaultState,
favoriteFolders: state.favoriteFolders,
recentFolders: state.recentFolders, recentFolders: state.recentFolders,
importMode: state.importMode importMode: state.importMode
}; };

View file

@ -629,6 +629,9 @@
"FailedToLoadMovieFromAPI": "Failed to load movie from API", "FailedToLoadMovieFromAPI": "Failed to load movie from API",
"FailedToUpdateSettings": "Failed to update settings", "FailedToUpdateSettings": "Failed to update settings",
"False": "False", "False": "False",
"FavoriteFolderAdd": "Add Favorite Folder",
"FavoriteFolderRemove": "Remove Favorite Folder",
"FavoriteFolders": "Favorite Folders",
"FeatureRequests": "Feature Requests", "FeatureRequests": "Feature Requests",
"File": "File", "File": "File",
"FileBrowser": "File Browser", "FileBrowser": "File Browser",