mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2026-05-18 21:36:56 -04:00
Media metadata extraction & Thumbnailer rework (#2285)
* initial ffprobe commit * Working slim down version ffprobe * Auto format ffprobe and deps source * Remove show_pixel_formats logic - Fix do_bitexact incorrect check in main after last changes - Fix some clangd warning * Remove show_* and print_format options and their respective logic * Rework ffprobe into simple_ffprobe - Simplify ffprobe logic into a simple program that gather and print a media file metadata * Reduce the amount of ffmpeg log messages while generating thumbnails * Fix completly wrong comments * mend * Start modeling ffmpeg extracted metadata on schema - Start porting ffprobe code to rust - Rename some references to media_data to exif_data * Finish modeling media info data - Add MediaProgram, MediaStream, MediaCodec, MediaVideoProps, MediaAudioProps, MediaSubtitleProps to Schema - Fix simple_ffproble to use its custom print_codec, instead of ffmpeg's impl * Add relation between MediaInfo and FilePath - Remove shared properties from MediaInfo and related structs - Implement Iterator for FFmpegDict * Fix and update schema * Data models and start populating MediaInfo in rust * Finish populating media info, chapters and program * Improve FFmpegFormatContext data raw pointer access - Implement stream data gathering * Impl FFmpegCodecContext, retrieve codec information - Improve some unsafe pointer uses - Impl from FFmpegFormatContext to MediaInfo conversion * Fix FFmpegDict Drop * Fix some crago warnings * Impl retrieval of video props - Fix C char* to Rust String convertion * Impl retrieval of audio and subtitle props - Fill props for MediaCodec * Remove simple_ffprobe now that the Rust impl is done * Fix schema to match actually retrieved media info - Fix import some FFmpeg constants instead of directly using values * Rework movie_decoder - Re-implement create_scale_string and add support anamorphic video - Improve C pointer access for FFmpegFormatContext and FFmpegCodecContext - Use newer FFmpeg abstractions in movie_decoder * Fix incorrect props when initializing MovieDecoder * Remove unecessary lifetimes * Added more native wrappers for some FFmpeg native objects used in movie_decoder * Remove FFmpegPacket - Some more improvements to movie_decoder * WIP * Some small fixes * More fixes Rename movie_decoder to frame_decoder Remove more references to film_strips * fmt * Fix duplicate migration for job error changes * fix rebase * Solving segfaults, fuck C lang Co-authored-by: Vítor Vasconcellos <HeavenVolkoff@users.noreply.github.com> * Update rust to version 1.77 - Pin rust version with rust-toolchain.toml - Change from dtolnay/rust-toolchain to IronCoreLabs/rust-toolchain for rust-toolchain support - Remove unused function and imports - Replace most CString uses with new c literal string * More segfault solving and other minor fixes Co-authored-by: Vítor Vasconcellos <HeavenVolkoff@users.noreply.github.com> * Fix ffmpeg rotation filter breaking portrait video thumbnails #2150 - Plus some other misc fixes * Auto format * Retrieve video/audio metadata on frontend * Auto format * First draft on ffmpeg data save on db Co-authored-by: Vítor Vasconcellos <HeavenVolkoff@users.noreply.github.com> * Fix some incorrect changes to prisma schema * Some fixes for the FFmpegData schema - Expand logic to save FFmpegData to db * A ton of things Co-authored-by: Vítor Vasconcellos <HeavenVolkoff@users.noreply.github.com> * Integrating ffmpeg media data in jobs and API * Rspc can't BigInt * 🙄 * Add initial ffmpeg metadata entries to Inspector - Fix ephemeral metadata api to match the files metadata api call * Fix Inspector not showing ffmpeg metadata * Add bitrate, start time and chapters video metadata to Inspector - Fix backend BigInt conversion incorrectly using i32 instead of u32 - Change FFmpegFormatContext/FFmpegMetaData bit_rate to i64 - Rename byteSize to humanizeSize - Expand humanizeSize logic to allow handling bits and Binary units - Move capitalize to @sd/client utils * Solving some issues * Fix ffmpeg probe getting incorrect stream id and breaking database unique constraint - Fix humanizeSize breaking when receiving floating numbers - Fix incorrect equality in StatCard - Fix unhandled error in Dialog when trying to remove an unknown dialog * fmt * small improvements - Remove some unecessary recursion_limit directive - Remove unused app_image releated functions - Fix metadata query enabled flag * Add migration for ffmpeg media data * Fix cypress test * Requested changes * Implement feedback - Update locale keys for all languages - Add pnpm command to update all language keys * Fix thumb reactivity in non indexed locations --------- Co-authored-by: Ericson Soares <ericson.ds999@gmail.com> Co-authored-by: Vítor Vasconcellos <HeavenVolkoff@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
853f0d4185
commit
e797b02e65
@@ -1,13 +1,13 @@
|
||||
import dayjs from 'dayjs';
|
||||
import customParseFormat from 'dayjs/plugin/customParseFormat'; // import plugin
|
||||
|
||||
import utc from 'dayjs/plugin/utc'; // import plugin
|
||||
|
||||
import {
|
||||
capitalize,
|
||||
CoordinatesFormat,
|
||||
MediaDate,
|
||||
ExifMetadata,
|
||||
FFmpegMetadata,
|
||||
humanizeSize,
|
||||
int32ArrayToBigInt,
|
||||
MediaLocation,
|
||||
MediaMetadata,
|
||||
MediaData as RemoteMediaData,
|
||||
useSelector,
|
||||
useUnitFormatStore
|
||||
} from '@sd/client';
|
||||
@@ -18,38 +18,6 @@ import { Platform, usePlatform } from '~/util/Platform';
|
||||
import { explorerStore } from '../store';
|
||||
import { MetaData } from './index';
|
||||
|
||||
interface Props {
|
||||
data: MediaMetadata;
|
||||
}
|
||||
|
||||
// const DateFormatWithTz = 'YYYY-MM-DD HH:mm:ss ZZ';
|
||||
// const DateFormatWithoutTz = 'YYYY-MM-DD HH:mm:ss';
|
||||
|
||||
// const formatMediaDate = (datetime: MediaDate): { formatted: string; raw: string } | undefined => {
|
||||
// dayjs.extend(customParseFormat);
|
||||
// dayjs.extend(utc);
|
||||
|
||||
// // dayjs.tz.setDefault(dayjs.tz.guess());
|
||||
|
||||
// const getTzData = (dt: string): [string, number] => {
|
||||
// if (dt.includes('+'))
|
||||
// return [DateFormatWithTz, Number.parseInt(dt.substring(dt.indexOf('+'), 3))];
|
||||
// return [DateFormatWithoutTz, 0];
|
||||
// };
|
||||
|
||||
// const [tzFormat, tzOffset] = getTzData(datetime);
|
||||
|
||||
// console.log({
|
||||
// formatted: dayjs(datetime, tzFormat).utcOffset(tzOffset).format('HH:mm:ss, MMM Do YYYY'),
|
||||
// raw: datetime
|
||||
// });
|
||||
|
||||
// return {
|
||||
// formatted: dayjs(datetime, tzFormat).utcOffset(tzOffset).format('HH:mm:ss, MMM Do YYYY'),
|
||||
// raw: datetime
|
||||
// };
|
||||
// };
|
||||
|
||||
const formatLocationDD = (loc: MediaLocation, dp?: number): string => {
|
||||
// the lack of a + here will mean that coordinates may have padding at the end
|
||||
// google does the same (if one is larger than the other, the smaller one will be padded with zeroes)
|
||||
@@ -99,24 +67,154 @@ const UrlMetadataValue = (props: { text: string; url: string; platform: Platform
|
||||
</a>
|
||||
);
|
||||
|
||||
// const orientations = {
|
||||
// Normal: 'Normal',
|
||||
// MirroredHorizontal: 'Horizontally mirrored',
|
||||
// MirroredHorizontalAnd90CW: 'Mirrored horizontally and rotated 90° clockwise',
|
||||
// MirroredHorizontalAnd270CW: 'Mirrored horizontally and rotated 270° clockwise',
|
||||
// MirroredVertical: 'Vertically mirrored',
|
||||
// CW90: 'Rotated 90° clockwise',
|
||||
// CW180: 'Rotated 180° clockwise',
|
||||
// CW270: 'Rotated 270° clockwise'
|
||||
// };
|
||||
|
||||
const MediaData = ({ data }: Props) => {
|
||||
const ExifMediaData = (data: ExifMetadata) => {
|
||||
const platform = usePlatform();
|
||||
const { t } = useLocale();
|
||||
const coordinatesFormat = useUnitFormatStore().coordinatesFormat;
|
||||
const showMoreInfo = useSelector(explorerStore, (s) => s.showMoreInfo);
|
||||
|
||||
return data.type === 'Image' ? (
|
||||
return (
|
||||
<>
|
||||
<MetaData
|
||||
label="Date"
|
||||
tooltipValue={data.date_taken ?? null} // should show full raw value
|
||||
// should show localised, utc-offset value or plain value with tooltip mentioning that we don't have the timezone metadata
|
||||
value={data.date_taken ?? null}
|
||||
/>
|
||||
<MetaData label="Type" value="Image" />
|
||||
<MetaData
|
||||
label="Location"
|
||||
tooltipValue={data.location && formatLocation(data.location, coordinatesFormat)}
|
||||
value={
|
||||
data.location && (
|
||||
<UrlMetadataValue
|
||||
url={`https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(
|
||||
formatLocation(data.location, 'dd')
|
||||
)}`}
|
||||
text={formatLocation(
|
||||
data.location,
|
||||
coordinatesFormat,
|
||||
coordinatesFormat === 'dd' ? 4 : 0
|
||||
)}
|
||||
platform={platform}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
<MetaData
|
||||
label="Plus Code"
|
||||
value={
|
||||
data.location?.pluscode && (
|
||||
<UrlMetadataValue
|
||||
url={`https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(
|
||||
data.location.pluscode
|
||||
)}`}
|
||||
text={data.location.pluscode}
|
||||
platform={platform}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
<MetaData
|
||||
label="Resolution"
|
||||
value={`${data.resolution.width} x ${data.resolution.height}`}
|
||||
/>
|
||||
<MetaData label="Device" value={data.camera_data.device_make} />
|
||||
<MetaData label="Model" value={data.camera_data.device_model} />
|
||||
<MetaData label="Color profile" value={data.camera_data.color_profile} />
|
||||
<MetaData label="Color space" value={data.camera_data.color_space} />
|
||||
<MetaData label="Flash" value={data.camera_data.flash?.mode} />
|
||||
<MetaData
|
||||
label="Zoom"
|
||||
value={
|
||||
data.camera_data &&
|
||||
data.camera_data.zoom &&
|
||||
!Number.isNaN(data.camera_data.zoom)
|
||||
? `${data.camera_data.zoom.toFixed(2) + 'x'}`
|
||||
: '--'
|
||||
}
|
||||
/>
|
||||
<MetaData label="Iso" value={data.camera_data.iso} />
|
||||
<MetaData label="Software" value={data.camera_data.software} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const FFmpegMediaData = (data: FFmpegMetadata) => {
|
||||
const { t } = useLocale();
|
||||
|
||||
const streamKinds = new Set(
|
||||
data.programs.flatMap((program) => program.streams.map((stream) => stream.codec?.kind))
|
||||
);
|
||||
const type = streamKinds.has('video')
|
||||
? 'Video'
|
||||
: streamKinds.has('audio')
|
||||
? 'Audio'
|
||||
: capitalize(streamKinds.values().next().value) ?? 'Unknown';
|
||||
|
||||
const bit_rate = humanizeSize(int32ArrayToBigInt(data.bit_rate), {
|
||||
is_bit: true,
|
||||
base_unit: 'binary',
|
||||
use_plural: false
|
||||
});
|
||||
|
||||
const duration_ms = data.duration ? int32ArrayToBigInt(data.duration) / 1000n : null;
|
||||
const duration = duration_ms
|
||||
? dayjs.duration(
|
||||
Number(duration_ms / 1000n) + Number(duration_ms % 1000n) / 1000,
|
||||
'seconds'
|
||||
)
|
||||
: null;
|
||||
|
||||
const start_time_ms = data.start_time ? int32ArrayToBigInt(data.start_time) / 1000n : null;
|
||||
const start_time = start_time_ms
|
||||
? dayjs.duration(
|
||||
Number(start_time_ms / 1000n) + Number(start_time_ms % 1000n) / 1000,
|
||||
'seconds'
|
||||
)
|
||||
: null;
|
||||
|
||||
const chapters = data.chapters
|
||||
.map((chapter) => {
|
||||
const num = BigInt(chapter.time_base_num);
|
||||
const den = BigInt(chapter.time_base_den);
|
||||
|
||||
const start = dayjs.duration(
|
||||
Number((int32ArrayToBigInt(chapter.start) * num) / den),
|
||||
'seconds'
|
||||
);
|
||||
|
||||
const end = dayjs.duration(
|
||||
Number((int32ArrayToBigInt(chapter.end) * num) / den),
|
||||
'seconds'
|
||||
);
|
||||
|
||||
return `${start.format('HH:mm:ss')} - ${end.format('HH:mm:ss')}`;
|
||||
})
|
||||
.join('\n');
|
||||
|
||||
return (
|
||||
<>
|
||||
<MetaData label={t('type')} value={type} />
|
||||
<MetaData label="Bitrate" value={`${bit_rate.value} ${bit_rate.unit}/s`} />
|
||||
{duration && <MetaData label={t('duration')} value={duration.format('HH:mm:ss.SSS')} />}
|
||||
{start_time && (
|
||||
<MetaData label={t('start_time')} value={start_time.format('HH:mm:ss.SSS')} />
|
||||
)}
|
||||
{chapters && <MetaData label={t('chapters')} value={chapters} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface Props {
|
||||
data: RemoteMediaData;
|
||||
}
|
||||
|
||||
export const MediaData = ({ data }: Props) => {
|
||||
const { t } = useLocale();
|
||||
const showMoreInfo = useSelector(explorerStore, (s) => s.showMoreInfo);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-0 py-2">
|
||||
<Accordion
|
||||
isOpen={showMoreInfo}
|
||||
@@ -124,70 +222,10 @@ const MediaData = ({ data }: Props) => {
|
||||
variant="apple"
|
||||
title={t('more_info')}
|
||||
>
|
||||
<MetaData
|
||||
label="Date"
|
||||
tooltipValue={data.date_taken ?? null} // should show full raw value
|
||||
// should show localised, utc-offset value or plain value with tooltip mentioning that we don't have the timezone metadata
|
||||
value={data.date_taken ?? null}
|
||||
/>
|
||||
<MetaData label="Type" value={data.type} />
|
||||
<MetaData
|
||||
label="Location"
|
||||
tooltipValue={data.location && formatLocation(data.location, coordinatesFormat)}
|
||||
value={
|
||||
data.location && (
|
||||
<UrlMetadataValue
|
||||
url={`https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(
|
||||
formatLocation(data.location, 'dd')
|
||||
)}`}
|
||||
text={formatLocation(
|
||||
data.location,
|
||||
coordinatesFormat,
|
||||
coordinatesFormat === 'dd' ? 4 : 0
|
||||
)}
|
||||
platform={platform}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
<MetaData
|
||||
label="Plus Code"
|
||||
value={
|
||||
data.location?.pluscode && (
|
||||
<UrlMetadataValue
|
||||
url={`https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(
|
||||
data.location.pluscode
|
||||
)}`}
|
||||
text={data.location.pluscode}
|
||||
platform={platform}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
<MetaData
|
||||
label="Resolution"
|
||||
value={`${data.resolution.width} x ${data.resolution.height}`}
|
||||
/>
|
||||
<MetaData label="Device" value={data.camera_data.device_make} />
|
||||
<MetaData label="Model" value={data.camera_data.device_model} />
|
||||
<MetaData label="Color profile" value={data.camera_data.color_profile} />
|
||||
<MetaData label="Color space" value={data.camera_data.color_space} />
|
||||
<MetaData label="Flash" value={data.camera_data.flash?.mode} />
|
||||
<MetaData
|
||||
label="Zoom"
|
||||
value={
|
||||
data.camera_data &&
|
||||
data.camera_data.zoom &&
|
||||
!Number.isNaN(data.camera_data.zoom)
|
||||
? `${data.camera_data.zoom.toFixed(2) + 'x'}`
|
||||
: '--'
|
||||
}
|
||||
/>
|
||||
<MetaData label="Iso" value={data.camera_data.iso} />
|
||||
<MetaData label="Software" value={data.camera_data.software} />
|
||||
{'Exif' in data ? ExifMediaData(data.Exif) : FFmpegMediaData(data.FFmpeg)}
|
||||
</Accordion>
|
||||
</div>
|
||||
) : null;
|
||||
);
|
||||
};
|
||||
|
||||
export default MediaData;
|
||||
|
||||
@@ -27,11 +27,11 @@ import { useLocation } from 'react-router';
|
||||
import { Link as NavLink } from 'react-router-dom';
|
||||
import Sticky from 'react-sticky-el';
|
||||
import {
|
||||
byteSize,
|
||||
FilePath,
|
||||
FilePathWithObject,
|
||||
getExplorerItemData,
|
||||
getItemFilePath,
|
||||
humanizeSize,
|
||||
NonIndexedPathItem,
|
||||
Object,
|
||||
ObjectKindEnum,
|
||||
@@ -235,21 +235,21 @@ export const SingleItemMetadata = ({ item }: { item: ExplorerItem }) => {
|
||||
});
|
||||
|
||||
const filesMediaData = useLibraryQuery(['files.getMediaData', objectData?.id ?? -1], {
|
||||
enabled: objectData?.kind === ObjectKindEnum.Image && readyToFetch
|
||||
enabled: objectData != null && readyToFetch
|
||||
});
|
||||
|
||||
const ephemeralLocationMediaData = useBridgeQuery(
|
||||
['ephemeralFiles.getMediaData', ephemeralPathData != null ? ephemeralPathData.path : ''],
|
||||
{
|
||||
enabled: ephemeralPathData?.kind === ObjectKindEnum.Image && readyToFetch
|
||||
enabled: ephemeralPathData != null && readyToFetch
|
||||
}
|
||||
);
|
||||
|
||||
const mediaData = filesMediaData ?? ephemeralLocationMediaData ?? null;
|
||||
const mediaData = filesMediaData.data ?? ephemeralLocationMediaData.data ?? null;
|
||||
|
||||
const fullPath = queriedFullPath.data ?? ephemeralPathData?.path;
|
||||
|
||||
const { name, isDir, kind, size, casId, dateCreated, dateAccessed, dateModified, dateIndexed } =
|
||||
const { isDir, kind, size, casId, dateCreated, dateAccessed, dateModified, dateIndexed } =
|
||||
useExplorerItemData(item);
|
||||
|
||||
const pubId = objectData != null ? uniqueId(objectData) : null;
|
||||
@@ -365,7 +365,7 @@ export const SingleItemMetadata = ({ item }: { item: ExplorerItem }) => {
|
||||
</MetaContainer>
|
||||
)}
|
||||
|
||||
{mediaData.data && <MediaData data={mediaData.data} />}
|
||||
{mediaData && <MediaData data={mediaData} />}
|
||||
|
||||
<MetaContainer className="flex !flex-row flex-wrap gap-1 overflow-hidden">
|
||||
<InfoPill>{isDir ? t('folder') : translateKindName(kind)}</InfoPill>
|
||||
@@ -483,7 +483,7 @@ const MultiItemMetadata = ({ items }: { items: ExplorerItem[] }) => {
|
||||
getExplorerItemData(item);
|
||||
|
||||
if (item.type !== 'NonIndexedPath' || !item.item.is_dir) {
|
||||
metadata.size = (metadata.size ?? BigInt(0)) + size.original;
|
||||
metadata.size = (metadata.size ?? BigInt(0)) + BigInt(size.original);
|
||||
}
|
||||
|
||||
if (dateCreated)
|
||||
@@ -529,7 +529,7 @@ const MultiItemMetadata = ({ items }: { items: ExplorerItem[] }) => {
|
||||
<MetaData
|
||||
icon={Cube}
|
||||
label={t('size')}
|
||||
value={metadata.size !== null ? `${byteSize(metadata.size)}` : null}
|
||||
value={metadata.size !== null ? `${humanizeSize(metadata.size)}` : null}
|
||||
/>
|
||||
<MetaData
|
||||
icon={Clock}
|
||||
@@ -638,12 +638,14 @@ interface MetaDataProps {
|
||||
|
||||
export const MetaData = ({ icon: Icon, label, value, tooltipValue, onClick }: MetaDataProps) => {
|
||||
return (
|
||||
<div className="flex items-center text-xs text-ink-dull" onClick={onClick}>
|
||||
<div className="flex content-start justify-start text-xs text-ink-dull" onClick={onClick}>
|
||||
{Icon && <Icon weight="bold" className="mr-2 shrink-0" />}
|
||||
<span className="mr-2 flex-1 whitespace-nowrap">{label}</span>
|
||||
<span className="mr-2 flex flex-1 items-start justify-items-start whitespace-nowrap">
|
||||
{label}
|
||||
</span>
|
||||
<Tooltip
|
||||
label={tooltipValue || value}
|
||||
className="truncate text-ink"
|
||||
className="truncate whitespace-pre text-ink"
|
||||
tooltipClassName="max-w-none"
|
||||
>
|
||||
{value ?? '--'}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import clsx from 'clsx';
|
||||
import { memo, useMemo } from 'react';
|
||||
import {
|
||||
byteSize,
|
||||
getItemFilePath,
|
||||
humanizeSize,
|
||||
useLibraryQuery,
|
||||
useSelector,
|
||||
type ExplorerItem
|
||||
@@ -142,7 +142,7 @@ const ItemSize = () => {
|
||||
(!isRenaming || !item.selected);
|
||||
|
||||
const bytes = useMemo(
|
||||
() => showSize && byteSize(filePath?.size_in_bytes_bytes),
|
||||
() => showSize && humanizeSize(filePath?.size_in_bytes_bytes),
|
||||
[filePath?.size_in_bytes_bytes, showSize]
|
||||
);
|
||||
|
||||
|
||||
@@ -10,11 +10,11 @@ import dayjs from 'dayjs';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { stringify } from 'uuid';
|
||||
import {
|
||||
byteSize,
|
||||
getExplorerItemData,
|
||||
getIndexedItemFilePath,
|
||||
getItemFilePath,
|
||||
getItemObject,
|
||||
humanizeSize,
|
||||
useSelector,
|
||||
type ExplorerItem
|
||||
} from '@sd/client';
|
||||
@@ -122,7 +122,7 @@ export const useTable = () => {
|
||||
!filePath.size_in_bytes_bytes ||
|
||||
(filePath.is_dir && item.type === 'NonIndexedPath')
|
||||
? '-'
|
||||
: byteSize(filePath.size_in_bytes_bytes);
|
||||
: humanizeSize(filePath.size_in_bytes_bytes);
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Info } from '@phosphor-icons/react';
|
||||
import clsx from 'clsx';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { byteSize, Statistics, useLibraryContext, useLibraryQuery } from '@sd/client';
|
||||
import { humanizeSize, Statistics, useLibraryContext, useLibraryQuery } from '@sd/client';
|
||||
import { Tooltip } from '@sd/ui';
|
||||
import { useCounter, useLocale } from '~/hooks';
|
||||
|
||||
@@ -23,7 +23,7 @@ const StatItem = (props: StatItemProps) => {
|
||||
// The acts as a cache of the value of `mounted` on the first render of this `StateItem`.
|
||||
const [isMounted] = useState(mounted);
|
||||
|
||||
const size = byteSize(bytes);
|
||||
const size = humanizeSize(bytes);
|
||||
const count = useCounter({
|
||||
name: title,
|
||||
end: size.value,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ReactComponent as Ellipsis } from '@sd/assets/svgs/ellipsis.svg';
|
||||
import { useMemo } from 'react';
|
||||
import { byteSize } from '@sd/client';
|
||||
import { humanizeSize } from '@sd/client';
|
||||
import { Button, Card, tw } from '@sd/ui';
|
||||
import { Icon } from '~/components';
|
||||
|
||||
@@ -17,7 +17,7 @@ const Pill = tw.div`px-1.5 py-[1px] rounded text-tiny font-medium text-ink-dull
|
||||
const LocationCard = ({ icon, name, connectionType, ...stats }: LocationCardProps) => {
|
||||
const { totalSpace } = useMemo(() => {
|
||||
return {
|
||||
totalSpace: byteSize(stats.totalSpace)
|
||||
totalSpace: humanizeSize(stats.totalSpace)
|
||||
};
|
||||
}, [stats]);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ReactComponent as Ellipsis } from '@sd/assets/svgs/ellipsis.svg';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { byteSize } from '@sd/client';
|
||||
import { humanizeSize } from '@sd/client';
|
||||
import { Button, Card, CircularProgress, tw } from '@sd/ui';
|
||||
import { Icon } from '~/components';
|
||||
import { useIsDark, useLocale } from '~/hooks';
|
||||
@@ -22,12 +22,12 @@ const StatCard = ({ icon, name, connectionType, ...stats }: StatCardProps) => {
|
||||
const isDark = useIsDark();
|
||||
|
||||
const { totalSpace, freeSpace, usedSpaceSpace } = useMemo(() => {
|
||||
const totalSpace = byteSize(stats.totalSpace);
|
||||
const freeSpace = stats.freeSpace == null ? totalSpace : byteSize(stats.freeSpace);
|
||||
const totalSpace = humanizeSize(stats.totalSpace);
|
||||
const freeSpace = stats.freeSpace == null ? totalSpace : humanizeSize(stats.freeSpace);
|
||||
return {
|
||||
totalSpace,
|
||||
freeSpace,
|
||||
usedSpaceSpace: byteSize(totalSpace.original - freeSpace.original)
|
||||
usedSpaceSpace: humanizeSize(totalSpace.original - freeSpace.original)
|
||||
};
|
||||
}, [stats]);
|
||||
|
||||
@@ -36,7 +36,7 @@ const StatCard = ({ icon, name, connectionType, ...stats }: StatCardProps) => {
|
||||
}, []);
|
||||
|
||||
const progress = useMemo(() => {
|
||||
if (!mounted || totalSpace.original === 0n) return 0;
|
||||
if (!mounted || totalSpace.original === 0) return 0;
|
||||
return Math.floor((usedSpaceSpace.value / totalSpace.value) * 100);
|
||||
}, [mounted, totalSpace, usedSpaceSpace]);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { iconNames } from '@sd/assets/util';
|
||||
import { memo, useEffect, useMemo, useState } from 'react';
|
||||
import { byteSize, useDiscoveredPeers, useLibraryQuery } from '@sd/client';
|
||||
import { humanizeSize, useDiscoveredPeers, useLibraryQuery } from '@sd/client';
|
||||
import { Card } from '@sd/ui';
|
||||
import { Icon } from '~/components';
|
||||
import { useCounter, useLocale } from '~/hooks';
|
||||
@@ -21,10 +21,10 @@ export const Component = () => {
|
||||
const info = useMemo(() => {
|
||||
if (locations.data && discoveredPeers) {
|
||||
const statistics = stats.data?.statistics;
|
||||
const tb_capacity = byteSize(statistics?.total_bytes_capacity);
|
||||
const free_space = byteSize(statistics?.total_bytes_free);
|
||||
const library_db_size = byteSize(statistics?.library_db_size);
|
||||
const preview_media = byteSize(statistics?.preview_media_bytes);
|
||||
const tb_capacity = humanizeSize(statistics?.total_bytes_capacity);
|
||||
const free_space = humanizeSize(statistics?.total_bytes_free);
|
||||
const library_db_size = humanizeSize(statistics?.library_db_size);
|
||||
const preview_media = humanizeSize(statistics?.preview_media_bytes);
|
||||
const data: {
|
||||
icon: keyof typeof iconNames;
|
||||
title?: string;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router';
|
||||
import {
|
||||
arraysEqual,
|
||||
byteSize,
|
||||
humanizeSize,
|
||||
Location,
|
||||
useLibraryMutation,
|
||||
useOnlineLocations
|
||||
@@ -66,10 +66,10 @@ export default ({ location }: Props) => {
|
||||
}}
|
||||
>
|
||||
<span className="max-w-[34px] truncate text-xs text-ink-dull">
|
||||
{byteSize(location.size_in_bytes).value}
|
||||
{humanizeSize(location.size_in_bytes).value}
|
||||
</span>
|
||||
<span className="ml-px text-[10px] text-ink-dull/60">
|
||||
{t(`size_${byteSize(location.size_in_bytes).unit.toLowerCase()}`)}
|
||||
{t(`size_${humanizeSize(location.size_in_bytes).unit.toLowerCase()}`)}
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
@@ -31,8 +31,6 @@ const dayjsLocales: Record<string, any> = {
|
||||
|
||||
## Syncing locales
|
||||
|
||||
This command will help you sync locales with the source language (en) and find missing keys.
|
||||
This command will help you sync all locales with the source language (en) and update missing keys.
|
||||
|
||||
`npx i18next-locales-sync -p en -s it -l ./interface/locales`
|
||||
|
||||
replace `it` with the language you want to sync with the source language.
|
||||
`pnpm i18n:sync`
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
"changelog": "سجل التغييرات",
|
||||
"changelog_page_description": "انظر إلى الميزات الجديدة الرائعة التي نقوم بإضافتها",
|
||||
"changelog_page_title": "سجل التغييرات",
|
||||
"chapters": "Chapters",
|
||||
"checksum": "التحقق من الصحة",
|
||||
"clear_finished_jobs": "مسح الوظائف المنتهية",
|
||||
"client": "العميل",
|
||||
@@ -125,6 +126,7 @@
|
||||
"debug_mode_description": "تمكين ميزات التصحيح الإضافية داخل التطبيق.",
|
||||
"default": "الافتراضي",
|
||||
"descending": "Descending",
|
||||
"duration": "Duration",
|
||||
"random": "عشوائي",
|
||||
"ipv4_listeners_error": "Error creating the IPv4 listeners. Please check your firewall settings!",
|
||||
"ipv4_ipv6_listeners_error": "Error creating the IPv4 and IPv6 listeners. Please check your firewall settings!",
|
||||
@@ -577,6 +579,7 @@
|
||||
"square_thumbnails": "مصغرات مربعة",
|
||||
"star_on_github": "ضع نجمة على GitHub",
|
||||
"starts_with": "starts with",
|
||||
"start_time": "Start Time",
|
||||
"stop": "إيقاف",
|
||||
"success": "نجاح",
|
||||
"support": "الدعم",
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
"changelog": "Што новага",
|
||||
"changelog_page_description": "Даведайцеся, якія новыя магчымасці мы дадалі",
|
||||
"changelog_page_title": "Спіс змен",
|
||||
"chapters": "Chapters",
|
||||
"checksum": "Кантрольная сума",
|
||||
"clear_finished_jobs": "Ачысціць скончаныя заданні",
|
||||
"client": "Кліент",
|
||||
@@ -125,6 +126,7 @@
|
||||
"debug_mode_description": "Уключыце дадатковыя функцыі адладкі ў дадатку.",
|
||||
"default": "Стандартны",
|
||||
"descending": "Па змяншэнні",
|
||||
"duration": "Duration",
|
||||
"random": "Выпадковы",
|
||||
"ipv4_listeners_error": "Памылка пры стварэнні слухачоў IPv4. Калі ласка, праверце наладкі брандмаўэра!",
|
||||
"ipv4_ipv6_listeners_error": "Памылка пры стварэнні слухачоў IPv4 і IPv6. Калі ласка, праверце наладкі брандмаўэра!",
|
||||
@@ -335,6 +337,7 @@
|
||||
"kind_one": "Тып",
|
||||
"kind_few": "Тыпа",
|
||||
"kind_many": "Тыпаў",
|
||||
"kind_other": "Kinds",
|
||||
"label": "Ярлык",
|
||||
"labels": "Ярлыкі",
|
||||
"language": "Мова",
|
||||
@@ -363,6 +366,7 @@
|
||||
"location_one": "Лакацыя",
|
||||
"location_few": "Лакацыі",
|
||||
"location_many": "Лакацый",
|
||||
"location_other": "Locations",
|
||||
"location_connected_tooltip": "Лакацыя правяраецца на змены",
|
||||
"location_disconnected_tooltip": "Лакацыя не правяраецца на змены",
|
||||
"location_display_name_info": "Імя гэтага месцазнаходжання, якое будзе адлюстроўвацца на бакавой панэлі. Гэта дзеянне не пераназаве фактычнай тэчкі на дыску.",
|
||||
@@ -569,6 +573,7 @@
|
||||
"square_thumbnails": "Квадратныя эскізы",
|
||||
"star_on_github": "Паставіць зорку на GitHub",
|
||||
"starts_with": "пачынаецца з",
|
||||
"start_time": "Start Time",
|
||||
"stop": "Спыніць",
|
||||
"success": "Поспех",
|
||||
"support": "Падтрымка",
|
||||
@@ -587,6 +592,7 @@
|
||||
"tag_one": "Тэг",
|
||||
"tag_few": "Тэга",
|
||||
"tag_many": "Тэгаў",
|
||||
"tag_other": "Tags",
|
||||
"tags": "Тэгі",
|
||||
"tags_description": "Кіруйце сваімі тэгамі.",
|
||||
"tags_notice_message": "Гэтаму тэгу не прысвоена ні аднаго элемента.",
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
"changelog": "Änderungsprotokoll",
|
||||
"changelog_page_description": "Sehe, welche coolen neuen Funktionen wir machen",
|
||||
"changelog_page_title": "Änderungsprotokoll",
|
||||
"chapters": "Chapters",
|
||||
"checksum": "Prüfsumme",
|
||||
"clear_finished_jobs": "Beendete Aufgaben entfernen",
|
||||
"client": "Client",
|
||||
@@ -125,6 +126,7 @@
|
||||
"debug_mode_description": "Zusätzliche Debugging-Funktionen in der App aktivieren.",
|
||||
"default": "Standard",
|
||||
"descending": "Absteigend",
|
||||
"duration": "Duration",
|
||||
"random": "Zufällig",
|
||||
"ipv4_listeners_error": "Error creating the IPv4 listeners. Please check your firewall settings!",
|
||||
"ipv4_ipv6_listeners_error": "Error creating the IPv4 and IPv6 listeners. Please check your firewall settings!",
|
||||
@@ -565,6 +567,7 @@
|
||||
"square_thumbnails": "Quadratische Vorschaubilder",
|
||||
"star_on_github": "Auf GitHub als Favorit markieren",
|
||||
"starts_with": "beginnt mit",
|
||||
"start_time": "Start Time",
|
||||
"stop": "Stoppen",
|
||||
"success": "Erfolg",
|
||||
"support": "Unterstützung",
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
"changelog": "Changelog",
|
||||
"changelog_page_description": "See what cool new features we're making",
|
||||
"changelog_page_title": "Changelog",
|
||||
"chapters": "Chapters",
|
||||
"checksum": "Checksum",
|
||||
"clear_finished_jobs": "Clear out finished jobs",
|
||||
"client": "Client",
|
||||
@@ -125,6 +126,7 @@
|
||||
"debug_mode_description": "Enable extra debugging features within the app.",
|
||||
"default": "Default",
|
||||
"descending": "Descending",
|
||||
"duration": "Duration",
|
||||
"random": "Random",
|
||||
"ipv4_listeners_error": "Error creating the IPv4 listeners. Please check your firewall settings!",
|
||||
"ipv4_ipv6_listeners_error": "Error creating the IPv4 and IPv6 listeners. Please check your firewall settings!",
|
||||
@@ -565,6 +567,7 @@
|
||||
"square_thumbnails": "Square Thumbnails",
|
||||
"star_on_github": "Star on GitHub",
|
||||
"starts_with": "starts with",
|
||||
"start_time": "Start Time",
|
||||
"stop": "Stop",
|
||||
"success": "Success",
|
||||
"support": "Support",
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
"changelog": "Registro de cambios",
|
||||
"changelog_page_description": "Mira qué nuevas funciones geniales estamos creando",
|
||||
"changelog_page_title": "Registro de cambios",
|
||||
"chapters": "Chapters",
|
||||
"checksum": "Suma de verificación",
|
||||
"clear_finished_jobs": "Eliminar trabajos finalizados",
|
||||
"client": "Cliente",
|
||||
@@ -125,6 +126,7 @@
|
||||
"debug_mode_description": "Habilitar funciones de depuración adicionales dentro de la aplicación.",
|
||||
"default": "Predeterminado",
|
||||
"descending": "Descendente",
|
||||
"duration": "Duration",
|
||||
"random": "Aleatorio",
|
||||
"ipv4_listeners_error": "Error creating the IPv4 listeners. Please check your firewall settings!",
|
||||
"ipv4_ipv6_listeners_error": "Error creating the IPv4 and IPv6 listeners. Please check your firewall settings!",
|
||||
@@ -332,6 +334,7 @@
|
||||
"kilometers": "Kilómetros",
|
||||
"kind": "Tipo",
|
||||
"kind_one": "Tipo",
|
||||
"kind_many": "Kinds",
|
||||
"kind_other": "Tipos",
|
||||
"label": "Etiqueta",
|
||||
"labels": "Etiquetas",
|
||||
@@ -359,6 +362,7 @@
|
||||
"local_node": "Nodo Local",
|
||||
"location": "Ubicación",
|
||||
"location_one": "Ubicación",
|
||||
"location_many": "Locations",
|
||||
"location_other": "Ubicaciones",
|
||||
"location_connected_tooltip": "La ubicación está siendo vigilada en busca de cambios",
|
||||
"location_disconnected_tooltip": "La ubicación no está siendo vigilada en busca de cambios",
|
||||
@@ -566,6 +570,7 @@
|
||||
"square_thumbnails": "Miniaturas Cuadradas",
|
||||
"star_on_github": "Dar estrella en GitHub",
|
||||
"starts_with": "comienza con",
|
||||
"start_time": "Start Time",
|
||||
"stop": "Detener",
|
||||
"success": "Éxito",
|
||||
"support": "Soporte",
|
||||
@@ -582,6 +587,7 @@
|
||||
"system": "Sistema",
|
||||
"tag": "Etiqueta",
|
||||
"tag_one": "Etiqueta",
|
||||
"tag_many": "Tags",
|
||||
"tag_other": "Etiquetas",
|
||||
"tags": "Etiquetas",
|
||||
"tags_description": "Administra tus etiquetas.",
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
"changelog": "Journal des modifications",
|
||||
"changelog_page_description": "Découvrez les nouvelles fonctionnalités cool que nous développons",
|
||||
"changelog_page_title": "Changelog",
|
||||
"chapters": "Chapters",
|
||||
"checksum": "Somme de contrôle",
|
||||
"clear_finished_jobs": "Effacer les travaux terminés",
|
||||
"client": "Client",
|
||||
@@ -125,6 +126,7 @@
|
||||
"debug_mode_description": "Activez des fonctionnalités de débogage supplémentaires dans l'application.",
|
||||
"default": "Défaut",
|
||||
"descending": "Descente",
|
||||
"duration": "Duration",
|
||||
"random": "Aléatoire",
|
||||
"ipv4_listeners_error": "Error creating the IPv4 listeners. Please check your firewall settings!",
|
||||
"ipv4_ipv6_listeners_error": "Error creating the IPv4 and IPv6 listeners. Please check your firewall settings!",
|
||||
@@ -332,6 +334,7 @@
|
||||
"kilometers": "Kilomètres",
|
||||
"kind": "Type",
|
||||
"kind_one": "Type",
|
||||
"kind_many": "Kinds",
|
||||
"kind_other": "Types",
|
||||
"label": "Étiquette",
|
||||
"labels": "Étiquettes",
|
||||
@@ -359,6 +362,7 @@
|
||||
"local_node": "Nœud local",
|
||||
"location": "Localisation",
|
||||
"location_one": "Localisation",
|
||||
"location_many": "Locations",
|
||||
"location_other": "Localisation",
|
||||
"location_connected_tooltip": "L'emplacement est surveillé pour les changements",
|
||||
"location_disconnected_tooltip": "L'emplacement n'est pas surveillé pour les changements",
|
||||
@@ -566,6 +570,7 @@
|
||||
"square_thumbnails": "Vignettes carrées",
|
||||
"star_on_github": "Mettre une étoile sur GitHub",
|
||||
"starts_with": "commence par",
|
||||
"start_time": "Start Time",
|
||||
"stop": "Arrêter",
|
||||
"success": "Succès",
|
||||
"support": "Support",
|
||||
@@ -582,7 +587,9 @@
|
||||
"system": "Système",
|
||||
"tag": "Étiquette",
|
||||
"tag_one": "Étiquette",
|
||||
"tag_many": "Tags",
|
||||
"tag_other": "Étiquettes",
|
||||
"tags": "Tags",
|
||||
"tags_description": "Gérer vos étiquettes.",
|
||||
"tags_notice_message": "Aucun élément attribué à cette balise.",
|
||||
"telemetry_description": "Activez pour fournir aux développeurs des données détaillées d'utilisation et de télémesure afin d'améliorer l'application. Désactivez pour n'envoyer que les données de base : votre statut d'activité, la version de l'application, la version du noyau et la plateforme (par exemple, mobile, web ou ordinateur de bureau).",
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
"changelog": "Changelog",
|
||||
"changelog_page_description": "Scopri quali nuove fantastiche funzionalità stiamo realizzando",
|
||||
"changelog_page_title": "Changelog",
|
||||
"chapters": "Chapters",
|
||||
"checksum": "Checksum",
|
||||
"clear_finished_jobs": "Cancella i lavori completati",
|
||||
"client": "Client",
|
||||
@@ -125,6 +126,7 @@
|
||||
"debug_mode_description": "Abilita funzionalità di debug aggiuntive all'interno dell'app.",
|
||||
"default": "Predefinito",
|
||||
"descending": "In discesa",
|
||||
"duration": "Duration",
|
||||
"random": "Casuale",
|
||||
"ipv4_listeners_error": "Error creating the IPv4 listeners. Please check your firewall settings!",
|
||||
"ipv4_ipv6_listeners_error": "Error creating the IPv4 and IPv6 listeners. Please check your firewall settings!",
|
||||
@@ -332,6 +334,7 @@
|
||||
"kilometers": "Kilometri",
|
||||
"kind": "Tipo",
|
||||
"kind_one": "Tipo",
|
||||
"kind_many": "Kinds",
|
||||
"kind_other": "Tipi",
|
||||
"label": "Etichetta",
|
||||
"labels": "Etichette",
|
||||
@@ -359,6 +362,7 @@
|
||||
"local_node": "Nodo Locale",
|
||||
"location": "Posizione",
|
||||
"location_one": "Posizione",
|
||||
"location_many": "Locations",
|
||||
"location_other": "Luoghi",
|
||||
"location_connected_tooltip": "La posizione è monitorata per i cambiamenti",
|
||||
"location_disconnected_tooltip": "La posizione non è monitorata per i cambiamenti",
|
||||
@@ -566,6 +570,7 @@
|
||||
"square_thumbnails": "Miniature quadrate",
|
||||
"star_on_github": "Aggiungi ai preferiti su GitHub",
|
||||
"starts_with": "inizia con",
|
||||
"start_time": "Start Time",
|
||||
"stop": "Stop",
|
||||
"success": "Successo",
|
||||
"support": "Supporto",
|
||||
@@ -582,7 +587,9 @@
|
||||
"system": "Sistema",
|
||||
"tag": "Tag",
|
||||
"tag_one": "Tag",
|
||||
"tag_many": "Tags",
|
||||
"tag_other": "Tags",
|
||||
"tags": "Tags",
|
||||
"tags_description": "Gestisci i tuoi tags.",
|
||||
"tags_notice_message": "Nessun elemento assegnato a questo tag.",
|
||||
"telemetry_description": "Attiva per fornire agli sviluppatori dati dettagliati sull'utilizzo e sulla telemetria per migliorare l'app. Disattiva per inviare solo i dati di base: stato della tua attività, versione dell'app, versione principale e piattaforma (ad esempio mobile, web o desktop).",
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
"changelog": "変更履歴",
|
||||
"changelog_page_description": "Spacedriveの魅力ある新機能をご確認ください。",
|
||||
"changelog_page_title": "変更履歴",
|
||||
"chapters": "Chapters",
|
||||
"checksum": "チェックサム",
|
||||
"clear_finished_jobs": "完了ジョブを削除",
|
||||
"client": "クライアント",
|
||||
@@ -125,6 +126,7 @@
|
||||
"debug_mode_description": "アプリ内で追加のデバッグ機能を有効にします。",
|
||||
"default": "デフォルト",
|
||||
"descending": "下降",
|
||||
"duration": "Duration",
|
||||
"random": "ランダム",
|
||||
"ipv4_listeners_error": "Error creating the IPv4 listeners. Please check your firewall settings!",
|
||||
"ipv4_ipv6_listeners_error": "Error creating the IPv4 and IPv6 listeners. Please check your firewall settings!",
|
||||
@@ -561,6 +563,7 @@
|
||||
"square_thumbnails": "正方形のサムネイル",
|
||||
"star_on_github": "GitHub上のスター",
|
||||
"starts_with": "で始まる。",
|
||||
"start_time": "Start Time",
|
||||
"stop": "中止",
|
||||
"success": "成功",
|
||||
"support": "サポート",
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
"changelog": "Wijzigingslogboek",
|
||||
"changelog_page_description": "Zie welke coole nieuwe functies we aan het maken zijn",
|
||||
"changelog_page_title": "Wijzigingslogboek",
|
||||
"chapters": "Chapters",
|
||||
"checksum": "Controlegetal",
|
||||
"clear_finished_jobs": "Ruim voltooide taken op",
|
||||
"client": "Client",
|
||||
@@ -125,6 +126,7 @@
|
||||
"debug_mode_description": "Schakel extra debugging functies in de app in.",
|
||||
"default": "Standaard",
|
||||
"descending": "Aflopend",
|
||||
"duration": "Duration",
|
||||
"random": "Willekeurig",
|
||||
"ipv4_listeners_error": "Error creating the IPv4 listeners. Please check your firewall settings!",
|
||||
"ipv4_ipv6_listeners_error": "Error creating the IPv4 and IPv6 listeners. Please check your firewall settings!",
|
||||
@@ -565,6 +567,7 @@
|
||||
"square_thumbnails": "Vierkante Miniaturen",
|
||||
"star_on_github": "Ster op GitHub",
|
||||
"starts_with": "begint met",
|
||||
"start_time": "Start Time",
|
||||
"stop": "Stop",
|
||||
"success": "Succes",
|
||||
"support": "Ondersteuning",
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
"changelog": "Что нового",
|
||||
"changelog_page_description": "Узнайте, какие новые возможности мы добавили",
|
||||
"changelog_page_title": "Список изменений",
|
||||
"chapters": "Chapters",
|
||||
"checksum": "Контрольная сумма",
|
||||
"clear_finished_jobs": "Очистить законченные задачи",
|
||||
"client": "Клиент",
|
||||
@@ -125,6 +126,7 @@
|
||||
"debug_mode_description": "Включите дополнительные функции отладки в приложении.",
|
||||
"default": "Стандартный",
|
||||
"descending": "По убыванию",
|
||||
"duration": "Duration",
|
||||
"random": "Случайный",
|
||||
"ipv4_listeners_error": "Ошибка при создании слушателей IPv4. Пожалуйста, проверьте настройки брандмауэра!",
|
||||
"ipv4_ipv6_listeners_error": "Ошибка при создании слушателей IPv4 и IPv6. Пожалуйста, проверьте настройки брандмауэра!",
|
||||
@@ -335,6 +337,7 @@
|
||||
"kind_one": "Тип",
|
||||
"kind_few": "Типа",
|
||||
"kind_many": "Типов",
|
||||
"kind_other": "Kinds",
|
||||
"label": "Ярлык",
|
||||
"labels": "Ярлыки",
|
||||
"language": "Язык",
|
||||
@@ -363,6 +366,7 @@
|
||||
"location_one": "Локация",
|
||||
"location_few": "Локации",
|
||||
"location_many": "Локаций",
|
||||
"location_other": "Locations",
|
||||
"location_connected_tooltip": "Локация проверяется на изменения",
|
||||
"location_disconnected_tooltip": "Локация не проверяется на изменения",
|
||||
"location_display_name_info": "Имя этого месторасположения, которое будет отображаться на боковой панели. Это действие не переименует фактическую папку на диске.",
|
||||
@@ -569,6 +573,7 @@
|
||||
"square_thumbnails": "Квадратные эскизы",
|
||||
"star_on_github": "Поставить звезду на GitHub",
|
||||
"starts_with": "начинается с",
|
||||
"start_time": "Start Time",
|
||||
"stop": "Остановить",
|
||||
"success": "Успех",
|
||||
"support": "Поддержка",
|
||||
@@ -587,6 +592,7 @@
|
||||
"tag_one": "Тег",
|
||||
"tag_few": "Тега",
|
||||
"tag_many": "Тегов",
|
||||
"tag_other": "Tags",
|
||||
"tags": "Теги",
|
||||
"tags_description": "Управляйте своими тегами.",
|
||||
"tags_notice_message": "Этому тегу не присвоено ни одного элемента.",
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
"changelog": "Değişiklikler",
|
||||
"changelog_page_description": "Yaptığımız havalı yeni özellikleri görün",
|
||||
"changelog_page_title": "Değişiklikler",
|
||||
"chapters": "Chapters",
|
||||
"checksum": "Kontrol Toplamı",
|
||||
"clear_finished_jobs": "Biten işleri temizle",
|
||||
"client": "İstemci",
|
||||
@@ -125,6 +126,7 @@
|
||||
"debug_mode_description": "Uygulama içinde ek hata ayıklama özelliklerini etkinleştir.",
|
||||
"default": "Varsayılan",
|
||||
"descending": "Alçalma",
|
||||
"duration": "Duration",
|
||||
"random": "Rastgele",
|
||||
"ipv4_listeners_error": "Error creating the IPv4 listeners. Please check your firewall settings!",
|
||||
"ipv4_ipv6_listeners_error": "Error creating the IPv4 and IPv6 listeners. Please check your firewall settings!",
|
||||
@@ -565,6 +567,7 @@
|
||||
"square_thumbnails": "Kare Küçük Resimler",
|
||||
"star_on_github": "GitHub'da Yıldızla",
|
||||
"starts_with": "ile başlar",
|
||||
"start_time": "Start Time",
|
||||
"stop": "Durdur",
|
||||
"success": "Başarılı",
|
||||
"support": "Destek",
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
"changelog": "更新日志",
|
||||
"changelog_page_description": "看看我们在开发哪些酷炫的新功能",
|
||||
"changelog_page_title": "更新日志",
|
||||
"chapters": "Chapters",
|
||||
"checksum": "校验和",
|
||||
"clear_finished_jobs": "清除已完成的任务",
|
||||
"client": "客户端",
|
||||
@@ -125,6 +126,7 @@
|
||||
"debug_mode_description": "启用本应用额外的调试功能。",
|
||||
"default": "默认",
|
||||
"descending": "降序",
|
||||
"duration": "Duration",
|
||||
"random": "随机的",
|
||||
"ipv4_listeners_error": "Error creating the IPv4 listeners. Please check your firewall settings!",
|
||||
"ipv4_ipv6_listeners_error": "Error creating the IPv4 and IPv6 listeners. Please check your firewall settings!",
|
||||
@@ -561,6 +563,7 @@
|
||||
"square_thumbnails": "方形缩略图",
|
||||
"star_on_github": "在 GitHub 上送一个 star",
|
||||
"starts_with": "以。。开始",
|
||||
"start_time": "Start Time",
|
||||
"stop": "停止",
|
||||
"success": "成功",
|
||||
"support": "支持",
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
"changelog": "變更日誌",
|
||||
"changelog_page_description": "了解我們正在創建的酷炫新功能",
|
||||
"changelog_page_title": "變更日誌",
|
||||
"chapters": "Chapters",
|
||||
"checksum": "校驗和",
|
||||
"clear_finished_jobs": "清除已完成的工作",
|
||||
"client": "客戶端",
|
||||
@@ -125,6 +126,7 @@
|
||||
"debug_mode_description": "在應用程序中啟用額外的除錯功能。",
|
||||
"default": "默認",
|
||||
"descending": "降序",
|
||||
"duration": "Duration",
|
||||
"random": "隨機的",
|
||||
"ipv4_listeners_error": "Error creating the IPv4 listeners. Please check your firewall settings!",
|
||||
"ipv4_ipv6_listeners_error": "Error creating the IPv4 and IPv6 listeners. Please check your firewall settings!",
|
||||
@@ -561,6 +563,7 @@
|
||||
"square_thumbnails": "方形縮略圖",
|
||||
"star_on_github": "在GitHub上給星",
|
||||
"starts_with": "以。",
|
||||
"start_time": "Start Time",
|
||||
"stop": "停止",
|
||||
"success": "成功",
|
||||
"support": "支持",
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import { capitalize } from '@sd/client';
|
||||
import { keySymbols, ModifierKeys, modifierSymbols } from '@sd/ui';
|
||||
|
||||
import { OperatingSystem } from '../util/Platform';
|
||||
|
||||
function capitalize<T extends string>(string: T): Capitalize<T> {
|
||||
return (string.charAt(0).toUpperCase() + string.slice(1)) as Capitalize<T>;
|
||||
}
|
||||
|
||||
export function keybind<T extends string>(
|
||||
modifers: ModifierKeys[],
|
||||
keys: T[],
|
||||
|
||||
Reference in New Issue
Block a user