From e21f538aa49ce86d21c735b113685d9db62f4021 Mon Sep 17 00:00:00 2001 From: jeffvli Date: Tue, 16 Dec 2025 20:24:46 -0800 Subject: [PATCH] add item list selection dialog --- src/i18n/locales/en.json | 4 + .../item-grid-list/item-grid-list.module.css | 1 + .../item-grid-list/item-grid-list.tsx | 6 +- .../item-table-list.module.css | 1 + .../item-table-list/item-table-list.tsx | 2 + .../item-list/selection-dialog.module.css | 22 +++ .../components/item-list/selection-dialog.tsx | 135 ++++++++++++++++++ src/shared/components/icon/icon.tsx | 3 + 8 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 src/renderer/components/item-list/selection-dialog.module.css create mode 100644 src/renderer/components/item-list/selection-dialog.tsx diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 330a5b325..398932163 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -2,11 +2,14 @@ "action": { "addToFavorites": "add to $t(entity.favorite_other)", "addToPlaylist": "add to $t(entity.playlist_one)", + "addOrRemoveFromSelection": "add or remove from selection", + "selectRangeOfItems": "select a range of items", "clearQueue": "clear queue", "createPlaylist": "create $t(entity.playlist_one)", "createRadioStation": "create $t(entity.radioStation_one)", "deletePlaylist": "delete $t(entity.playlist_one)", "deleteRadioStation": "delete $t(entity.radioStation_one)", + "selectAll": "select all", "deselectAll": "deselect all", "downloadStarted": "started download of {{count}} items", "editPlaylist": "edit $t(entity.playlist_one)", @@ -37,6 +40,7 @@ } }, "common": { + "countSelected": "{{count}} selected", "explicitStatus": "explicit status", "action_one": "action", "action_other": "actions", diff --git a/src/renderer/components/item-list/item-grid-list/item-grid-list.module.css b/src/renderer/components/item-list/item-grid-list/item-grid-list.module.css index c901f7520..e7fbbcae3 100644 --- a/src/renderer/components/item-list/item-grid-list/item-grid-list.module.css +++ b/src/renderer/components/item-list/item-grid-list/item-grid-list.module.css @@ -1,4 +1,5 @@ .item-grid-container { + position: relative; display: flex; flex-direction: column !important; width: 100%; diff --git a/src/renderer/components/item-list/item-grid-list/item-grid-list.tsx b/src/renderer/components/item-list/item-grid-list/item-grid-list.tsx index 8aba2f3c3..b7c6f185e 100644 --- a/src/renderer/components/item-list/item-grid-list/item-grid-list.tsx +++ b/src/renderer/components/item-list/item-grid-list/item-grid-list.tsx @@ -41,6 +41,7 @@ import { useItemListState, useItemListStateSubscription, } 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 { animationProps } from '/@/shared/components/animations/animation-props'; import { useElementSize } from '/@/shared/hooks/use-element-size'; @@ -742,7 +743,10 @@ const BaseItemGridList = ({ /> )} - + + + + ); }; diff --git a/src/renderer/components/item-list/item-table-list/item-table-list.module.css b/src/renderer/components/item-list/item-table-list/item-table-list.module.css index 5e323aa33..8ca518124 100644 --- a/src/renderer/components/item-list/item-table-list/item-table-list.module.css +++ b/src/renderer/components/item-list/item-table-list/item-table-list.module.css @@ -1,4 +1,5 @@ .item-table-list-container { + position: relative; display: flex; flex-direction: column; height: 100%; diff --git a/src/renderer/components/item-list/item-table-list/item-table-list.tsx b/src/renderer/components/item-list/item-table-list/item-table-list.tsx index acaf31fca..454a8f029 100644 --- a/src/renderer/components/item-list/item-table-list/item-table-list.tsx +++ b/src/renderer/components/item-list/item-table-list/item-table-list.tsx @@ -34,6 +34,7 @@ import { 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 { useStickyTableHeader } from '/@/renderer/components/item-list/item-table-list/hooks/use-sticky-table-header'; +import { SelectionDialog } from '/@/renderer/components/item-list/selection-dialog'; import { ItemControls, ItemListHandle, @@ -2318,6 +2319,7 @@ const BaseItemTableList = ({ totalRowCount={totalRowCount} /> + ); }; diff --git a/src/renderer/components/item-list/selection-dialog.module.css b/src/renderer/components/item-list/selection-dialog.module.css new file mode 100644 index 000000000..4ffa6309b --- /dev/null +++ b/src/renderer/components/item-list/selection-dialog.module.css @@ -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; +} diff --git a/src/renderer/components/item-list/selection-dialog.tsx b/src/renderer/components/item-list/selection-dialog.tsx new file mode 100644 index 000000000..203fd0ac7 --- /dev/null +++ b/src/renderer/components/item-list/selection-dialog.tsx @@ -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: CTRL, + control2: A, + label: i18n.t('action.selectAll', { postProcess: 'sentenceCase' }), + }, + { + control1: CTRL, + control2: , + label: i18n.t('action.addOrRemoveFromSelection', { postProcess: 'sentenceCase' }), + }, + { + control1: SHIFT, + control2: , + 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) => { + 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 ( + + {isOpen && ( + + + + + + + + + + + + + {controls.map((control) => ( + + + {control.control1} + + + + + {control.control2} + + + {control.label} + + + ))} + +
+
+
+ + {t('common.countSelected', { count: selectedCount })} + +
+ + + + + +
+
+ )} +
+ ); +}; diff --git a/src/shared/components/icon/icon.tsx b/src/shared/components/icon/icon.tsx index 794949cf7..0031db60f 100644 --- a/src/shared/components/icon/icon.tsx +++ b/src/shared/components/icon/icon.tsx @@ -114,6 +114,7 @@ import { LuX, } from 'react-icons/lu'; import { MdOutlineVisibility, MdOutlineVisibilityOff } from 'react-icons/md'; +import { PiMouseLeftClickFill, PiMouseRightClickFill } from 'react-icons/pi'; import { RiPlayListAddLine, RiRepeat2Line, RiRepeatOneLine } from 'react-icons/ri'; import { SiMusicbrainz } from 'react-icons/si'; @@ -202,6 +203,8 @@ export const AppIcon = { metadata: LuBookOpen, microphone: LuMicVocal, minus: LuMinus, + mouseLeftClick: PiMouseLeftClickFill, + mouseRightClick: PiMouseRightClickFill, panelRightClose: LuPanelRightClose, panelRightOpen: LuPanelRightOpen, pin: LuPin,