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,