mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-06-30 12:33:31 +00:00
[ENG-293] Tag assign mode (#2462)
* Rename top bar icon *style* to *classlist* * Update pnpm-lock.yaml * Add user-facing name for tag assign mode: "Assign tags" * Refactor path bar to allow tag bar display above * Oops, use original PATH_BAR_HEIGHT * rename ExplorerPath to more consistent ExplorerPathBar * remove debug tag assign mode toggle * fix straggling old reference to ExplorerPathBar * rename useShortcuts to useExplorerShortcuts * add `DEV` to turbo.json env deps * tag assign mode list display + keyboard toggle * Update pnpm-lock.yaml after merge * Use new query style in ExplorerTagBar * WIP debugging list bug * Fix to the `<slot />` bug * Remove awaiting tag assign keypress state variable * Add tag assign mode localization lines * Add actual tag assign mode functionality * Use localization for bulk tag assignment * i18n: Add proper "file" plurals and nest within tag assignment msgs * implement tag hotkey assignment * Use i18n for unknown error in tag assignment * use type for tag bulk assign keycodes * remove custom ordering todo * add awaiting-assign visual state to tag bar tags * Make Escape cancel tag bar hotkey assignment + add a11y + remove focus state on keypress * Remove tag assign mutation success from tag bar * Remove tab index `-1` from tag bar tag * Update tag bar awaiting hotkey assignment colors * Fix tag bar styling + sort properly * Fix some missed `TOP_BAR_ICON_CLASSLIST` references * improve tag ui & fix location redir * fix pathbar caret and layout adjustment experiment * Add better tag bar layout for users with lots of tags * set padding if tags overflow --------- Co-authored-by: Arnab Chakraborty <11457760+Rocky43007@users.noreply.github.com> Co-authored-by: ameer2468 <33054370+ameer2468@users.noreply.github.com>
This commit is contained in:
parent
18235c6f09
commit
0688406adc
|
@ -21,9 +21,10 @@ import { lookup } from './RevealInNativeExplorer';
|
||||||
import { useExplorerDroppable } from './useExplorerDroppable';
|
import { useExplorerDroppable } from './useExplorerDroppable';
|
||||||
import { useExplorerSearchParams } from './util';
|
import { useExplorerSearchParams } from './util';
|
||||||
|
|
||||||
|
// todo: ENTIRELY replace with computed combined pathbar+tagbar height
|
||||||
export const PATH_BAR_HEIGHT = 32;
|
export const PATH_BAR_HEIGHT = 32;
|
||||||
|
|
||||||
export const ExplorerPath = memo(() => {
|
export const ExplorerPathBar = memo(() => {
|
||||||
const os = useOperatingSystem(true);
|
const os = useOperatingSystem(true);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [{ path: searchPath }] = useExplorerSearchParams();
|
const [{ path: searchPath }] = useExplorerSearchParams();
|
||||||
|
@ -118,13 +119,16 @@ export const ExplorerPath = memo(() => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="group absolute inset-x-0 bottom-0 z-50 flex items-center border-t border-t-app-line bg-app/90 px-3.5 text-[11px] text-ink-dull backdrop-blur-lg"
|
className={clsx(
|
||||||
style={{ height: PATH_BAR_HEIGHT }}
|
'group flex items-center border-t border-t-app-line bg-app/90 px-3.5 text-[11px] text-ink-dull backdrop-blur-lg',
|
||||||
|
`h-[${PATH_BAR_HEIGHT}px]`
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{paths.map((path) => (
|
{paths.map((path, idx) => (
|
||||||
<Path
|
<Path
|
||||||
key={path.pathname}
|
key={path.pathname}
|
||||||
path={path}
|
path={path}
|
||||||
|
isLast={idx === paths.length - 1}
|
||||||
locationPath={location?.path ?? ''}
|
locationPath={location?.path ?? ''}
|
||||||
onClick={() => handleOnClick(path)}
|
onClick={() => handleOnClick(path)}
|
||||||
disabled={path.pathname === (searchPath ?? (location && '/'))}
|
disabled={path.pathname === (searchPath ?? (location && '/'))}
|
||||||
|
@ -148,9 +152,10 @@ interface PathProps {
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
locationPath: string;
|
locationPath: string;
|
||||||
|
isLast: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Path = ({ path, onClick, disabled, locationPath }: PathProps) => {
|
const Path = ({ path, onClick, disabled, locationPath, isLast }: PathProps) => {
|
||||||
const isDark = useIsDark();
|
const isDark = useIsDark();
|
||||||
const { revealItems } = usePlatform();
|
const { revealItems } = usePlatform();
|
||||||
const { library } = useLibraryContext();
|
const { library } = useLibraryContext();
|
||||||
|
@ -192,7 +197,7 @@ const Path = ({ path, onClick, disabled, locationPath }: PathProps) => {
|
||||||
<button
|
<button
|
||||||
ref={setDroppableRef}
|
ref={setDroppableRef}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'group flex items-center gap-1 rounded px-1 py-0.5',
|
'flex items-center gap-1 rounded p-1',
|
||||||
(isDroppable || contextMenuOpen) && [
|
(isDroppable || contextMenuOpen) && [
|
||||||
isDark ? 'bg-app-button/70' : 'bg-app-darkerBox'
|
isDark ? 'bg-app-button/70' : 'bg-app-darkerBox'
|
||||||
],
|
],
|
||||||
|
@ -205,11 +210,7 @@ const Path = ({ path, onClick, disabled, locationPath }: PathProps) => {
|
||||||
>
|
>
|
||||||
<Icon name="Folder" size={16} alt="Folder" />
|
<Icon name="Folder" size={16} alt="Folder" />
|
||||||
<span className="max-w-xs truncate text-ink-dull">{path.name}</span>
|
<span className="max-w-xs truncate text-ink-dull">{path.name}</span>
|
||||||
<CaretRight
|
{!isLast && <CaretRight weight="bold" className="text-ink-dull" size={10} />}
|
||||||
weight="bold"
|
|
||||||
className="text-ink-dull group-last:hidden"
|
|
||||||
size={10}
|
|
||||||
/>
|
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
>
|
>
|
336
interface/app/$libraryId/Explorer/ExplorerTagBar.tsx
Normal file
336
interface/app/$libraryId/Explorer/ExplorerTagBar.tsx
Normal file
|
@ -0,0 +1,336 @@
|
||||||
|
import { Circle } from '@phosphor-icons/react';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import { ReactNode, useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import {
|
||||||
|
ExplorerItem,
|
||||||
|
Tag,
|
||||||
|
Target,
|
||||||
|
useLibraryMutation,
|
||||||
|
useLibraryQuery,
|
||||||
|
useRspcContext,
|
||||||
|
useSelector
|
||||||
|
} from '@sd/client';
|
||||||
|
import { Shortcut, toast } from '@sd/ui';
|
||||||
|
import { useIsDark, useKeybind, useLocale, useOperatingSystem } from '~/hooks';
|
||||||
|
import { keybindForOs } from '~/util/keybinds';
|
||||||
|
|
||||||
|
import { useExplorerContext } from './Context';
|
||||||
|
import { explorerStore } from './store';
|
||||||
|
|
||||||
|
export const TAG_BAR_HEIGHT = 64;
|
||||||
|
const NUMBER_KEYCODES: string[][] = [
|
||||||
|
['Key1'],
|
||||||
|
['Key2'],
|
||||||
|
['Key3'],
|
||||||
|
['Key4'],
|
||||||
|
['Key5'],
|
||||||
|
['Key6'],
|
||||||
|
['Key7'],
|
||||||
|
['Key8'],
|
||||||
|
['Key9']
|
||||||
|
];
|
||||||
|
|
||||||
|
// TODO: hoist this to somewhere higher as a utility function
|
||||||
|
const toTarget = (data: ExplorerItem): Target => {
|
||||||
|
if (!data || !('id' in data.item))
|
||||||
|
throw new Error('Tried to convert an invalid object to Target.');
|
||||||
|
|
||||||
|
return (
|
||||||
|
data.type === 'Object'
|
||||||
|
? {
|
||||||
|
Object: data.item.id
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
FilePath: data.item.id
|
||||||
|
}
|
||||||
|
) satisfies Target;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TagBulkAssignHotkeys = typeof explorerStore.tagBulkAssignHotkeys;
|
||||||
|
function getHotkeysWithNewAssignment(
|
||||||
|
hotkeys: TagBulkAssignHotkeys,
|
||||||
|
options:
|
||||||
|
| {
|
||||||
|
unassign?: false;
|
||||||
|
tagId: number;
|
||||||
|
hotkey: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
unassign: true;
|
||||||
|
tagId: number;
|
||||||
|
hotkey?: string;
|
||||||
|
}
|
||||||
|
): TagBulkAssignHotkeys {
|
||||||
|
const hotkeysWithoutCurrentTag = hotkeys.filter(
|
||||||
|
({ hotkey, tagId }) => !(tagId === options.tagId || hotkey === options.hotkey)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (options.unassign) {
|
||||||
|
return hotkeysWithoutCurrentTag;
|
||||||
|
}
|
||||||
|
|
||||||
|
return hotkeysWithoutCurrentTag.concat({
|
||||||
|
hotkey: options.hotkey,
|
||||||
|
tagId: options.tagId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// million-ignore
|
||||||
|
export const ExplorerTagBar = () => {
|
||||||
|
const [tagBulkAssignHotkeys] = useSelector(explorerStore, (s) => [s.tagBulkAssignHotkeys]);
|
||||||
|
const explorer = useExplorerContext();
|
||||||
|
const rspc = useRspcContext();
|
||||||
|
const tagsRef = useRef<HTMLUListElement | null>(null);
|
||||||
|
const [isTagsOverflowing, setIsTagsOverflowing] = useState(false);
|
||||||
|
|
||||||
|
const updateOverflowState = () => {
|
||||||
|
const element = tagsRef.current;
|
||||||
|
if (element) {
|
||||||
|
setIsTagsOverflowing(
|
||||||
|
element.scrollHeight > element.clientHeight || element.scrollWidth > element.clientWidth
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const element = tagsRef.current;
|
||||||
|
if (!element) return;
|
||||||
|
//handles initial render when not resizing
|
||||||
|
setIsTagsOverflowing(element.scrollHeight > element.clientHeight || element.scrollWidth > element.clientWidth)
|
||||||
|
//make sure state updates when window resizing
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
updateOverflowState();
|
||||||
|
})
|
||||||
|
//remove listeners on unmount
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', () => {
|
||||||
|
updateOverflowState();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [tagsRef])
|
||||||
|
|
||||||
|
const [tagListeningForKeyPress, setTagListeningForKeyPress] = useState<number | undefined>();
|
||||||
|
|
||||||
|
const { data: allTags = [] } = useLibraryQuery(['tags.list']);
|
||||||
|
const mutation = useLibraryMutation(['tags.assign'], {
|
||||||
|
onSuccess: () => rspc.queryClient.invalidateQueries(['search.paths'])
|
||||||
|
});
|
||||||
|
|
||||||
|
const { t } = useLocale();
|
||||||
|
|
||||||
|
// This will automagically listen for any keypress 1-9 while the tag bar is visible.
|
||||||
|
// These listeners will unmount when ExplorerTagBar is unmounted.
|
||||||
|
useKeybind(
|
||||||
|
NUMBER_KEYCODES,
|
||||||
|
async (e) => {
|
||||||
|
const targets = Array.from(explorer.selectedItems.entries()).map((item) =>
|
||||||
|
toTarget(item[0])
|
||||||
|
);
|
||||||
|
|
||||||
|
// Silent fail if no files are selected
|
||||||
|
if (targets.length < 1) return;
|
||||||
|
|
||||||
|
const keyPressed = e.key;
|
||||||
|
|
||||||
|
let tag: Tag | undefined;
|
||||||
|
|
||||||
|
findTag: {
|
||||||
|
const tagId = tagBulkAssignHotkeys.find(
|
||||||
|
({ hotkey }) => hotkey === keyPressed
|
||||||
|
)?.tagId;
|
||||||
|
const foundTag = allTags.find((t) => t.id === tagId);
|
||||||
|
|
||||||
|
if (!foundTag) break findTag;
|
||||||
|
|
||||||
|
tag = foundTag;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tag) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await mutation.mutateAsync({
|
||||||
|
targets,
|
||||||
|
tag_id: tag.id,
|
||||||
|
unassign: false
|
||||||
|
});
|
||||||
|
|
||||||
|
toast(
|
||||||
|
t('tags_bulk_assigned', {
|
||||||
|
tag_name: tag.name,
|
||||||
|
file_count: targets.length
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
type: 'success'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
let msg: string = t('error_unknown');
|
||||||
|
|
||||||
|
if (err instanceof Error || (typeof err === 'object' && err && 'message' in err)) {
|
||||||
|
msg = err.message as string;
|
||||||
|
} else if (typeof err === 'string') {
|
||||||
|
msg = err;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error('Tag assignment failed with error', err);
|
||||||
|
|
||||||
|
let failedToastMessage: string = t('tags_bulk_failed_without_tag', {
|
||||||
|
file_count: targets.length,
|
||||||
|
error_message: msg
|
||||||
|
});
|
||||||
|
|
||||||
|
if (tag)
|
||||||
|
failedToastMessage = t('tags_bulk_failed_with_tag', {
|
||||||
|
tag_name: tag.name,
|
||||||
|
file_count: targets.length,
|
||||||
|
error_message: msg
|
||||||
|
});
|
||||||
|
|
||||||
|
toast(failedToastMessage, {
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: typeof tagListeningForKeyPress !== 'number'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
'flex flex-row flex-wrap-reverse items-center justify-between gap-1 border-t border-t-app-line bg-app/90 px-3.5 py-2 text-ink-dull backdrop-blur-lg',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<em className="text-sm tracking-wide">{t('tags_bulk_instructions')}</em>
|
||||||
|
|
||||||
|
<ul
|
||||||
|
ref={tagsRef}
|
||||||
|
// TODO: I want to replace this `overlay-scroll` style with a better system for non-horizontral-scroll mouse users, but
|
||||||
|
// for now this style looks the least disgusting. Probably will end up going for a left/right arrows button that dynamically
|
||||||
|
// shows/hides depending on scroll position.
|
||||||
|
className={clsx(
|
||||||
|
'flex-0 explorer-scroll my-1 flex max-w-full list-none flex-row gap-2 overflow-x-auto',
|
||||||
|
isTagsOverflowing ? 'pb-2' : 'pb-0'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* Did not want to write a .toSorted() predicate for this so lazy spreading things with hotkeys first then the rest after */}
|
||||||
|
{allTags
|
||||||
|
.toSorted((tagA, tagB) => {
|
||||||
|
// Sort this array by hotkeys 1-9 first, then unasssigned tags. I know, it's terrible.
|
||||||
|
// This 998/999 bit is likely terrible for sorting. I'm bad at writing sort predicates.
|
||||||
|
// Improvements (probably much simpler than this anyway) are much welcome <3
|
||||||
|
// -- iLynxcat 3/jun/2024
|
||||||
|
|
||||||
|
const hotkeyA = +(
|
||||||
|
tagBulkAssignHotkeys.find((k) => k.tagId === tagA.id)?.hotkey ?? 998
|
||||||
|
);
|
||||||
|
const hotkeyB = +(
|
||||||
|
tagBulkAssignHotkeys.find((k) => k.tagId === tagB.id)?.hotkey ?? 999
|
||||||
|
);
|
||||||
|
|
||||||
|
return hotkeyA - hotkeyB;
|
||||||
|
})
|
||||||
|
.map((tag) => (
|
||||||
|
<li key={tag.id}>
|
||||||
|
<TagItem
|
||||||
|
tag={tag}
|
||||||
|
assignKey={
|
||||||
|
tagBulkAssignHotkeys.find(({ tagId }) => tagId === tag.id)
|
||||||
|
?.hotkey
|
||||||
|
}
|
||||||
|
isAwaitingKeyPress={tagListeningForKeyPress === tag.id}
|
||||||
|
onClick={() => {
|
||||||
|
setTagListeningForKeyPress(tag.id);
|
||||||
|
}}
|
||||||
|
onKeyPress={(e) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
explorerStore.tagBulkAssignHotkeys =
|
||||||
|
getHotkeysWithNewAssignment(tagBulkAssignHotkeys, {
|
||||||
|
unassign: true,
|
||||||
|
tagId: tag.id
|
||||||
|
});
|
||||||
|
|
||||||
|
setTagListeningForKeyPress(undefined);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
explorerStore.tagBulkAssignHotkeys =
|
||||||
|
getHotkeysWithNewAssignment(tagBulkAssignHotkeys, {
|
||||||
|
tagId: tag.id,
|
||||||
|
hotkey: e.key
|
||||||
|
});
|
||||||
|
setTagListeningForKeyPress(undefined);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface TagItemProps {
|
||||||
|
tag: Tag;
|
||||||
|
assignKey?: string;
|
||||||
|
isAwaitingKeyPress: boolean;
|
||||||
|
onKeyPress: (e: KeyboardEvent) => void;
|
||||||
|
onClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TagItem = ({
|
||||||
|
tag,
|
||||||
|
assignKey,
|
||||||
|
isAwaitingKeyPress = false,
|
||||||
|
onKeyPress,
|
||||||
|
onClick
|
||||||
|
}: TagItemProps) => {
|
||||||
|
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||||
|
const isDark = useIsDark();
|
||||||
|
|
||||||
|
const os = useOperatingSystem(true);
|
||||||
|
const keybind = keybindForOs(os);
|
||||||
|
|
||||||
|
useKeybind(
|
||||||
|
[...NUMBER_KEYCODES, ['Escape']],
|
||||||
|
(e) => {
|
||||||
|
buttonRef.current?.blur(); // Hides the focus ring after Escape is pressed to cancel assignment
|
||||||
|
return onKeyPress(e);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: isAwaitingKeyPress
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={clsx('group flex items-center gap-1 rounded-lg border px-2.5 py-0.5', {
|
||||||
|
['border border-app-line bg-app-box']: !isAwaitingKeyPress && isDark,
|
||||||
|
['border-accent bg-app-box']: isAwaitingKeyPress && isDark,
|
||||||
|
['border-accent bg-app-lightBox']: isAwaitingKeyPress && !isDark
|
||||||
|
})}
|
||||||
|
ref={buttonRef}
|
||||||
|
onClick={onClick}
|
||||||
|
aria-live={isAwaitingKeyPress ? 'assertive' : 'off'}
|
||||||
|
aria-label={
|
||||||
|
isAwaitingKeyPress
|
||||||
|
? `Type a number to map it to the "${tag.name}" tag. Press escape to cancel.`
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Circle
|
||||||
|
fill={tag.color ?? 'grey'}
|
||||||
|
weight="fill"
|
||||||
|
alt=""
|
||||||
|
aria-hidden
|
||||||
|
className="size-2.5"
|
||||||
|
/>
|
||||||
|
<span className="max-w-xs truncate py-0.5 text-sm font-semibold text-ink-dull">
|
||||||
|
{tag.name}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{assignKey && <Shortcut chars={keybind([], [assignKey])} />}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
|
@ -5,7 +5,8 @@ import {
|
||||||
Rows,
|
Rows,
|
||||||
SidebarSimple,
|
SidebarSimple,
|
||||||
SlidersHorizontal,
|
SlidersHorizontal,
|
||||||
SquaresFour
|
SquaresFour,
|
||||||
|
Tag
|
||||||
} from '@phosphor-icons/react';
|
} from '@phosphor-icons/react';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
@ -15,7 +16,7 @@ import { useKeyMatcher, useLocale } from '~/hooks';
|
||||||
|
|
||||||
import { KeyManager } from '../KeyManager';
|
import { KeyManager } from '../KeyManager';
|
||||||
import { Spacedrop, SpacedropButton } from '../Spacedrop';
|
import { Spacedrop, SpacedropButton } from '../Spacedrop';
|
||||||
import TopBarOptions, { ToolOption, TOP_BAR_ICON_STYLE } from '../TopBar/TopBarOptions';
|
import TopBarOptions, { ToolOption, TOP_BAR_ICON_CLASSLIST } from '../TopBar/TopBarOptions';
|
||||||
import { useExplorerContext } from './Context';
|
import { useExplorerContext } from './Context';
|
||||||
import OptionsPanel from './OptionsPanel';
|
import OptionsPanel from './OptionsPanel';
|
||||||
import { explorerStore } from './store';
|
import { explorerStore } from './store';
|
||||||
|
@ -29,7 +30,7 @@ const layoutIcons: Record<ExplorerLayout, Icon> = {
|
||||||
export const useExplorerTopBarOptions = () => {
|
export const useExplorerTopBarOptions = () => {
|
||||||
const [showInspector, tagAssignMode] = useSelector(explorerStore, (s) => [
|
const [showInspector, tagAssignMode] = useSelector(explorerStore, (s) => [
|
||||||
s.showInspector,
|
s.showInspector,
|
||||||
s.tagAssignMode
|
s.isTagAssignModeActive
|
||||||
]);
|
]);
|
||||||
const explorer = useExplorerContext();
|
const explorer = useExplorerContext();
|
||||||
const controlIcon = useKeyMatcher('Meta').icon;
|
const controlIcon = useKeyMatcher('Meta').icon;
|
||||||
|
@ -48,7 +49,7 @@ export const useExplorerTopBarOptions = () => {
|
||||||
const option = {
|
const option = {
|
||||||
layout,
|
layout,
|
||||||
toolTipLabel: t(`${layout}_view`),
|
toolTipLabel: t(`${layout}_view`),
|
||||||
icon: <Icon className={TOP_BAR_ICON_STYLE} />,
|
icon: <Icon className={TOP_BAR_ICON_CLASSLIST} />,
|
||||||
keybinds: [controlIcon, (i + 1).toString()],
|
keybinds: [controlIcon, (i + 1).toString()],
|
||||||
topBarActive:
|
topBarActive:
|
||||||
!explorer.isLoadingPreferences && settings.layoutMode === layout,
|
!explorer.isLoadingPreferences && settings.layoutMode === layout,
|
||||||
|
@ -73,7 +74,7 @@ export const useExplorerTopBarOptions = () => {
|
||||||
const controlOptions: ToolOption[] = [
|
const controlOptions: ToolOption[] = [
|
||||||
{
|
{
|
||||||
toolTipLabel: t('explorer_settings'),
|
toolTipLabel: t('explorer_settings'),
|
||||||
icon: <SlidersHorizontal className={TOP_BAR_ICON_STYLE} />,
|
icon: <SlidersHorizontal className={TOP_BAR_ICON_CLASSLIST} />,
|
||||||
popOverComponent: <OptionsPanel />,
|
popOverComponent: <OptionsPanel />,
|
||||||
individual: true,
|
individual: true,
|
||||||
showAtResolution: 'sm:flex'
|
showAtResolution: 'sm:flex'
|
||||||
|
@ -87,7 +88,7 @@ export const useExplorerTopBarOptions = () => {
|
||||||
icon: (
|
icon: (
|
||||||
<SidebarSimple
|
<SidebarSimple
|
||||||
weight={showInspector ? 'fill' : 'regular'}
|
weight={showInspector ? 'fill' : 'regular'}
|
||||||
className={clsx(TOP_BAR_ICON_STYLE, '-scale-x-100')}
|
className={clsx(TOP_BAR_ICON_CLASSLIST, '-scale-x-100')}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
individual: true,
|
individual: true,
|
||||||
|
@ -118,11 +119,28 @@ export const useExplorerTopBarOptions = () => {
|
||||||
showAtResolution: 'xl:flex'
|
showAtResolution: 'xl:flex'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
toolTipLabel: t('key_manager'),
|
toolTipLabel: 'Key Manager',
|
||||||
icon: <Key className={TOP_BAR_ICON_STYLE} />,
|
icon: <Key className={TOP_BAR_ICON_CLASSLIST} />,
|
||||||
popOverComponent: <KeyManager />,
|
popOverComponent: <KeyManager />,
|
||||||
individual: true,
|
individual: true,
|
||||||
showAtResolution: 'xl:flex'
|
showAtResolution: 'xl:flex'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
toolTipLabel: 'Assign tags',
|
||||||
|
icon: (
|
||||||
|
<Tag
|
||||||
|
weight={tagAssignMode ? 'fill' : 'regular'}
|
||||||
|
className={TOP_BAR_ICON_CLASSLIST}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
// TODO: Assign tag mode is not yet implemented!
|
||||||
|
onClick: () =>
|
||||||
|
(explorerStore.isTagAssignModeActive = !explorerStore.isTagAssignModeActive),
|
||||||
|
// TODO: remove once tag-assign-mode impl complete
|
||||||
|
// onClick: () => toast.info('Coming soon!'),
|
||||||
|
topBarActive: tagAssignMode,
|
||||||
|
individual: true,
|
||||||
|
showAtResolution: 'xl:flex'
|
||||||
}
|
}
|
||||||
// {
|
// {
|
||||||
// toolTipLabel: 'Tag Assign Mode',
|
// toolTipLabel: 'Tag Assign Mode',
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { tw } from '@sd/ui';
|
||||||
|
|
||||||
import { useTopBarContext } from '../../TopBar/Context';
|
import { useTopBarContext } from '../../TopBar/Context';
|
||||||
import { useExplorerContext } from '../Context';
|
import { useExplorerContext } from '../Context';
|
||||||
import { PATH_BAR_HEIGHT } from '../ExplorerPath';
|
import { PATH_BAR_HEIGHT } from '../ExplorerPathBar';
|
||||||
import { useDragScrollable } from './useDragScrollable';
|
import { useDragScrollable } from './useDragScrollable';
|
||||||
|
|
||||||
const Trigger = tw.div`absolute inset-x-0 h-10 pointer-events-none`;
|
const Trigger = tw.div`absolute inset-x-0 h-10 pointer-events-none`;
|
||||||
|
|
|
@ -94,7 +94,7 @@ export const View = ({ emptyNotice, ...contextProps }: ExplorerViewProps) => {
|
||||||
|
|
||||||
const activeItem = useActiveItem();
|
const activeItem = useActiveItem();
|
||||||
|
|
||||||
useShortcuts();
|
useExplorerShortcuts();
|
||||||
|
|
||||||
useShortcut('explorerEscape', () => explorer.resetSelectedItems([]), {
|
useShortcut('explorerEscape', () => explorer.resetSelectedItems([]), {
|
||||||
disabled: !selectable || explorer.selectedItems.size === 0
|
disabled: !selectable || explorer.selectedItems.size === 0
|
||||||
|
@ -192,9 +192,12 @@ export const View = ({ emptyNotice, ...contextProps }: ExplorerViewProps) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const useShortcuts = () => {
|
const useExplorerShortcuts = () => {
|
||||||
const explorer = useExplorerContext();
|
const explorer = useExplorerContext();
|
||||||
const isRenaming = useSelector(explorerStore, (s) => s.isRenaming);
|
const [isRenaming, tagAssignMode] = useSelector(explorerStore, (s) => [
|
||||||
|
s.isRenaming,
|
||||||
|
s.isTagAssignModeActive
|
||||||
|
]);
|
||||||
const quickPreviewStore = useQuickPreviewStore();
|
const quickPreviewStore = useQuickPreviewStore();
|
||||||
|
|
||||||
const meta = useKeyMatcher('Meta');
|
const meta = useKeyMatcher('Meta');
|
||||||
|
@ -207,6 +210,10 @@ const useShortcuts = () => {
|
||||||
useShortcut('duplicateObject', duplicate);
|
useShortcut('duplicateObject', duplicate);
|
||||||
useShortcut('pasteObject', paste);
|
useShortcut('pasteObject', paste);
|
||||||
|
|
||||||
|
useShortcut('toggleTagAssignMode', (e) => {
|
||||||
|
explorerStore.isTagAssignModeActive = !tagAssignMode;
|
||||||
|
});
|
||||||
|
|
||||||
useShortcut('toggleQuickPreview', (e) => {
|
useShortcut('toggleQuickPreview', (e) => {
|
||||||
if (isRenaming || dialogManager.isAnyDialogOpen()) return;
|
if (isRenaming || dialogManager.isAnyDialogOpen()) return;
|
||||||
if (explorerStore.isCMDPOpen) return;
|
if (explorerStore.isCMDPOpen) return;
|
||||||
|
|
|
@ -13,7 +13,7 @@ import { useTopBarContext } from '../TopBar/Context';
|
||||||
import { useExplorerContext } from './Context';
|
import { useExplorerContext } from './Context';
|
||||||
import ContextMenu from './ContextMenu';
|
import ContextMenu from './ContextMenu';
|
||||||
import DismissibleNotice from './DismissibleNotice';
|
import DismissibleNotice from './DismissibleNotice';
|
||||||
import { ExplorerPath, PATH_BAR_HEIGHT } from './ExplorerPath';
|
import { ExplorerPathBar, PATH_BAR_HEIGHT } from './ExplorerPathBar';
|
||||||
import { Inspector, INSPECTOR_WIDTH } from './Inspector';
|
import { Inspector, INSPECTOR_WIDTH } from './Inspector';
|
||||||
import ExplorerContextMenu from './ParentContextMenu';
|
import ExplorerContextMenu from './ParentContextMenu';
|
||||||
import { getQuickPreviewStore } from './QuickPreview/store';
|
import { getQuickPreviewStore } from './QuickPreview/store';
|
||||||
|
@ -24,6 +24,9 @@ import { EmptyNotice } from './View/EmptyNotice';
|
||||||
|
|
||||||
import 'react-slidedown/lib/slidedown.css';
|
import 'react-slidedown/lib/slidedown.css';
|
||||||
|
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
import { ExplorerTagBar } from './ExplorerTagBar';
|
||||||
import { useExplorerDnd } from './useExplorerDnd';
|
import { useExplorerDnd } from './useExplorerDnd';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
@ -38,7 +41,10 @@ interface Props {
|
||||||
export default function Explorer(props: PropsWithChildren<Props>) {
|
export default function Explorer(props: PropsWithChildren<Props>) {
|
||||||
const explorer = useExplorerContext();
|
const explorer = useExplorerContext();
|
||||||
const layoutStore = useExplorerLayoutStore();
|
const layoutStore = useExplorerLayoutStore();
|
||||||
const showInspector = useSelector(explorerStore, (s) => s.showInspector);
|
const [showInspector, showTagBar] = useSelector(explorerStore, (s) => [
|
||||||
|
s.showInspector,
|
||||||
|
s.isTagAssignModeActive
|
||||||
|
]);
|
||||||
|
|
||||||
const showPathBar = explorer.showPathBar && layoutStore.showPathBar;
|
const showPathBar = explorer.showPathBar && layoutStore.showPathBar;
|
||||||
const rspc = useRspcLibraryContext();
|
const rspc = useRspcLibraryContext();
|
||||||
|
@ -117,14 +123,20 @@ export default function Explorer(props: PropsWithChildren<Props>) {
|
||||||
</div>
|
</div>
|
||||||
</ExplorerContextMenu>
|
</ExplorerContextMenu>
|
||||||
|
|
||||||
{showPathBar && <ExplorerPath />}
|
{/* TODO: wrap path bar and tag bar in nice wrapper, ideally animate tag bar in/out directly above path bar */}
|
||||||
|
<div className="absolute inset-x-0 bottom-0 z-50 flex flex-col">
|
||||||
|
{showTagBar && <ExplorerTagBar />}
|
||||||
|
{showPathBar && <ExplorerPathBar />}
|
||||||
|
</div>
|
||||||
|
|
||||||
{showInspector && (
|
{showInspector && (
|
||||||
<Inspector
|
<Inspector
|
||||||
className="no-scrollbar absolute right-1.5 top-0 pb-3 pl-3 pr-1.5"
|
className={clsx(
|
||||||
|
'no-scrollbar absolute right-1.5 top-0 pb-3 pl-3 pr-1.5',
|
||||||
|
showPathBar && `b-[${PATH_BAR_HEIGHT}px]`
|
||||||
|
)}
|
||||||
style={{
|
style={{
|
||||||
paddingTop: topBar.topBarHeight + 12,
|
paddingTop: topBar.topBarHeight + 12
|
||||||
bottom: showPathBar ? PATH_BAR_HEIGHT : 0
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
import { proxy } from 'valtio';
|
||||||
|
import { proxySet } from 'valtio/utils';
|
||||||
|
import { z } from 'zod';
|
||||||
import {
|
import {
|
||||||
ThumbKey,
|
ThumbKey,
|
||||||
resetStore,
|
resetStore,
|
||||||
|
@ -7,9 +10,6 @@ import {
|
||||||
type ExplorerSettings,
|
type ExplorerSettings,
|
||||||
type Ordering
|
type Ordering
|
||||||
} from '@sd/client';
|
} from '@sd/client';
|
||||||
import { proxy } from 'valtio';
|
|
||||||
import { proxySet } from 'valtio/utils';
|
|
||||||
import { z } from 'zod';
|
|
||||||
import i18n from '~/app/I18n';
|
import i18n from '~/app/I18n';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -98,7 +98,6 @@ type DragState =
|
||||||
};
|
};
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
tagAssignMode: false,
|
|
||||||
showInspector: false,
|
showInspector: false,
|
||||||
showMoreInfo: false,
|
showMoreInfo: false,
|
||||||
newLocationToRedirect: null as null | number,
|
newLocationToRedirect: null as null | number,
|
||||||
|
@ -106,12 +105,15 @@ const state = {
|
||||||
newThumbnails: proxySet() as Set<string>,
|
newThumbnails: proxySet() as Set<string>,
|
||||||
cutCopyState: { type: 'Idle' } as CutCopyState,
|
cutCopyState: { type: 'Idle' } as CutCopyState,
|
||||||
drag: null as null | DragState,
|
drag: null as null | DragState,
|
||||||
|
isTagAssignModeActive: false,
|
||||||
isDragSelecting: false,
|
isDragSelecting: false,
|
||||||
isRenaming: false,
|
isRenaming: false,
|
||||||
// Used for disabling certain keyboard shortcuts when command palette is open
|
// Used for disabling certain keyboard shortcuts when command palette is open
|
||||||
isCMDPOpen: false,
|
isCMDPOpen: false,
|
||||||
isContextMenuOpen: false,
|
isContextMenuOpen: false,
|
||||||
quickRescanLastRun: Date.now() - 200
|
quickRescanLastRun: Date.now() - 200,
|
||||||
|
// Map = { hotkey: '0'...'9', tagId: 1234 }
|
||||||
|
tagBulkAssignHotkeys: [] as Array<{ hotkey: string; tagId: number }>
|
||||||
};
|
};
|
||||||
|
|
||||||
export function flattenThumbnailKey(thumbKey: ThumbKey) {
|
export function flattenThumbnailKey(thumbKey: ThumbKey) {
|
||||||
|
|
|
@ -15,7 +15,7 @@ import { useDropzone, useLocale, useOnDndLeave } from '~/hooks';
|
||||||
import { hardwareModelToIcon } from '~/util/hardware';
|
import { hardwareModelToIcon } from '~/util/hardware';
|
||||||
import { usePlatform } from '~/util/Platform';
|
import { usePlatform } from '~/util/Platform';
|
||||||
|
|
||||||
import { TOP_BAR_ICON_STYLE } from '../TopBar/TopBarOptions';
|
import { TOP_BAR_ICON_CLASSLIST } from '../TopBar/TopBarOptions';
|
||||||
import { useIncomingSpacedropToast, useSpacedropProgressToast } from './toast';
|
import { useIncomingSpacedropToast, useSpacedropProgressToast } from './toast';
|
||||||
|
|
||||||
// TODO: This is super hacky so should probs be rewritten but for now it works.
|
// TODO: This is super hacky so should probs be rewritten but for now it works.
|
||||||
|
@ -57,7 +57,7 @@ export function SpacedropButton({ triggerOpen }: { triggerOpen: () => void }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={ref} className={dndState === 'active' && !isPanelOpen ? 'animate-bounce' : ''}>
|
<div ref={ref} className={dndState === 'active' && !isPanelOpen ? 'animate-bounce' : ''}>
|
||||||
<Planet className={TOP_BAR_ICON_STYLE} />
|
<Planet className={TOP_BAR_ICON_CLASSLIST} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import React, { forwardRef, HTMLAttributes } from 'react';
|
||||||
import { Popover, usePopover } from '@sd/ui';
|
import { Popover, usePopover } from '@sd/ui';
|
||||||
|
|
||||||
import TopBarButton, { TopBarButtonProps } from './TopBarButton';
|
import TopBarButton, { TopBarButtonProps } from './TopBarButton';
|
||||||
import { ToolOption, TOP_BAR_ICON_STYLE } from './TopBarOptions';
|
import { ToolOption, TOP_BAR_ICON_CLASSLIST } from './TopBarOptions';
|
||||||
|
|
||||||
const GroupTool = forwardRef<
|
const GroupTool = forwardRef<
|
||||||
HTMLButtonElement,
|
HTMLButtonElement,
|
||||||
|
@ -40,7 +40,7 @@ export default ({ toolOptions, className }: Props) => {
|
||||||
popover={popover}
|
popover={popover}
|
||||||
trigger={
|
trigger={
|
||||||
<TopBarButton>
|
<TopBarButton>
|
||||||
<DotsThreeCircle className={TOP_BAR_ICON_STYLE} />
|
<DotsThreeCircle className={TOP_BAR_ICON_CLASSLIST} />
|
||||||
</TopBarButton>
|
</TopBarButton>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
|
@ -26,7 +26,7 @@ interface TopBarChildrenProps {
|
||||||
options?: ToolOption[][];
|
options?: ToolOption[][];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TOP_BAR_ICON_STYLE = 'm-0.5 w-[18px] h-[18px] text-ink-dull';
|
export const TOP_BAR_ICON_CLASSLIST = 'm-0.5 w-[18px] h-[18px] text-ink-dull';
|
||||||
|
|
||||||
export default ({ options }: TopBarChildrenProps) => {
|
export default ({ options }: TopBarChildrenProps) => {
|
||||||
const [windowSize, setWindowSize] = useState(0);
|
const [windowSize, setWindowSize] = useState(0);
|
||||||
|
@ -193,7 +193,7 @@ export function WindowsControls({ windowSize }: { windowSize: number }) {
|
||||||
active={false}
|
active={false}
|
||||||
onClick={() => appWindow.minimize()}
|
onClick={() => appWindow.minimize()}
|
||||||
>
|
>
|
||||||
<Minus weight="regular" className={clsx(TOP_BAR_ICON_STYLE)} />
|
<Minus weight="regular" className={clsx(TOP_BAR_ICON_CLASSLIST)} />
|
||||||
</TopBarButton>
|
</TopBarButton>
|
||||||
<TopBarButton
|
<TopBarButton
|
||||||
rounding="both"
|
rounding="both"
|
||||||
|
@ -204,9 +204,9 @@ export function WindowsControls({ windowSize }: { windowSize: number }) {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{maximized ? (
|
{maximized ? (
|
||||||
<Cards weight="regular" className={clsx(TOP_BAR_ICON_STYLE)} />
|
<Cards weight="regular" className={clsx(TOP_BAR_ICON_CLASSLIST)} />
|
||||||
) : (
|
) : (
|
||||||
<Square weight="regular" className={clsx(TOP_BAR_ICON_STYLE)} />
|
<Square weight="regular" className={clsx(TOP_BAR_ICON_CLASSLIST)} />
|
||||||
)}
|
)}
|
||||||
</TopBarButton>
|
</TopBarButton>
|
||||||
<TopBarButton
|
<TopBarButton
|
||||||
|
@ -215,7 +215,7 @@ export function WindowsControls({ windowSize }: { windowSize: number }) {
|
||||||
active={false}
|
active={false}
|
||||||
onClick={() => appWindow.close()}
|
onClick={() => appWindow.close()}
|
||||||
>
|
>
|
||||||
<X weight="regular" className={clsx(TOP_BAR_ICON_STYLE, 'hover:text-white')} />
|
<X weight="regular" className={clsx(TOP_BAR_ICON_CLASSLIST, 'hover:text-white')} />
|
||||||
</TopBarButton>
|
</TopBarButton>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -35,7 +35,7 @@ import { SearchContextProvider, SearchOptions, useSearchFromSearchParams } from
|
||||||
import SearchBar from '../search/SearchBar';
|
import SearchBar from '../search/SearchBar';
|
||||||
import { useSearchExplorerQuery } from '../search/useSearchExplorerQuery';
|
import { useSearchExplorerQuery } from '../search/useSearchExplorerQuery';
|
||||||
import { TopBarPortal } from '../TopBar/Portal';
|
import { TopBarPortal } from '../TopBar/Portal';
|
||||||
import { TOP_BAR_ICON_STYLE } from '../TopBar/TopBarOptions';
|
import { TOP_BAR_ICON_CLASSLIST } from '../TopBar/TopBarOptions';
|
||||||
import LocationOptions from './LocationOptions';
|
import LocationOptions from './LocationOptions';
|
||||||
|
|
||||||
export const Component = () => {
|
export const Component = () => {
|
||||||
|
@ -151,7 +151,7 @@ const LocationExplorer = ({ location }: { location: Location; path?: string }) =
|
||||||
{
|
{
|
||||||
toolTipLabel: t('reload'),
|
toolTipLabel: t('reload'),
|
||||||
onClick: () => rescan(location.id),
|
onClick: () => rescan(location.id),
|
||||||
icon: <ArrowClockwise className={TOP_BAR_ICON_STYLE} />,
|
icon: <ArrowClockwise className={TOP_BAR_ICON_CLASSLIST} />,
|
||||||
individual: true,
|
individual: true,
|
||||||
showAtResolution: 'xl:flex'
|
showAtResolution: 'xl:flex'
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,6 +28,7 @@ export const useIsLocationIndexing = (locationId: number): boolean => {
|
||||||
) {
|
) {
|
||||||
return job.completed_task_count === 0;
|
return job.completed_task_count === 0;
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
})
|
})
|
||||||
) || false;
|
) || false;
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
|
import { useLibraryQuery, useSelector } from '@sd/client';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useNavigate } from 'react-router';
|
import { useNavigate } from 'react-router';
|
||||||
import { useLibraryQuery, useSelector } from '@sd/client';
|
|
||||||
import { explorerStore } from '~/app/$libraryId/Explorer/store';
|
import { explorerStore } from '~/app/$libraryId/Explorer/store';
|
||||||
|
|
||||||
import { LibraryIdParamsSchema } from '../app/route-schemas';
|
import { LibraryIdParamsSchema } from '../app/route-schemas';
|
||||||
|
|
|
@ -37,6 +37,10 @@ const shortcuts = {
|
||||||
macOS: ['Meta', 'KeyJ'],
|
macOS: ['Meta', 'KeyJ'],
|
||||||
all: ['Control', 'KeyJ']
|
all: ['Control', 'KeyJ']
|
||||||
},
|
},
|
||||||
|
toggleTagAssignMode: {
|
||||||
|
macOS: ['Meta', 'Alt', 'KeyT'],
|
||||||
|
all: ['Control', 'Alt', 'KeyT']
|
||||||
|
},
|
||||||
navBackwardHistory: {
|
navBackwardHistory: {
|
||||||
macOS: ['Meta', '['],
|
macOS: ['Meta', '['],
|
||||||
all: ['Control', '[']
|
all: ['Control', '[']
|
||||||
|
|
|
@ -260,14 +260,16 @@
|
||||||
"feedback_login_description": "تسجيل الدخول يسمح لنا بالرد على ملاحظاتك",
|
"feedback_login_description": "تسجيل الدخول يسمح لنا بالرد على ملاحظاتك",
|
||||||
"feedback_placeholder": "ملاحظاتك...",
|
"feedback_placeholder": "ملاحظاتك...",
|
||||||
"feedback_toast_error_message": "حدث خطأ أثناء إرسال ملاحظاتك. يرجى المحاولة مرة أخرى.",
|
"feedback_toast_error_message": "حدث خطأ أثناء إرسال ملاحظاتك. يرجى المحاولة مرة أخرى.",
|
||||||
"file": "file",
|
|
||||||
"file_already_exist_in_this_location": "الملف موجود بالفعل في هذا الموقع",
|
"file_already_exist_in_this_location": "الملف موجود بالفعل في هذا الموقع",
|
||||||
"file_directory_name": "اسم الملف/الدليل",
|
"file_directory_name": "اسم الملف/الدليل",
|
||||||
"file_extension_description": "امتداد الملف (على سبيل المثال، .mp4، .jpg، .txt)",
|
"file_extension_description": "امتداد الملف (على سبيل المثال، .mp4، .jpg، .txt)",
|
||||||
"file_from": "File {{file}} from {{name}}",
|
"file_from": "File {{file}} from {{name}}",
|
||||||
"file_indexing_rules": "قواعد فهرسة الملفات",
|
"file_indexing_rules": "قواعد فهرسة الملفات",
|
||||||
|
"file_one": "file",
|
||||||
"file_picker_not_supported": "File picker not supported on this platform",
|
"file_picker_not_supported": "File picker not supported on this platform",
|
||||||
"files": "files",
|
"file_two": "ملفات",
|
||||||
|
"file_zero": "ملفات",
|
||||||
|
"files_many": "files",
|
||||||
"filter": "Filter",
|
"filter": "Filter",
|
||||||
"filters": "مرشحات",
|
"filters": "مرشحات",
|
||||||
"flash": "فلاش",
|
"flash": "فلاش",
|
||||||
|
|
|
@ -260,14 +260,16 @@
|
||||||
"feedback_login_description": "Уваход у сістэму дазваляе нам адказваць на ваш фідбэк",
|
"feedback_login_description": "Уваход у сістэму дазваляе нам адказваць на ваш фідбэк",
|
||||||
"feedback_placeholder": "Ваш фідбэк...",
|
"feedback_placeholder": "Ваш фідбэк...",
|
||||||
"feedback_toast_error_message": "Пры адпраўленні вашага фідбэку адбылася абмыла. Калі ласка, паспрабуйце яшчэ раз.",
|
"feedback_toast_error_message": "Пры адпраўленні вашага фідбэку адбылася абмыла. Калі ласка, паспрабуйце яшчэ раз.",
|
||||||
"file": "файл",
|
|
||||||
"file_already_exist_in_this_location": "Файл ужо існуе ў гэтай лакацыі",
|
"file_already_exist_in_this_location": "Файл ужо існуе ў гэтай лакацыі",
|
||||||
"file_directory_name": "Імя файла/папкі",
|
"file_directory_name": "Імя файла/папкі",
|
||||||
"file_extension_description": "Пашырэнне файла (напрыклад, .mp4, .jpg, .txt)",
|
"file_extension_description": "Пашырэнне файла (напрыклад, .mp4, .jpg, .txt)",
|
||||||
"file_from": "Файл {{file}} з {{name}}",
|
"file_from": "Файл {{file}} з {{name}}",
|
||||||
"file_indexing_rules": "Правілы індэксацыі файлаў",
|
"file_indexing_rules": "Правілы індэксацыі файлаў",
|
||||||
|
"file_one": "файл",
|
||||||
"file_picker_not_supported": "Сістэма выбару файлаў не падтрымліваецца на гэтай платформе",
|
"file_picker_not_supported": "Сістэма выбару файлаў не падтрымліваецца на гэтай платформе",
|
||||||
"files": "файлы",
|
"file_two": "файлы",
|
||||||
|
"file_zero": "файлы",
|
||||||
|
"files_many": "файлы",
|
||||||
"filter": "Фільтр",
|
"filter": "Фільтр",
|
||||||
"filters": "Фільтры",
|
"filters": "Фільтры",
|
||||||
"flash": "Успышка",
|
"flash": "Успышка",
|
||||||
|
|
|
@ -260,14 +260,16 @@
|
||||||
"feedback_login_description": "Die Anmeldung ermöglicht es uns, auf Ihr Feedback zu antworten",
|
"feedback_login_description": "Die Anmeldung ermöglicht es uns, auf Ihr Feedback zu antworten",
|
||||||
"feedback_placeholder": "Ihr Feedback...",
|
"feedback_placeholder": "Ihr Feedback...",
|
||||||
"feedback_toast_error_message": "Beim Senden deines Feedbacks ist ein Fehler aufgetreten. Bitte versuche es erneut.",
|
"feedback_toast_error_message": "Beim Senden deines Feedbacks ist ein Fehler aufgetreten. Bitte versuche es erneut.",
|
||||||
"file": "file",
|
|
||||||
"file_already_exist_in_this_location": "Die Datei existiert bereits an diesem Speicherort",
|
"file_already_exist_in_this_location": "Die Datei existiert bereits an diesem Speicherort",
|
||||||
"file_directory_name": "Datei-/Verzeichnisname",
|
"file_directory_name": "Datei-/Verzeichnisname",
|
||||||
"file_extension_description": "Dateierweiterung (z. B. .mp4, .jpg, .txt)",
|
"file_extension_description": "Dateierweiterung (z. B. .mp4, .jpg, .txt)",
|
||||||
"file_from": "File {{file}} from {{name}}",
|
"file_from": "File {{file}} from {{name}}",
|
||||||
"file_indexing_rules": "Dateiindizierungsregeln",
|
"file_indexing_rules": "Dateiindizierungsregeln",
|
||||||
|
"file_one": "file",
|
||||||
"file_picker_not_supported": "File picker not supported on this platform",
|
"file_picker_not_supported": "File picker not supported on this platform",
|
||||||
"files": "files",
|
"file_two": "Dateien",
|
||||||
|
"file_zero": "Dateien",
|
||||||
|
"files_many": "files",
|
||||||
"filter": "Filter",
|
"filter": "Filter",
|
||||||
"filters": "Filter",
|
"filters": "Filter",
|
||||||
"flash": "Blitz",
|
"flash": "Blitz",
|
||||||
|
|
|
@ -216,6 +216,7 @@
|
||||||
"error": "Error",
|
"error": "Error",
|
||||||
"error_loading_original_file": "Error loading original file",
|
"error_loading_original_file": "Error loading original file",
|
||||||
"error_message": "Error: {{error}}.",
|
"error_message": "Error: {{error}}.",
|
||||||
|
"error_unknown": "An unknown error occurred.",
|
||||||
"executable": "Executable",
|
"executable": "Executable",
|
||||||
"expand": "Expand",
|
"expand": "Expand",
|
||||||
"explorer": "Explorer",
|
"explorer": "Explorer",
|
||||||
|
@ -262,14 +263,17 @@
|
||||||
"feedback_login_description": "Logging in allows us to respond to your feedback",
|
"feedback_login_description": "Logging in allows us to respond to your feedback",
|
||||||
"feedback_placeholder": "Your feedback...",
|
"feedback_placeholder": "Your feedback...",
|
||||||
"feedback_toast_error_message": "There was an error submitting your feedback. Please try again.",
|
"feedback_toast_error_message": "There was an error submitting your feedback. Please try again.",
|
||||||
"file": "file",
|
|
||||||
"file_already_exist_in_this_location": "File already exists in this location",
|
"file_already_exist_in_this_location": "File already exists in this location",
|
||||||
"file_directory_name": "File/Directory name",
|
"file_directory_name": "File/Directory name",
|
||||||
"file_extension_description": "File extension (e.g., .mp4, .jpg, .txt)",
|
"file_extension_description": "File extension (e.g., .mp4, .jpg, .txt)",
|
||||||
"file_from": "File {{file}} from {{name}}",
|
"file_from": "File {{file}} from {{name}}",
|
||||||
"file_indexing_rules": "File indexing rules",
|
"file_indexing_rules": "File indexing rules",
|
||||||
|
"file_many": "files",
|
||||||
|
"file_one": "file",
|
||||||
|
"file_other": "files",
|
||||||
"file_picker_not_supported": "File picker not supported on this platform",
|
"file_picker_not_supported": "File picker not supported on this platform",
|
||||||
"files": "files",
|
"file_two": "files",
|
||||||
|
"file_zero": "files",
|
||||||
"filter": "Filter",
|
"filter": "Filter",
|
||||||
"filters": "Filters",
|
"filters": "Filters",
|
||||||
"flash": "Flash",
|
"flash": "Flash",
|
||||||
|
@ -670,6 +674,11 @@
|
||||||
"tag_one": "Tag",
|
"tag_one": "Tag",
|
||||||
"tag_other": "Tags",
|
"tag_other": "Tags",
|
||||||
"tags": "Tags",
|
"tags": "Tags",
|
||||||
|
"tags_bulk_assigned": "Assigned tag \"{{tag_name}}\" to {{file_count}} $t(file, { \"count\": {{file_count}} }).",
|
||||||
|
"tags_bulk_failed_with_tag": "Could not assign tag \"{{tag_name}}\" to {{file_count}} $t(file, { \"count\": {{file_count}} }): {{error_message}}",
|
||||||
|
"tags_bulk_failed_without_tag": "Could not tag {{file_count}} $t(file, { \"count\": {{file_count}} }): {{error_message}}",
|
||||||
|
"tags_bulk_instructions": "Select one or more files and press a key to assign the corresponding tag.",
|
||||||
|
"tags_bulk_mode_active": "Tag assign mode is enabled.",
|
||||||
"tags_description": "Manage your tags.",
|
"tags_description": "Manage your tags.",
|
||||||
"tags_notice_message": "No items assigned to this tag.",
|
"tags_notice_message": "No items assigned to this tag.",
|
||||||
"task": "task",
|
"task": "task",
|
||||||
|
|
|
@ -260,14 +260,16 @@
|
||||||
"feedback_login_description": "Iniciar sesión nos permite responder a tu retroalimentación",
|
"feedback_login_description": "Iniciar sesión nos permite responder a tu retroalimentación",
|
||||||
"feedback_placeholder": "Tu retroalimentación...",
|
"feedback_placeholder": "Tu retroalimentación...",
|
||||||
"feedback_toast_error_message": "Hubo un error al enviar tu retroalimentación. Por favor, inténtalo de nuevo.",
|
"feedback_toast_error_message": "Hubo un error al enviar tu retroalimentación. Por favor, inténtalo de nuevo.",
|
||||||
"file": "file",
|
|
||||||
"file_already_exist_in_this_location": "El archivo ya existe en esta ubicación",
|
"file_already_exist_in_this_location": "El archivo ya existe en esta ubicación",
|
||||||
"file_directory_name": "Nombre de archivo/directorio",
|
"file_directory_name": "Nombre de archivo/directorio",
|
||||||
"file_extension_description": "Extensión de archivo (por ejemplo, .mp4, .jpg, .txt)",
|
"file_extension_description": "Extensión de archivo (por ejemplo, .mp4, .jpg, .txt)",
|
||||||
"file_from": "File {{file}} from {{name}}",
|
"file_from": "File {{file}} from {{name}}",
|
||||||
"file_indexing_rules": "Reglas de indexación de archivos",
|
"file_indexing_rules": "Reglas de indexación de archivos",
|
||||||
|
"file_one": "file",
|
||||||
"file_picker_not_supported": "File picker not supported on this platform",
|
"file_picker_not_supported": "File picker not supported on this platform",
|
||||||
"files": "files",
|
"file_two": "archivos",
|
||||||
|
"file_zero": "archivos",
|
||||||
|
"files_many": "files",
|
||||||
"filter": "Filtro",
|
"filter": "Filtro",
|
||||||
"filters": "Filtros",
|
"filters": "Filtros",
|
||||||
"flash": "Destello",
|
"flash": "Destello",
|
||||||
|
|
|
@ -260,14 +260,16 @@
|
||||||
"feedback_login_description": "La connexion nous permet de répondre à votre retour d'information",
|
"feedback_login_description": "La connexion nous permet de répondre à votre retour d'information",
|
||||||
"feedback_placeholder": "Votre retour d'information...",
|
"feedback_placeholder": "Votre retour d'information...",
|
||||||
"feedback_toast_error_message": "Une erreur s'est produite lors de l'envoi de votre retour d'information. Veuillez réessayer.",
|
"feedback_toast_error_message": "Une erreur s'est produite lors de l'envoi de votre retour d'information. Veuillez réessayer.",
|
||||||
"file": "file",
|
|
||||||
"file_already_exist_in_this_location": "Le fichier existe déjà à cet emplacement",
|
"file_already_exist_in_this_location": "Le fichier existe déjà à cet emplacement",
|
||||||
"file_directory_name": "Nom du fichier/répertoire",
|
"file_directory_name": "Nom du fichier/répertoire",
|
||||||
"file_extension_description": "Extension de fichier (par exemple, .mp4, .jpg, .txt)",
|
"file_extension_description": "Extension de fichier (par exemple, .mp4, .jpg, .txt)",
|
||||||
"file_from": "File {{file}} from {{name}}",
|
"file_from": "File {{file}} from {{name}}",
|
||||||
"file_indexing_rules": "Règles d'indexation des fichiers",
|
"file_indexing_rules": "Règles d'indexation des fichiers",
|
||||||
|
"file_one": "file",
|
||||||
"file_picker_not_supported": "File picker not supported on this platform",
|
"file_picker_not_supported": "File picker not supported on this platform",
|
||||||
"files": "files",
|
"file_two": "des dossiers",
|
||||||
|
"file_zero": "des dossiers",
|
||||||
|
"files_many": "files",
|
||||||
"filter": "Filtre",
|
"filter": "Filtre",
|
||||||
"filters": "Filtres",
|
"filters": "Filtres",
|
||||||
"flash": "Éclair",
|
"flash": "Éclair",
|
||||||
|
|
|
@ -260,14 +260,16 @@
|
||||||
"feedback_login_description": "Effettuando l'accesso possiamo rispondere al tuo feedback",
|
"feedback_login_description": "Effettuando l'accesso possiamo rispondere al tuo feedback",
|
||||||
"feedback_placeholder": "Il tuo feedback...",
|
"feedback_placeholder": "Il tuo feedback...",
|
||||||
"feedback_toast_error_message": "Si è verificato un errore durante l'invio del tuo feedback. Riprova.",
|
"feedback_toast_error_message": "Si è verificato un errore durante l'invio del tuo feedback. Riprova.",
|
||||||
"file": "file",
|
|
||||||
"file_already_exist_in_this_location": "Il file esiste già in questa posizione",
|
"file_already_exist_in_this_location": "Il file esiste già in questa posizione",
|
||||||
"file_directory_name": "Nome del file/directory",
|
"file_directory_name": "Nome del file/directory",
|
||||||
"file_extension_description": "Estensione del file (ad esempio, .mp4, .jpg, .txt)",
|
"file_extension_description": "Estensione del file (ad esempio, .mp4, .jpg, .txt)",
|
||||||
"file_from": "File {{file}} from {{name}}",
|
"file_from": "File {{file}} from {{name}}",
|
||||||
"file_indexing_rules": "Regole di indicizzazione dei file",
|
"file_indexing_rules": "Regole di indicizzazione dei file",
|
||||||
|
"file_one": "file",
|
||||||
"file_picker_not_supported": "File picker not supported on this platform",
|
"file_picker_not_supported": "File picker not supported on this platform",
|
||||||
"files": "files",
|
"file_two": "File",
|
||||||
|
"file_zero": "File",
|
||||||
|
"files_many": "files",
|
||||||
"filter": "Filtro",
|
"filter": "Filtro",
|
||||||
"filters": "Filtri",
|
"filters": "Filtri",
|
||||||
"flash": "Veloce",
|
"flash": "Veloce",
|
||||||
|
|
|
@ -260,14 +260,16 @@
|
||||||
"feedback_login_description": "ログインすることで、フィードバックを送ることができます。",
|
"feedback_login_description": "ログインすることで、フィードバックを送ることができます。",
|
||||||
"feedback_placeholder": "フィードバックを入力...",
|
"feedback_placeholder": "フィードバックを入力...",
|
||||||
"feedback_toast_error_message": "フィードバックの送信中にエラーが発生しました。もう一度お試しください。",
|
"feedback_toast_error_message": "フィードバックの送信中にエラーが発生しました。もう一度お試しください。",
|
||||||
"file": "ファイル",
|
|
||||||
"file_already_exist_in_this_location": "このファイルは既にこのロケーションに存在します",
|
"file_already_exist_in_this_location": "このファイルは既にこのロケーションに存在します",
|
||||||
"file_directory_name": "ファイル/ディレクトリ名",
|
"file_directory_name": "ファイル/ディレクトリ名",
|
||||||
"file_extension_description": "ファイル拡張子 (例: .mp4、.jpg、.txt)",
|
"file_extension_description": "ファイル拡張子 (例: .mp4、.jpg、.txt)",
|
||||||
"file_from": "File {{file}} from {{name}}",
|
"file_from": "File {{file}} from {{name}}",
|
||||||
"file_indexing_rules": "ファイルのインデックス化ルール",
|
"file_indexing_rules": "ファイルのインデックス化ルール",
|
||||||
|
"file_one": "ファイル",
|
||||||
"file_picker_not_supported": "このプラットフォームではファイルピッカーはサポートされていません",
|
"file_picker_not_supported": "このプラットフォームではファイルピッカーはサポートされていません",
|
||||||
"files": "ファイル",
|
"file_two": "ファイル",
|
||||||
|
"file_zero": "ファイル",
|
||||||
|
"files_many": "ファイル",
|
||||||
"filter": "フィルター",
|
"filter": "フィルター",
|
||||||
"filters": "フィルター",
|
"filters": "フィルター",
|
||||||
"flash": "閃光",
|
"flash": "閃光",
|
||||||
|
|
|
@ -260,14 +260,16 @@
|
||||||
"feedback_login_description": "Inloggen stelt ons in staat om te reageren op jouw feedback",
|
"feedback_login_description": "Inloggen stelt ons in staat om te reageren op jouw feedback",
|
||||||
"feedback_placeholder": "Jouw feedback...",
|
"feedback_placeholder": "Jouw feedback...",
|
||||||
"feedback_toast_error_message": "Er is een fout opgetreden bij het verzenden van je feedback. Probeer het opnieuw.",
|
"feedback_toast_error_message": "Er is een fout opgetreden bij het verzenden van je feedback. Probeer het opnieuw.",
|
||||||
"file": "file",
|
|
||||||
"file_already_exist_in_this_location": "Bestand bestaat al op deze locatie",
|
"file_already_exist_in_this_location": "Bestand bestaat al op deze locatie",
|
||||||
"file_directory_name": "Bestands-/mapnaam",
|
"file_directory_name": "Bestands-/mapnaam",
|
||||||
"file_extension_description": "Bestandsextensie (bijvoorbeeld .mp4, .jpg, .txt)",
|
"file_extension_description": "Bestandsextensie (bijvoorbeeld .mp4, .jpg, .txt)",
|
||||||
"file_from": "File {{file}} from {{name}}",
|
"file_from": "File {{file}} from {{name}}",
|
||||||
"file_indexing_rules": "Bestand indexeringsregels",
|
"file_indexing_rules": "Bestand indexeringsregels",
|
||||||
|
"file_one": "file",
|
||||||
"file_picker_not_supported": "File picker not supported on this platform",
|
"file_picker_not_supported": "File picker not supported on this platform",
|
||||||
"files": "files",
|
"file_two": "bestanden",
|
||||||
|
"file_zero": "bestanden",
|
||||||
|
"files_many": "files",
|
||||||
"filter": "Filter",
|
"filter": "Filter",
|
||||||
"filters": "Filters",
|
"filters": "Filters",
|
||||||
"flash": "Flash",
|
"flash": "Flash",
|
||||||
|
|
|
@ -260,14 +260,16 @@
|
||||||
"feedback_login_description": "Вход в систему позволяет нам отвечать на ваш фидбек",
|
"feedback_login_description": "Вход в систему позволяет нам отвечать на ваш фидбек",
|
||||||
"feedback_placeholder": "Ваш фидбек...",
|
"feedback_placeholder": "Ваш фидбек...",
|
||||||
"feedback_toast_error_message": "При отправке вашего фидбека произошла ошибка. Пожалуйста, попробуйте еще раз.",
|
"feedback_toast_error_message": "При отправке вашего фидбека произошла ошибка. Пожалуйста, попробуйте еще раз.",
|
||||||
"file": "файл",
|
|
||||||
"file_already_exist_in_this_location": "Файл уже существует в этой локации",
|
"file_already_exist_in_this_location": "Файл уже существует в этой локации",
|
||||||
"file_directory_name": "Имя файла/папки",
|
"file_directory_name": "Имя файла/папки",
|
||||||
"file_extension_description": "Расширение файла (например, .mp4, .jpg, .txt)",
|
"file_extension_description": "Расширение файла (например, .mp4, .jpg, .txt)",
|
||||||
"file_from": "Файл {{file}} из {{name}}",
|
"file_from": "Файл {{file}} из {{name}}",
|
||||||
"file_indexing_rules": "Правила индексации файлов",
|
"file_indexing_rules": "Правила индексации файлов",
|
||||||
|
"file_one": "файл",
|
||||||
"file_picker_not_supported": "Система выбора файлов не поддерживается на этой платформе",
|
"file_picker_not_supported": "Система выбора файлов не поддерживается на этой платформе",
|
||||||
"files": "файлы",
|
"file_two": "файлы",
|
||||||
|
"file_zero": "файлы",
|
||||||
|
"files_many": "файлы",
|
||||||
"filter": "Фильтр",
|
"filter": "Фильтр",
|
||||||
"filters": "Фильтры",
|
"filters": "Фильтры",
|
||||||
"flash": "Вспышка",
|
"flash": "Вспышка",
|
||||||
|
|
|
@ -260,14 +260,16 @@
|
||||||
"feedback_login_description": "Giriş yapmak, geribildiriminize yanıt vermemizi sağlar",
|
"feedback_login_description": "Giriş yapmak, geribildiriminize yanıt vermemizi sağlar",
|
||||||
"feedback_placeholder": "Geribildiriminiz...",
|
"feedback_placeholder": "Geribildiriminiz...",
|
||||||
"feedback_toast_error_message": "Geribildiriminizi gönderirken bir hata oluştu. Lütfen tekrar deneyin.",
|
"feedback_toast_error_message": "Geribildiriminizi gönderirken bir hata oluştu. Lütfen tekrar deneyin.",
|
||||||
"file": "file",
|
|
||||||
"file_already_exist_in_this_location": "Dosya bu konumda zaten mevcut",
|
"file_already_exist_in_this_location": "Dosya bu konumda zaten mevcut",
|
||||||
"file_directory_name": "Dosya/Dizin adı",
|
"file_directory_name": "Dosya/Dizin adı",
|
||||||
"file_extension_description": "Dosya uzantısı (ör. .mp4, .jpg, .txt)",
|
"file_extension_description": "Dosya uzantısı (ör. .mp4, .jpg, .txt)",
|
||||||
"file_from": "File {{file}} from {{name}}",
|
"file_from": "File {{file}} from {{name}}",
|
||||||
"file_indexing_rules": "Dosya İndeksleme Kuralları",
|
"file_indexing_rules": "Dosya İndeksleme Kuralları",
|
||||||
|
"file_one": "file",
|
||||||
"file_picker_not_supported": "File picker not supported on this platform",
|
"file_picker_not_supported": "File picker not supported on this platform",
|
||||||
"files": "files",
|
"file_two": "Dosyalar",
|
||||||
|
"file_zero": "Dosyalar",
|
||||||
|
"files_many": "files",
|
||||||
"filter": "Filtre",
|
"filter": "Filtre",
|
||||||
"filters": "Filtreler",
|
"filters": "Filtreler",
|
||||||
"flash": "Flaş",
|
"flash": "Flaş",
|
||||||
|
|
|
@ -260,14 +260,16 @@
|
||||||
"feedback_login_description": "登录使我们能够回复您的反馈",
|
"feedback_login_description": "登录使我们能够回复您的反馈",
|
||||||
"feedback_placeholder": "您的反馈...",
|
"feedback_placeholder": "您的反馈...",
|
||||||
"feedback_toast_error_message": "提交反馈时出错,请重试。",
|
"feedback_toast_error_message": "提交反馈时出错,请重试。",
|
||||||
"file": "file",
|
|
||||||
"file_already_exist_in_this_location": "文件已存在于此位置",
|
"file_already_exist_in_this_location": "文件已存在于此位置",
|
||||||
"file_directory_name": "文件/目录名称",
|
"file_directory_name": "文件/目录名称",
|
||||||
"file_extension_description": "文件扩展名(例如 .mp4、.jpg、.txt)",
|
"file_extension_description": "文件扩展名(例如 .mp4、.jpg、.txt)",
|
||||||
"file_from": "File {{file}} from {{name}}",
|
"file_from": "File {{file}} from {{name}}",
|
||||||
"file_indexing_rules": "文件索引规则",
|
"file_indexing_rules": "文件索引规则",
|
||||||
|
"file_one": "file",
|
||||||
"file_picker_not_supported": "File picker not supported on this platform",
|
"file_picker_not_supported": "File picker not supported on this platform",
|
||||||
"files": "files",
|
"file_two": "文件",
|
||||||
|
"file_zero": "文件",
|
||||||
|
"files_many": "files",
|
||||||
"filter": "筛选",
|
"filter": "筛选",
|
||||||
"filters": "过滤器",
|
"filters": "过滤器",
|
||||||
"flash": "闪光",
|
"flash": "闪光",
|
||||||
|
|
|
@ -260,14 +260,16 @@
|
||||||
"feedback_login_description": "登入可讓我們回應您的回饋",
|
"feedback_login_description": "登入可讓我們回應您的回饋",
|
||||||
"feedback_placeholder": "您的回饋...",
|
"feedback_placeholder": "您的回饋...",
|
||||||
"feedback_toast_error_message": "提交回饋時發生錯誤。請重試。",
|
"feedback_toast_error_message": "提交回饋時發生錯誤。請重試。",
|
||||||
"file": "file",
|
|
||||||
"file_already_exist_in_this_location": "該位置已存在該檔案",
|
"file_already_exist_in_this_location": "該位置已存在該檔案",
|
||||||
"file_directory_name": "檔案/目錄名稱",
|
"file_directory_name": "檔案/目錄名稱",
|
||||||
"file_extension_description": "檔案副檔名(例如 .mp4、.jpg、.txt)",
|
"file_extension_description": "檔案副檔名(例如 .mp4、.jpg、.txt)",
|
||||||
"file_from": "File {{file}} from {{name}}",
|
"file_from": "File {{file}} from {{name}}",
|
||||||
"file_indexing_rules": "文件索引規則",
|
"file_indexing_rules": "文件索引規則",
|
||||||
|
"file_one": "file",
|
||||||
"file_picker_not_supported": "File picker not supported on this platform",
|
"file_picker_not_supported": "File picker not supported on this platform",
|
||||||
"files": "files",
|
"file_two": "文件",
|
||||||
|
"file_zero": "文件",
|
||||||
|
"files_many": "files",
|
||||||
"filter": "篩選",
|
"filter": "篩選",
|
||||||
"filters": "篩選器",
|
"filters": "篩選器",
|
||||||
"flash": "閃光",
|
"flash": "閃光",
|
||||||
|
|
|
@ -16,5 +16,5 @@
|
||||||
"cache": false
|
"cache": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"globalEnv": ["PORT", "NODE_ENV", "GENERATE_SOURCEMAP"]
|
"globalEnv": ["PORT", "NODE_ENV", "GENERATE_SOURCEMAP", "DEV"]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue