mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-10 04:30:25 +02:00
add item list selection dialog
This commit is contained in:
@@ -2,11 +2,14 @@
|
|||||||
"action": {
|
"action": {
|
||||||
"addToFavorites": "add to $t(entity.favorite_other)",
|
"addToFavorites": "add to $t(entity.favorite_other)",
|
||||||
"addToPlaylist": "add to $t(entity.playlist_one)",
|
"addToPlaylist": "add to $t(entity.playlist_one)",
|
||||||
|
"addOrRemoveFromSelection": "add or remove from selection",
|
||||||
|
"selectRangeOfItems": "select a range of items",
|
||||||
"clearQueue": "clear queue",
|
"clearQueue": "clear queue",
|
||||||
"createPlaylist": "create $t(entity.playlist_one)",
|
"createPlaylist": "create $t(entity.playlist_one)",
|
||||||
"createRadioStation": "create $t(entity.radioStation_one)",
|
"createRadioStation": "create $t(entity.radioStation_one)",
|
||||||
"deletePlaylist": "delete $t(entity.playlist_one)",
|
"deletePlaylist": "delete $t(entity.playlist_one)",
|
||||||
"deleteRadioStation": "delete $t(entity.radioStation_one)",
|
"deleteRadioStation": "delete $t(entity.radioStation_one)",
|
||||||
|
"selectAll": "select all",
|
||||||
"deselectAll": "deselect all",
|
"deselectAll": "deselect all",
|
||||||
"downloadStarted": "started download of {{count}} items",
|
"downloadStarted": "started download of {{count}} items",
|
||||||
"editPlaylist": "edit $t(entity.playlist_one)",
|
"editPlaylist": "edit $t(entity.playlist_one)",
|
||||||
@@ -37,6 +40,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
|
"countSelected": "{{count}} selected",
|
||||||
"explicitStatus": "explicit status",
|
"explicitStatus": "explicit status",
|
||||||
"action_one": "action",
|
"action_one": "action",
|
||||||
"action_other": "actions",
|
"action_other": "actions",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
.item-grid-container {
|
.item-grid-container {
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column !important;
|
flex-direction: column !important;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import {
|
|||||||
useItemListState,
|
useItemListState,
|
||||||
useItemListStateSubscription,
|
useItemListStateSubscription,
|
||||||
} from '/@/renderer/components/item-list/helpers/item-list-state';
|
} from '/@/renderer/components/item-list/helpers/item-list-state';
|
||||||
|
import { SelectionDialog } from '/@/renderer/components/item-list/selection-dialog';
|
||||||
import { ItemControls, ItemListHandle } from '/@/renderer/components/item-list/types';
|
import { ItemControls, ItemListHandle } from '/@/renderer/components/item-list/types';
|
||||||
import { animationProps } from '/@/shared/components/animations/animation-props';
|
import { animationProps } from '/@/shared/components/animations/animation-props';
|
||||||
import { useElementSize } from '/@/shared/hooks/use-element-size';
|
import { useElementSize } from '/@/shared/hooks/use-element-size';
|
||||||
@@ -742,7 +743,10 @@ const BaseItemGridList = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</AutoSizer>
|
</AutoSizer>
|
||||||
<ExpandedContainer internalState={internalState} itemType={itemType} />
|
<AnimatePresence presenceAffectsLayout>
|
||||||
|
<ExpandedContainer internalState={internalState} itemType={itemType} />
|
||||||
|
<SelectionDialog internalState={internalState} />
|
||||||
|
</AnimatePresence>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
.item-table-list-container {
|
.item-table-list-container {
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import {
|
|||||||
import { parseTableColumns } from '/@/renderer/components/item-list/helpers/parse-table-columns';
|
import { parseTableColumns } from '/@/renderer/components/item-list/helpers/parse-table-columns';
|
||||||
import { useStickyTableGroupRows } from '/@/renderer/components/item-list/item-table-list/hooks/use-sticky-table-group-rows';
|
import { useStickyTableGroupRows } from '/@/renderer/components/item-list/item-table-list/hooks/use-sticky-table-group-rows';
|
||||||
import { useStickyTableHeader } from '/@/renderer/components/item-list/item-table-list/hooks/use-sticky-table-header';
|
import { useStickyTableHeader } from '/@/renderer/components/item-list/item-table-list/hooks/use-sticky-table-header';
|
||||||
|
import { SelectionDialog } from '/@/renderer/components/item-list/selection-dialog';
|
||||||
import {
|
import {
|
||||||
ItemControls,
|
ItemControls,
|
||||||
ItemListHandle,
|
ItemListHandle,
|
||||||
@@ -2318,6 +2319,7 @@ const BaseItemTableList = ({
|
|||||||
totalRowCount={totalRowCount}
|
totalRowCount={totalRowCount}
|
||||||
/>
|
/>
|
||||||
<ExpandedContainer internalState={internalState} itemType={itemType} />
|
<ExpandedContainer internalState={internalState} itemType={itemType} />
|
||||||
|
<SelectionDialog internalState={internalState} />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
.selection-indicator {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 50%;
|
||||||
|
z-index: 100;
|
||||||
|
min-width: 320px;
|
||||||
|
padding: var(--theme-spacing-sm) var(--theme-spacing-md);
|
||||||
|
color: var(--theme-colors-surface-foreground);
|
||||||
|
background: color-mix(in srgb, var(--theme-colors-surface) 85%, transparent);
|
||||||
|
border: 1px solid color-mix(in srgb, var(--theme-colors-border) 50%, transparent);
|
||||||
|
border-radius: var(--theme-radius-md);
|
||||||
|
box-shadow:
|
||||||
|
2px 2px 10px 2px rgb(0 0 0 / 40%),
|
||||||
|
0 0 0 1px rgb(255 255 255 / 5%);
|
||||||
|
backdrop-filter: blur(12px) saturate(180%);
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-icon {
|
||||||
|
display: flex;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
import { AnimatePresence, motion } from 'motion/react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import styles from './selection-dialog.module.css';
|
||||||
|
|
||||||
|
import i18n from '/@/i18n/i18n';
|
||||||
|
import {
|
||||||
|
ItemListStateActions,
|
||||||
|
useItemListStateSubscription,
|
||||||
|
} from '/@/renderer/components/item-list/helpers/item-list-state';
|
||||||
|
import { ContextMenuController } from '/@/renderer/features/context-menu/context-menu-controller';
|
||||||
|
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||||
|
import { animationProps } from '/@/shared/components/animations/animation-props';
|
||||||
|
import { Group } from '/@/shared/components/group/group';
|
||||||
|
import { HoverCard } from '/@/shared/components/hover-card/hover-card';
|
||||||
|
import { Icon } from '/@/shared/components/icon/icon';
|
||||||
|
import { Kbd } from '/@/shared/components/kbd/kbd';
|
||||||
|
import { Table } from '/@/shared/components/table/table';
|
||||||
|
import { Text } from '/@/shared/components/text/text';
|
||||||
|
|
||||||
|
const controls = [
|
||||||
|
{
|
||||||
|
control1: <Kbd>CTRL</Kbd>,
|
||||||
|
control2: <Kbd>A</Kbd>,
|
||||||
|
label: i18n.t('action.selectAll', { postProcess: 'sentenceCase' }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
control1: <Kbd>CTRL</Kbd>,
|
||||||
|
control2: <Icon fill="default" icon="mouseLeftClick" />,
|
||||||
|
label: i18n.t('action.addOrRemoveFromSelection', { postProcess: 'sentenceCase' }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
control1: <Kbd>SHIFT</Kbd>,
|
||||||
|
control2: <Icon fill="default" icon="mouseLeftClick" />,
|
||||||
|
label: i18n.t('action.selectRangeOfItems', { postProcess: 'sentenceCase' }),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const SelectionDialog = ({ internalState }: { internalState: ItemListStateActions }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const isListExpanded = useItemListStateSubscription(internalState, (state) =>
|
||||||
|
state ? state.expanded.size > 0 : false,
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectedCount = useItemListStateSubscription(internalState, (state) =>
|
||||||
|
state ? state.selected.size : 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleClearSelection = () => {
|
||||||
|
internalState.clearSelected();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpenMoreActions = (event: React.MouseEvent<unknown>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
const selectedItems = internalState.getSelected();
|
||||||
|
|
||||||
|
if (selectedItems.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ContextMenuController.call({
|
||||||
|
cmd: { items: selectedItems as any[], type: (selectedItems[0] as any)._itemType },
|
||||||
|
event,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const isOpen = selectedCount > 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatePresence initial={false} mode="sync">
|
||||||
|
{isOpen && (
|
||||||
|
<motion.div
|
||||||
|
{...animationProps.fadeIn}
|
||||||
|
className={styles.selectionIndicator}
|
||||||
|
style={{ bottom: isListExpanded ? '320px' : '1rem' }}
|
||||||
|
>
|
||||||
|
<Group gap="xl" justify="space-between">
|
||||||
|
<Group gap="sm">
|
||||||
|
<HoverCard offset={20} position="top">
|
||||||
|
<HoverCard.Target>
|
||||||
|
<span className={styles.infoIcon}>
|
||||||
|
<Icon icon="keyboard" />
|
||||||
|
</span>
|
||||||
|
</HoverCard.Target>
|
||||||
|
<HoverCard.Dropdown>
|
||||||
|
<Table>
|
||||||
|
<Table.Tbody>
|
||||||
|
{controls.map((control) => (
|
||||||
|
<Table.Tr key={control.label}>
|
||||||
|
<Table.Td ta="start">
|
||||||
|
{control.control1}
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>+</Table.Td>
|
||||||
|
<Table.Td ta="center">
|
||||||
|
{control.control2}
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<Text size="xs">{control.label}</Text>
|
||||||
|
</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
))}
|
||||||
|
</Table.Tbody>
|
||||||
|
</Table>
|
||||||
|
</HoverCard.Dropdown>
|
||||||
|
</HoverCard>
|
||||||
|
<Text fw={500} isNoSelect size="sm">
|
||||||
|
{t('common.countSelected', { count: selectedCount })}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Group gap="xs">
|
||||||
|
<ActionIcon
|
||||||
|
icon="x"
|
||||||
|
iconProps={{ size: 'xl' }}
|
||||||
|
onClick={handleClearSelection}
|
||||||
|
size="xs"
|
||||||
|
variant="subtle"
|
||||||
|
/>
|
||||||
|
<ActionIcon
|
||||||
|
icon="ellipsisHorizontal"
|
||||||
|
iconProps={{ size: 'xl' }}
|
||||||
|
onClick={handleOpenMoreActions}
|
||||||
|
size="xs"
|
||||||
|
variant="subtle"
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -114,6 +114,7 @@ import {
|
|||||||
LuX,
|
LuX,
|
||||||
} from 'react-icons/lu';
|
} from 'react-icons/lu';
|
||||||
import { MdOutlineVisibility, MdOutlineVisibilityOff } from 'react-icons/md';
|
import { MdOutlineVisibility, MdOutlineVisibilityOff } from 'react-icons/md';
|
||||||
|
import { PiMouseLeftClickFill, PiMouseRightClickFill } from 'react-icons/pi';
|
||||||
import { RiPlayListAddLine, RiRepeat2Line, RiRepeatOneLine } from 'react-icons/ri';
|
import { RiPlayListAddLine, RiRepeat2Line, RiRepeatOneLine } from 'react-icons/ri';
|
||||||
import { SiMusicbrainz } from 'react-icons/si';
|
import { SiMusicbrainz } from 'react-icons/si';
|
||||||
|
|
||||||
@@ -202,6 +203,8 @@ export const AppIcon = {
|
|||||||
metadata: LuBookOpen,
|
metadata: LuBookOpen,
|
||||||
microphone: LuMicVocal,
|
microphone: LuMicVocal,
|
||||||
minus: LuMinus,
|
minus: LuMinus,
|
||||||
|
mouseLeftClick: PiMouseLeftClickFill,
|
||||||
|
mouseRightClick: PiMouseRightClickFill,
|
||||||
panelRightClose: LuPanelRightClose,
|
panelRightClose: LuPanelRightClose,
|
||||||
panelRightOpen: LuPanelRightOpen,
|
panelRightOpen: LuPanelRightOpen,
|
||||||
pin: LuPin,
|
pin: LuPin,
|
||||||
|
|||||||
Reference in New Issue
Block a user