From 7a8e68c8cf23fd1a8c61fa5d05ea9848479d6cb3 Mon Sep 17 00:00:00 2001 From: Adam Benhassen Date: Sun, 26 Jul 2026 00:37:27 +0200 Subject: [PATCH] feat: add flat layout toggle for song tables One click swaps a song table between its default columns and a flat layout (index, title, artist, album, date added, duration) with no artwork. Reaching that layout previously meant toggling eight columns by hand in the config modal, per list. Switching back restores the list's factory defaults rather than a hardcoded song order, so album detail keeps its track number column and playlist detail keeps its reorder handle. Compact row height drops from 40px to 32px; 40px left too much dead space for a single line of text. --- src/i18n/locales/en.json | 3 + .../item-table-list/column-presets.ts | 54 +++++++++++++++++ .../item-table-list/item-table-list.tsx | 2 +- .../components/album-detail-content.tsx | 2 + .../album-artist-detail-content.tsx | 3 + .../components/folder-list-header-filters.tsx | 2 + .../components/play-queue-list-controls.tsx | 4 +- ...aylist-detail-song-list-header-filters.tsx | 14 +++-- .../search/components/search-header.tsx | 8 ++- .../components/list-layout-toggle-button.tsx | 58 +++++++++++++++++++ .../components/song-list-header-filters.tsx | 2 + src/renderer/store/settings.store.ts | 3 + 12 files changed, 147 insertions(+), 8 deletions(-) create mode 100644 src/renderer/components/item-list/item-table-list/column-presets.ts create mode 100644 src/renderer/features/shared/components/list-layout-toggle-button.tsx diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index b251ec10a3..c26bf8cc62 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -1346,6 +1346,9 @@ "trackNumber": "Track Number", "year": "$t(common.year)" }, + "layout": { + "toggle": "Switch layout" + }, "view": { "detail": "Detail", "grid": "Grid", diff --git a/src/renderer/components/item-list/item-table-list/column-presets.ts b/src/renderer/components/item-list/item-table-list/column-presets.ts new file mode 100644 index 0000000000..64d05c92af --- /dev/null +++ b/src/renderer/components/item-list/item-table-list/column-presets.ts @@ -0,0 +1,54 @@ +import { ItemTableListColumnConfig } from '/@/renderer/store'; +import { TableColumn } from '/@/shared/types/types'; + +/** + * Flat, artwork-free column layout: index, title, artist, album, date added, duration. + */ +export const FLAT_COLUMN_ORDER: TableColumn[] = [ + TableColumn.ROW_INDEX, + TableColumn.TITLE, + TableColumn.ARTIST, + TableColumn.ALBUM, + TableColumn.DATE_ADDED, + TableColumn.DURATION, +]; + +/** + * Enables and reorders the columns in `order`, keeping every other column in place but disabled. + * Widths, pins and alignment the user configured are preserved. + */ +export const applyColumnOrder = ( + columns: ItemTableListColumnConfig[], + order: TableColumn[], +): ItemTableListColumnConfig[] => { + const byId = new Map(columns.map((column) => [column.id, column])); + const ordered = order + .map((id) => byId.get(id)) + .filter((column): column is ItemTableListColumnConfig => column !== undefined) + .map((column) => ({ ...column, isEnabled: true })); + + const orderedIds = new Set(ordered.map((column) => column.id)); + const rest = columns + .filter((column) => !orderedIds.has(column.id)) + .map((column) => ({ ...column, isEnabled: false })); + + return [...ordered, ...rest]; +}; + +/** + * True when the enabled columns are exactly `order` (minus any column this list doesn't have). + */ +export const isColumnOrderActive = ( + columns: ItemTableListColumnConfig[], + order: TableColumn[], +): boolean => { + const availableIds = new Set(columns.map((column) => column.id)); + const expected = order.filter((id) => availableIds.has(id)); + const enabled = columns.filter((column) => column.isEnabled).map((column) => column.id); + + return ( + expected.length > 0 && + enabled.length === expected.length && + enabled.every((id, index) => id === expected[index]) + ); +}; 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 f1aa81e44d..a40a54a28d 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 @@ -121,7 +121,7 @@ const hasRequiredStateItemProperties = ( }; export enum TableItemSize { - COMPACT = 40, + COMPACT = 32, DEFAULT = 64, LARGE = 88, } diff --git a/src/renderer/features/albums/components/album-detail-content.tsx b/src/renderer/features/albums/components/album-detail-content.tsx index 58aaa8f04d..5a4781f564 100644 --- a/src/renderer/features/albums/components/album-detail-content.tsx +++ b/src/renderer/features/albums/components/album-detail-content.tsx @@ -26,6 +26,7 @@ import { ListConfigMenu, SONG_DISPLAY_TYPES, } from '/@/renderer/features/shared/components/list-config-menu'; +import { ListLayoutToggleButton } from '/@/renderer/features/shared/components/list-layout-toggle-button'; import { CLIENT_SIDE_SONG_FILTERS, ListSortByDropdownControlled, @@ -895,6 +896,7 @@ const AlbumDetailSongsTable = ({ songs }: AlbumDetailSongsTableProps) => { setSortOrder={(value) => setSortOrder(value as SortOrder)} sortOrder={sortOrder} /> + + + { + - + + {type === ItemListKey.QUEUE_SONG && } ) : ( - + <> + + + )} diff --git a/src/renderer/features/search/components/search-header.tsx b/src/renderer/features/search/components/search-header.tsx index bcb57553d7..c93ac2af3d 100644 --- a/src/renderer/features/search/components/search-header.tsx +++ b/src/renderer/features/search/components/search-header.tsx @@ -15,6 +15,7 @@ import { ListConfigMenu, SONG_DISPLAY_TYPES, } from '/@/renderer/features/shared/components/list-config-menu'; +import { ListLayoutToggleButton } from '/@/renderer/features/shared/components/list-layout-toggle-button'; import { SearchInput } from '/@/renderer/features/shared/components/search-input'; import { AppRoute } from '/@/renderer/router/routes'; import { Button, ButtonGroup } from '/@/shared/components/button/button'; @@ -120,7 +121,12 @@ export const SearchHeader = ({ navigationId }: SearchHeaderProps) => { {t('entity.artist', { count: 2 })} - + + {itemType === LibraryItem.SONG && ( + + )} + + diff --git a/src/renderer/features/shared/components/list-layout-toggle-button.tsx b/src/renderer/features/shared/components/list-layout-toggle-button.tsx new file mode 100644 index 0000000000..77853b7deb --- /dev/null +++ b/src/renderer/features/shared/components/list-layout-toggle-button.tsx @@ -0,0 +1,58 @@ +import { useTranslation } from 'react-i18next'; + +import { + applyColumnOrder, + FLAT_COLUMN_ORDER, + isColumnOrderActive, +} from '/@/renderer/components/item-list/item-table-list/column-presets'; +import { + getDefaultListSettings, + useListSettings, + useSettingsStoreActions, +} from '/@/renderer/store'; +import { ActionIcon } from '/@/shared/components/action-icon/action-icon'; +import { ItemListKey, ListDisplayType } from '/@/shared/types/types'; + +interface ListLayoutToggleButtonProps { + listKey: ItemListKey; +} + +export const ListLayoutToggleButton = ({ listKey }: ListLayoutToggleButtonProps) => { + const { t } = useTranslation(); + const list = useListSettings(listKey); + const { setList } = useSettingsStoreActions(); + + if (list?.display !== ListDisplayType.TABLE || !list.table?.columns) { + return null; + } + + const isFlat = isColumnOrderActive(list.table.columns, FLAT_COLUMN_ORDER); + + const handleToggle = () => { + if (isFlat) { + const defaultTable = getDefaultListSettings(listKey)?.table; + if (!defaultTable) return; + setList(listKey, { + table: { columns: defaultTable.columns, size: defaultTable.size }, + }); + return; + } + + setList(listKey, { + table: { + columns: applyColumnOrder(list.table.columns, FLAT_COLUMN_ORDER), + size: 'compact', + }, + }); + }; + + return ( + + ); +}; diff --git a/src/renderer/features/songs/components/song-list-header-filters.tsx b/src/renderer/features/songs/components/song-list-header-filters.tsx index 26ee8259ac..35dc41de20 100644 --- a/src/renderer/features/songs/components/song-list-header-filters.tsx +++ b/src/renderer/features/songs/components/song-list-header-filters.tsx @@ -13,6 +13,7 @@ import { isFilterValueSet, ListFiltersModal, } from '/@/renderer/features/shared/components/list-filters'; +import { ListLayoutToggleButton } from '/@/renderer/features/shared/components/list-layout-toggle-button'; import { ListRefreshButton } from '/@/renderer/features/shared/components/list-refresh-button'; import { ListSortByDropdown } from '/@/renderer/features/shared/components/list-sort-by-dropdown'; import { ListSortOrderToggleButton } from '/@/renderer/features/shared/components/list-sort-order-toggle-button'; @@ -94,6 +95,7 @@ export const SongListHeaderFilters = ({ toggleGenreTarget }: { toggleGenreTarget + export const migrateSettings = (settings: SettingsState, settingsVersion: number): SettingsState => useSettingsStore.persist.getOptions().migrate!(settings, settingsVersion) as SettingsState; +export const getDefaultListSettings = (type: ItemListKey) => + initialState.lists[type as keyof typeof initialState.lists] as ItemListSettings | undefined; + export const useListSettings = (type: ItemListKey) => useSettingsStore( (state) => state.lists[type as keyof typeof state.lists],