[ENG-1506] Tags in Explorer (#2532)

* wip

* tags in explorer

* cleanup

* i18n
This commit is contained in:
ameer2468 2024-06-01 22:11:31 +03:00 committed by GitHub
parent 48f5715839
commit a6cb61584e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 106 additions and 23 deletions

View file

@ -174,6 +174,15 @@ export default () => {
explorerLayout.showPathBar = value; explorerLayout.showPathBar = value;
}} }}
/> />
<RadixCheckbox
checked={layoutStore.showTags}
label={t('show_tags')}
name="showTags"
onCheckedChange={(value) => {
if (typeof value !== 'boolean') return;
explorerLayout.showTags = value;
}}
/>
{settings.layoutMode === 'grid' && ( {settings.layoutMode === 'grid' && (
<RadixCheckbox <RadixCheckbox

View file

@ -1,12 +1,15 @@
import clsx from 'clsx';
import { memo, useMemo } from 'react';
import { import {
Tag,
getItemFilePath, getItemFilePath,
getItemObject,
humanizeSize, humanizeSize,
useExplorerLayoutStore,
useLibraryQuery, useLibraryQuery,
useSelector, useSelector,
type ExplorerItem type ExplorerItem
} from '@sd/client'; } from '@sd/client';
import clsx from 'clsx';
import { memo, useMemo } from 'react';
import { useLocale } from '~/hooks'; import { useLocale } from '~/hooks';
import { useExplorerContext } from '../../../Context'; import { useExplorerContext } from '../../../Context';
@ -52,7 +55,6 @@ export const GridViewItem = memo((props: GridViewItemProps) => {
const InnerDroppable = () => { const InnerDroppable = () => {
const item = useGridViewItemContext(); const item = useGridViewItemContext();
const { isDroppable } = useExplorerDroppableContext(); const { isDroppable } = useExplorerDroppableContext();
return ( return (
<> <>
<div <div
@ -63,7 +65,6 @@ const InnerDroppable = () => {
> >
<ItemFileThumb /> <ItemFileThumb />
</div> </div>
<ItemMetadata /> <ItemMetadata />
</> </>
); );
@ -103,6 +104,7 @@ const ItemFileThumb = () => {
const ItemMetadata = () => { const ItemMetadata = () => {
const item = useGridViewItemContext(); const item = useGridViewItemContext();
const { isDroppable } = useExplorerDroppableContext(); const { isDroppable } = useExplorerDroppableContext();
const explorerLayout = useExplorerLayoutStore();
const isRenaming = useSelector(explorerStore, (s) => s.isRenaming && item.selected); const isRenaming = useSelector(explorerStore, (s) => s.isRenaming && item.selected);
@ -117,11 +119,35 @@ const ItemMetadata = () => {
selected={item.selected} selected={item.selected}
/> />
<ItemSize /> <ItemSize />
{explorerLayout.showTags && <ItemTags />}
{item.data.type === 'Label' && <LabelItemCount data={item.data} />} {item.data.type === 'Label' && <LabelItemCount data={item.data} />}
</ExplorerDraggable> </ExplorerDraggable>
); );
}; };
const ItemTags = () => {
const item = useGridViewItemContext();
const object = getItemObject(item.data);
const filePath = getItemFilePath(item.data);
const data = object || filePath;
const tags = data && 'tags' in data ? data.tags : [];
return (
<div
className='relative mt-1 flex w-full flex-row items-center justify-center'
style={{
left: tags.length * 1
}}
>
{tags?.slice(0, 3).map((tag: {tag: Tag}, i: number) => (
<div key={tag.tag.id} className='relative size-2.5 rounded-full border border-app' style={{
backgroundColor: tag.tag.color!,
right: i * 4,
}}/>
))}
</div>
);
}
const ItemSize = () => { const ItemSize = () => {
const item = useGridViewItemContext(); const item = useGridViewItemContext();
const { showBytesInGridView } = useExplorerContext().useSettingsSnapshot(); const { showBytesInGridView } = useExplorerContext().useSettingsSnapshot();

View file

@ -1,6 +1,7 @@
import { Grid, useGrid } from '@virtual-grid/react'; import { Grid, useGrid } from '@virtual-grid/react';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useExplorerLayoutStore } from '@sd/client';
import { useExplorerContext } from '../../Context'; import { useExplorerContext } from '../../Context';
import { getItemData, getItemId, uniqueId } from '../../util'; import { getItemData, getItemId, uniqueId } from '../../util';
import { useExplorerViewContext } from '../Context'; import { useExplorerViewContext } from '../Context';
@ -15,10 +16,12 @@ export const GridView = () => {
const explorer = useExplorerContext(); const explorer = useExplorerContext();
const explorerView = useExplorerViewContext(); const explorerView = useExplorerViewContext();
const explorerSettings = explorer.useSettingsSnapshot(); const explorerSettings = explorer.useSettingsSnapshot();
const layoutStore = useExplorerLayoutStore();
const itemDetailsHeight = 44 + (explorerSettings.showBytesInGridView ? 20 : 0); const itemDetailsHeight = (layoutStore.showTags ? 60 : 44) + (explorerSettings.showBytesInGridView ? 20 : 0);
const itemHeight = explorerSettings.gridItemSize + itemDetailsHeight; const itemHeight = explorerSettings.gridItemSize + itemDetailsHeight;
const BOTTOM_PADDING = layoutStore.showTags ? 16 : 12;
const grid = useGrid({ const grid = useGrid({
scrollRef: explorer.scrollRef, scrollRef: explorer.scrollRef,
count: explorer.items?.length ?? 0, count: explorer.items?.length ?? 0,
@ -26,7 +29,7 @@ export const GridView = () => {
columns: 'auto', columns: 'auto',
size: { width: explorerSettings.gridItemSize, height: itemHeight }, size: { width: explorerSettings.gridItemSize, height: itemHeight },
padding: { padding: {
bottom: PADDING + (explorerView.scrollPadding?.bottom ?? 0), bottom: BOTTOM_PADDING + (explorerView.scrollPadding?.bottom ?? 0),
x: PADDING, x: PADDING,
y: PADDING y: PADDING
}, },

View file

@ -1,7 +1,7 @@
import { getItemFilePath, useSelector, type ExplorerItem } from '@sd/client';
import { flexRender, type Cell } from '@tanstack/react-table'; import { flexRender, type Cell } from '@tanstack/react-table';
import clsx from 'clsx'; import clsx from 'clsx';
import { memo, useMemo } from 'react'; import { memo, useMemo } from 'react';
import { getItemFilePath, useSelector, type ExplorerItem } from '@sd/client';
import { TABLE_PADDING_X } from '.'; import { TABLE_PADDING_X } from '.';
import { useExplorerContext } from '../../Context'; import { useExplorerContext } from '../../Context';

View file

@ -1,3 +1,13 @@
import {
getExplorerItemData,
getIndexedItemFilePath,
getItemFilePath,
getItemObject,
humanizeSize,
useExplorerLayoutStore,
useSelector,
type ExplorerItem
} from '@sd/client';
import { import {
CellContext, CellContext,
functionalUpdate, functionalUpdate,
@ -9,15 +19,6 @@ import clsx from 'clsx';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { memo, useMemo } from 'react'; import { memo, useMemo } from 'react';
import { stringify } from 'uuid'; import { stringify } from 'uuid';
import {
getExplorerItemData,
getIndexedItemFilePath,
getItemFilePath,
getItemObject,
humanizeSize,
useSelector,
type ExplorerItem
} from '@sd/client';
import { useLocale } from '~/hooks'; import { useLocale } from '~/hooks';
import { useExplorerContext } from '../../Context'; import { useExplorerContext } from '../../Context';
@ -48,6 +49,7 @@ const NameCell = memo(({ item, selected }: { item: ExplorerItem; selected: boole
const explorer = useExplorerContext(); const explorer = useExplorerContext();
const explorerSettings = explorer.useSettingsSnapshot(); const explorerSettings = explorer.useSettingsSnapshot();
const explorerLayout = useExplorerLayoutStore();
return ( return (
<div className="flex"> <div className="flex">
@ -66,15 +68,41 @@ const NameCell = memo(({ item, selected }: { item: ExplorerItem; selected: boole
selected={selected} selected={selected}
allowHighlight={false} allowHighlight={false}
style={{ fontSize: LIST_VIEW_TEXT_SIZES[explorerSettings.listViewTextSize] }} style={{ fontSize: LIST_VIEW_TEXT_SIZES[explorerSettings.listViewTextSize] }}
className="absolute top-1/2 z-10 max-w-full -translate-y-1/2" className="absolute top-1/2 z-10 -translate-y-1/2"
idleClassName="!w-full" idleClassName={clsx(explorerLayout.showTags ? '!w-4/5' : '!w-full')}
editLines={3} editLines={3}
/> />
{explorerLayout.showTags && (
<Tags item={item}/>
)}
</div> </div>
</div> </div>
); );
}); });
const Tags = ({item}: {item: ExplorerItem}) => {
const object = getItemObject(item);
const filePath = getItemFilePath(item);
const data = object || filePath;
const tags = data && 'tags' in data ? data.tags : [];
return (
<div
className='relative flex size-full flex-row items-center justify-end self-center'
style={{
marginLeft: tags.length * 4
}}
>
{tags.map(({tag}, i: number) => (
<div key={tag.id} className='relative size-2.5 rounded-full border border-app' style={{
backgroundColor: tag.color || 'transparent',
right: i * 4
}}/>
))}
</div>
)
}
const KindCell = ({ kind }: { kind: string }) => { const KindCell = ({ kind }: { kind: string }) => {
const explorer = useExplorerContext(); const explorer = useExplorerContext();
const explorerSettings = explorer.useSettingsSnapshot(); const explorerSettings = explorer.useSettingsSnapshot();

View file

@ -1,7 +1,3 @@
import { useCallback, useEffect, useMemo, useRef, useState, type RefObject } from 'react';
import { useDebouncedCallback } from 'use-debounce';
import { proxy, snapshot, subscribe, useSnapshot } from 'valtio';
import { z } from 'zod';
import type { import type {
ExplorerItem, ExplorerItem,
ExplorerLayout, ExplorerLayout,
@ -12,6 +8,10 @@ import type {
Tag Tag
} from '@sd/client'; } from '@sd/client';
import { ObjectKindEnum, type Ordering, type OrderingKeys } from '@sd/client'; import { ObjectKindEnum, type Ordering, type OrderingKeys } from '@sd/client';
import { useCallback, useEffect, useMemo, useRef, useState, type RefObject } from 'react';
import { useDebouncedCallback } from 'use-debounce';
import { proxy, snapshot, subscribe, useSnapshot } from 'valtio';
import { z } from 'zod';
import { createDefaultExplorerSettings } from './store'; import { createDefaultExplorerSettings } from './store';
import { uniqueId } from './util'; import { uniqueId } from './util';

View file

@ -5,9 +5,10 @@ import {
ToastDefautlColor, ToastDefautlColor,
useLibraryMutation, useLibraryMutation,
usePlausibleEvent, usePlausibleEvent,
useRspcLibraryContext,
useZodForm useZodForm
} from '@sd/client'; } from '@sd/client';
import { Dialog, InputField, useDialog, UseDialogProps, z } from '@sd/ui'; import { Dialog, InputField, UseDialogProps, useDialog, z } from '@sd/ui';
import { ColorPicker } from '~/components'; import { ColorPicker } from '~/components';
import { useLocale } from '~/hooks'; import { useLocale } from '~/hooks';
@ -22,10 +23,12 @@ export type AssignTagItems = Array<
export function useAssignItemsToTag() { export function useAssignItemsToTag() {
const submitPlausibleEvent = usePlausibleEvent(); const submitPlausibleEvent = usePlausibleEvent();
const rspc = useRspcLibraryContext();
const mutation = useLibraryMutation(['tags.assign'], { const mutation = useLibraryMutation(['tags.assign'], {
onSuccess: () => { onSuccess: () => {
submitPlausibleEvent({ event: { type: 'tagAssign' } }); submitPlausibleEvent({ event: { type: 'tagAssign' } });
rspc.queryClient.invalidateQueries(['search.paths']);
} }
}); });

View file

@ -614,6 +614,7 @@
"show_object_size": "إظهار حجم الكائن", "show_object_size": "إظهار حجم الكائن",
"show_path_bar": "إظهار شريط المسار", "show_path_bar": "إظهار شريط المسار",
"show_slider": "إظهار المنزلق", "show_slider": "إظهار المنزلق",
"show_tags": "أضهر العلامات",
"size": "الحجم", "size": "الحجم",
"size_b": "بايت", "size_b": "بايت",
"size_bs": "ب", "size_bs": "ب",

View file

@ -605,6 +605,7 @@
"show_object_size": "Памер аб'екта", "show_object_size": "Памер аб'екта",
"show_path_bar": "Адрасны радок", "show_path_bar": "Адрасны радок",
"show_slider": "Паказаць паўзунок", "show_slider": "Паказаць паўзунок",
"show_tags": "Паказаць тэгі",
"size": "Памер", "size": "Памер",
"size_b": "Б", "size_b": "Б",
"size_bs": "Б", "size_bs": "Б",

View file

@ -602,6 +602,7 @@
"show_object_size": "Objektgröße anzeigen", "show_object_size": "Objektgröße anzeigen",
"show_path_bar": "Pfadleiste anzeigen", "show_path_bar": "Pfadleiste anzeigen",
"show_slider": "Slider anzeigen", "show_slider": "Slider anzeigen",
"show_tags": "Tags anzeigen",
"size": "Größe", "size": "Größe",
"size_b": "B", "size_b": "B",
"size_bs": "B", "size_bs": "B",

View file

@ -614,6 +614,7 @@
"show_object_size": "Show Object size", "show_object_size": "Show Object size",
"show_path_bar": "Show Path Bar", "show_path_bar": "Show Path Bar",
"show_slider": "Show slider", "show_slider": "Show slider",
"show_tags": "Show Tags",
"size": "Size", "size": "Size",
"size_b": "B", "size_b": "B",
"size_bs": "Bs", "size_bs": "Bs",

View file

@ -604,6 +604,7 @@
"show_object_size": "Mostrar Tamaño del Objeto", "show_object_size": "Mostrar Tamaño del Objeto",
"show_path_bar": "Mostrar Barra de Ruta", "show_path_bar": "Mostrar Barra de Ruta",
"show_slider": "Mostrar deslizador", "show_slider": "Mostrar deslizador",
"show_tags": "Mostrar etiquetas",
"size": "Tamaño", "size": "Tamaño",
"size_b": "B", "size_b": "B",
"size_bs": "B", "size_bs": "B",

View file

@ -604,6 +604,7 @@
"show_object_size": "Afficher la taille de l'objet", "show_object_size": "Afficher la taille de l'objet",
"show_path_bar": "Afficher la barre de chemin", "show_path_bar": "Afficher la barre de chemin",
"show_slider": "Afficher le curseur", "show_slider": "Afficher le curseur",
"show_tags": "Voir les étiquettes",
"size": "Taille", "size": "Taille",
"size_b": "o", "size_b": "o",
"size_bs": "B", "size_bs": "B",

View file

@ -604,6 +604,7 @@
"show_object_size": "Mostra la dimensione dell'oggetto", "show_object_size": "Mostra la dimensione dell'oggetto",
"show_path_bar": "Mostra barra del percorso", "show_path_bar": "Mostra barra del percorso",
"show_slider": "Mostra slider", "show_slider": "Mostra slider",
"show_tags": "Mostra tag",
"size": "Dimensione", "size": "Dimensione",
"size_b": "B", "size_b": "B",
"size_bs": "B", "size_bs": "B",

View file

@ -598,6 +598,7 @@
"show_object_size": "ファイルサイズを表示", "show_object_size": "ファイルサイズを表示",
"show_path_bar": "パスバーを表示", "show_path_bar": "パスバーを表示",
"show_slider": "スライダーを表示", "show_slider": "スライダーを表示",
"show_tags": "タグを表示",
"size": "サイズ", "size": "サイズ",
"size_b": "バイト", "size_b": "バイト",
"size_bs": "B", "size_bs": "B",

View file

@ -602,6 +602,7 @@
"show_object_size": "Toon Object grootte", "show_object_size": "Toon Object grootte",
"show_path_bar": "Padbalk Tonen", "show_path_bar": "Padbalk Tonen",
"show_slider": "Toon schuifregelaar", "show_slider": "Toon schuifregelaar",
"show_tags": "Toon labels",
"size": "Grootte", "size": "Grootte",
"size_b": "B", "size_b": "B",
"size_bs": "B", "size_bs": "B",

View file

@ -605,6 +605,7 @@
"show_object_size": "Размер объекта", "show_object_size": "Размер объекта",
"show_path_bar": "Адресная строка", "show_path_bar": "Адресная строка",
"show_slider": "Показать ползунок", "show_slider": "Показать ползунок",
"show_tags": "Показать теги",
"size": "Размер", "size": "Размер",
"size_b": "Б", "size_b": "Б",
"size_bs": "Б", "size_bs": "Б",

View file

@ -602,6 +602,7 @@
"show_object_size": "Nesne Boyutunu Göster", "show_object_size": "Nesne Boyutunu Göster",
"show_path_bar": "Yol Çubuğunu Göster", "show_path_bar": "Yol Çubuğunu Göster",
"show_slider": "Kaydırıcıyı Göster", "show_slider": "Kaydırıcıyı Göster",
"show_tags": "Etiketleri göster",
"size": "Boyut", "size": "Boyut",
"size_b": "B", "size_b": "B",
"size_bs": "B", "size_bs": "B",

View file

@ -598,6 +598,7 @@
"show_object_size": "显示对象大小", "show_object_size": "显示对象大小",
"show_path_bar": "显示路径栏", "show_path_bar": "显示路径栏",
"show_slider": "显示滑块", "show_slider": "显示滑块",
"show_tags": "显示标签",
"size": "大小", "size": "大小",
"size_b": "乙", "size_b": "乙",
"size_bs": "乙", "size_bs": "乙",

View file

@ -598,6 +598,7 @@
"show_object_size": "顯示對象大小", "show_object_size": "顯示對象大小",
"show_path_bar": "顯示路徑條", "show_path_bar": "顯示路徑條",
"show_slider": "顯示滑塊", "show_slider": "顯示滑塊",
"show_tags": "顯示標籤",
"size": "大小", "size": "大小",
"size_b": "乙", "size_b": "乙",
"size_bs": "乙", "size_bs": "乙",

View file

@ -7,6 +7,7 @@ export const explorerLayout = createPersistedMutable(
'sd-explorer-layout', 'sd-explorer-layout',
createMutable({ createMutable({
showPathBar: true, showPathBar: true,
showTags: true,
showImageSlider: true, showImageSlider: true,
// might move this to a store called settings // might move this to a store called settings
defaultView: 'grid' as ExplorerLayout defaultView: 'grid' as ExplorerLayout