mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-04 09:53:30 +00:00
Merge remote-tracking branch 'origin/main' into eng-1793-introduce-error-handling-for-sd-core-sync-crate
This commit is contained in:
commit
3f05cec895
58
.github/workflows/ci.yml
vendored
58
.github/workflows/ci.yml
vendored
|
@ -20,9 +20,9 @@ concurrency:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
typescript:
|
typescript:
|
||||||
name: TypeScript
|
name: Type and style check
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
timeout-minutes: 3
|
timeout-minutes: 7
|
||||||
permissions: {}
|
permissions: {}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
|
@ -36,6 +36,15 @@ jobs:
|
||||||
- name: Perform typechecks
|
- name: Perform typechecks
|
||||||
run: pnpm typecheck
|
run: pnpm typecheck
|
||||||
|
|
||||||
|
- name: Perform style check
|
||||||
|
run: |-
|
||||||
|
set -eux
|
||||||
|
pnpm autoformat only-frontend
|
||||||
|
if [ -n "$(git diff --name-only --cached)" ]; then
|
||||||
|
echo "Some files are not correctly formatted. Please run 'pnpm autoformat' and commit the changes." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
eslint:
|
eslint:
|
||||||
name: ESLint
|
name: ESLint
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
|
@ -56,7 +65,7 @@ jobs:
|
||||||
cypress:
|
cypress:
|
||||||
name: Cypress
|
name: Cypress
|
||||||
runs-on: macos-14
|
runs-on: macos-14
|
||||||
timeout-minutes: 30
|
timeout-minutes: 45
|
||||||
permissions: {}
|
permissions: {}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
|
@ -174,11 +183,20 @@ jobs:
|
||||||
run: cargo fmt --all -- --check
|
run: cargo fmt --all -- --check
|
||||||
|
|
||||||
clippy:
|
clippy:
|
||||||
name: Clippy (${{ matrix.platform }})
|
|
||||||
runs-on: ${{ matrix.platform }}
|
|
||||||
strategy:
|
strategy:
|
||||||
|
fail-fast: true
|
||||||
matrix:
|
matrix:
|
||||||
platform: [ubuntu-22.04, macos-14, windows-latest]
|
settings:
|
||||||
|
- host: macos-13
|
||||||
|
target: x86_64-apple-darwin
|
||||||
|
- host: macos-14
|
||||||
|
target: aarch64-apple-darwin
|
||||||
|
- host: windows-latest
|
||||||
|
target: x86_64-pc-windows-msvc
|
||||||
|
- host: ubuntu-22.04
|
||||||
|
target: x86_64-unknown-linux-gnu
|
||||||
|
name: Clippy (${{ matrix.settings.host }})
|
||||||
|
runs-on: ${{ matrix.settings.host }}
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
timeout-minutes: 45
|
timeout-minutes: 45
|
||||||
|
@ -223,6 +241,7 @@ jobs:
|
||||||
- 'extensions/*/**'
|
- 'extensions/*/**'
|
||||||
- 'Cargo.toml'
|
- 'Cargo.toml'
|
||||||
- 'Cargo.lock'
|
- 'Cargo.lock'
|
||||||
|
- '.github/workflows/ci.yml'
|
||||||
|
|
||||||
- name: Setup System and Rust
|
- name: Setup System and Rust
|
||||||
if: steps.filter.outcome != 'success' || steps.filter.outputs.changes == 'true'
|
if: steps.filter.outcome != 'success' || steps.filter.outputs.changes == 'true'
|
||||||
|
@ -232,22 +251,15 @@ jobs:
|
||||||
|
|
||||||
- name: Run Clippy
|
- name: Run Clippy
|
||||||
if: steps.filter.outcome != 'success' || steps.filter.outputs.changes == 'true'
|
if: steps.filter.outcome != 'success' || steps.filter.outputs.changes == 'true'
|
||||||
uses: actions-rs-plus/clippy-check@v2
|
uses: giraffate/clippy-action@v1
|
||||||
with:
|
with:
|
||||||
args: --workspace --all-features --locked
|
reporter: github-pr-review
|
||||||
|
tool_name: 'Clippy (${{ matrix.settings.host }})'
|
||||||
|
filter_mode: diff_context
|
||||||
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
clippy_flags: --workspace --all-features --locked
|
||||||
|
fail_on_error: true
|
||||||
|
|
||||||
# test:
|
# - name: Run tests
|
||||||
# name: Test (${{ matrix.platform }})
|
# if: steps.filter.outcome != 'success' || steps.filter.outputs.changes == 'true'
|
||||||
# runs-on: ${{ matrix.platform }}
|
# run: cargo test --workspace --all-features --locked --target ${{ matrix.settings.target }}
|
||||||
# strategy:
|
|
||||||
# matrix:
|
|
||||||
# platform: [ubuntu-22.04, macos-latest, windows-latest]
|
|
||||||
# steps:
|
|
||||||
# - name: Checkout repository
|
|
||||||
# uses: actions/checkout@v4
|
|
||||||
#
|
|
||||||
# - name: Setup
|
|
||||||
# uses: ./.github/actions/setup
|
|
||||||
#
|
|
||||||
# - name: Test
|
|
||||||
# run: cargo test --workspace --all-features
|
|
||||||
|
|
|
@ -91,12 +91,14 @@ If you encounter any issues, ensure that you are using the following versions of
|
||||||
|
|
||||||
- Rust version: **1.78**
|
- Rust version: **1.78**
|
||||||
- Node version: **18.18**
|
- Node version: **18.18**
|
||||||
- Pnpm version: **9.0.6**
|
- Pnpm version: **9.1.1**
|
||||||
|
|
||||||
After cleaning out your build artifacts using `pnpm clean`, `git clean`, or `cargo clean`, it is necessary to re-run the `setup-system` script.
|
After cleaning out your build artifacts using `pnpm clean`, `git clean`, or `cargo clean`, it is necessary to re-run the `setup-system` script.
|
||||||
|
|
||||||
Make sure to read the [guidelines](https://spacedrive.com/docs/developers/prerequisites/guidelines) to ensure that your code follows a similar style to ours.
|
Make sure to read the [guidelines](https://spacedrive.com/docs/developers/prerequisites/guidelines) to ensure that your code follows a similar style to ours.
|
||||||
|
|
||||||
|
After you finish making your changes and committed them to your branch, make sure to execute `pnpm autoformat` to fix any style inconsistency in your code.
|
||||||
|
|
||||||
##### Mobile App
|
##### Mobile App
|
||||||
|
|
||||||
To run the mobile app:
|
To run the mobile app:
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { useDrawerStatus } from '@react-navigation/drawer';
|
import { useDrawerStatus } from '@react-navigation/drawer';
|
||||||
import { useNavigation } from '@react-navigation/native';
|
import { useNavigation } from '@react-navigation/native';
|
||||||
import { MotiView } from 'moti';
|
import { MotiView } from 'moti';
|
||||||
import { CaretRight, Gear, Lock, Plus } from 'phosphor-react-native';
|
import { CaretRight, CloudArrowDown, Gear, Lock, Plus } from 'phosphor-react-native';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { Alert, Pressable, Text, View } from 'react-native';
|
import { Alert, Pressable, Text, View } from 'react-native';
|
||||||
import { useClientContext } from '@sd/client';
|
import { useClientContext } from '@sd/client';
|
||||||
|
@ -12,6 +12,7 @@ import { AnimatedHeight } from '../animation/layout';
|
||||||
import { ModalRef } from '../layout/Modal';
|
import { ModalRef } from '../layout/Modal';
|
||||||
import CreateLibraryModal from '../modal/CreateLibraryModal';
|
import CreateLibraryModal from '../modal/CreateLibraryModal';
|
||||||
import { Divider } from '../primitive/Divider';
|
import { Divider } from '../primitive/Divider';
|
||||||
|
import ImportModalLibrary from '../modal/ImportLibraryModal';
|
||||||
|
|
||||||
const DrawerLibraryManager = () => {
|
const DrawerLibraryManager = () => {
|
||||||
const [dropdownClosed, setDropdownClosed] = useState(true);
|
const [dropdownClosed, setDropdownClosed] = useState(true);
|
||||||
|
@ -27,6 +28,7 @@ const DrawerLibraryManager = () => {
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation();
|
||||||
|
|
||||||
const modalRef = useRef<ModalRef>(null);
|
const modalRef = useRef<ModalRef>(null);
|
||||||
|
const modalRef_import = useRef<ModalRef>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
|
@ -91,6 +93,14 @@ const DrawerLibraryManager = () => {
|
||||||
<Text style={tw`text-sm font-semibold text-white`}>New Library</Text>
|
<Text style={tw`text-sm font-semibold text-white`}>New Library</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
<CreateLibraryModal ref={modalRef} />
|
<CreateLibraryModal ref={modalRef} />
|
||||||
|
<Pressable
|
||||||
|
style={tw`flex flex-row items-center px-1.5 py-[8px]`}
|
||||||
|
onPress={() => modalRef_import.current?.present()}
|
||||||
|
>
|
||||||
|
<CloudArrowDown size={18} weight="bold" color="white" style={tw`mr-2`} />
|
||||||
|
<Text style={tw`text-sm font-semibold text-white`}>Import Library</Text>
|
||||||
|
</Pressable>
|
||||||
|
<ImportModalLibrary ref={modalRef_import} />
|
||||||
{/* Manage Library */}
|
{/* Manage Library */}
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
|
|
|
@ -32,7 +32,7 @@ const ScreenContainer = ({
|
||||||
}}
|
}}
|
||||||
contentContainerStyle={twStyle('justify-between gap-10 py-6', style)}
|
contentContainerStyle={twStyle('justify-between gap-10 py-6', style)}
|
||||||
style={twStyle(
|
style={twStyle(
|
||||||
'flex-1 bg-black',
|
'bg-black',
|
||||||
tabHeight && { marginBottom: bottomTabBarHeight }
|
tabHeight && { marginBottom: bottomTabBarHeight }
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
140
apps/mobile/src/components/modal/ImportLibraryModal.tsx
Normal file
140
apps/mobile/src/components/modal/ImportLibraryModal.tsx
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
import { BottomSheetFlatList } from '@gorhom/bottom-sheet';
|
||||||
|
import { NavigationProp, useNavigation } from '@react-navigation/native';
|
||||||
|
import {
|
||||||
|
CloudLibrary,
|
||||||
|
useBridgeMutation,
|
||||||
|
useBridgeQuery,
|
||||||
|
useClientContext,
|
||||||
|
useRspcContext
|
||||||
|
} from '@sd/client';
|
||||||
|
import { forwardRef } from 'react';
|
||||||
|
import { ActivityIndicator, Text, View } from 'react-native';
|
||||||
|
import { Modal, ModalRef } from '~/components/layout/Modal';
|
||||||
|
import { Button } from '~/components/primitive/Button';
|
||||||
|
import useForwardedRef from '~/hooks/useForwardedRef';
|
||||||
|
import { tw } from '~/lib/tailwind';
|
||||||
|
import { RootStackParamList } from '~/navigation';
|
||||||
|
import { currentLibraryStore } from '~/utils/nav';
|
||||||
|
|
||||||
|
import Empty from '../layout/Empty';
|
||||||
|
import Fade from '../layout/Fade';
|
||||||
|
|
||||||
|
const ImportModalLibrary = forwardRef<ModalRef, unknown>((_, ref) => {
|
||||||
|
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
||||||
|
const modalRef = useForwardedRef(ref);
|
||||||
|
|
||||||
|
const { libraries } = useClientContext();
|
||||||
|
|
||||||
|
const cloudLibraries = useBridgeQuery(['cloud.library.list']);
|
||||||
|
const cloudLibrariesData = cloudLibraries.data?.filter(
|
||||||
|
(cloudLibrary) => !libraries.data?.find((l) => l.uuid === cloudLibrary.uuid)
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
ref={modalRef}
|
||||||
|
snapPoints={cloudLibrariesData?.length !== 0 ? ['30', '50'] : ['30']}
|
||||||
|
title="Join a Cloud Library"
|
||||||
|
showCloseButton
|
||||||
|
onDismiss={() => cloudLibraries.refetch()}
|
||||||
|
>
|
||||||
|
<View style={tw`relative flex-1`}>
|
||||||
|
{cloudLibraries.isLoading ? (
|
||||||
|
<View style={tw`mt-10 items-center justify-center`}>
|
||||||
|
<ActivityIndicator size="small" />
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<Fade
|
||||||
|
width={20}
|
||||||
|
height="100%"
|
||||||
|
fadeSides="top-bottom"
|
||||||
|
orientation="vertical"
|
||||||
|
color="bg-app-modal"
|
||||||
|
>
|
||||||
|
<BottomSheetFlatList
|
||||||
|
data={cloudLibrariesData}
|
||||||
|
contentContainerStyle={tw`px-4 pb-6 pt-5`}
|
||||||
|
ItemSeparatorComponent={() => <View style={tw`h-2`} />}
|
||||||
|
ListEmptyComponent={
|
||||||
|
<Empty
|
||||||
|
icon="Drive"
|
||||||
|
style={tw`mt-2 border-0`}
|
||||||
|
iconSize={46}
|
||||||
|
description="No cloud libraries available to join"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
keyExtractor={(item) => item.uuid}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
renderItem={({ item }) => (
|
||||||
|
<CloudLibraryCard
|
||||||
|
data={item}
|
||||||
|
navigation={navigation}
|
||||||
|
modalRef={modalRef}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Fade>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
data: CloudLibrary;
|
||||||
|
modalRef: React.RefObject<ModalRef>;
|
||||||
|
navigation: NavigationProp<RootStackParamList>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CloudLibraryCard = ({ data, modalRef, navigation }: Props) => {
|
||||||
|
const rspc = useRspcContext().queryClient;
|
||||||
|
const joinLibrary = useBridgeMutation(['cloud.library.join']);
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
key={data.uuid}
|
||||||
|
style={tw`flex flex-row items-center justify-between gap-2 rounded-md border border-app-box bg-app p-2`}
|
||||||
|
>
|
||||||
|
<Text numberOfLines={1} style={tw`max-w-[80%] text-sm font-bold text-ink`}>
|
||||||
|
{data.name}
|
||||||
|
</Text>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="accent"
|
||||||
|
disabled={joinLibrary.isLoading}
|
||||||
|
onPress={async () => {
|
||||||
|
const library = await joinLibrary.mutateAsync(data.uuid);
|
||||||
|
|
||||||
|
rspc.setQueryData(['library.list'], (libraries: any) => {
|
||||||
|
// The invalidation system beat us to it
|
||||||
|
if ((libraries || []).find((l: any) => l.uuid === library.uuid))
|
||||||
|
return libraries;
|
||||||
|
|
||||||
|
return [...(libraries || []), library];
|
||||||
|
});
|
||||||
|
|
||||||
|
currentLibraryStore.id = library.uuid;
|
||||||
|
|
||||||
|
navigation.navigate('Root', {
|
||||||
|
screen: 'Home',
|
||||||
|
params: {
|
||||||
|
screen: 'OverviewStack',
|
||||||
|
params: {
|
||||||
|
screen: 'Overview'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
modalRef.current?.dismiss();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={tw`text-sm font-medium text-white`}>
|
||||||
|
{joinLibrary.isLoading && joinLibrary.variables === data.uuid
|
||||||
|
? 'Joining...'
|
||||||
|
: 'Join'}
|
||||||
|
</Text>
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ImportModalLibrary;
|
26
apps/mobile/src/navigation/BackfillWaitingStack.tsx
Normal file
26
apps/mobile/src/navigation/BackfillWaitingStack.tsx
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import { createNativeStackNavigator, NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||||
|
import React from 'react';
|
||||||
|
import BackfillWaiting from '~/screens/BackfillWaiting';
|
||||||
|
|
||||||
|
const Stack = createNativeStackNavigator<BackfillWaitingStackParamList>();
|
||||||
|
|
||||||
|
export default function BackfillWaitingStack() {
|
||||||
|
return (
|
||||||
|
<Stack.Navigator initialRouteName="BackfillWaiting">
|
||||||
|
<Stack.Screen
|
||||||
|
name="BackfillWaiting"
|
||||||
|
component={BackfillWaiting}
|
||||||
|
options={{
|
||||||
|
headerShown: false
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Stack.Navigator>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BackfillWaitingStackParamList = {
|
||||||
|
BackfillWaiting: undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BackfillWaitingStackScreenProps<Screen extends keyof BackfillWaitingStackParamList> =
|
||||||
|
NativeStackScreenProps<BackfillWaitingStackParamList, Screen>;
|
|
@ -4,6 +4,7 @@ import NotFoundScreen from '~/screens/NotFound';
|
||||||
|
|
||||||
import DrawerNavigator, { DrawerNavParamList } from './DrawerNavigator';
|
import DrawerNavigator, { DrawerNavParamList } from './DrawerNavigator';
|
||||||
import SearchStack, { SearchStackParamList } from './SearchStack';
|
import SearchStack, { SearchStackParamList } from './SearchStack';
|
||||||
|
import BackfillWaitingStack, { BackfillWaitingStackParamList } from './BackfillWaitingStack';
|
||||||
|
|
||||||
const Stack = createNativeStackNavigator<RootStackParamList>();
|
const Stack = createNativeStackNavigator<RootStackParamList>();
|
||||||
// This is the main navigator we nest everything under.
|
// This is the main navigator we nest everything under.
|
||||||
|
@ -20,6 +21,11 @@ export default function RootNavigator() {
|
||||||
component={SearchStack}
|
component={SearchStack}
|
||||||
options={{ headerShown: false }}
|
options={{ headerShown: false }}
|
||||||
/>
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name="BackfillWaitingStack"
|
||||||
|
component={BackfillWaitingStack}
|
||||||
|
options={{ headerShown: false }}
|
||||||
|
/>
|
||||||
<Stack.Screen name="NotFound" component={NotFoundScreen} options={{ title: 'Oops!' }} />
|
<Stack.Screen name="NotFound" component={NotFoundScreen} options={{ title: 'Oops!' }} />
|
||||||
</Stack.Navigator>
|
</Stack.Navigator>
|
||||||
);
|
);
|
||||||
|
@ -28,6 +34,7 @@ export default function RootNavigator() {
|
||||||
export type RootStackParamList = {
|
export type RootStackParamList = {
|
||||||
Root: NavigatorScreenParams<DrawerNavParamList>;
|
Root: NavigatorScreenParams<DrawerNavParamList>;
|
||||||
SearchStack: NavigatorScreenParams<SearchStackParamList>;
|
SearchStack: NavigatorScreenParams<SearchStackParamList>;
|
||||||
|
BackfillWaitingStack: NavigatorScreenParams<BackfillWaitingStackParamList>;
|
||||||
NotFound: undefined;
|
NotFound: undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -12,10 +12,12 @@ import PrivacySettingsScreen from '~/screens/settings/client/PrivacySettings';
|
||||||
import AboutScreen from '~/screens/settings/info/About';
|
import AboutScreen from '~/screens/settings/info/About';
|
||||||
import DebugScreen from '~/screens/settings/info/Debug';
|
import DebugScreen from '~/screens/settings/info/Debug';
|
||||||
import SupportScreen from '~/screens/settings/info/Support';
|
import SupportScreen from '~/screens/settings/info/Support';
|
||||||
|
import CloudSettings from '~/screens/settings/library/CloudSettings/CloudSettings';
|
||||||
import EditLocationSettingsScreen from '~/screens/settings/library/EditLocationSettings';
|
import EditLocationSettingsScreen from '~/screens/settings/library/EditLocationSettings';
|
||||||
import LibraryGeneralSettingsScreen from '~/screens/settings/library/LibraryGeneralSettings';
|
import LibraryGeneralSettingsScreen from '~/screens/settings/library/LibraryGeneralSettings';
|
||||||
import LocationSettingsScreen from '~/screens/settings/library/LocationSettings';
|
import LocationSettingsScreen from '~/screens/settings/library/LocationSettings';
|
||||||
import NodesSettingsScreen from '~/screens/settings/library/NodesSettings';
|
import NodesSettingsScreen from '~/screens/settings/library/NodesSettings';
|
||||||
|
import SyncSettingsScreen from '~/screens/settings/library/SyncSettings';
|
||||||
import TagsSettingsScreen from '~/screens/settings/library/TagsSettings';
|
import TagsSettingsScreen from '~/screens/settings/library/TagsSettings';
|
||||||
import SettingsScreen from '~/screens/settings/Settings';
|
import SettingsScreen from '~/screens/settings/Settings';
|
||||||
|
|
||||||
|
@ -87,6 +89,16 @@ export default function SettingsStack() {
|
||||||
component={TagsSettingsScreen}
|
component={TagsSettingsScreen}
|
||||||
options={{ header: () => <Header navBack title="Tags" /> }}
|
options={{ header: () => <Header navBack title="Tags" /> }}
|
||||||
/>
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name="SyncSettings"
|
||||||
|
component={SyncSettingsScreen}
|
||||||
|
options={{ header: () => <Header navBack title="Sync" /> }}
|
||||||
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name="CloudSettings"
|
||||||
|
component={CloudSettings}
|
||||||
|
options={{ header: () => <Header navBack title="Cloud" /> }}
|
||||||
|
/>
|
||||||
{/* <Stack.Screen
|
{/* <Stack.Screen
|
||||||
name="KeysSettings"
|
name="KeysSettings"
|
||||||
component={KeysSettingsScreen}
|
component={KeysSettingsScreen}
|
||||||
|
@ -131,6 +143,8 @@ export type SettingsStackParamList = {
|
||||||
NodesSettings: undefined;
|
NodesSettings: undefined;
|
||||||
TagsSettings: undefined;
|
TagsSettings: undefined;
|
||||||
KeysSettings: undefined;
|
KeysSettings: undefined;
|
||||||
|
SyncSettings: undefined;
|
||||||
|
CloudSettings: undefined;
|
||||||
// Info
|
// Info
|
||||||
About: undefined;
|
About: undefined;
|
||||||
Support: undefined;
|
Support: undefined;
|
||||||
|
|
87
apps/mobile/src/screens/BackfillWaiting.tsx
Normal file
87
apps/mobile/src/screens/BackfillWaiting.tsx
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
import { useNavigation } from '@react-navigation/native';
|
||||||
|
import { AppLogo } from '@sd/assets/images';
|
||||||
|
import { useLibraryMutation, useLibraryQuery } from '@sd/client';
|
||||||
|
import { Image } from 'expo-image';
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { Dimensions, Text, View } from 'react-native';
|
||||||
|
import Animated, {
|
||||||
|
Easing,
|
||||||
|
useAnimatedStyle,
|
||||||
|
useSharedValue,
|
||||||
|
withRepeat,
|
||||||
|
withTiming
|
||||||
|
} from 'react-native-reanimated';
|
||||||
|
import { Circle, Defs, RadialGradient, Stop, Svg } from 'react-native-svg';
|
||||||
|
import { tw, twStyle } from '~/lib/tailwind';
|
||||||
|
|
||||||
|
const { width } = Dimensions.get('window');
|
||||||
|
|
||||||
|
const BackfillWaiting = () => {
|
||||||
|
const animation = useSharedValue(0);
|
||||||
|
const navigation = useNavigation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
animation.value = withRepeat(
|
||||||
|
withTiming(1, { duration: 5000, easing: Easing.inOut(Easing.ease) }),
|
||||||
|
-1,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}, [animation]);
|
||||||
|
|
||||||
|
const animatedStyle = useAnimatedStyle(() => {
|
||||||
|
return {
|
||||||
|
opacity: animation.value
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const enableSync = useLibraryMutation(['sync.backfill'], {
|
||||||
|
onSuccess: () => {
|
||||||
|
syncEnabled.refetch();
|
||||||
|
navigation.navigate('Root', {
|
||||||
|
screen: 'Home',
|
||||||
|
params: {
|
||||||
|
screen: 'SettingsStack',
|
||||||
|
params: {
|
||||||
|
screen: 'SyncSettings'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const syncEnabled = useLibraryQuery(['sync.enabled']);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
await enableSync.mutateAsync(null);
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={tw`flex-1 items-center justify-center bg-black`}>
|
||||||
|
<Animated.View style={[twStyle(`absolute items-center justify-center`, {
|
||||||
|
width: width * 2,
|
||||||
|
height: width * 2,
|
||||||
|
borderRadius: (width * 0.8) / 2,
|
||||||
|
}), animatedStyle]}>
|
||||||
|
<Svg height="100%" width="100%" viewBox="0 0 100 100">
|
||||||
|
<Defs>
|
||||||
|
<RadialGradient id="grad" cx="50%" cy="50%" r="50%" fx="50%" fy="50%">
|
||||||
|
<Stop offset="0%" stopColor="#4B0082" stopOpacity="1" />
|
||||||
|
<Stop offset="100%" stopColor="#000000" stopOpacity="0" />
|
||||||
|
</RadialGradient>
|
||||||
|
</Defs>
|
||||||
|
<Circle cx="50" cy="50" r="50" fill="url(#grad)" />
|
||||||
|
</Svg>
|
||||||
|
</Animated.View>
|
||||||
|
<Image source={AppLogo} style={tw`mb-4 h-[100px] w-[100px]`} />
|
||||||
|
<Text style={tw`mx-10 mb-4 text-center text-md leading-6 text-ink`}>
|
||||||
|
Library is being backfilled right now for Sync!
|
||||||
|
<Text style={tw`font-bold`}> Please hold </Text>
|
||||||
|
while this process takes place.
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BackfillWaiting;
|
|
@ -1,5 +1,8 @@
|
||||||
|
import { DebugState, useDebugState, useDebugStateEnabler } from '@sd/client';
|
||||||
import {
|
import {
|
||||||
|
ArrowsClockwise,
|
||||||
Books,
|
Books,
|
||||||
|
Cloud,
|
||||||
FlyingSaucer,
|
FlyingSaucer,
|
||||||
Gear,
|
Gear,
|
||||||
GearSix,
|
GearSix,
|
||||||
|
@ -14,7 +17,6 @@ import {
|
||||||
} from 'phosphor-react-native';
|
} from 'phosphor-react-native';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Platform, SectionList, Text, TouchableWithoutFeedback, View } from 'react-native';
|
import { Platform, SectionList, Text, TouchableWithoutFeedback, View } from 'react-native';
|
||||||
import { DebugState, useDebugState, useDebugStateEnabler } from '@sd/client';
|
|
||||||
import ScreenContainer from '~/components/layout/ScreenContainer';
|
import ScreenContainer from '~/components/layout/ScreenContainer';
|
||||||
import { SettingsItem } from '~/components/settings/SettingsItem';
|
import { SettingsItem } from '~/components/settings/SettingsItem';
|
||||||
import { tw, twStyle } from '~/lib/tailwind';
|
import { tw, twStyle } from '~/lib/tailwind';
|
||||||
|
@ -86,6 +88,16 @@ const sections: (debugState: DebugState) => SectionType[] = (debugState) => [
|
||||||
icon: TagSimple,
|
icon: TagSimple,
|
||||||
navigateTo: 'TagsSettings',
|
navigateTo: 'TagsSettings',
|
||||||
title: 'Tags',
|
title: 'Tags',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Cloud,
|
||||||
|
navigateTo: 'CloudSettings',
|
||||||
|
title: 'Cloud',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: ArrowsClockwise,
|
||||||
|
navigateTo: 'SyncSettings',
|
||||||
|
title: 'Sync',
|
||||||
rounded: 'bottom'
|
rounded: 'bottom'
|
||||||
}
|
}
|
||||||
// {
|
// {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Text, View } from 'react-native';
|
|
||||||
import { useBridgeQuery, useDebugState } from '@sd/client';
|
import { useBridgeQuery, useDebugState } from '@sd/client';
|
||||||
|
import { Text, View } from 'react-native';
|
||||||
import Card from '~/components/layout/Card';
|
import Card from '~/components/layout/Card';
|
||||||
import ScreenContainer from '~/components/layout/ScreenContainer';
|
import ScreenContainer from '~/components/layout/ScreenContainer';
|
||||||
import { Divider } from '~/components/primitive/Divider';
|
import { Divider } from '~/components/primitive/Divider';
|
||||||
|
|
|
@ -1,6 +1,14 @@
|
||||||
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Text, View } from 'react-native';
|
import { Text, View } from 'react-native';
|
||||||
import { toggleFeatureFlag, useDebugState, useFeatureFlags } from '@sd/client';
|
import {
|
||||||
|
auth,
|
||||||
|
toggleFeatureFlag,
|
||||||
|
useBridgeMutation,
|
||||||
|
useBridgeQuery,
|
||||||
|
useDebugState,
|
||||||
|
useFeatureFlags
|
||||||
|
} from '@sd/client';
|
||||||
import Card from '~/components/layout/Card';
|
import Card from '~/components/layout/Card';
|
||||||
import { Button } from '~/components/primitive/Button';
|
import { Button } from '~/components/primitive/Button';
|
||||||
import { tw } from '~/lib/tailwind';
|
import { tw } from '~/lib/tailwind';
|
||||||
|
@ -9,6 +17,10 @@ import { SettingsStackScreenProps } from '~/navigation/tabs/SettingsStack';
|
||||||
const DebugScreen = ({ navigation }: SettingsStackScreenProps<'Debug'>) => {
|
const DebugScreen = ({ navigation }: SettingsStackScreenProps<'Debug'>) => {
|
||||||
const debugState = useDebugState();
|
const debugState = useDebugState();
|
||||||
const featureFlags = useFeatureFlags();
|
const featureFlags = useFeatureFlags();
|
||||||
|
const origin = useBridgeQuery(['cloud.getApiOrigin']);
|
||||||
|
const setOrigin = useBridgeMutation(['cloud.setApiOrigin']);
|
||||||
|
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={tw`flex-1 p-4`}>
|
<View style={tw`flex-1 p-4`}>
|
||||||
|
@ -28,6 +40,37 @@ const DebugScreen = ({ navigation }: SettingsStackScreenProps<'Debug'>) => {
|
||||||
>
|
>
|
||||||
<Text style={tw`text-ink`}>Disable Debug Mode</Text>
|
<Text style={tw`text-ink`}>Disable Debug Mode</Text>
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
onPress={() => {
|
||||||
|
const url =
|
||||||
|
origin.data === 'https://app.spacedrive.com'
|
||||||
|
? 'http://localhost:3000'
|
||||||
|
: 'https://app.spacedrive.com';
|
||||||
|
setOrigin.mutateAsync(url).then(async () => {
|
||||||
|
await auth.logout();
|
||||||
|
await queryClient.invalidateQueries();
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={tw`text-ink`}>Toggle API Route ({origin.data})</Text>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onPress={() => {
|
||||||
|
navigation.popToTop();
|
||||||
|
navigation.navigate('BackfillWaitingStack', {
|
||||||
|
screen: 'BackfillWaiting'
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={tw`text-ink`}>Go to Backfill Waiting Page</Text>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onPress={async () => {
|
||||||
|
await auth.logout();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={tw`text-ink`}>Logout</Text>
|
||||||
|
</Button>
|
||||||
</Card>
|
</Card>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,123 @@
|
||||||
|
import { useLibraryContext, useLibraryMutation, useLibraryQuery } from '@sd/client';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { ActivityIndicator, FlatList, Text, View } from 'react-native';
|
||||||
|
import Card from '~/components/layout/Card';
|
||||||
|
import Empty from '~/components/layout/Empty';
|
||||||
|
import ScreenContainer from '~/components/layout/ScreenContainer';
|
||||||
|
import VirtualizedListWrapper from '~/components/layout/VirtualizedListWrapper';
|
||||||
|
import { Button } from '~/components/primitive/Button';
|
||||||
|
import { Divider } from '~/components/primitive/Divider';
|
||||||
|
import { styled, tw, twStyle } from '~/lib/tailwind';
|
||||||
|
import { useAuthStateSnapshot } from '~/stores/auth';
|
||||||
|
|
||||||
|
import Instance from './Instance';
|
||||||
|
import Library from './Library';
|
||||||
|
import Login from './Login';
|
||||||
|
import ThisInstance from './ThisInstance';
|
||||||
|
|
||||||
|
export const InfoBox = styled(View, 'rounded-md border gap-1 border-app bg-transparent p-2');
|
||||||
|
|
||||||
|
const CloudSettings = () => {
|
||||||
|
return (
|
||||||
|
<ScreenContainer scrollview={false} style={tw`gap-0 px-6 py-0`}>
|
||||||
|
<AuthSensitiveChild />
|
||||||
|
</ScreenContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const AuthSensitiveChild = () => {
|
||||||
|
const authState = useAuthStateSnapshot();
|
||||||
|
if (authState.status === 'loggedIn') return <Authenticated />;
|
||||||
|
if (authState.status === 'notLoggedIn' || authState.status === 'loggingIn') return <Login />;
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Authenticated = () => {
|
||||||
|
const { library } = useLibraryContext();
|
||||||
|
const cloudLibrary = useLibraryQuery(['cloud.library.get'], { retry: false });
|
||||||
|
const createLibrary = useLibraryMutation(['cloud.library.create']);
|
||||||
|
|
||||||
|
const cloudInstances = useMemo(
|
||||||
|
() =>
|
||||||
|
cloudLibrary.data?.instances.filter(
|
||||||
|
(instance) => instance.uuid !== library.instance_id
|
||||||
|
),
|
||||||
|
[cloudLibrary.data, library.instance_id]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (cloudLibrary.isLoading) {
|
||||||
|
return (
|
||||||
|
<View style={tw`flex-1 items-center justify-center`}>
|
||||||
|
<ActivityIndicator size="small" />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScreenContainer tabHeight={false}>
|
||||||
|
{cloudLibrary.data ? (
|
||||||
|
<View style={tw`flex-col items-start gap-5`}>
|
||||||
|
<Library cloudLibrary={cloudLibrary.data} />
|
||||||
|
<ThisInstance cloudLibrary={cloudLibrary.data} />
|
||||||
|
<Card style={tw`w-full`}>
|
||||||
|
<View style={tw`flex-row items-center gap-2`}>
|
||||||
|
<View
|
||||||
|
style={tw`self-start rounded border border-app-lightborder bg-app-highlight px-1.5 py-[2px]`}
|
||||||
|
>
|
||||||
|
<Text style={tw`text-xs font-semibold text-ink`}>
|
||||||
|
{cloudInstances?.length}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<Text style={tw`font-semibold text-ink`}>Instances</Text>
|
||||||
|
</View>
|
||||||
|
<Divider style={tw`mb-4 mt-2`} />
|
||||||
|
<VirtualizedListWrapper
|
||||||
|
scrollEnabled={false}
|
||||||
|
contentContainerStyle={tw`flex-1`}
|
||||||
|
horizontal
|
||||||
|
>
|
||||||
|
<FlatList
|
||||||
|
data={cloudInstances}
|
||||||
|
scrollEnabled={false}
|
||||||
|
ListEmptyComponent={
|
||||||
|
<Empty textStyle={tw`my-0`} description="No instances found" />
|
||||||
|
}
|
||||||
|
contentContainerStyle={twStyle(
|
||||||
|
cloudInstances?.length === 0 && 'flex-row'
|
||||||
|
)}
|
||||||
|
showsHorizontalScrollIndicator={false}
|
||||||
|
ItemSeparatorComponent={() => <View style={tw`h-2`} />}
|
||||||
|
renderItem={({ item }) => (
|
||||||
|
<Instance data={item} />
|
||||||
|
)}
|
||||||
|
keyExtractor={(item) => item.id}
|
||||||
|
numColumns={1}
|
||||||
|
/>
|
||||||
|
</VirtualizedListWrapper>
|
||||||
|
</Card>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<Card style={tw`relative py-10`}>
|
||||||
|
<Button
|
||||||
|
style={tw`mx-auto max-w-[82%]`}
|
||||||
|
disabled={createLibrary.isLoading}
|
||||||
|
onPress={async () => await createLibrary.mutateAsync(null)}
|
||||||
|
>
|
||||||
|
{createLibrary.isLoading ? (
|
||||||
|
<Text style={tw`text-ink`}>
|
||||||
|
Connecting library to Spacedrive Cloud...
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<Text style={tw`font-medium text-ink`}>
|
||||||
|
Connect library to Spacedrive Cloud
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</ScreenContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CloudSettings;
|
|
@ -0,0 +1,59 @@
|
||||||
|
import { CloudInstance, HardwareModel } from '@sd/client';
|
||||||
|
import { Text, View } from 'react-native';
|
||||||
|
import { tw } from '~/lib/tailwind';
|
||||||
|
|
||||||
|
import { Icon } from '~/components/icons/Icon';
|
||||||
|
import { hardwareModelToIcon } from '~/components/overview/Devices';
|
||||||
|
import { InfoBox } from './CloudSettings';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
data: CloudInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Instance = ({ data }: Props) => {
|
||||||
|
return (
|
||||||
|
<InfoBox style={tw`w-full gap-2`}>
|
||||||
|
<View>
|
||||||
|
<View style={tw`mx-auto my-2`}>
|
||||||
|
<Icon
|
||||||
|
name={
|
||||||
|
hardwareModelToIcon(
|
||||||
|
data.metadata.device_model as HardwareModel) as any}
|
||||||
|
size={60}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<Text numberOfLines={1} style={tw`mb-3 px-1 text-center text-sm font-medium font-semibold text-ink`}>{data.metadata.name}</Text>
|
||||||
|
<InfoBox>
|
||||||
|
<View style={tw`flex-row items-center gap-1`}>
|
||||||
|
<Text style={tw`text-sm font-medium text-ink`}>Id:</Text>
|
||||||
|
<Text numberOfLines={1} style={tw`max-w-[250px] text-ink-dull`}>
|
||||||
|
{data.id}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</InfoBox>
|
||||||
|
</View>
|
||||||
|
<View>
|
||||||
|
<InfoBox>
|
||||||
|
<View style={tw`flex-row items-center gap-1`}>
|
||||||
|
<Text style={tw`text-sm font-medium text-ink`}>UUID:</Text>
|
||||||
|
<Text numberOfLines={1} style={tw`max-w-[85%] text-ink-dull`}>
|
||||||
|
{data.uuid}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</InfoBox>
|
||||||
|
</View>
|
||||||
|
<View>
|
||||||
|
<InfoBox>
|
||||||
|
<View style={tw`flex-row items-center gap-1`}>
|
||||||
|
<Text style={tw`text-sm font-medium text-ink`}>Public key:</Text>
|
||||||
|
<Text numberOfLines={1} style={tw`max-w-3/4 text-ink-dull`}>
|
||||||
|
{data.identity}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</InfoBox>
|
||||||
|
</View>
|
||||||
|
</InfoBox>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Instance;
|
|
@ -0,0 +1,66 @@
|
||||||
|
import { CloudLibrary, useLibraryContext, useLibraryMutation } from '@sd/client';
|
||||||
|
import { CheckCircle, XCircle } from 'phosphor-react-native';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { Text, View } from 'react-native';
|
||||||
|
import Card from '~/components/layout/Card';
|
||||||
|
import { Button } from '~/components/primitive/Button';
|
||||||
|
import { Divider } from '~/components/primitive/Divider';
|
||||||
|
import { SettingsTitle } from '~/components/settings/SettingsContainer';
|
||||||
|
import { tw } from '~/lib/tailwind';
|
||||||
|
import { logout, useAuthStateSnapshot } from '~/stores/auth';
|
||||||
|
|
||||||
|
import { InfoBox } from './CloudSettings';
|
||||||
|
|
||||||
|
interface LibraryProps {
|
||||||
|
cloudLibrary?: CloudLibrary;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Library = ({ cloudLibrary }: LibraryProps) => {
|
||||||
|
const authState = useAuthStateSnapshot();
|
||||||
|
const { library } = useLibraryContext();
|
||||||
|
const syncLibrary = useLibraryMutation(['cloud.library.sync']);
|
||||||
|
const thisInstance = useMemo(
|
||||||
|
() => cloudLibrary?.instances.find((instance) => instance.uuid === library.instance_id),
|
||||||
|
[cloudLibrary, library.instance_id]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card style={tw`w-full`}>
|
||||||
|
<View style={tw`flex-row items-center justify-between`}>
|
||||||
|
<Text style={tw`font-medium text-ink`}>Library</Text>
|
||||||
|
{authState.status === 'loggedIn' && (
|
||||||
|
<Button variant="gray" size="sm" onPress={logout}>
|
||||||
|
<Text style={tw`text-xs font-semibold text-ink`}>Logout</Text>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
<Divider style={tw`mb-4 mt-2`} />
|
||||||
|
<SettingsTitle style={tw`mb-2`}>Name</SettingsTitle>
|
||||||
|
<InfoBox>
|
||||||
|
<Text style={tw`text-ink`}>{cloudLibrary?.name}</Text>
|
||||||
|
</InfoBox>
|
||||||
|
<Button
|
||||||
|
disabled={syncLibrary.isLoading || thisInstance !== undefined}
|
||||||
|
variant={thisInstance ? 'gray' : 'accent'}
|
||||||
|
onPress={() => syncLibrary.mutate(null)}
|
||||||
|
style={tw`mt-2 flex-row gap-1 py-2`}
|
||||||
|
>
|
||||||
|
{thisInstance ? (
|
||||||
|
<CheckCircle size={16} weight="fill" color={tw.color('green-400')} />
|
||||||
|
) : (
|
||||||
|
<XCircle
|
||||||
|
style={tw`rounded-full`}
|
||||||
|
size={16}
|
||||||
|
weight="fill"
|
||||||
|
color={tw.color('red-500')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Text style={tw`text-sm font-semibold text-ink`}>
|
||||||
|
{thisInstance !== undefined ? 'Library synced' : 'Library not synced'}
|
||||||
|
</Text>
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Library;
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { Text, View } from 'react-native';
|
||||||
|
import Card from '~/components/layout/Card';
|
||||||
|
import { Button } from '~/components/primitive/Button';
|
||||||
|
import { tw } from '~/lib/tailwind';
|
||||||
|
import { cancel, login, useAuthStateSnapshot } from '~/stores/auth';
|
||||||
|
|
||||||
|
const Login = () => {
|
||||||
|
const authState = useAuthStateSnapshot();
|
||||||
|
const buttonText = {
|
||||||
|
notLoggedIn: 'Login',
|
||||||
|
loggingIn: 'Cancel'
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<View style={tw`flex-1 flex-col items-center justify-center gap-2`}>
|
||||||
|
<Card style={tw`w-full items-center justify-center p-6`}>
|
||||||
|
<Text style={tw`mb-4 max-w-[60%] text-center text-ink`}>
|
||||||
|
To access cloud related features, please login
|
||||||
|
</Text>
|
||||||
|
{(authState.status === 'notLoggedIn' || authState.status === 'loggingIn') && (
|
||||||
|
<Button
|
||||||
|
variant="accent"
|
||||||
|
style={tw`mx-auto max-w-[50%]`}
|
||||||
|
onPress={async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (authState.status === 'loggingIn') {
|
||||||
|
await cancel();
|
||||||
|
} else {
|
||||||
|
await login();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={tw`font-medium text-ink`}>{buttonText[authState.status]}</Text>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Login;
|
|
@ -0,0 +1,70 @@
|
||||||
|
import { CloudLibrary, HardwareModel, useLibraryContext } from '@sd/client';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { Text, View } from 'react-native';
|
||||||
|
import Card from '~/components/layout/Card';
|
||||||
|
import { Divider } from '~/components/primitive/Divider';
|
||||||
|
import { tw } from '~/lib/tailwind';
|
||||||
|
|
||||||
|
import { Icon } from '~/components/icons/Icon';
|
||||||
|
import { hardwareModelToIcon } from '~/components/overview/Devices';
|
||||||
|
import { InfoBox } from './CloudSettings';
|
||||||
|
|
||||||
|
interface ThisInstanceProps {
|
||||||
|
cloudLibrary?: CloudLibrary;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ThisInstance = ({ cloudLibrary }: ThisInstanceProps) => {
|
||||||
|
const { library } = useLibraryContext();
|
||||||
|
const thisInstance = useMemo(
|
||||||
|
() => cloudLibrary?.instances.find((instance) => instance.uuid === library.instance_id),
|
||||||
|
[cloudLibrary, library.instance_id]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!thisInstance) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card style={tw`w-full gap-2`}>
|
||||||
|
<View>
|
||||||
|
<Text style={tw`mb-1 font-semibold text-ink`}>This Instance</Text>
|
||||||
|
<Divider />
|
||||||
|
</View>
|
||||||
|
<View style={tw`mx-auto my-2 items-center`}>
|
||||||
|
<Icon
|
||||||
|
name={
|
||||||
|
hardwareModelToIcon(
|
||||||
|
thisInstance.metadata.device_model as HardwareModel) as any}
|
||||||
|
size={60}
|
||||||
|
/>
|
||||||
|
<Text numberOfLines={1} style={tw`px-1 font-semibold text-ink`}>{thisInstance.metadata.name}</Text>
|
||||||
|
</View>
|
||||||
|
<View>
|
||||||
|
<InfoBox>
|
||||||
|
<View style={tw`flex-row items-center gap-1`}>
|
||||||
|
<Text style={tw`text-sm font-medium text-ink`}>Id:</Text>
|
||||||
|
<Text style={tw`max-w-[250px] text-ink-dull`}>{thisInstance.id}</Text>
|
||||||
|
</View>
|
||||||
|
</InfoBox>
|
||||||
|
</View>
|
||||||
|
<View>
|
||||||
|
<InfoBox>
|
||||||
|
<View style={tw`flex-row items-center gap-1`}>
|
||||||
|
<Text style={tw`text-sm font-medium text-ink`}>UUID:</Text>
|
||||||
|
<Text numberOfLines={1} style={tw`max-w-[85%] text-ink-dull`}>{thisInstance.uuid}</Text>
|
||||||
|
</View>
|
||||||
|
</InfoBox>
|
||||||
|
</View>
|
||||||
|
<View>
|
||||||
|
<InfoBox>
|
||||||
|
<View style={tw`flex-row items-center gap-1`}>
|
||||||
|
<Text style={tw`text-sm font-medium text-ink`}>Publc Key:</Text>
|
||||||
|
<Text numberOfLines={1} style={tw`max-w-3/4 text-ink-dull`}>
|
||||||
|
{thisInstance.identity}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</InfoBox>
|
||||||
|
</View>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ThisInstance;
|
133
apps/mobile/src/screens/settings/library/SyncSettings.tsx
Normal file
133
apps/mobile/src/screens/settings/library/SyncSettings.tsx
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
import { inferSubscriptionResult } from '@oscartbeaumont-sd/rspc-client';
|
||||||
|
import { MotiView } from 'moti';
|
||||||
|
import { Circle } from 'phosphor-react-native';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Text, View } from 'react-native';
|
||||||
|
import {
|
||||||
|
Procedures,
|
||||||
|
useLibraryMutation,
|
||||||
|
useLibraryQuery,
|
||||||
|
useLibrarySubscription
|
||||||
|
} from '@sd/client';
|
||||||
|
import Card from '~/components/layout/Card';
|
||||||
|
import ScreenContainer from '~/components/layout/ScreenContainer';
|
||||||
|
import { Button } from '~/components/primitive/Button';
|
||||||
|
import { tw } from '~/lib/tailwind';
|
||||||
|
import { SettingsStackScreenProps } from '~/navigation/tabs/SettingsStack';
|
||||||
|
|
||||||
|
const SyncSettingsScreen = ({ navigation }: SettingsStackScreenProps<'SyncSettings'>) => {
|
||||||
|
const syncEnabled = useLibraryQuery(['sync.enabled']);
|
||||||
|
const [data, setData] = useState<inferSubscriptionResult<Procedures, 'library.actors'>>({});
|
||||||
|
|
||||||
|
const [startBackfill, setStart] = useState(false);
|
||||||
|
|
||||||
|
useLibrarySubscription(['library.actors'], { onData: setData });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (startBackfill === true) {
|
||||||
|
navigation.navigate('BackfillWaitingStack', {
|
||||||
|
screen: 'BackfillWaiting'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [startBackfill, navigation]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScreenContainer scrollview={false} style={tw`gap-0 px-6`}>
|
||||||
|
{syncEnabled.data === false ? (
|
||||||
|
<View style={tw`flex-1 justify-center`}>
|
||||||
|
<Card style={tw`relative py-10`}>
|
||||||
|
<Button
|
||||||
|
variant={'accent'}
|
||||||
|
style={tw`mx-auto max-w-[82%]`}
|
||||||
|
onPress={() => setStart(true)}
|
||||||
|
>
|
||||||
|
<Text style={tw`font-medium text-white`}>
|
||||||
|
Start Backfill Operations
|
||||||
|
</Text>
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<View style={tw`flex-row flex-wrap gap-2`}>
|
||||||
|
{Object.keys(data).map((key) => {
|
||||||
|
return (
|
||||||
|
<Card style={tw`w-[48%]`} key={key}>
|
||||||
|
<OnlineIndicator online={data[key] ?? false} />
|
||||||
|
<Text
|
||||||
|
key={key}
|
||||||
|
style={tw`mb-3 mt-1 flex-col items-center justify-center text-left text-xs text-white`}
|
||||||
|
>
|
||||||
|
{key}
|
||||||
|
</Text>
|
||||||
|
{data[key] ? <StopButton name={key} /> : <StartButton name={key} />}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</ScreenContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SyncSettingsScreen;
|
||||||
|
|
||||||
|
function OnlineIndicator({ online }: { online: boolean }) {
|
||||||
|
const size = 6;
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={tw`mb-1 h-6 w-6 items-center justify-center rounded-full border border-app-inputborder bg-app-input p-2`}
|
||||||
|
>
|
||||||
|
{online ? (
|
||||||
|
<View style={tw`relative items-center justify-center`}>
|
||||||
|
<MotiView
|
||||||
|
from={{ scale: 0, opacity: 1 }}
|
||||||
|
animate={{ scale: 3, opacity: 0 }}
|
||||||
|
transition={{
|
||||||
|
type: 'timing',
|
||||||
|
duration: 1500,
|
||||||
|
loop: true,
|
||||||
|
repeatReverse: false,
|
||||||
|
delay: 1000
|
||||||
|
}}
|
||||||
|
style={tw`absolute z-10 h-2 w-2 items-center justify-center rounded-full bg-green-500`}
|
||||||
|
/>
|
||||||
|
<View style={tw`h-2 w-2 rounded-full bg-green-500`} />
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<Circle size={size} color={tw.color('red-400')} weight="fill" />
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function StartButton({ name }: { name: string }) {
|
||||||
|
const startActor = useLibraryMutation(['library.startActor']);
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="accent"
|
||||||
|
size="sm"
|
||||||
|
disabled={startActor.isLoading}
|
||||||
|
onPress={() => startActor.mutate(name)}
|
||||||
|
>
|
||||||
|
<Text style={tw`text-xs font-medium text-ink`}>
|
||||||
|
{startActor.isLoading ? 'Starting' : 'Start'}
|
||||||
|
</Text>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function StopButton({ name }: { name: string }) {
|
||||||
|
const stopActor = useLibraryMutation(['library.stopActor']);
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="accent"
|
||||||
|
size="sm"
|
||||||
|
disabled={stopActor.isLoading}
|
||||||
|
onPress={() => stopActor.mutate(name)}
|
||||||
|
>
|
||||||
|
<Text style={tw`text-xs font-medium text-ink`}>
|
||||||
|
{stopActor.isLoading ? 'Stopping' : 'Stop'}
|
||||||
|
</Text>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
100
apps/mobile/src/stores/auth.ts
Normal file
100
apps/mobile/src/stores/auth.ts
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
import { RSPCError } from '@oscartbeaumont-sd/rspc-client';
|
||||||
|
import { nonLibraryClient, useSolidStore } from '@sd/client';
|
||||||
|
import { Linking } from 'react-native';
|
||||||
|
import { createMutable } from 'solid-js/store';
|
||||||
|
|
||||||
|
interface Store {
|
||||||
|
state: { status: 'loading' | 'notLoggedIn' | 'loggingIn' | 'loggedIn' | 'loggingOut' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// inner object so we can overwrite it in one assignment
|
||||||
|
const store = createMutable<Store>({
|
||||||
|
state: {
|
||||||
|
status: 'loading'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export function useAuthStateSnapshot() {
|
||||||
|
return useSolidStore(store).state;
|
||||||
|
}
|
||||||
|
|
||||||
|
nonLibraryClient
|
||||||
|
.query(['auth.me'])
|
||||||
|
.then(() => (store.state = { status: 'loggedIn' }))
|
||||||
|
.catch((e) => {
|
||||||
|
if (e instanceof RSPCError && e.code === 401) {
|
||||||
|
// TODO: handle error?
|
||||||
|
console.error("error", e);
|
||||||
|
}
|
||||||
|
store.state = { status: 'notLoggedIn' };
|
||||||
|
});
|
||||||
|
|
||||||
|
type CallbackStatus = 'success' | { error: string } | 'cancel';
|
||||||
|
const loginCallbacks = new Set<(status: CallbackStatus) => void>();
|
||||||
|
|
||||||
|
function onError(error: string) {
|
||||||
|
loginCallbacks.forEach((cb) => cb({ error }));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function login() {
|
||||||
|
if (store.state.status !== 'notLoggedIn') return;
|
||||||
|
|
||||||
|
store.state = { status: 'loggingIn' };
|
||||||
|
|
||||||
|
let authCleanup = nonLibraryClient.addSubscription(['auth.loginSession'], {
|
||||||
|
onData(data) {
|
||||||
|
if (data === 'Complete') {
|
||||||
|
loginCallbacks.forEach((cb) => cb('success'));
|
||||||
|
} else if ('Error' in data) {
|
||||||
|
console.error('[auth] error: ', data.Error);
|
||||||
|
onError(data.Error);
|
||||||
|
} else {
|
||||||
|
console.log('[auth] verification url: ', data.Start.verification_url_complete);
|
||||||
|
Promise.resolve()
|
||||||
|
.then(() => Linking.openURL(data.Start.verification_url_complete))
|
||||||
|
.then(
|
||||||
|
(res) => {
|
||||||
|
authCleanup = res;
|
||||||
|
},
|
||||||
|
(e) => onError(e.message)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError(e) {
|
||||||
|
onError(e.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Promise<void>((res, rej) => {
|
||||||
|
const cb = async (status: CallbackStatus) => {
|
||||||
|
loginCallbacks.delete(cb);
|
||||||
|
|
||||||
|
if (status === 'success') {
|
||||||
|
store.state = { status: 'loggedIn' };
|
||||||
|
nonLibraryClient.query(['auth.me']);
|
||||||
|
res();
|
||||||
|
} else {
|
||||||
|
store.state = { status: 'notLoggedIn' };
|
||||||
|
rej(JSON.stringify(status));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
loginCallbacks.add(cb);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function set_logged_in() {
|
||||||
|
store.state = { status: 'loggedIn' };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function logout() {
|
||||||
|
store.state = { status: 'loggingOut' };
|
||||||
|
nonLibraryClient.mutation(['auth.logout']);
|
||||||
|
nonLibraryClient.query(['auth.me']);
|
||||||
|
store.state = { status: 'notLoggedIn' };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function cancel() {
|
||||||
|
await loginCallbacks.forEach(async (cb) => await cb('cancel'));
|
||||||
|
await loginCallbacks.clear();
|
||||||
|
store.state = { status: 'notLoggedIn' };
|
||||||
|
}
|
|
@ -32,13 +32,21 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
|
||||||
}
|
}
|
||||||
|
|
||||||
async_stream::stream! {
|
async_stream::stream! {
|
||||||
|
let device_type = if cfg!(target_arch = "wasm32") {
|
||||||
|
"web".to_string()
|
||||||
|
} else if cfg!(target_os = "ios") || cfg!(target_os = "android") {
|
||||||
|
"mobile".to_string()
|
||||||
|
} else {
|
||||||
|
"desktop".to_string()
|
||||||
|
};
|
||||||
|
|
||||||
let auth_response = match match node
|
let auth_response = match match node
|
||||||
.http
|
.http
|
||||||
.post(&format!(
|
.post(&format!(
|
||||||
"{}/login/device/code",
|
"{}/login/device/code",
|
||||||
&node.env.api_url.lock().await
|
&node.env.api_url.lock().await
|
||||||
))
|
))
|
||||||
.form(&[("client_id", &node.env.client_id)])
|
.form(&[("client_id", &node.env.client_id), ("device", &device_type)])
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
|
|
|
@ -141,7 +141,9 @@ impl Node {
|
||||||
config,
|
config,
|
||||||
event_bus,
|
event_bus,
|
||||||
libraries,
|
libraries,
|
||||||
cloud_sync_flag: Arc::new(AtomicBool::new(false)),
|
cloud_sync_flag: Arc::new(AtomicBool::new(
|
||||||
|
cfg!(target_os = "ios") || cfg!(target_os = "android"),
|
||||||
|
)),
|
||||||
http: reqwest::Client::new(),
|
http: reqwest::Client::new(),
|
||||||
env,
|
env,
|
||||||
#[cfg(feature = "ai")]
|
#[cfg(feature = "ai")]
|
||||||
|
|
|
@ -326,7 +326,7 @@ where
|
||||||
I: ArrayLength<u8>,
|
I: ArrayLength<u8>,
|
||||||
{
|
{
|
||||||
fn from(value: &Key) -> Self {
|
fn from(value: &Key) -> Self {
|
||||||
value.expose().iter().copied().collect() // TODO(brxken128): streamline this?
|
GenericArray::clone_from_slice(value.expose())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
---
|
|
||||||
index: 100
|
|
||||||
---
|
|
||||||
|
|
||||||
# Folder Sync
|
|
|
@ -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) {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Gear, Lock, Plus } from '@phosphor-icons/react';
|
import { CloudArrowDown, Gear, Lock, Plus } from '@phosphor-icons/react';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useClientContext } from '@sd/client';
|
import { useClientContext } from '@sd/client';
|
||||||
import { dialogManager, Dropdown, DropdownMenu } from '@sd/ui';
|
import { dialogManager, Dropdown, DropdownMenu } from '@sd/ui';
|
||||||
|
@ -6,6 +6,7 @@ import { useLocale } from '~/hooks';
|
||||||
|
|
||||||
import CreateDialog from '../../../settings/node/libraries/CreateDialog';
|
import CreateDialog from '../../../settings/node/libraries/CreateDialog';
|
||||||
import { useSidebarContext } from './Context';
|
import { useSidebarContext } from './Context';
|
||||||
|
import JoinDialog from '~/app/$libraryId/settings/node/libraries/JoinDialog';
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const { library, libraries, currentLibraryId } = useClientContext();
|
const { library, libraries, currentLibraryId } = useClientContext();
|
||||||
|
@ -62,6 +63,13 @@ export default () => {
|
||||||
onClick={() => dialogManager.create((dp) => <CreateDialog {...dp} />)}
|
onClick={() => dialogManager.create((dp) => <CreateDialog {...dp} />)}
|
||||||
className="font-medium"
|
className="font-medium"
|
||||||
/>
|
/>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
label={t('join_library')}
|
||||||
|
icon={CloudArrowDown}
|
||||||
|
iconProps={{ weight: 'bold', size: 16 }}
|
||||||
|
onClick={() => dialogManager.create((dp) => <JoinDialog librariesCtx={libraries.data} {...dp} />)}
|
||||||
|
className="font-medium"
|
||||||
|
/>
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
label={t('manage_library')}
|
label={t('manage_library')}
|
||||||
icon={Gear}
|
icon={Gear}
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,8 +1,20 @@
|
||||||
import { auth, useLibraryContext, useLibraryMutation, useLibraryQuery } from '@sd/client';
|
import { CheckCircle, XCircle } from '@phosphor-icons/react';
|
||||||
import { Button } from '@sd/ui';
|
import {
|
||||||
|
CloudInstance,
|
||||||
|
CloudLibrary,
|
||||||
|
HardwareModel,
|
||||||
|
auth,
|
||||||
|
useLibraryContext,
|
||||||
|
useLibraryMutation,
|
||||||
|
useLibraryQuery
|
||||||
|
} from '@sd/client';
|
||||||
|
import { Button, Card, Loader, tw } from '@sd/ui';
|
||||||
|
import { Suspense, useMemo } from 'react';
|
||||||
|
import { Icon } from '~/components';
|
||||||
import { AuthRequiredOverlay } from '~/components/AuthRequiredOverlay';
|
import { AuthRequiredOverlay } from '~/components/AuthRequiredOverlay';
|
||||||
import { LoginButton } from '~/components/LoginButton';
|
import { LoginButton } from '~/components/LoginButton';
|
||||||
import { useRouteTitle } from '~/hooks';
|
import { useLocale, useRouteTitle } from '~/hooks';
|
||||||
|
import { hardwareModelToIcon } from '~/util/hardware';
|
||||||
|
|
||||||
export const Component = () => {
|
export const Component = () => {
|
||||||
useRouteTitle('Cloud');
|
useRouteTitle('Cloud');
|
||||||
|
@ -12,7 +24,14 @@ export const Component = () => {
|
||||||
const authSensitiveChild = () => {
|
const authSensitiveChild = () => {
|
||||||
if (authState.status === 'loggedIn') return <Authenticated />;
|
if (authState.status === 'loggedIn') return <Authenticated />;
|
||||||
if (authState.status === 'notLoggedIn' || authState.status === 'loggingIn')
|
if (authState.status === 'notLoggedIn' || authState.status === 'loggingIn')
|
||||||
return <LoginButton />;
|
return (
|
||||||
|
<div className="flex size-full items-center justify-center">
|
||||||
|
<Card className="flex flex-col gap-4 !p-6">
|
||||||
|
<p>To access cloud related features, please login</p>
|
||||||
|
<LoginButton />
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
@ -20,74 +39,192 @@ export const Component = () => {
|
||||||
return <div className="flex size-full flex-col items-start p-4">{authSensitiveChild()}</div>;
|
return <div className="flex size-full flex-col items-start p-4">{authSensitiveChild()}</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const DataBox = tw.div`max-w-[300px] rounded-md border border-app-line/50 bg-app-lightBox/20 p-2`;
|
||||||
|
const Count = tw.div`min-w-[20px] flex h-[20px] px-1 items-center justify-center rounded-full border border-app-button/40 text-[9px]`;
|
||||||
|
|
||||||
|
// million-ignore
|
||||||
function Authenticated() {
|
function Authenticated() {
|
||||||
const { library } = useLibraryContext();
|
const { library } = useLibraryContext();
|
||||||
|
|
||||||
const cloudLibrary = useLibraryQuery(['cloud.library.get'], { suspense: true, retry: false });
|
const cloudLibrary = useLibraryQuery(['cloud.library.get'], { suspense: true, retry: false });
|
||||||
|
|
||||||
const createLibrary = useLibraryMutation(['cloud.library.create']);
|
const createLibrary = useLibraryMutation(['cloud.library.create']);
|
||||||
const syncLibrary = useLibraryMutation(['cloud.library.sync']);
|
const { t } = useLocale();
|
||||||
|
|
||||||
const thisInstance = cloudLibrary.data?.instances.find(
|
const thisInstance = useMemo(() => {
|
||||||
(instance) => instance.uuid === library.instance_id
|
if (!cloudLibrary.data) return undefined;
|
||||||
);
|
return cloudLibrary.data.instances.find(
|
||||||
|
(instance) => instance.uuid === library.instance_id
|
||||||
|
);
|
||||||
|
}, [cloudLibrary.data, library.instance_id]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Suspense
|
||||||
|
fallback={
|
||||||
|
<div className="flex size-full items-center justify-center">
|
||||||
|
<Loader />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
{cloudLibrary.data ? (
|
{cloudLibrary.data ? (
|
||||||
<div className="flex flex-col items-start space-y-2">
|
<div className="flex flex-col items-start gap-10">
|
||||||
<div>
|
<Library thisInstance={thisInstance} cloudLibrary={cloudLibrary.data} />
|
||||||
<p>Library</p>
|
{thisInstance && <ThisInstance instance={thisInstance} />}
|
||||||
<p>Name: {cloudLibrary.data.name}</p>
|
<Instances instances={cloudLibrary.data.instances} />
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
disabled={syncLibrary.isLoading}
|
|
||||||
onClick={() => {
|
|
||||||
syncLibrary.mutateAsync(null);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Sync Library
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{thisInstance && (
|
|
||||||
<div>
|
|
||||||
<p>This Instance</p>
|
|
||||||
<p>Id: {thisInstance.id}</p>
|
|
||||||
<p>UUID: {thisInstance.uuid}</p>
|
|
||||||
<p>Public Key: {thisInstance.identity}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div>
|
|
||||||
<p>Instances</p>
|
|
||||||
<ul className="space-y-4 pl-4">
|
|
||||||
{cloudLibrary.data.instances
|
|
||||||
.filter((instance) => instance.uuid !== library.instance_id)
|
|
||||||
.map((instance) => (
|
|
||||||
<li key={instance.id}>
|
|
||||||
<p>Id: {instance.id}</p>
|
|
||||||
<p>UUID: {instance.uuid}</p>
|
|
||||||
<p>Public Key: {instance.identity}</p>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="relative">
|
<div className="relative flex size-full flex-col items-center justify-center">
|
||||||
<AuthRequiredOverlay />
|
<AuthRequiredOverlay />
|
||||||
<Button
|
<Button
|
||||||
disabled={createLibrary.isLoading}
|
disabled={createLibrary.isLoading}
|
||||||
|
variant="accent"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
createLibrary.mutateAsync(null);
|
createLibrary.mutateAsync(null);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{createLibrary.isLoading
|
{createLibrary.isLoading
|
||||||
? 'Connecting library to Spacedrive Cloud...'
|
? t('connecting_library_to_cloud')
|
||||||
: 'Connect library to Spacedrive Cloud'}
|
: t('connect_library_to_cloud')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</Suspense>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// million-ignore
|
||||||
|
const Instances = ({ instances }: { instances: CloudInstance[] }) => {
|
||||||
|
const { library } = useLibraryContext();
|
||||||
|
const filteredInstances = instances.filter((instance) => instance.uuid !== library.instance_id);
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<div className="flex flex-row items-center gap-3">
|
||||||
|
<p className="text-medium font-bold">Instances</p>
|
||||||
|
<Count>{filteredInstances.length}</Count>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row flex-wrap gap-2">
|
||||||
|
{filteredInstances.map((instance) => (
|
||||||
|
<Card
|
||||||
|
key={instance.id}
|
||||||
|
className="flex-col items-center gap-4 bg-app-box/50 !p-5"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col items-center gap-2">
|
||||||
|
<Icon
|
||||||
|
name={
|
||||||
|
hardwareModelToIcon(
|
||||||
|
instance.metadata.device_model as HardwareModel
|
||||||
|
) as any
|
||||||
|
}
|
||||||
|
size={70}
|
||||||
|
/>
|
||||||
|
<p className="max-w-[250px] truncate text-xs font-medium">
|
||||||
|
{instance.metadata.name}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<DataBox>
|
||||||
|
<p className="truncate text-xs font-medium">
|
||||||
|
Id:{' '}
|
||||||
|
<span className="font-normal text-ink-dull">{instance.id}</span>
|
||||||
|
</p>
|
||||||
|
</DataBox>
|
||||||
|
<DataBox>
|
||||||
|
<p className="truncate text-xs font-medium">
|
||||||
|
UUID:{' '}
|
||||||
|
<span className="font-normal text-ink-dull">
|
||||||
|
{instance.uuid}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</DataBox>
|
||||||
|
<DataBox>
|
||||||
|
<p className="truncate text-xs font-medium">
|
||||||
|
Public Key:{' '}
|
||||||
|
<span className="font-normal text-ink-dull">
|
||||||
|
{instance.identity}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</DataBox>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface LibraryProps {
|
||||||
|
cloudLibrary: CloudLibrary;
|
||||||
|
thisInstance: CloudInstance | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// million-ignore
|
||||||
|
const Library = ({ thisInstance, cloudLibrary }: LibraryProps) => {
|
||||||
|
const syncLibrary = useLibraryMutation(['cloud.library.sync']);
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<p className="text-medium font-bold">Library</p>
|
||||||
|
<Card className="flex-row items-center gap-6 !px-2">
|
||||||
|
<p className="font-medium">
|
||||||
|
Name: <span className="font-normal text-ink-dull">{cloudLibrary.name}</span>
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
disabled={syncLibrary.isLoading || thisInstance !== undefined}
|
||||||
|
variant={thisInstance === undefined ? 'accent' : 'gray'}
|
||||||
|
className="flex flex-row items-center gap-1 !text-ink"
|
||||||
|
onClick={() => syncLibrary.mutateAsync(null)}
|
||||||
|
>
|
||||||
|
{thisInstance === undefined ? (
|
||||||
|
<XCircle weight="fill" size={15} className="text-red-400" />
|
||||||
|
) : (
|
||||||
|
<CheckCircle weight="fill" size={15} className="text-green-400" />
|
||||||
|
)}
|
||||||
|
{thisInstance === undefined ? 'Sync Library' : 'Library synced'}
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ThisInstanceProps {
|
||||||
|
instance: CloudInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// million-ignore
|
||||||
|
const ThisInstance = ({ instance }: ThisInstanceProps) => {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<p className="text-medium font-bold">This Instance</p>
|
||||||
|
<Card className="flex-col items-center gap-4 bg-app-box/50 !p-5">
|
||||||
|
<div className="flex flex-col items-center gap-2">
|
||||||
|
<Icon
|
||||||
|
name={
|
||||||
|
hardwareModelToIcon(
|
||||||
|
instance.metadata.device_model as HardwareModel
|
||||||
|
) as any
|
||||||
|
}
|
||||||
|
size={70}
|
||||||
|
/>
|
||||||
|
<p className="max-w-[160px] truncate text-xs font-medium">
|
||||||
|
{instance.metadata.name}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<DataBox>
|
||||||
|
<p className="truncate text-xs font-medium">
|
||||||
|
Id: <span className="font-normal text-ink-dull">{instance.id}</span>
|
||||||
|
</p>
|
||||||
|
</DataBox>
|
||||||
|
<DataBox>
|
||||||
|
<p className="truncate text-xs font-medium">
|
||||||
|
UUID: <span className="font-normal text-ink-dull">{instance.uuid}</span>
|
||||||
|
</p>
|
||||||
|
</DataBox>
|
||||||
|
<DataBox>
|
||||||
|
<p className="truncate text-xs font-medium">
|
||||||
|
Public Key:{' '}
|
||||||
|
<span className="font-normal text-ink-dull">{instance.identity}</span>
|
||||||
|
</p>
|
||||||
|
</DataBox>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</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'
|
||||||
}
|
}
|
||||||
|
|
105
interface/app/$libraryId/settings/node/libraries/JoinDialog.tsx
Normal file
105
interface/app/$libraryId/settings/node/libraries/JoinDialog.tsx
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
import {
|
||||||
|
LibraryConfigWrapped,
|
||||||
|
useBridgeMutation,
|
||||||
|
useBridgeQuery,
|
||||||
|
useClientContext,
|
||||||
|
useLibraryContext,
|
||||||
|
usePlausibleEvent,
|
||||||
|
useZodForm
|
||||||
|
} from '@sd/client';
|
||||||
|
import { Button, Dialog, Select, SelectOption, toast, useDialog, UseDialogProps, z } from '@sd/ui';
|
||||||
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { useNavigate } from 'react-router';
|
||||||
|
import { useLocale } from '~/hooks';
|
||||||
|
import { usePlatform } from '~/util/Platform';
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
libraryId: z.string().refine((value) => value !== 'select_library', {
|
||||||
|
message: 'Please select a library'
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
export default (props: UseDialogProps & { librariesCtx: LibraryConfigWrapped[] | undefined }) => {
|
||||||
|
const cloudLibraries = useBridgeQuery(['cloud.library.list']);
|
||||||
|
const joinLibrary = useBridgeMutation(['cloud.library.join']);
|
||||||
|
|
||||||
|
const { t } = useLocale();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const platform = usePlatform();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const form = useZodForm({ schema, defaultValues: { libraryId: 'select_library' } });
|
||||||
|
|
||||||
|
// const queryClient = useQueryClient();
|
||||||
|
// const submitPlausibleEvent = usePlausibleEvent();
|
||||||
|
// const platform = usePlatform();
|
||||||
|
|
||||||
|
const onSubmit = form.handleSubmit(async (data) => {
|
||||||
|
try {
|
||||||
|
const library = await joinLibrary.mutateAsync(data.libraryId);
|
||||||
|
|
||||||
|
queryClient.setQueryData(['library.list'], (libraries: any) => {
|
||||||
|
// The invalidation system beat us to it
|
||||||
|
if ((libraries || []).find((l: any) => l.uuid === library.uuid)) return libraries;
|
||||||
|
|
||||||
|
return [...(libraries || []), library];
|
||||||
|
});
|
||||||
|
|
||||||
|
platform.refreshMenuBar && platform.refreshMenuBar();
|
||||||
|
|
||||||
|
navigate(`/${library.uuid}`, { replace: true });
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(e);
|
||||||
|
toast.error(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
form={form}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
dialog={useDialog(props)}
|
||||||
|
submitDisabled={!form.formState.isValid}
|
||||||
|
title={t('join_library')}
|
||||||
|
closeLabel={t('close')}
|
||||||
|
cancelLabel={t('cancel')}
|
||||||
|
description={t('join_library_description')}
|
||||||
|
ctaLabel={form.formState.isSubmitting ? t('joining') : t('join')}
|
||||||
|
>
|
||||||
|
<div className="mt-5 space-y-4">
|
||||||
|
{cloudLibraries.isLoading && <span>{t('loading')}...</span>}
|
||||||
|
{cloudLibraries.data && (
|
||||||
|
<Select
|
||||||
|
value={form.watch('libraryId')}
|
||||||
|
size="sm"
|
||||||
|
className="w-full"
|
||||||
|
onChange={(key) => {
|
||||||
|
console.log('Key:', key);
|
||||||
|
// Update the form value
|
||||||
|
form.setValue('libraryId', key, {
|
||||||
|
shouldValidate: true
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectOption value="select_library">
|
||||||
|
{t('select_library')}
|
||||||
|
</SelectOption>
|
||||||
|
{cloudLibraries.data
|
||||||
|
.filter(
|
||||||
|
(cloudLibrary) =>
|
||||||
|
!props.librariesCtx?.find(
|
||||||
|
(l: any) => l.uuid === cloudLibrary.uuid
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.map((cloudLibrary) => (
|
||||||
|
<SelectOption key={cloudLibrary.uuid} value={cloudLibrary.uuid}>
|
||||||
|
{cloudLibrary.name}
|
||||||
|
</SelectOption>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,16 +1,21 @@
|
||||||
import { useBridgeQuery, useLibraryContext } from '@sd/client';
|
import { useBridgeQuery, useClientContext, useFeatureFlag, useLibraryContext } from '@sd/client';
|
||||||
import { Button, dialogManager } from '@sd/ui';
|
import { Button, dialogManager } from '@sd/ui';
|
||||||
import { useLocale } from '~/hooks';
|
import { useLocale } from '~/hooks';
|
||||||
|
|
||||||
import { Heading } from '../../Layout';
|
import { Heading } from '../../Layout';
|
||||||
import CreateDialog from './CreateDialog';
|
import CreateDialog from './CreateDialog';
|
||||||
|
import JoinDialog from './JoinDialog';
|
||||||
import ListItem from './ListItem';
|
import ListItem from './ListItem';
|
||||||
|
|
||||||
export const Component = () => {
|
export const Component = () => {
|
||||||
const librariesQuery = useBridgeQuery(['library.list']);
|
const librariesQuery = useBridgeQuery(['library.list']);
|
||||||
const libraries = librariesQuery.data;
|
const libraries = librariesQuery.data;
|
||||||
|
|
||||||
|
const cloudEnabled = useFeatureFlag('cloudSync');
|
||||||
|
|
||||||
const { library } = useLibraryContext();
|
const { library } = useLibraryContext();
|
||||||
|
const { libraries: librariesCtx } = useClientContext();
|
||||||
|
const librariesCtxData = librariesCtx.data;
|
||||||
|
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|
||||||
|
@ -30,10 +35,22 @@ export const Component = () => {
|
||||||
>
|
>
|
||||||
{t('add_library')}
|
{t('add_library')}
|
||||||
</Button>
|
</Button>
|
||||||
|
{cloudEnabled && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
dialogManager.create((dp) => (
|
||||||
|
<JoinDialog librariesCtx={librariesCtxData} {...dp} />
|
||||||
|
));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('join_library')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{libraries
|
{libraries
|
||||||
?.sort((a, b) => {
|
?.sort((a, b) => {
|
||||||
|
|
|
@ -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', '[']
|
||||||
|
|
|
@ -89,7 +89,9 @@
|
||||||
"connect_cloud_description": "Connect your cloud accounts to Spacedrive.",
|
"connect_cloud_description": "Connect your cloud accounts to Spacedrive.",
|
||||||
"connect_device": "Connect a device",
|
"connect_device": "Connect a device",
|
||||||
"connect_device_description": "Spacedrive works best on all your devices.",
|
"connect_device_description": "Spacedrive works best on all your devices.",
|
||||||
|
"connect_library_to_cloud": "قم بتوصيل المكتبة بـ Spacedrive Cloud",
|
||||||
"connected": "متصل",
|
"connected": "متصل",
|
||||||
|
"connecting_library_to_cloud": "جارٍ توصيل المكتبة بـ Spacedrive Cloud...",
|
||||||
"contacts": "جهات الاتصال",
|
"contacts": "جهات الاتصال",
|
||||||
"contacts_description": "إدارة جهات الاتصال الخاصة بك في Spacedrive.",
|
"contacts_description": "إدارة جهات الاتصال الخاصة بك في Spacedrive.",
|
||||||
"contains": "contains",
|
"contains": "contains",
|
||||||
|
@ -258,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": "فلاش",
|
||||||
|
|
|
@ -89,7 +89,9 @@
|
||||||
"connect_cloud_description": "Падключыце воблачныя акаўнты да Spacedrive.",
|
"connect_cloud_description": "Падключыце воблачныя акаўнты да Spacedrive.",
|
||||||
"connect_device": "Падключыце прыладу",
|
"connect_device": "Падключыце прыладу",
|
||||||
"connect_device_description": "Spacedrive лепш за ўсё працуе пры выкарыстанні на ўсіх вашых прыладах.",
|
"connect_device_description": "Spacedrive лепш за ўсё працуе пры выкарыстанні на ўсіх вашых прыладах.",
|
||||||
|
"connect_library_to_cloud": "Падключыце бібліятэку да Spacedrive Cloud",
|
||||||
"connected": "Падключана",
|
"connected": "Падключана",
|
||||||
|
"connecting_library_to_cloud": "Падключэнне бібліятэкі да Spacedrive Cloud...",
|
||||||
"contacts": "Кантакты",
|
"contacts": "Кантакты",
|
||||||
"contacts_description": "Кіруйце кантактамі ў Spacedrive.",
|
"contacts_description": "Кіруйце кантактамі ў Spacedrive.",
|
||||||
"contains": "змяшчае",
|
"contains": "змяшчае",
|
||||||
|
@ -258,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": "Успышка",
|
||||||
|
|
|
@ -89,7 +89,9 @@
|
||||||
"connect_cloud_description": "Verbinde deine Cloud-Konten mit Spacedrive.",
|
"connect_cloud_description": "Verbinde deine Cloud-Konten mit Spacedrive.",
|
||||||
"connect_device": "Ein Gerät anschließen",
|
"connect_device": "Ein Gerät anschließen",
|
||||||
"connect_device_description": "Spacedrive funktioniert am besten auf all deinen Geräten.",
|
"connect_device_description": "Spacedrive funktioniert am besten auf all deinen Geräten.",
|
||||||
|
"connect_library_to_cloud": "Bibliothek mit Spacedrive Cloud verbinden",
|
||||||
"connected": "Verbunden",
|
"connected": "Verbunden",
|
||||||
|
"connecting_library_to_cloud": "Bibliothek mit Spacedrive Cloud verbinden …",
|
||||||
"contacts": "Kontakte",
|
"contacts": "Kontakte",
|
||||||
"contacts_description": "Verwalte deine Kontakte in Spacedrive.",
|
"contacts_description": "Verwalte deine Kontakte in Spacedrive.",
|
||||||
"contains": "enthält",
|
"contains": "enthält",
|
||||||
|
@ -258,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",
|
||||||
|
|
|
@ -90,7 +90,9 @@
|
||||||
"connect_cloud_description": "Connect your cloud accounts to Spacedrive.",
|
"connect_cloud_description": "Connect your cloud accounts to Spacedrive.",
|
||||||
"connect_device": "Connect a device",
|
"connect_device": "Connect a device",
|
||||||
"connect_device_description": "Spacedrive works best on all your devices.",
|
"connect_device_description": "Spacedrive works best on all your devices.",
|
||||||
|
"connect_library_to_cloud": "Connect library to Spacedrive Cloud",
|
||||||
"connected": "Connected",
|
"connected": "Connected",
|
||||||
|
"connecting_library_to_cloud": "Connecting library to Spacedrive Cloud...",
|
||||||
"contacts": "Contacts",
|
"contacts": "Contacts",
|
||||||
"contacts_description": "Manage your contacts in Spacedrive.",
|
"contacts_description": "Manage your contacts in Spacedrive.",
|
||||||
"contains": "contains",
|
"contains": "contains",
|
||||||
|
@ -214,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",
|
||||||
|
@ -260,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",
|
||||||
|
@ -356,6 +362,7 @@
|
||||||
"join_discord": "Join Discord",
|
"join_discord": "Join Discord",
|
||||||
"join_library": "Join a Library",
|
"join_library": "Join a Library",
|
||||||
"join_library_description": "Libraries are a secure, on-device database. Your files remain where they are, the Library catalogs them and stores all Spacedrive related data.",
|
"join_library_description": "Libraries are a secure, on-device database. Your files remain where they are, the Library catalogs them and stores all Spacedrive related data.",
|
||||||
|
"joining": "Joining",
|
||||||
"key": "Key",
|
"key": "Key",
|
||||||
"key_manager": "Key Manager",
|
"key_manager": "Key Manager",
|
||||||
"key_manager_description": "Create encryption keys, mount and unmount your keys to see files decrypted on the fly.",
|
"key_manager_description": "Create encryption keys, mount and unmount your keys to see files decrypted on the fly.",
|
||||||
|
@ -597,6 +604,7 @@
|
||||||
"security_description": "Keep your client safe.",
|
"security_description": "Keep your client safe.",
|
||||||
"see_less": "See less",
|
"see_less": "See less",
|
||||||
"see_more": "See more",
|
"see_more": "See more",
|
||||||
|
"select_library": "Select a Cloud Library",
|
||||||
"send": "Send",
|
"send": "Send",
|
||||||
"send_report": "Send Report",
|
"send_report": "Send Report",
|
||||||
"sender": "Sender",
|
"sender": "Sender",
|
||||||
|
@ -666,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",
|
||||||
|
|
|
@ -89,7 +89,9 @@
|
||||||
"connect_cloud_description": "Conecta tus cuentas en la nube a Spacedrive.",
|
"connect_cloud_description": "Conecta tus cuentas en la nube a Spacedrive.",
|
||||||
"connect_device": "Conectar un dispositivo",
|
"connect_device": "Conectar un dispositivo",
|
||||||
"connect_device_description": "Spacedrive funciona mejor en todos tus dispositivos.",
|
"connect_device_description": "Spacedrive funciona mejor en todos tus dispositivos.",
|
||||||
|
"connect_library_to_cloud": "Conecte la biblioteca a Spacedrive Cloud",
|
||||||
"connected": "Conectado",
|
"connected": "Conectado",
|
||||||
|
"connecting_library_to_cloud": "Conectando la biblioteca a Spacedrive Cloud...",
|
||||||
"contacts": "Contactos",
|
"contacts": "Contactos",
|
||||||
"contacts_description": "Administra tus contactos en Spacedrive.",
|
"contacts_description": "Administra tus contactos en Spacedrive.",
|
||||||
"contains": "contiene",
|
"contains": "contiene",
|
||||||
|
@ -258,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",
|
||||||
|
|
|
@ -89,7 +89,9 @@
|
||||||
"connect_cloud_description": "Connectez vos comptes cloud à Spacedrive.",
|
"connect_cloud_description": "Connectez vos comptes cloud à Spacedrive.",
|
||||||
"connect_device": "Connecter un appareil",
|
"connect_device": "Connecter un appareil",
|
||||||
"connect_device_description": "Spacedrive fonctionne mieux sur tous vos appareils.",
|
"connect_device_description": "Spacedrive fonctionne mieux sur tous vos appareils.",
|
||||||
|
"connect_library_to_cloud": "Connecter la bibliothèque à Spacedrive Cloud",
|
||||||
"connected": "Connecté",
|
"connected": "Connecté",
|
||||||
|
"connecting_library_to_cloud": "Connexion de la bibliothèque à Spacedrive Cloud...",
|
||||||
"contacts": "Contacts",
|
"contacts": "Contacts",
|
||||||
"contacts_description": "Gérez vos contacts dans Spacedrive.",
|
"contacts_description": "Gérez vos contacts dans Spacedrive.",
|
||||||
"contains": "contient",
|
"contains": "contient",
|
||||||
|
@ -258,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",
|
||||||
|
|
|
@ -89,7 +89,9 @@
|
||||||
"connect_cloud_description": "Collegate i vostri account cloud a Spacedrive.",
|
"connect_cloud_description": "Collegate i vostri account cloud a Spacedrive.",
|
||||||
"connect_device": "Collegare un dispositivo",
|
"connect_device": "Collegare un dispositivo",
|
||||||
"connect_device_description": "Spacedrive funziona al meglio su tutti i dispositivi.",
|
"connect_device_description": "Spacedrive funziona al meglio su tutti i dispositivi.",
|
||||||
|
"connect_library_to_cloud": "Connetti la libreria a Spacedrive Cloud",
|
||||||
"connected": "Connesso",
|
"connected": "Connesso",
|
||||||
|
"connecting_library_to_cloud": "Collegamento della libreria a Spacedrive Cloud...",
|
||||||
"contacts": "Contatti",
|
"contacts": "Contatti",
|
||||||
"contacts_description": "Gestisci i tuoi contatti su Spacedrive.",
|
"contacts_description": "Gestisci i tuoi contatti su Spacedrive.",
|
||||||
"contains": "contiene",
|
"contains": "contiene",
|
||||||
|
@ -258,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",
|
||||||
|
|
|
@ -89,7 +89,9 @@
|
||||||
"connect_cloud_description": "クラウドアカウントをSpacedriveに接続する。",
|
"connect_cloud_description": "クラウドアカウントをSpacedriveに接続する。",
|
||||||
"connect_device": "デバイスを接続する",
|
"connect_device": "デバイスを接続する",
|
||||||
"connect_device_description": "Spacedriveはすべてのデバイスで最適に機能します。",
|
"connect_device_description": "Spacedriveはすべてのデバイスで最適に機能します。",
|
||||||
|
"connect_library_to_cloud": "ライブラリをSpacedrive Cloudに接続する",
|
||||||
"connected": "接続中",
|
"connected": "接続中",
|
||||||
|
"connecting_library_to_cloud": "ライブラリを Spacedrive Cloud に接続しています...",
|
||||||
"contacts": "連絡先",
|
"contacts": "連絡先",
|
||||||
"contacts_description": "Spacedriveで連絡先を管理。",
|
"contacts_description": "Spacedriveで連絡先を管理。",
|
||||||
"contains": "が次を含む",
|
"contains": "が次を含む",
|
||||||
|
@ -258,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": "閃光",
|
||||||
|
|
|
@ -89,7 +89,9 @@
|
||||||
"connect_cloud_description": "Verbind uw cloudaccounts met Spacedrive.",
|
"connect_cloud_description": "Verbind uw cloudaccounts met Spacedrive.",
|
||||||
"connect_device": "Een apparaat aansluiten",
|
"connect_device": "Een apparaat aansluiten",
|
||||||
"connect_device_description": "Spacedrive werkt het beste op al uw apparaten.",
|
"connect_device_description": "Spacedrive werkt het beste op al uw apparaten.",
|
||||||
|
"connect_library_to_cloud": "Verbind de bibliotheek met Spacedrive Cloud",
|
||||||
"connected": "Verbonden",
|
"connected": "Verbonden",
|
||||||
|
"connecting_library_to_cloud": "Bibliotheek verbinden met Spacedrive Cloud...",
|
||||||
"contacts": "Contacten",
|
"contacts": "Contacten",
|
||||||
"contacts_description": "Beheer je contacten in Spacedrive.",
|
"contacts_description": "Beheer je contacten in Spacedrive.",
|
||||||
"contains": "bevatten",
|
"contains": "bevatten",
|
||||||
|
@ -258,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",
|
||||||
|
|
|
@ -89,7 +89,9 @@
|
||||||
"connect_cloud_description": "Подключите облачные аккаунты к Spacedrive.",
|
"connect_cloud_description": "Подключите облачные аккаунты к Spacedrive.",
|
||||||
"connect_device": "Подключите устройство",
|
"connect_device": "Подключите устройство",
|
||||||
"connect_device_description": "Spacedrive лучше всего работает при использовании на всех ваших устройствах.",
|
"connect_device_description": "Spacedrive лучше всего работает при использовании на всех ваших устройствах.",
|
||||||
|
"connect_library_to_cloud": "Подключите библиотеку к Spacedrive Cloud",
|
||||||
"connected": "Подключено",
|
"connected": "Подключено",
|
||||||
|
"connecting_library_to_cloud": "Подключение библиотеки к Spacedrive Cloud...",
|
||||||
"contacts": "Контакты",
|
"contacts": "Контакты",
|
||||||
"contacts_description": "Управляйте контактами в Spacedrive.",
|
"contacts_description": "Управляйте контактами в Spacedrive.",
|
||||||
"contains": "содержит",
|
"contains": "содержит",
|
||||||
|
@ -258,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": "Вспышка",
|
||||||
|
|
|
@ -89,7 +89,9 @@
|
||||||
"connect_cloud_description": "Bulut hesaplarınızı Spacedrive'a bağlayın.",
|
"connect_cloud_description": "Bulut hesaplarınızı Spacedrive'a bağlayın.",
|
||||||
"connect_device": "Bir cihaz bağlayın",
|
"connect_device": "Bir cihaz bağlayın",
|
||||||
"connect_device_description": "Spacedrive tüm cihazlarınızda en iyi şekilde çalışır.",
|
"connect_device_description": "Spacedrive tüm cihazlarınızda en iyi şekilde çalışır.",
|
||||||
|
"connect_library_to_cloud": "Kitaplığı Spacedrive Cloud'a bağlayın",
|
||||||
"connected": "Bağlı",
|
"connected": "Bağlı",
|
||||||
|
"connecting_library_to_cloud": "Kitaplık Spacedrive Cloud'a bağlanıyor...",
|
||||||
"contacts": "Kişiler",
|
"contacts": "Kişiler",
|
||||||
"contacts_description": "Kişilerinizi Spacedrive'da yönetin.",
|
"contacts_description": "Kişilerinizi Spacedrive'da yönetin.",
|
||||||
"contains": "içerir",
|
"contains": "içerir",
|
||||||
|
@ -258,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ş",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"about": "关于",
|
"about": "关于",
|
||||||
"about_vision_text": "我们很多人拥有不止云账户,磁盘没有备份,数据也有丢失的风险。我们依赖于像 Google 照片、iCloud 这样的云服务,但是它们容量有限,且互操作性几乎为零,云服务和操作系统之间也无法协作。我们的照片不应该困在一种生态系统中,也不应该当广告数据来收割。它们应该与操作系统无关、永久保存、由我们自己所有。我们创造的数据是我们的遗产,它们的寿命会比我们还要长——既然这些数据定义了我们的生活,开源技术是确保我们对这些数据拥有绝对控制权的唯一方式,它的规模没有限制。",
|
"about_vision_text": "我们很多人拥有不止一个云账户,磁盘没有备份,数据也有丢失的风险。我们依赖于像 Google 相册、iCloud 这样的云服务,但是它们容量有限,且互操作性几乎为零,云服务和操作系统之间也无法协作。我们的照片不应该困在单一一种生态系统中,也不应该被用于广告营销而被收割。它们应该与操作系统无关、永久保存、由我们自己所有。我们创造的数据是我们的遗产,它们的寿命会比我们还要长。开源技术是唯一能确保我们绝对控制定义我们生活的数据,并在无限规模上保留这些定义了我们的生活的数据的方式。",
|
||||||
"about_vision_title": "项目远景",
|
"about_vision_title": "项目远景",
|
||||||
"accept": "接受",
|
"accept": "接受",
|
||||||
"accept_files": "Accept files",
|
"accept_files": "Accept files",
|
||||||
|
@ -18,16 +18,16 @@
|
||||||
"add_location_tooltip": "将路径添加为索引",
|
"add_location_tooltip": "将路径添加为索引",
|
||||||
"add_locations": "添加位置",
|
"add_locations": "添加位置",
|
||||||
"add_tag": "添加标签",
|
"add_tag": "添加标签",
|
||||||
"added_location": "Added Location {{name}}",
|
"added_location": "已添加位置 {{name}}",
|
||||||
"adding_location": "Adding Location {{name}}",
|
"adding_location": "添加位置 {{name}}",
|
||||||
"advanced": "先进的",
|
"advanced": "高级",
|
||||||
"advanced_settings": "高级设置",
|
"advanced_settings": "高级设置",
|
||||||
"album": "Album",
|
"album": "相册",
|
||||||
"alias": "Alias",
|
"alias": "别名",
|
||||||
"all_jobs_have_been_cleared": "所有任务已清除。",
|
"all_jobs_have_been_cleared": "所有任务已清除。",
|
||||||
"alpha_release_description": "感谢试用 Spacedrive。现在 Spacedrive 处于 Alpha 发布阶段,展示了激动人心的新功能。作为初始版本,它可能包含一些错误。我们恳请您在我们的 Discord 频道上反馈遇到的任何问题,您宝贵的反馈将有助于极大增强用户体验。",
|
"alpha_release_description": "感谢试用 Spacedrive。现在 Spacedrive 处于 Alpha 发布阶段,展示了激动人心的新功能。作为初始版本,它可能包含一些错误。我们恳请您在我们的 Discord 频道上反馈遇到的任何问题,您宝贵的反馈将有助于极大增强用户体验。",
|
||||||
"alpha_release_title": "Alpha 版本",
|
"alpha_release_title": "Alpha 版本",
|
||||||
"app_crashed": "APP CRASHED",
|
"app_crashed": "应用程序崩溃了",
|
||||||
"app_crashed_description": "We're past the event horizon...",
|
"app_crashed_description": "We're past the event horizon...",
|
||||||
"appearance": "外观",
|
"appearance": "外观",
|
||||||
"appearance_description": "调整客户端的外观。",
|
"appearance_description": "调整客户端的外观。",
|
||||||
|
@ -39,9 +39,9 @@
|
||||||
"ascending": "上升",
|
"ascending": "上升",
|
||||||
"ask_spacedrive": "询问 Spacedrive",
|
"ask_spacedrive": "询问 Spacedrive",
|
||||||
"assign_tag": "分配标签",
|
"assign_tag": "分配标签",
|
||||||
"audio": "Audio",
|
"audio": "音频",
|
||||||
"audio_preview_not_supported": "不支持音频预览。",
|
"audio_preview_not_supported": "不支持音频预览。",
|
||||||
"auto": "汽车",
|
"auto": "自动",
|
||||||
"back": "返回",
|
"back": "返回",
|
||||||
"backfill_sync": "回填同步操作",
|
"backfill_sync": "回填同步操作",
|
||||||
"backfill_sync_description": "库暂停直至回填完成",
|
"backfill_sync_description": "库暂停直至回填完成",
|
||||||
|
@ -50,7 +50,7 @@
|
||||||
"bitrate": "比特率",
|
"bitrate": "比特率",
|
||||||
"blur_effects": "模糊效果",
|
"blur_effects": "模糊效果",
|
||||||
"blur_effects_description": "某些组件将应用模糊效果。",
|
"blur_effects_description": "某些组件将应用模糊效果。",
|
||||||
"book": "book",
|
"book": "书籍",
|
||||||
"cancel": "取消",
|
"cancel": "取消",
|
||||||
"cancel_selection": "取消选择",
|
"cancel_selection": "取消选择",
|
||||||
"canceled": "取消",
|
"canceled": "取消",
|
||||||
|
@ -82,14 +82,16 @@
|
||||||
"completed": "完全的",
|
"completed": "完全的",
|
||||||
"completed_with_errors": "已完成但有错误",
|
"completed_with_errors": "已完成但有错误",
|
||||||
"compress": "压缩",
|
"compress": "压缩",
|
||||||
"config": "Config",
|
"config": "配置文件",
|
||||||
"configure_location": "配置位置",
|
"configure_location": "配置文件位置",
|
||||||
"confirm": "Confirm",
|
"confirm": "确认",
|
||||||
"connect_cloud": "连接云",
|
"connect_cloud": "连接云",
|
||||||
"connect_cloud_description": "将您的云帐户连接到 Spacedrive。",
|
"connect_cloud_description": "将您的云帐户连接到 Spacedrive。",
|
||||||
"connect_device": "连接设备",
|
"connect_device": "连接设备",
|
||||||
"connect_device_description": "Spacedrive 在您的所有设备上都能发挥最佳效果。",
|
"connect_device_description": "Spacedrive 在您的所有设备上都能发挥最佳效果。",
|
||||||
|
"connect_library_to_cloud": "将图书馆连接到 Spacedrive Cloud",
|
||||||
"connected": "已连接",
|
"connected": "已连接",
|
||||||
|
"connecting_library_to_cloud": "将图书馆连接到 Spacedrive Cloud...",
|
||||||
"contacts": "联系人",
|
"contacts": "联系人",
|
||||||
"contacts_description": "在 Spacedrive 中管理您的联系人。",
|
"contacts_description": "在 Spacedrive 中管理您的联系人。",
|
||||||
"contains": "包含",
|
"contains": "包含",
|
||||||
|
@ -126,7 +128,7 @@
|
||||||
"cut": "剪切",
|
"cut": "剪切",
|
||||||
"cut_object": "剪切对象",
|
"cut_object": "剪切对象",
|
||||||
"cut_success": "剪切项目",
|
"cut_success": "剪切项目",
|
||||||
"dark": "Dark",
|
"dark": "暗色",
|
||||||
"data_folder": "数据文件夹",
|
"data_folder": "数据文件夹",
|
||||||
"database": "Database",
|
"database": "Database",
|
||||||
"date": "日期",
|
"date": "日期",
|
||||||
|
@ -151,10 +153,10 @@
|
||||||
"delete_location_description": "删除位置时,Spacedrive 会从数据库中移除所有与之相关的文件,但是不会删除文件本身。",
|
"delete_location_description": "删除位置时,Spacedrive 会从数据库中移除所有与之相关的文件,但是不会删除文件本身。",
|
||||||
"delete_object": "删除对象",
|
"delete_object": "删除对象",
|
||||||
"delete_rule": "删除规则",
|
"delete_rule": "删除规则",
|
||||||
"delete_rule_confirmation": "Are you sure you want to delete this rule?",
|
"delete_rule_confirmation": "您确定要删除这个规则吗?",
|
||||||
"delete_tag": "删除标签",
|
"delete_tag": "删除标签",
|
||||||
"delete_tag_description": "您确定要删除这个标签吗?此操作不能撤销,打过标签的文件将会取消标签。",
|
"delete_tag_description": "您确定要删除这个标签吗?此操作不能撤销,打过标签的文件将会丢失标签。",
|
||||||
"delete_warning": "警告:这将永久删除您的{{type}},我们目前还没有回收站…",
|
"delete_warning": "警告:这将永久删除您的{{type}},我们目前还没有回收站……",
|
||||||
"descending": "降序",
|
"descending": "降序",
|
||||||
"description": "描述",
|
"description": "描述",
|
||||||
"deselect": "取消选择",
|
"deselect": "取消选择",
|
||||||
|
@ -199,10 +201,10 @@
|
||||||
"enabled": "启用",
|
"enabled": "启用",
|
||||||
"encrypt": "加密",
|
"encrypt": "加密",
|
||||||
"encrypt_library": "加密库",
|
"encrypt_library": "加密库",
|
||||||
"encrypt_library_coming_soon": "库加密即将推出",
|
"encrypt_library_coming_soon": "资料库加密即将推出",
|
||||||
"encrypt_library_description": "为这个库启用加密,这只会加密Spacedrive数据库,不会加密文件本身。",
|
"encrypt_library_description": "为这个库启用加密,这只会加密Spacedrive数据库,不会加密文件本身。",
|
||||||
"encrypted": "Encrypted",
|
"encrypted": "已加密",
|
||||||
"ends_with": "以。。结束",
|
"ends_with": "以... 结束",
|
||||||
"ephemeral_notice_browse": "直接从您的设备浏览您的文件和文件夹。",
|
"ephemeral_notice_browse": "直接从您的设备浏览您的文件和文件夹。",
|
||||||
"ephemeral_notice_consider_indexing": "考虑索引您本地的位置,以获得更快和更高效的浏览。",
|
"ephemeral_notice_consider_indexing": "考虑索引您本地的位置,以获得更快和更高效的浏览。",
|
||||||
"equals": "是",
|
"equals": "是",
|
||||||
|
@ -258,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": "闪光",
|
||||||
|
@ -333,30 +337,30 @@
|
||||||
"invalid_glob": "无效的全局变量",
|
"invalid_glob": "无效的全局变量",
|
||||||
"invalid_name": "名称无效",
|
"invalid_name": "名称无效",
|
||||||
"invalid_path": "路径无效",
|
"invalid_path": "路径无效",
|
||||||
"ipv4_ipv6_listeners_error": "Error creating the IPv4 and IPv6 listeners. Please check your firewall settings!",
|
"ipv4_ipv6_listeners_error": "创建 IPv4 和 IPv6 监听器出错。请检查防火墙设置!",
|
||||||
"ipv4_listeners_error": "Error creating the IPv4 listeners. Please check your firewall settings!",
|
"ipv4_listeners_error": "创建 IPv4 监听器出错。请检查防火墙设置!",
|
||||||
"ipv6": "IPv6网络",
|
"ipv6": "IPv6网络",
|
||||||
"ipv6_description": "允许使用 IPv6 网络进行点对点通信",
|
"ipv6_description": "允许使用 IPv6 网络进行点对点通信",
|
||||||
"ipv6_listeners_error": "Error creating the IPv6 listeners. Please check your firewall settings!",
|
"ipv6_listeners_error": "创建 IPv6 监听器时出错。请检查防火墙设置!",
|
||||||
"is": "是",
|
"is": "是",
|
||||||
"is_not": "不是",
|
"is_not": "不是",
|
||||||
"item": "item",
|
"item": "item",
|
||||||
"item_size": "项目大小",
|
"item_size": "项目大小",
|
||||||
"items": "items",
|
"items": "项目",
|
||||||
"job_error_description": "作业已完成,但有错误。\n请参阅下面的错误日志以获取更多信息。\n如果您需要帮助,请联系支持人员并提供此错误。",
|
"job_error_description": "作业已完成,但有错误。\n请参阅下面的错误日志以获取更多信息。\n如果您需要帮助,请联系支持人员并提供此错误。",
|
||||||
"job_has_been_canceled": "作业已取消。",
|
"job_has_been_canceled": "作业已取消。",
|
||||||
"job_has_been_paused": "作业已暂停。",
|
"job_has_been_paused": "作业已暂停。",
|
||||||
"job_has_been_removed": "作业已移除。",
|
"job_has_been_removed": "作业已移除。",
|
||||||
"job_has_been_resumed": "作业已恢复。",
|
"job_has_been_resumed": "作业已恢复。",
|
||||||
"join": "加入",
|
"join": "加入",
|
||||||
"join_discord": "加入Discord",
|
"join_discord": "加入 Discord",
|
||||||
"join_library": "加入一个库",
|
"join_library": "加入一个资料库",
|
||||||
"join_library_description": "库是一个安全的,设备上的数据库。您的文件保持原位,库对其进行目录编制并存储所有Spacedrive相关数据。",
|
"join_library_description": "库是一个安全的,设备上的数据库。您的文件保持原位,库对其进行目录编制并存储所有Spacedrive相关数据。",
|
||||||
"key": "键位",
|
"key": "键位",
|
||||||
"key_manager": "密钥管理器",
|
"key_manager": "密钥管理器",
|
||||||
"key_manager_description": "创建加密密钥,挂载和卸载密钥以即时查看解密文件。",
|
"key_manager_description": "创建加密密钥,挂载和卸载密钥以即时查看解密文件。",
|
||||||
"keybinds": "快捷键",
|
"keybinds": "快捷键",
|
||||||
"keybinds_description": "查看和管理客户端键绑定",
|
"keybinds_description": "查看和管理客户端快捷键",
|
||||||
"keys": "密钥",
|
"keys": "密钥",
|
||||||
"kilometers": "千米",
|
"kilometers": "千米",
|
||||||
"kind": "种类",
|
"kind": "种类",
|
||||||
|
@ -379,8 +383,8 @@
|
||||||
"library_overview": "库概览",
|
"library_overview": "库概览",
|
||||||
"library_settings": "库设置",
|
"library_settings": "库设置",
|
||||||
"library_settings_description": "与当前活动库相关的一般设置。",
|
"library_settings_description": "与当前活动库相关的一般设置。",
|
||||||
"light": "光",
|
"light": "浅色",
|
||||||
"link": "Link",
|
"link": "链接",
|
||||||
"list_view": "列表视图",
|
"list_view": "列表视图",
|
||||||
"list_view_notice_description": "通过列表视图轻松导航您的文件和文件夹。这种视图以简单、有组织的列表形式显示文件,让您能够快速定位和访问所需文件。",
|
"list_view_notice_description": "通过列表视图轻松导航您的文件和文件夹。这种视图以简单、有组织的列表形式显示文件,让您能够快速定位和访问所需文件。",
|
||||||
"loading": "正在加载",
|
"loading": "正在加载",
|
||||||
|
@ -451,7 +455,7 @@
|
||||||
"network_settings_advanced_description": "有关当前网络设置的高级信息。",
|
"network_settings_advanced_description": "有关当前网络设置的高级信息。",
|
||||||
"network_settings_description": "与网络和连接相关的设置。",
|
"network_settings_description": "与网络和连接相关的设置。",
|
||||||
"networking": "网络",
|
"networking": "网络",
|
||||||
"networking_error": "Error starting up networking!",
|
"networking_error": "网络启动错误!",
|
||||||
"networking_port": "网络端口",
|
"networking_port": "网络端口",
|
||||||
"networking_port_description": "Spacedrive 点对点网络通信使用的端口。除非您有防火墙来限制,否则应保持此项禁用。不要在互联网上暴露自己!",
|
"networking_port_description": "Spacedrive 点对点网络通信使用的端口。除非您有防火墙来限制,否则应保持此项禁用。不要在互联网上暴露自己!",
|
||||||
"new": "新的",
|
"new": "新的",
|
||||||
|
@ -462,7 +466,7 @@
|
||||||
"new_tab": "新标签",
|
"new_tab": "新标签",
|
||||||
"new_tag": "新标签",
|
"new_tag": "新标签",
|
||||||
"new_update_available": "新版本可用!",
|
"new_update_available": "新版本可用!",
|
||||||
"no_apps_available": "No apps available",
|
"no_apps_available": "没有可用的应用程序",
|
||||||
"no_favorite_items": "没有最喜欢的物品",
|
"no_favorite_items": "没有最喜欢的物品",
|
||||||
"no_git_files": "没有 Git 文件",
|
"no_git_files": "没有 Git 文件",
|
||||||
"no_hidden_files": "没有隐藏文件",
|
"no_hidden_files": "没有隐藏文件",
|
||||||
|
@ -494,21 +498,21 @@
|
||||||
"open": "打开",
|
"open": "打开",
|
||||||
"open_file": "打开文件",
|
"open_file": "打开文件",
|
||||||
"open_in_new_tab": "在新标签页中打开",
|
"open_in_new_tab": "在新标签页中打开",
|
||||||
"open_logs": "Open Logs",
|
"open_logs": "查看日志",
|
||||||
"open_new_location_once_added": "添加新位置后立即打开",
|
"open_new_location_once_added": "添加新位置后立即打开",
|
||||||
"open_new_tab": "打开新标签页",
|
"open_new_tab": "打开新标签页",
|
||||||
"open_object": "打开对象",
|
"open_object": "打开对象",
|
||||||
"open_object_from_quick_preview_in_native_file_manager": "在本机文件管理器中从快速预览中打开对象",
|
"open_object_from_quick_preview_in_native_file_manager": "在本机文件管理器中从快速预览中打开对象",
|
||||||
"open_settings": "打开设置",
|
"open_settings": "打开设置",
|
||||||
"open_with": "打开方式",
|
"open_with": "打开方式",
|
||||||
"opening_trash": "Opening Trash",
|
"opening_trash": "打开回收站",
|
||||||
"or": "或",
|
"or": "或",
|
||||||
"overview": "概览",
|
"overview": "概览",
|
||||||
"p2p_visibility": "P2P 可见性",
|
"p2p_visibility": "P2P 可见性",
|
||||||
"p2p_visibility_contacts_only": "仅限联系人",
|
"p2p_visibility_contacts_only": "仅限联系人",
|
||||||
"p2p_visibility_description": "配置谁可以看到您的 Spacedrive 安装。",
|
"p2p_visibility_description": "配置谁可以看到您的 Spacedrive 安装。",
|
||||||
"p2p_visibility_disabled": "残疾人",
|
"p2p_visibility_disabled": "关闭",
|
||||||
"p2p_visibility_everyone": "每个人",
|
"p2p_visibility_everyone": "所有人",
|
||||||
"package": "Package",
|
"package": "Package",
|
||||||
"page": "页面",
|
"page": "页面",
|
||||||
"page_shortcut_description": "应用程序中的不同页面",
|
"page_shortcut_description": "应用程序中的不同页面",
|
||||||
|
@ -526,8 +530,8 @@
|
||||||
"paused": "已暂停",
|
"paused": "已暂停",
|
||||||
"peers": "个端点",
|
"peers": "个端点",
|
||||||
"people": "人们",
|
"people": "人们",
|
||||||
"pin": "别针",
|
"pin": "Pin",
|
||||||
"please_select_emoji": "Please select an emoji",
|
"please_select_emoji": "请选择一个表情",
|
||||||
"prefix_a": "a",
|
"prefix_a": "a",
|
||||||
"preview_media_bytes": "预览媒体",
|
"preview_media_bytes": "预览媒体",
|
||||||
"preview_media_bytes_description": "所有预览媒体文件(例如缩略图)的总大小。",
|
"preview_media_bytes_description": "所有预览媒体文件(例如缩略图)的总大小。",
|
||||||
|
@ -535,7 +539,7 @@
|
||||||
"privacy_description": "Spacedrive是为隐私而构建的,这就是为什么我们是开源的,以本地优先。因此,我们会非常明确地告诉您与我们分享了什么数据。",
|
"privacy_description": "Spacedrive是为隐私而构建的,这就是为什么我们是开源的,以本地优先。因此,我们会非常明确地告诉您与我们分享了什么数据。",
|
||||||
"queued": "排队",
|
"queued": "排队",
|
||||||
"quick_preview": "快速预览",
|
"quick_preview": "快速预览",
|
||||||
"quick_rescan_started": "Quick rescan started",
|
"quick_rescan_started": "正在快速重新扫描目录。",
|
||||||
"quick_view": "快速查看",
|
"quick_view": "快速查看",
|
||||||
"random": "随机的",
|
"random": "随机的",
|
||||||
"receiver": "接收者",
|
"receiver": "接收者",
|
||||||
|
@ -563,10 +567,10 @@
|
||||||
"rescan_directory": "重新扫描目录",
|
"rescan_directory": "重新扫描目录",
|
||||||
"rescan_location": "重新扫描位置",
|
"rescan_location": "重新扫描位置",
|
||||||
"reset": "重置",
|
"reset": "重置",
|
||||||
"reset_and_quit": "Reset & Quit App",
|
"reset_and_quit": "重置并退出应用程序",
|
||||||
"reset_confirmation": "Are you sure you want to reset Spacedrive? Your database will be deleted.",
|
"reset_confirmation": "您确定要重置 Spacedrive 吗?您的数据库将被删除。",
|
||||||
"reset_to_continue": "We detected you may have created your library with an older version of Spacedrive. Please reset it to continue using the app!",
|
"reset_to_continue": "我们检测到您可能使用旧版本的 Spacedrive 创建了您的资料库。请重置以继续使用应用程序!",
|
||||||
"reset_warning": "YOU WILL LOSE ANY EXISTING SPACEDRIVE DATA!",
|
"reset_warning": "您会丢失现有的所有 Spacedrive数据!",
|
||||||
"resolution": "解决",
|
"resolution": "解决",
|
||||||
"resources": "资源",
|
"resources": "资源",
|
||||||
"restore": "恢复",
|
"restore": "恢复",
|
||||||
|
@ -574,14 +578,14 @@
|
||||||
"retry": "重试",
|
"retry": "重试",
|
||||||
"reveal_in_native_file_manager": "在本机文件管理器中显示",
|
"reveal_in_native_file_manager": "在本机文件管理器中显示",
|
||||||
"revel_in_browser": "在{{browser}}中显示",
|
"revel_in_browser": "在{{browser}}中显示",
|
||||||
"rules": "Rules",
|
"rules": "规则",
|
||||||
"running": "运行中",
|
"running": "运行中",
|
||||||
"save": "保存",
|
"save": "保存",
|
||||||
"save_changes": "保存更改",
|
"save_changes": "保存更改",
|
||||||
"save_search": "保存搜索",
|
"save_search": "保存搜索",
|
||||||
"save_spacedrop": "Save Spacedrop",
|
"save_spacedrop": "保存 Spacedrop",
|
||||||
"saved_searches": "保存的搜索",
|
"saved_searches": "保存的搜索",
|
||||||
"screenshot": "Screenshot",
|
"screenshot": "屏幕截图",
|
||||||
"search": "搜索",
|
"search": "搜索",
|
||||||
"search_extensions": "搜索扩展",
|
"search_extensions": "搜索扩展",
|
||||||
"search_for_files_and_actions": "搜索文件和操作...",
|
"search_for_files_and_actions": "搜索文件和操作...",
|
||||||
|
@ -589,10 +593,10 @@
|
||||||
"secure_delete": "安全删除",
|
"secure_delete": "安全删除",
|
||||||
"security": "安全",
|
"security": "安全",
|
||||||
"security_description": "确保您的客户端安全。",
|
"security_description": "确保您的客户端安全。",
|
||||||
"see_less": "See less",
|
"see_less": "更少",
|
||||||
"see_more": "See more",
|
"see_more": "更多",
|
||||||
"send": "发送",
|
"send": "发送",
|
||||||
"send_report": "Send Report",
|
"send_report": "发送报告",
|
||||||
"sender": "发件人",
|
"sender": "发件人",
|
||||||
"sender_description": "此过程将同步操作发送到 Spacedrive Cloud。",
|
"sender_description": "此过程将同步操作发送到 Spacedrive Cloud。",
|
||||||
"settings": "设置",
|
"settings": "设置",
|
||||||
|
@ -601,7 +605,7 @@
|
||||||
"share_anonymous_usage": "分享匿名使用情况",
|
"share_anonymous_usage": "分享匿名使用情况",
|
||||||
"share_anonymous_usage_description": "分享完全匿名的遥测数据,帮助开发者改进应用程序",
|
"share_anonymous_usage_description": "分享完全匿名的遥测数据,帮助开发者改进应用程序",
|
||||||
"share_bare_minimum": "分享最基本信息",
|
"share_bare_minimum": "分享最基本信息",
|
||||||
"share_bare_minimum_description": "只分享我是Spacedrive的活跃用户和一些技术细节",
|
"share_bare_minimum_description": "只分享我是 Spacedrive 的活跃用户和一些技术细节",
|
||||||
"sharing": "共享",
|
"sharing": "共享",
|
||||||
"sharing_description": "管理有权访问您的库的人。",
|
"sharing_description": "管理有权访问您的库的人。",
|
||||||
"show_details": "显示详情",
|
"show_details": "显示详情",
|
||||||
|
@ -612,16 +616,16 @@
|
||||||
"show_slider": "显示滑块",
|
"show_slider": "显示滑块",
|
||||||
"show_tags": "显示标签",
|
"show_tags": "显示标签",
|
||||||
"size": "大小",
|
"size": "大小",
|
||||||
"size_b": "乙",
|
"size_b": "B",
|
||||||
"size_bs": "乙",
|
"size_bs": "Bs",
|
||||||
"size_gb": "国标",
|
"size_gb": "GB",
|
||||||
"size_gbs": "GB",
|
"size_gbs": "GBs",
|
||||||
"size_kb": "千字节",
|
"size_kb": "kB",
|
||||||
"size_kbs": "千字节",
|
"size_kbs": "kBs",
|
||||||
"size_mb": "MB",
|
"size_mb": "MB",
|
||||||
"size_mbs": "MB",
|
"size_mbs": "MB",
|
||||||
"size_tb": "结核病",
|
"size_tb": "TB",
|
||||||
"size_tbs": "TB",
|
"size_tbs": "TBs",
|
||||||
"skip_login": "跳过登录",
|
"skip_login": "跳过登录",
|
||||||
"software": "软件",
|
"software": "软件",
|
||||||
"sort_by": "排序依据",
|
"sort_by": "排序依据",
|
||||||
|
@ -633,16 +637,16 @@
|
||||||
"spacedrop_already_progress": "Spacedrop 已在进行中",
|
"spacedrop_already_progress": "Spacedrop 已在进行中",
|
||||||
"spacedrop_contacts_only": "仅限联系人",
|
"spacedrop_contacts_only": "仅限联系人",
|
||||||
"spacedrop_description": "与在您的网络上运行 Spacedrive 的设备即时共享。",
|
"spacedrop_description": "与在您的网络上运行 Spacedrive 的设备即时共享。",
|
||||||
"spacedrop_disabled": "残疾人",
|
"spacedrop_disabled": "关闭",
|
||||||
"spacedrop_everyone": "每个人",
|
"spacedrop_everyone": "所有人",
|
||||||
"spacedrop_rejected": "Spacedrop 被拒绝",
|
"spacedrop_rejected": "Spacedrop 被拒绝",
|
||||||
"square_thumbnails": "方形缩略图",
|
"square_thumbnails": "方形缩略图",
|
||||||
"star_on_github": "在 GitHub 上送一个 star",
|
"star_on_github": "在 GitHub 上送一个 star",
|
||||||
"start": "开始",
|
"start": "开始",
|
||||||
"starting": "开始...",
|
"starting": "开始...",
|
||||||
"starts_with": "以。。开始",
|
"starts_with": "以...开始",
|
||||||
"stop": "停止",
|
"stop": "停止",
|
||||||
"stopping": "停止...",
|
"stopping": "正在停止...",
|
||||||
"success": "成功",
|
"success": "成功",
|
||||||
"support": "支持",
|
"support": "支持",
|
||||||
"switch_to_grid_view": "切换到网格视图",
|
"switch_to_grid_view": "切换到网格视图",
|
||||||
|
@ -652,21 +656,21 @@
|
||||||
"switch_to_previous_tab": "切换到上一个标签页",
|
"switch_to_previous_tab": "切换到上一个标签页",
|
||||||
"sync": "同步",
|
"sync": "同步",
|
||||||
"syncPreviewMedia_label": "将此位置的预览媒体与您的设备同步",
|
"syncPreviewMedia_label": "将此位置的预览媒体与您的设备同步",
|
||||||
"sync_description": "管理Spacedrive的同步方式。",
|
"sync_description": "管理 Spacedrive 的同步方式。",
|
||||||
"sync_with_library": "与库同步",
|
"sync_with_library": "与资料库同步",
|
||||||
"sync_with_library_description": "如果启用,您的键绑定将与库同步,否则它们只适用于此客户端。",
|
"sync_with_library_description": "如果启用,您的快捷方式将与资料库同步,否则它们只适用于此客户端。",
|
||||||
"system": "系统",
|
"system": "系统",
|
||||||
"tag": "标签",
|
"tag": "标签",
|
||||||
"tag_other": "标签",
|
"tag_other": "标签",
|
||||||
"tags": "标签",
|
"tags": "标签",
|
||||||
"tags_description": "管理您的标签。",
|
"tags_description": "管理您的标签。",
|
||||||
"tags_notice_message": "没有项目分配给该标签。",
|
"tags_notice_message": "没有项目分配给该标签。",
|
||||||
"task": "task",
|
"task": "任务",
|
||||||
"task_other": "tasks",
|
"task_other": "任务",
|
||||||
"telemetry_description": "启用以向开发者提供详细的使用情况和遥测数据来改善应用程序。禁用则将只发送基本数据:您的活动状态、应用版本、应用内核版本以及平台(例如移动端、web 端或桌面端)。",
|
"telemetry_description": "启用以向开发者提供详细的使用情况和遥测数据来改善应用程序。禁用则将只发送基本数据:您的活动状态、应用版本、应用内核版本以及平台(例如移动端、web 端或桌面端)。",
|
||||||
"telemetry_title": "共享额外的遥测和使用数据",
|
"telemetry_title": "共享额外的遥测和使用数据",
|
||||||
"temperature": "温度",
|
"temperature": "温度",
|
||||||
"text": "Text",
|
"text": "文本",
|
||||||
"text_file": "文本文件",
|
"text_file": "文本文件",
|
||||||
"text_size": "文字大小",
|
"text_size": "文字大小",
|
||||||
"thank_you_for_your_feedback": "感谢您的反馈!",
|
"thank_you_for_your_feedback": "感谢您的反馈!",
|
||||||
|
@ -685,42 +689,42 @@
|
||||||
"toggle_sidebar": "切换侧边栏",
|
"toggle_sidebar": "切换侧边栏",
|
||||||
"tools": "工具",
|
"tools": "工具",
|
||||||
"total_bytes_capacity": "总容量",
|
"total_bytes_capacity": "总容量",
|
||||||
"total_bytes_capacity_description": "连接到库的所有节点的总容量。 在 Alpha 期间可能会显示不正确的值。",
|
"total_bytes_capacity_description": "连接到资料库的所有节点的总容量。 在 Alpha 期间可能会显示不正确的值。",
|
||||||
"total_bytes_free": "可用空间",
|
"total_bytes_free": "可用空间",
|
||||||
"total_bytes_free_description": "连接到库的所有节点上的可用空间。",
|
"total_bytes_free_description": "连接到资料库的所有节点上的可用空间。",
|
||||||
"total_bytes_used": "总使用空间",
|
"total_bytes_used": "总使用空间",
|
||||||
"total_bytes_used_description": "连接到库的所有节点上使用的总空间。",
|
"total_bytes_used_description": "连接到资料库的所有节点上使用的总空间。",
|
||||||
"trash": "垃圾",
|
"trash": "回收站",
|
||||||
"type": "类型",
|
"type": "类型",
|
||||||
"ui_animations": "用户界面动画",
|
"ui_animations": "用户界面动画",
|
||||||
"ui_animations_description": "打开和关闭时对话框和其他用户界面元素将产生动画效果。",
|
"ui_animations_description": "打开和关闭时对话框和其他用户界面元素将产生动画效果。",
|
||||||
"unknown": "Unknown",
|
"unknown": "未知",
|
||||||
"unnamed_location": "未命名位置",
|
"unnamed_location": "未命名位置",
|
||||||
"update": "更新",
|
"update": "更新",
|
||||||
"update_downloaded": "更新已下载。重新启动 Spacedrive 以安装",
|
"update_downloaded": "更新已下载。重新启动 Spacedrive 以安装",
|
||||||
"updated_successfully": "成功更新,您当前使用的是版本 {{version}}",
|
"updated_successfully": "成功更新,您当前使用的版本是 {{version}}",
|
||||||
"uploaded_file": "Uploaded file!",
|
"uploaded_file": "文件成功上传!",
|
||||||
"usage": "使用情况",
|
"usage": "使用情况",
|
||||||
"usage_description": "您的库使用情况和硬件信息",
|
"usage_description": "您的库使用情况和硬件信息",
|
||||||
"vaccum": "真空",
|
"vaccum": "清理",
|
||||||
"vaccum_library": "真空库",
|
"vaccum_library": "清理库",
|
||||||
"vaccum_library_description": "重新打包数据库以释放不必要的空间。",
|
"vaccum_library_description": "重新打包数据库以释放不必要的空间。",
|
||||||
"value": "值",
|
"value": "值",
|
||||||
"value_required": "所需值",
|
"value_required": "所需值",
|
||||||
"version": "版本 {{version}}",
|
"version": "版本 {{version}}",
|
||||||
"video": "Video",
|
"video": "视频",
|
||||||
"video_preview_not_supported": "不支持视频预览。",
|
"video_preview_not_supported": "不支持视频预览。",
|
||||||
"view_changes": "查看更改",
|
"view_changes": "查看更改",
|
||||||
"want_to_do_this_later": "想稍后再做吗?",
|
"want_to_do_this_later": "想稍后再做吗?",
|
||||||
"web_page_archive": "Web Page Archive",
|
"web_page_archive": "网页归档",
|
||||||
"website": "网站",
|
"website": "网页",
|
||||||
"widget": "小部件",
|
"widget": "小部件",
|
||||||
"with_descendants": "与后代",
|
"with_descendants": "子目录",
|
||||||
"your_account": "您的账户",
|
"your_account": "您的账户",
|
||||||
"your_account_description": "Spacedrive账号和信息。",
|
"your_account_description": "Spacedrive 账号和信息。",
|
||||||
"your_local_network": "您的本地网络",
|
"your_local_network": "您的本地网络",
|
||||||
"your_privacy": "您的隐私",
|
"your_privacy": "您的隐私",
|
||||||
"zoom": "飞涨",
|
"zoom": "缩放",
|
||||||
"zoom_in": "放大",
|
"zoom_in": "放大",
|
||||||
"zoom_out": "缩小"
|
"zoom_out": "缩小"
|
||||||
}
|
}
|
|
@ -89,7 +89,9 @@
|
||||||
"connect_cloud_description": "將您的雲端帳戶連接到 Spacedrive。",
|
"connect_cloud_description": "將您的雲端帳戶連接到 Spacedrive。",
|
||||||
"connect_device": "連接裝置",
|
"connect_device": "連接裝置",
|
||||||
"connect_device_description": "Spacedrive 在您的所有裝置上都能發揮最佳效果。",
|
"connect_device_description": "Spacedrive 在您的所有裝置上都能發揮最佳效果。",
|
||||||
|
"connect_library_to_cloud": "將圖書館連接到 Spacedrive Cloud",
|
||||||
"connected": "已連接",
|
"connected": "已連接",
|
||||||
|
"connecting_library_to_cloud": "正在將圖書館連接到 Spacedrive Cloud...",
|
||||||
"contacts": "聯繫人",
|
"contacts": "聯繫人",
|
||||||
"contacts_description": "在Spacedrive中管理您的聯繫人。",
|
"contacts_description": "在Spacedrive中管理您的聯繫人。",
|
||||||
"contains": "包含",
|
"contains": "包含",
|
||||||
|
@ -258,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": "閃光",
|
||||||
|
|
|
@ -8,6 +8,10 @@ export function hardwareModelToIcon(hardwareModel: HardwareModel) {
|
||||||
return 'Laptop';
|
return 'Laptop';
|
||||||
case 'MacStudio':
|
case 'MacStudio':
|
||||||
return 'SilverBox';
|
return 'SilverBox';
|
||||||
|
case 'IPhone':
|
||||||
|
return 'Mobile';
|
||||||
|
case 'Android':
|
||||||
|
return 'MobileAndroid';
|
||||||
case 'MacMini':
|
case 'MacMini':
|
||||||
return 'MiniSilverBox';
|
return 'MiniSilverBox';
|
||||||
case 'Other':
|
case 'Other':
|
||||||
|
|
|
@ -31,7 +31,8 @@
|
||||||
"lint:fix": "turbo run lint -- --fix",
|
"lint:fix": "turbo run lint -- --fix",
|
||||||
"clean": "cargo clean; git clean -qfX .",
|
"clean": "cargo clean; git clean -qfX .",
|
||||||
"test-data": "./scripts/test-data.sh",
|
"test-data": "./scripts/test-data.sh",
|
||||||
"i18n:sync": "npx i18next-locales-sync -p en -s $(find ./interface/locales -wholename '*/common.json' | awk -F'/' '$4 != \"en\" { ORS=\" \"; print $4 }') -l ./interface/locales"
|
"i18n:sync": "npx i18next-locales-sync -p en -s $(find ./interface/locales -wholename '*/common.json' | awk -F'/' '$4 != \"en\" { ORS=\" \"; print $4 }') -l ./interface/locales",
|
||||||
|
"autoformat": "./scripts/autoformat.sh"
|
||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"patchedDependencies": {
|
"patchedDependencies": {
|
||||||
|
|
|
@ -41,7 +41,7 @@ function onError(error: string) {
|
||||||
loginCallbacks.forEach((cb) => cb({ error }));
|
loginCallbacks.forEach((cb) => cb({ error }));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function login(config: ProviderConfig) {
|
export async function login(config: ProviderConfig) {
|
||||||
if (store.state.status !== 'notLoggedIn') return;
|
if (store.state.status !== 'notLoggedIn') return;
|
||||||
|
|
||||||
store.state = { status: 'loggingIn' };
|
store.state = { status: 'loggingIn' };
|
||||||
|
@ -86,10 +86,10 @@ export function login(config: ProviderConfig) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function logout() {
|
export async function logout() {
|
||||||
store.state = { status: 'loggingOut' };
|
store.state = { status: 'loggingOut' };
|
||||||
nonLibraryClient.mutation(['auth.logout']);
|
await nonLibraryClient.mutation(['auth.logout']);
|
||||||
nonLibraryClient.query(['auth.me']);
|
await nonLibraryClient.query(['auth.me']);
|
||||||
store.state = { status: 'notLoggedIn' };
|
store.state = { status: 'notLoggedIn' };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -65,7 +65,7 @@ export const Select = forwardRef(
|
||||||
</RS.Trigger>
|
</RS.Trigger>
|
||||||
|
|
||||||
<RS.Portal>
|
<RS.Portal>
|
||||||
<RS.Content className="z-50 rounded-md border border-app-line bg-app-box shadow-2xl shadow-app-shade/20 ">
|
<RS.Content className="z-[100] rounded-md border border-app-line bg-app-box shadow-2xl shadow-app-shade/20 ">
|
||||||
<RS.Viewport className="p-1">{props.children}</RS.Viewport>
|
<RS.Viewport className="p-1">{props.children}</RS.Viewport>
|
||||||
</RS.Content>
|
</RS.Content>
|
||||||
</RS.Portal>
|
</RS.Portal>
|
||||||
|
|
75
scripts/autoformat.sh
Executable file
75
scripts/autoformat.sh
Executable file
|
@ -0,0 +1,75 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -Eeumo pipefail
|
||||||
|
|
||||||
|
has() {
|
||||||
|
command -v "$1" >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_exit() {
|
||||||
|
_exit=$?
|
||||||
|
set +e
|
||||||
|
trap '' SIGINT
|
||||||
|
trap - EXIT
|
||||||
|
if [ "$_exit" -ne 0 ]; then
|
||||||
|
git restore --staged .
|
||||||
|
git restore .
|
||||||
|
fi
|
||||||
|
exit "$_exit"
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
set +e
|
||||||
|
trap '' SIGINT
|
||||||
|
trap - EXIT
|
||||||
|
jobs -p | xargs kill -SIGTERM
|
||||||
|
git restore --staged .
|
||||||
|
git restore .
|
||||||
|
kill -- -$$ 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
if ! has git pnpm; then
|
||||||
|
echo "Missing at on of the required dependencies: git, pnpm" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
__dirname="$(CDPATH='' cd "$(dirname "$0")" && pwd -P)"
|
||||||
|
|
||||||
|
# Change to the root directory of the repository
|
||||||
|
cd "$__dirname/.."
|
||||||
|
|
||||||
|
if [ -n "$(git diff --name-only HEAD)" ] || [ -n "$(git ls-files --others --exclude-standard)" ]; then
|
||||||
|
echo "Uncommitted changes found. Please commit or stash your changes before running this script." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Find the common ancestor of the current branch and main
|
||||||
|
if [ -n "${CI:-}" ]; then
|
||||||
|
ancestor="HEAD"
|
||||||
|
elif ! ancestor="$(git merge-base HEAD origin/main)"; then
|
||||||
|
echo "Failed to find the common ancestor of the current branch and main." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Handle errors and cleanup after formating has started
|
||||||
|
trap 'handle_exit' EXIT
|
||||||
|
trap 'cleanup' SIGINT
|
||||||
|
|
||||||
|
# Run the linter and formatter for frontend
|
||||||
|
# Use a background processes to avoid pnpm weird handling of CTRL+C
|
||||||
|
pnpm run -r lint --fix &
|
||||||
|
wait
|
||||||
|
pnpm run format &
|
||||||
|
wait
|
||||||
|
|
||||||
|
if [ "${1:-}" != "only-frontend" ]; then
|
||||||
|
# Run clippy and formatter for backend
|
||||||
|
cargo clippy --fix --all --all-targets --all-features --allow-dirty --allow-staged
|
||||||
|
cargo fmt --all
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Add all fixes for changes made in this branch
|
||||||
|
git diff --cached --name-only "$ancestor" | xargs git add
|
||||||
|
|
||||||
|
# Restore unrelated changes
|
||||||
|
git restore .
|
|
@ -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