mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-02 10:03:28 +00:00
[MOB-23] Mobile Hardware Information for Overview Page (#2106)
* wip for iDevices * Working HardwareModel Info for iOS * wip * Merge 'main' into 'mob-hw-info-overview' * Half-Working `get_volume()` * Objective c bridge to talk to FS * Working objc bridge The bridge works now, and we can now access the iOS file system using the native objective-c APIs instead for proper values, including on the simulator. * Isolate `icrate` for `ios` deployments only * Working Stats for Android * Clean Up + `pnpm format` * Fix to FSInfoResult Type Due to the RNFS fork change, I had to change the types to make it so it doesn't fail building and CI. * iOS Device Name Fix
This commit is contained in:
parent
55d2ec7a6a
commit
3bd1622e93
53
Cargo.lock
generated
53
Cargo.lock
generated
|
@ -1181,6 +1181,25 @@ version = "0.2.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae"
|
||||
|
||||
[[package]]
|
||||
name = "block-sys"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae85a0696e7ea3b835a453750bf002770776609115e6d25c6d2ff28a8200f7e7"
|
||||
dependencies = [
|
||||
"objc-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block2"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e58aa60e59d8dbfcc36138f5f18be5f24394d33b38b24f7fd0b1caa33095f22f"
|
||||
dependencies = [
|
||||
"block-sys",
|
||||
"objc2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "brotli"
|
||||
version = "3.4.0"
|
||||
|
@ -3693,6 +3712,16 @@ dependencies = [
|
|||
"png",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icrate"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e286f4b975ac6c054971a0600a9b76438b332edace54bff79c71c9d3adfc9772"
|
||||
dependencies = [
|
||||
"block2",
|
||||
"objc2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ident_case"
|
||||
version = "1.0.1"
|
||||
|
@ -5541,6 +5570,28 @@ dependencies = [
|
|||
"objc_id",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc-sys"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7c71324e4180d0899963fc83d9d241ac39e699609fc1025a850aadac8257459"
|
||||
|
||||
[[package]]
|
||||
name = "objc2"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a9c7f0d511a4ce26b078183179dca908171cfc69f88986fe36c5138e1834476"
|
||||
dependencies = [
|
||||
"objc-sys",
|
||||
"objc2-encode",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-encode"
|
||||
version = "4.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2ff06a6505cde0766484f38d8479ac8e6d31c66fbc2d5492f65ca8c091456379"
|
||||
|
||||
[[package]]
|
||||
name = "objc_exception"
|
||||
version = "0.1.2"
|
||||
|
@ -7712,9 +7763,11 @@ dependencies = [
|
|||
"http-body",
|
||||
"http-range",
|
||||
"hyper",
|
||||
"icrate",
|
||||
"image",
|
||||
"int-enum",
|
||||
"itertools 0.12.0",
|
||||
"libc",
|
||||
"mini-moka",
|
||||
"normpath",
|
||||
"notify",
|
||||
|
|
|
@ -98,7 +98,7 @@ export const Document = defineDocumentType(() => ({
|
|||
.replace(/^.+?(\/)/, '')
|
||||
.split('/')
|
||||
.slice(-1)[0]
|
||||
)
|
||||
)
|
||||
},
|
||||
section: {
|
||||
type: 'string',
|
||||
|
|
|
@ -86,7 +86,7 @@ export function Platform({ platform, ...props }: ComponentProps<'a'> & PlatformP
|
|||
href={`${BASE_DL_LINK}/${platform.os}/${links[0].arch}`}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
)
|
||||
: (props: any) => <button {...props} />
|
||||
: (props: any) => <div {...props} />;
|
||||
|
||||
|
|
|
@ -27,10 +27,10 @@ TARGET_DIRECTORY="${__dirname}/../../../../../target"
|
|||
mkdir -p "$TARGET_DIRECTORY"
|
||||
TARGET_DIRECTORY="$(CDPATH='' cd -- "$TARGET_DIRECTORY" && pwd -P)"
|
||||
|
||||
if [ "${CONFIGURATION:-}" != "Debug" ]; then
|
||||
CARGO_FLAGS=--release
|
||||
export CARGO_FLAGS
|
||||
fi
|
||||
# if [ "${CONFIGURATION:-}" != "Debug" ]; then
|
||||
# CARGO_FLAGS=--release
|
||||
# export CARGO_FLAGS
|
||||
# fi
|
||||
|
||||
# Required for CI and for everyone I guess?
|
||||
export PATH="${CARGO_HOME:-"${HOME}/.cargo"}/bin:$PATH"
|
||||
|
|
|
@ -37,9 +37,9 @@
|
|||
"class-variance-authority": "^0.7.0",
|
||||
"dayjs": "^1.11.10",
|
||||
"event-target-polyfill": "^0.0.3",
|
||||
"expo": "~50.0.6",
|
||||
"expo": "~50.0.7",
|
||||
"expo-av": "^13.10.5",
|
||||
"expo-blur": "^12.9.1",
|
||||
"expo-blur": "^12.9.2",
|
||||
"expo-build-properties": "~0.11.1",
|
||||
"expo-linking": "~6.2.2",
|
||||
"expo-media-library": "~15.9.1",
|
||||
|
@ -53,6 +53,7 @@
|
|||
"react-hook-form": "^7.47.0",
|
||||
"react-native": "0.73.4",
|
||||
"react-native-circular-progress": "^1.3.9",
|
||||
"react-native-device-info": "^10.13.1",
|
||||
"react-native-document-picker": "^9.0.1",
|
||||
"react-native-file-viewer": "^2.1.5",
|
||||
"react-native-gesture-handler": "~2.14.1",
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { useNavigation } from '@react-navigation/native';
|
||||
import { Tag, useCache, useLibraryQuery, useNodes } from '@sd/client';
|
||||
import { DotsThreeOutlineVertical, Eye, Pen, Plus, Trash } from 'phosphor-react-native';
|
||||
import React, { useRef } from 'react';
|
||||
import { Animated, Pressable, Text, View } from 'react-native';
|
||||
import { FlatList, Swipeable } from 'react-native-gesture-handler';
|
||||
import { ClassInput } from 'twrnc/dist/esm/types';
|
||||
import { Tag, useCache, useLibraryQuery, useNodes } from '@sd/client';
|
||||
import { ModalRef } from '~/components/layout/Modal';
|
||||
import { tw, twStyle } from '~/lib/tailwind';
|
||||
import { BrowseStackScreenProps } from '~/navigation/tabs/BrowseStack';
|
||||
|
@ -24,12 +24,7 @@ type TagItemProps = {
|
|||
viewStyle?: 'grid' | 'list';
|
||||
};
|
||||
|
||||
export const TagItem = ({
|
||||
tag,
|
||||
onPress,
|
||||
tagStyle,
|
||||
viewStyle = 'grid'
|
||||
}: TagItemProps) => {
|
||||
export const TagItem = ({ tag, onPress, tagStyle, viewStyle = 'grid' }: TagItemProps) => {
|
||||
const modalRef = useRef<ModalRef>(null);
|
||||
|
||||
const renderTagView = () => (
|
||||
|
|
|
@ -41,8 +41,8 @@ const Job = ({ progress, message, error }: JobProps) => {
|
|||
const progressColor = error
|
||||
? tw.color('red-500')
|
||||
: progress === 100
|
||||
? tw.color('green-500')
|
||||
: tw.color('accent');
|
||||
? tw.color('green-500')
|
||||
: tw.color('accent');
|
||||
return (
|
||||
<View
|
||||
style={tw`h-[170px] w-[310px] flex-col rounded-md border border-app-line/50 bg-app-box/50`}
|
||||
|
|
|
@ -88,8 +88,8 @@ const Explorer = ({ items }: ExplorerProps) => {
|
|||
item.type === 'NonIndexedPath'
|
||||
? item.item.path
|
||||
: item.type === 'SpacedropPeer'
|
||||
? item.item.name
|
||||
: item.item.id.toString()
|
||||
? item.item.name
|
||||
: item.item.id.toString()
|
||||
}
|
||||
renderItem={({ item }) => (
|
||||
<Pressable onPress={() => handlePress(item)}>
|
||||
|
|
|
@ -48,10 +48,9 @@ export default function Header({
|
|||
|
||||
return (
|
||||
<View
|
||||
style={twStyle(
|
||||
'relative h-auto w-full border-b border-app-line/50 bg-mobile-header',
|
||||
{ paddingTop: headerHeight }
|
||||
)}
|
||||
style={twStyle('relative h-auto w-full border-b border-app-line/50 bg-mobile-header', {
|
||||
paddingTop: headerHeight
|
||||
})}
|
||||
>
|
||||
<View style={tw`mx-auto h-auto w-full justify-center px-5 pb-4`}>
|
||||
<View style={tw`w-full flex-row items-center justify-between`}>
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import * as RNFS from '@dr.pogodin/react-native-fs';
|
||||
import { AlphaRSPCError } from '@oscartbeaumont-sd/rspc-client/v2';
|
||||
import { UseQueryResult } from '@tanstack/react-query';
|
||||
import React from 'react';
|
||||
import { Text, View } from 'react-native';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Platform, Text, View } from 'react-native';
|
||||
import DeviceInfo from 'react-native-device-info';
|
||||
import { ScrollView } from 'react-native-gesture-handler';
|
||||
import { HardwareModel, NodeState, StatisticsResponse } from '@sd/client';
|
||||
import { tw, twStyle } from '~/lib/tailwind';
|
||||
|
@ -23,45 +25,86 @@ export function hardwareModelToIcon(hardwareModel: HardwareModel) {
|
|||
return 'Laptop';
|
||||
case 'MacStudio':
|
||||
return 'SilverBox';
|
||||
case 'IPhone':
|
||||
return 'Mobile';
|
||||
case 'IPad':
|
||||
return 'Tablet';
|
||||
case 'Simulator':
|
||||
return 'Drive';
|
||||
case 'Android':
|
||||
return 'Mobile';
|
||||
default:
|
||||
return 'Laptop';
|
||||
}
|
||||
}
|
||||
|
||||
const Devices = ({ node, stats }: Props) => {
|
||||
// We don't need the totalSpaceEx and freeSpaceEx fields
|
||||
const [sizeInfo, setSizeInfo] = useState<Omit<RNFS.FSInfoResultT, "totalSpaceEx" | "freeSpaceEx">>({ freeSpace: 0, totalSpace: 0 });
|
||||
const [deviceName, setDeviceName] = useState<string>("");
|
||||
|
||||
useEffect(() => {
|
||||
const getFSInfo = async () => {
|
||||
return await RNFS.getFSInfo();
|
||||
};
|
||||
getFSInfo().then((size) => {
|
||||
setSizeInfo(size);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const totalSpace =
|
||||
Platform.OS === 'android'
|
||||
? sizeInfo.totalSpace.toString()
|
||||
: stats.data?.statistics?.total_bytes_capacity || '0';
|
||||
const freeSpace =
|
||||
Platform.OS === 'android'
|
||||
? sizeInfo.freeSpace.toString()
|
||||
: stats.data?.statistics?.total_bytes_free || '0';
|
||||
|
||||
useEffect(() => {
|
||||
if (Platform.OS === 'android') {
|
||||
DeviceInfo.getDeviceName().then((name) => {
|
||||
setDeviceName(name);
|
||||
});
|
||||
} else if (node) {
|
||||
setDeviceName(node.name);
|
||||
}
|
||||
}, [node]);
|
||||
|
||||
return (
|
||||
<OverviewSection title="Devices" count={node ? 1 : 0}>
|
||||
<View>
|
||||
<Fade height={'100%'} width={30} color="mobile-screen">
|
||||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={tw`px-6`}
|
||||
>
|
||||
{node && (
|
||||
<StatCard
|
||||
name={node.name}
|
||||
icon={hardwareModelToIcon(node.device_model as any)}
|
||||
totalSpace={stats.data?.statistics?.total_bytes_capacity || '0'}
|
||||
freeSpace={stats.data?.statistics?.total_bytes_free || '0'}
|
||||
color="#0362FF"
|
||||
connectionType={null}
|
||||
/>
|
||||
)}
|
||||
<NewCard
|
||||
icons={['Laptop', 'Server', 'SilverBox', 'Tablet']}
|
||||
text="Spacedrive works best on all your devices."
|
||||
style={twStyle(node ? 'ml-2' : 'ml-0')}
|
||||
button={() => (
|
||||
<Button variant="transparent">
|
||||
<Text style={tw`font-bold text-ink-dull`}>Coming soon</Text>
|
||||
</Button>
|
||||
)}
|
||||
<OverviewSection title="Devices" count={node ? 1 : 0}>
|
||||
<View>
|
||||
<Fade height={'100%'} width={30} color="mobile-screen">
|
||||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={tw`px-6`}
|
||||
>
|
||||
{node && (
|
||||
<StatCard
|
||||
name={deviceName}
|
||||
// TODO (Optional): Use Brand Type for Different Android Models/iOS Models using DeviceInfo.getBrand()
|
||||
icon={hardwareModelToIcon(node.device_model as any)}
|
||||
totalSpace={totalSpace}
|
||||
freeSpace={freeSpace}
|
||||
color="#0362FF"
|
||||
connectionType={null}
|
||||
/>
|
||||
</ScrollView>
|
||||
</Fade>
|
||||
</View>
|
||||
</OverviewSection>
|
||||
)}
|
||||
<NewCard
|
||||
icons={['Laptop', 'Server', 'SilverBox', 'Tablet']}
|
||||
text="Spacedrive works best on all your devices."
|
||||
style={twStyle(node ? 'ml-2' : 'ml-0')}
|
||||
button={() => (
|
||||
<Button variant="transparent">
|
||||
<Text style={tw`font-bold text-ink-dull`}>Coming soon</Text>
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
</ScrollView>
|
||||
</Fade>
|
||||
</View>
|
||||
</OverviewSection>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import * as RNFS from '@dr.pogodin/react-native-fs';
|
|||
import { AlphaRSPCError } from '@oscartbeaumont-sd/rspc-client/v2';
|
||||
import { UseQueryResult } from '@tanstack/react-query';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Text, View } from 'react-native';
|
||||
import { Platform, Text, View } from 'react-native';
|
||||
import { ClassInput } from 'twrnc/dist/esm/types';
|
||||
import { byteSize, Statistics, StatisticsResponse, useLibraryContext } from '@sd/client';
|
||||
import useCounter from '~/hooks/useCounter';
|
||||
|
@ -31,7 +31,7 @@ const StatItem = ({ title, bytes, isLoading, style }: StatItemProps) => {
|
|||
return (
|
||||
<View
|
||||
style={twStyle(
|
||||
'flex flex-col items-center justify-center rounded-md border border-app-line/50 bg-app-box/50 p-2',
|
||||
'border-app-line/50 bg-app-box/50 flex flex-col items-center justify-center rounded-md border p-2',
|
||||
style,
|
||||
{
|
||||
hidden: isLoading
|
||||
|
@ -73,6 +73,7 @@ const OverviewStats = ({ stats }: Props) => {
|
|||
return await RNFS.getFSInfo();
|
||||
};
|
||||
getFSInfo().then((size) => {
|
||||
console.log('size', size);
|
||||
setSizeInfo(size);
|
||||
});
|
||||
}, []);
|
||||
|
@ -90,6 +91,8 @@ const OverviewStats = ({ stats }: Props) => {
|
|||
bytes = BigInt(sizeInfo.freeSpace);
|
||||
} else if (key === 'total_bytes_capacity') {
|
||||
bytes = BigInt(sizeInfo.totalSpace);
|
||||
} else if (key === 'total_bytes_used' && Platform.OS === 'android') {
|
||||
bytes = BigInt(sizeInfo.totalSpace - sizeInfo.freeSpace);
|
||||
}
|
||||
return (
|
||||
<StatItem
|
||||
|
|
|
@ -19,8 +19,8 @@ export function SettingsItem(props: SettingsItemProps) {
|
|||
props.rounded === 'top'
|
||||
? 'border-t border-r border-l border-app-input'
|
||||
: props.rounded === 'bottom'
|
||||
? 'border-b border-app-input border-r border-l'
|
||||
: 'border-app-input border-l border-r';
|
||||
? 'border-b border-app-input border-r border-l'
|
||||
: 'border-app-input border-l border-r';
|
||||
return (
|
||||
<Pressable onPress={props.onPress}>
|
||||
<View style={twStyle(' border-app-line/50 bg-app-box/50', borderRounded, border)}>
|
||||
|
|
|
@ -28,71 +28,71 @@ export default function TabNavigator() {
|
|||
labelStyle: Style;
|
||||
testID: string;
|
||||
}[] = [
|
||||
{
|
||||
name: 'OverviewStack',
|
||||
component: OverviewStack,
|
||||
icon: (
|
||||
<TabBarButton
|
||||
resourceName="tabs"
|
||||
animationName="animate"
|
||||
artboardName="overview"
|
||||
style={{ width: 28 }}
|
||||
active={activeIndex === 0}
|
||||
/>
|
||||
),
|
||||
label: 'Overview',
|
||||
labelStyle: tw`text-[10px] font-semibold`,
|
||||
testID: 'overview-tab'
|
||||
},
|
||||
{
|
||||
name: 'NetworkStack',
|
||||
component: NetworkStack,
|
||||
icon: (
|
||||
<TabBarButton
|
||||
resourceName="tabs"
|
||||
animationName="animate"
|
||||
artboardName="network"
|
||||
style={{ width: 18, maxHeight: 23 }}
|
||||
active={activeIndex === 1}
|
||||
/>
|
||||
),
|
||||
label: 'Network',
|
||||
labelStyle: tw`text-[10px] font-semibold`,
|
||||
testID: 'network-tab'
|
||||
},
|
||||
{
|
||||
name: 'BrowseStack',
|
||||
component: BrowseStack,
|
||||
icon: (
|
||||
<TabBarButton
|
||||
resourceName="tabs"
|
||||
animationName="animate"
|
||||
artboardName="browse"
|
||||
style={{ width: 20 }}
|
||||
active={activeIndex === 2}
|
||||
/>
|
||||
),
|
||||
label: 'Browse',
|
||||
labelStyle: tw`text-[10px] font-semibold`,
|
||||
testID: 'browse-tab'
|
||||
},
|
||||
{
|
||||
name: 'SettingsStack',
|
||||
component: SettingsStack,
|
||||
icon: (
|
||||
<TabBarButton
|
||||
resourceName="tabs"
|
||||
animationName="animate"
|
||||
artboardName="settings"
|
||||
style={{ width: 19 }}
|
||||
active={activeIndex === 3}
|
||||
/>
|
||||
),
|
||||
label: 'Settings',
|
||||
labelStyle: tw`text-[10px] font-semibold`,
|
||||
testID: 'settings-tab'
|
||||
}
|
||||
];
|
||||
{
|
||||
name: 'OverviewStack',
|
||||
component: OverviewStack,
|
||||
icon: (
|
||||
<TabBarButton
|
||||
resourceName="tabs"
|
||||
animationName="animate"
|
||||
artboardName="overview"
|
||||
style={{ width: 28 }}
|
||||
active={activeIndex === 0}
|
||||
/>
|
||||
),
|
||||
label: 'Overview',
|
||||
labelStyle: tw`text-[10px] font-semibold`,
|
||||
testID: 'overview-tab'
|
||||
},
|
||||
{
|
||||
name: 'NetworkStack',
|
||||
component: NetworkStack,
|
||||
icon: (
|
||||
<TabBarButton
|
||||
resourceName="tabs"
|
||||
animationName="animate"
|
||||
artboardName="network"
|
||||
style={{ width: 18, maxHeight: 23 }}
|
||||
active={activeIndex === 1}
|
||||
/>
|
||||
),
|
||||
label: 'Network',
|
||||
labelStyle: tw`text-[10px] font-semibold`,
|
||||
testID: 'network-tab'
|
||||
},
|
||||
{
|
||||
name: 'BrowseStack',
|
||||
component: BrowseStack,
|
||||
icon: (
|
||||
<TabBarButton
|
||||
resourceName="tabs"
|
||||
animationName="animate"
|
||||
artboardName="browse"
|
||||
style={{ width: 20 }}
|
||||
active={activeIndex === 2}
|
||||
/>
|
||||
),
|
||||
label: 'Browse',
|
||||
labelStyle: tw`text-[10px] font-semibold`,
|
||||
testID: 'browse-tab'
|
||||
},
|
||||
{
|
||||
name: 'SettingsStack',
|
||||
component: SettingsStack,
|
||||
icon: (
|
||||
<TabBarButton
|
||||
resourceName="tabs"
|
||||
animationName="animate"
|
||||
artboardName="settings"
|
||||
style={{ width: 19 }}
|
||||
active={activeIndex === 3}
|
||||
/>
|
||||
),
|
||||
label: 'Settings',
|
||||
labelStyle: tw`text-[10px] font-semibold`,
|
||||
testID: 'settings-tab'
|
||||
}
|
||||
];
|
||||
return (
|
||||
<Tab.Navigator
|
||||
id="tab"
|
||||
|
|
|
@ -37,6 +37,6 @@ export type RootStackScreenProps<Screen extends keyof RootStackParamList> = Stac
|
|||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace ReactNavigation {
|
||||
interface RootParamList extends RootStackParamList { }
|
||||
interface RootParamList extends RootStackParamList {}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ export default function LocationScreen({ navigation, route }: BrowseStackScreenP
|
|||
take: 100
|
||||
}
|
||||
]);
|
||||
|
||||
|
||||
const pathsItemsReferences = useMemo(() => paths.data?.items ?? [], [paths.data]);
|
||||
useNodes(paths.data?.nodes);
|
||||
const pathsItems = useCache(pathsItemsReferences);
|
||||
|
@ -52,5 +52,5 @@ export default function LocationScreen({ navigation, route }: BrowseStackScreenP
|
|||
getExplorerStore().path = path ?? '';
|
||||
}, [id, path]);
|
||||
|
||||
return <Explorer items={pathsItems} />
|
||||
return <Explorer items={pathsItems} />;
|
||||
}
|
||||
|
|
|
@ -118,7 +118,7 @@ const sections: (debugState: DebugState) => SectionType[] = (debugState) => [
|
|||
title: 'Debug',
|
||||
rounded: 'bottom'
|
||||
}
|
||||
] as const)
|
||||
] as const)
|
||||
: [])
|
||||
]
|
||||
}
|
||||
|
|
|
@ -22,10 +22,11 @@ const AboutScreen = () => {
|
|||
<View style={tw.style('flex flex-col')}>
|
||||
<Text style={tw.style('text-2xl font-bold text-white')}>
|
||||
Spacedrive{' '}
|
||||
{`for ${Platform.OS === 'android'
|
||||
? Platform.OS[0]?.toUpperCase() + Platform.OS.slice(1)
|
||||
: Platform.OS[0] + Platform.OS.slice(1).toUpperCase()
|
||||
}`}
|
||||
{`for ${
|
||||
Platform.OS === 'android'
|
||||
? Platform.OS[0]?.toUpperCase() + Platform.OS.slice(1)
|
||||
: Platform.OS[0] + Platform.OS.slice(1).toUpperCase()
|
||||
}`}
|
||||
</Text>
|
||||
<Text style={tw.style('mt-1 text-sm text-ink-dull')}>
|
||||
The file manager from the future.
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
import Tags from '~/screens/Tags';
|
||||
|
||||
const TagsSettingsScreen = () => {
|
||||
|
||||
return (
|
||||
<Tags viewStyle="list" />
|
||||
);
|
||||
return <Tags viewStyle="list" />;
|
||||
};
|
||||
|
||||
export default TagsSettingsScreen;
|
||||
|
|
|
@ -58,7 +58,7 @@ const ScreenshotWrapper = ({
|
|||
margin: '0 auto',
|
||||
position: 'relative',
|
||||
overflow: 'hidden'
|
||||
}
|
||||
}
|
||||
: {}
|
||||
}
|
||||
>
|
||||
|
|
|
@ -109,6 +109,7 @@ http-body = "0.4.5"
|
|||
http-range = "0.1.5"
|
||||
int-enum = "0.5.0"
|
||||
itertools = "0.12.0"
|
||||
libc = "0.2.153"
|
||||
mini-moka = "0.10.2"
|
||||
notify = { git="https://github.com/notify-rs/notify.git", rev="c3929ed114fbb0bc7457a9a498260461596b00ca", default-features = false, features = [
|
||||
"macos_fsevent",
|
||||
|
@ -141,6 +142,9 @@ features = ["vendored"]
|
|||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
plist = "1"
|
||||
|
||||
[target.'cfg(target_os = "ios")'.dependencies]
|
||||
icrate = { version = "0.1.0", features = ["Foundation", "Foundation_NSFileManager", "Foundation_NSString", "Foundation_NSNumber"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tracing-test = "^0.2.4"
|
||||
aovec = "1.1.0"
|
||||
|
|
|
@ -202,11 +202,13 @@ impl Node {
|
|||
|
||||
// Set a default if the user hasn't set an override
|
||||
if std::env::var("RUST_LOG") == Err(std::env::VarError::NotPresent) {
|
||||
let level = if cfg!(debug_assertions) {
|
||||
"debug"
|
||||
} else {
|
||||
"info"
|
||||
};
|
||||
// let level = if cfg!(debug_assertions) {
|
||||
// "debug"
|
||||
// } else {
|
||||
// "info"
|
||||
// };
|
||||
|
||||
let level = "debug";
|
||||
|
||||
// let level = "debug"; // Exists for now to debug the location manager
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ use std::str;
|
|||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
use strum_macros::{Display, EnumIter};
|
||||
use sysinfo::{System, SystemExt};
|
||||
|
||||
#[repr(i32)]
|
||||
#[derive(Debug, Clone, Display, Copy, EnumIter, Type, Serialize, Deserialize, Eq, PartialEq)]
|
||||
|
@ -19,6 +20,8 @@ pub enum HardwareModel {
|
|||
IMacPro,
|
||||
IPad,
|
||||
IPhone,
|
||||
Simulator,
|
||||
Android,
|
||||
}
|
||||
|
||||
impl HardwareModel {
|
||||
|
@ -62,7 +65,83 @@ pub fn get_hardware_model_name() -> Result<HardwareModel, Error> {
|
|||
))
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
#[cfg(target_os = "ios")]
|
||||
{
|
||||
use std::ffi::CString;
|
||||
use std::ptr;
|
||||
|
||||
extern "C" {
|
||||
fn sysctlbyname(
|
||||
name: *const libc::c_char,
|
||||
oldp: *mut libc::c_void,
|
||||
oldlenp: *mut usize,
|
||||
newp: *mut libc::c_void,
|
||||
newlen: usize,
|
||||
) -> libc::c_int;
|
||||
}
|
||||
|
||||
fn get_device_type() -> Option<String> {
|
||||
let mut size: usize = 0;
|
||||
let name = CString::new("hw.machine").expect("CString::new failed");
|
||||
|
||||
// First, get the size of the buffer needed
|
||||
unsafe {
|
||||
sysctlbyname(
|
||||
name.as_ptr(),
|
||||
ptr::null_mut(),
|
||||
&mut size,
|
||||
ptr::null_mut(),
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
// Allocate a buffer with the correct size
|
||||
let mut buffer: Vec<u8> = vec![0; size];
|
||||
|
||||
// Get the actual machine type
|
||||
unsafe {
|
||||
sysctlbyname(
|
||||
name.as_ptr(),
|
||||
buffer.as_mut_ptr() as *mut libc::c_void,
|
||||
&mut size,
|
||||
ptr::null_mut(),
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
// Convert the buffer to a String
|
||||
let machine_type = String::from_utf8_lossy(&buffer).trim().to_string();
|
||||
|
||||
// Check if the device is an iPad or iPhone
|
||||
if machine_type.starts_with("iPad") {
|
||||
Some("iPad".to_string())
|
||||
} else if machine_type.starts_with("iPhone") {
|
||||
Some("iPhone".to_string())
|
||||
} else if machine_type.starts_with("arm") {
|
||||
Some("Simulator".to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(device_type) = get_device_type() {
|
||||
let hardware_model = HardwareModel::from_display_name(&device_type.as_str());
|
||||
|
||||
Ok(hardware_model)
|
||||
} else {
|
||||
Err(Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
"Failed to get hardware model name",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
Ok(HardwareModel::Android)
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "macos", target_os = "ios", target_os = "android")))]
|
||||
{
|
||||
Err(Error::new(
|
||||
std::io::ErrorKind::Unsupported,
|
||||
|
|
|
@ -1,21 +1,29 @@
|
|||
// Adapted from: https://github.com/kimlimjustin/xplorer/blob/f4f3590d06783d64949766cc2975205a3b689a56/src-tauri/src/drives.rs
|
||||
|
||||
use sd_cache::Model;
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fmt::Display,
|
||||
hash::{Hash, Hasher},
|
||||
os::unix::fs::MetadataExt,
|
||||
path::PathBuf,
|
||||
sync::OnceLock,
|
||||
};
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
use icrate::{
|
||||
objc2::runtime::{Class, Object},
|
||||
objc2::{msg_send, sel},
|
||||
Foundation::{self, ns_string, NSFileManager, NSFileSystemSize, NSNumber, NSString},
|
||||
};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_with::{serde_as, DisplayFromStr};
|
||||
use specta::Type;
|
||||
use sysinfo::{DiskExt, System, SystemExt};
|
||||
use thiserror::Error;
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::error;
|
||||
use tracing::{error, info};
|
||||
|
||||
pub mod watcher;
|
||||
|
||||
|
@ -224,6 +232,51 @@ pub async fn get_volumes() -> Vec<Volume> {
|
|||
volumes
|
||||
}
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
pub async fn get_volumes() -> Vec<Volume> {
|
||||
let mut volumes: Vec<Volume> = Vec::new();
|
||||
|
||||
unsafe {
|
||||
let file_manager = NSFileManager::defaultManager();
|
||||
|
||||
let root_dir = NSString::from_str("/");
|
||||
|
||||
let root_dir_ref = root_dir.as_ref();
|
||||
|
||||
let attributes = file_manager
|
||||
.attributesOfFileSystemForPath_error(root_dir_ref)
|
||||
.unwrap();
|
||||
|
||||
let attributes_ref = attributes.as_ref();
|
||||
|
||||
// Total space
|
||||
let key = NSString::from_str("NSFileSystemSize");
|
||||
let key_ref = key.as_ref();
|
||||
|
||||
let t = attributes_ref.get(key_ref).unwrap();
|
||||
let total_space: u64 = msg_send![t, unsignedLongLongValue];
|
||||
|
||||
// Used space
|
||||
let key = NSString::from_str("NSFileSystemFreeSize");
|
||||
let key_ref = key.as_ref();
|
||||
|
||||
let t = attributes_ref.get(key_ref).unwrap();
|
||||
let free_space: u64 = msg_send![t, unsignedLongLongValue];
|
||||
|
||||
volumes.push(Volume {
|
||||
name: "Root".to_string(),
|
||||
disk_type: DiskType::SSD,
|
||||
file_system: Some("APFS".to_string()),
|
||||
mount_points: vec![PathBuf::from("/")],
|
||||
total_capacity: total_space,
|
||||
available_capacity: free_space,
|
||||
is_root_filesystem: true,
|
||||
});
|
||||
}
|
||||
|
||||
volumes
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
|
@ -245,7 +298,9 @@ struct HDIUtilInfo {
|
|||
images: Vec<ImageInfo>,
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
// Android does not work via sysinfo and JNI is a pain to maintain. Therefore, we use React-Native-FS to get the volume data of the device.
|
||||
// We leave the function though to be built for Android because otherwise, the build will fail.
|
||||
#[cfg(not(any(target_os = "linux", target_os = "ios")))]
|
||||
pub async fn get_volumes() -> Vec<Volume> {
|
||||
use futures::future;
|
||||
use tokio::process::Command;
|
||||
|
|
|
@ -136,7 +136,7 @@ export function ErrorPage({
|
|||
? sendReportBtn()
|
||||
: sentryBrowserLazy.then(({ captureException }) =>
|
||||
captureException(message)
|
||||
)
|
||||
)
|
||||
}
|
||||
>
|
||||
Send report
|
||||
|
|
|
@ -200,7 +200,7 @@ const Tags = ({ items, parentRef }: Props & { parentRef: RefObject<HTMLDivElemen
|
|||
tag.id,
|
||||
unassign
|
||||
? // use objects that already have tag
|
||||
items.flatMap((item) => {
|
||||
items.flatMap((item) => {
|
||||
if (
|
||||
item.type === 'Object' ||
|
||||
item.type === 'Path'
|
||||
|
@ -209,18 +209,24 @@ const Tags = ({ items, parentRef }: Props & { parentRef: RefObject<HTMLDivElemen
|
|||
}
|
||||
|
||||
return [];
|
||||
})
|
||||
})
|
||||
: // use objects that don't have tag
|
||||
items.flatMap<AssignTagItems[number]>((item) => {
|
||||
if (item.type === 'Object') {
|
||||
if (!objectsWithTag.has(item.item.id))
|
||||
items.flatMap<AssignTagItems[number]>(
|
||||
(item) => {
|
||||
if (item.type === 'Object') {
|
||||
if (
|
||||
!objectsWithTag.has(
|
||||
item.item.id
|
||||
)
|
||||
)
|
||||
return [item];
|
||||
} else if (item.type === 'Path') {
|
||||
return [item];
|
||||
} else if (item.type === 'Path') {
|
||||
return [item];
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}),
|
||||
return [];
|
||||
}
|
||||
),
|
||||
unassign
|
||||
);
|
||||
|
||||
|
|
|
@ -41,13 +41,13 @@ export const Delete = new ConditionalItem({
|
|||
locationId: selectedFilePaths[0].location_id,
|
||||
rescan,
|
||||
pathIds: selectedFilePaths.map((p) => p.id)
|
||||
}
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const ephemeralArgs = isNonEmpty(selectedEphemeralPaths)
|
||||
? {
|
||||
paths: selectedEphemeralPaths.map((p) => p.path)
|
||||
}
|
||||
}
|
||||
: undefined;
|
||||
const deleteKeybind = useKeysMatcher(['Meta', 'Backspace']);
|
||||
|
||||
|
|
|
@ -152,7 +152,7 @@ export const FileThumb = forwardRef<HTMLImageElement, ThumbProps>((props, ref) =
|
|||
? [
|
||||
'min-h-full min-w-full object-cover object-center',
|
||||
_childClassName
|
||||
]
|
||||
]
|
||||
: className,
|
||||
props.frame && !(itemData.kind === 'Video' && props.blackBars)
|
||||
? frameClassName
|
||||
|
|
|
@ -605,8 +605,8 @@ const RenameInput = ({ name, onRename }: RenameInputProps) => {
|
|||
quickPreview.background
|
||||
? 'border-white/[.12] bg-white/10 backdrop-blur-sm'
|
||||
: isDark
|
||||
? 'border-app-line bg-app-input'
|
||||
: 'border-black/[.075] bg-black/[.075]'
|
||||
? 'border-app-line bg-app-input'
|
||||
: 'border-black/[.075] bg-black/[.075]'
|
||||
)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onFocus={() => highlightName()}
|
||||
|
|
|
@ -80,7 +80,7 @@ const InnerCell = memo(
|
|||
: flexRender(props.cell.column.columnDef.cell, {
|
||||
...props.cell.getContext(),
|
||||
selected: props.selected
|
||||
})}
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -429,7 +429,7 @@ export const ListView = memo(() => {
|
|||
range.direction
|
||||
? keyDirection !== range.direction
|
||||
: backRange?.direction &&
|
||||
(backRange.sorted.start.index === frontRange?.sorted.start.index ||
|
||||
(backRange.sorted.start.index === frontRange?.sorted.start.index ||
|
||||
backRange.sorted.end.index === frontRange?.sorted.end.index)
|
||||
) {
|
||||
explorer.removeSelectedItem(range.end.original);
|
||||
|
@ -797,11 +797,11 @@ export const ListView = memo(() => {
|
|||
explorerSettings.order &&
|
||||
(orderKey.startsWith('object.')
|
||||
? orderKey.split('object.')[1] ===
|
||||
header.id
|
||||
header.id
|
||||
: orderKey === header.id)
|
||||
? getOrderingDirection(
|
||||
explorerSettings.order
|
||||
)
|
||||
)
|
||||
: null;
|
||||
|
||||
const cellContent = flexRender(
|
||||
|
@ -849,7 +849,8 @@ export const ListView = memo(() => {
|
|||
)
|
||||
? value.split(
|
||||
'object.'
|
||||
)[1] === header.id
|
||||
)[1] ===
|
||||
header.id
|
||||
: value ===
|
||||
header.id;
|
||||
}
|
||||
|
|
|
@ -130,8 +130,8 @@ export const useRanges = ({ ranges, rows }: UseRangesProps) => {
|
|||
options.direction === 'down'
|
||||
? _ranges[targetRangeIndex + 1]
|
||||
: options.direction === 'up'
|
||||
? _ranges[targetRangeIndex - 1]
|
||||
: _ranges[targetRangeIndex + 1] || _ranges[targetRangeIndex - 1];
|
||||
? _ranges[targetRangeIndex - 1]
|
||||
: _ranges[targetRangeIndex + 1] || _ranges[targetRangeIndex - 1];
|
||||
|
||||
if (!closestRange) return;
|
||||
|
||||
|
|
|
@ -27,9 +27,9 @@ export type OrderingKeys<T extends Ordering> = T extends Ordering
|
|||
[K in T['field']]: OrderingValue<T, K> extends SortOrder
|
||||
? K
|
||||
: OrderingValue<T, K> extends Ordering
|
||||
? `${K}.${OrderingKeys<OrderingValue<T, K>>}`
|
||||
: never;
|
||||
}[T['field']]
|
||||
? `${K}.${OrderingKeys<OrderingValue<T, K>>}`
|
||||
: never;
|
||||
}[T['field']]
|
||||
: never;
|
||||
|
||||
export function orderingKey(ordering: Ordering): OrderingKey {
|
||||
|
|
|
@ -150,7 +150,7 @@ export const useExplorerDroppable = ({
|
|||
z.ZodLiteral<ExplorerItemType>,
|
||||
...z.ZodLiteral<ExplorerItemType>[]
|
||||
]
|
||||
)
|
||||
)
|
||||
: z.literal(allowedType)
|
||||
});
|
||||
|
||||
|
|
|
@ -105,8 +105,8 @@ export default function LocalSection() {
|
|||
item.mountPoint === '/'
|
||||
? 'Root'
|
||||
: item.index === 0
|
||||
? item.volume.name
|
||||
: item.mountPoint;
|
||||
? item.volume.name
|
||||
: item.mountPoint;
|
||||
|
||||
const toPath =
|
||||
locationId !== undefined
|
||||
|
@ -128,8 +128,8 @@ export default function LocalSection() {
|
|||
item.volume.file_system === 'exfat'
|
||||
? 'SD'
|
||||
: item.volume.name === 'Macintosh HD'
|
||||
? 'HDD'
|
||||
: 'Drive'
|
||||
? 'HDD'
|
||||
: 'Drive'
|
||||
}
|
||||
/>
|
||||
<Name>{name}</Name>
|
||||
|
|
|
@ -2,15 +2,21 @@ import { Planet } from '@phosphor-icons/react';
|
|||
import clsx from 'clsx';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { proxy } from 'valtio';
|
||||
import { HardwareModel, useBridgeMutation, useDiscoveredPeers, useP2PEvents, useSelector } from '@sd/client';
|
||||
import {
|
||||
HardwareModel,
|
||||
useBridgeMutation,
|
||||
useDiscoveredPeers,
|
||||
useP2PEvents,
|
||||
useSelector
|
||||
} from '@sd/client';
|
||||
import { toast } from '@sd/ui';
|
||||
import { Icon } from '~/components';
|
||||
import { useDropzone, useLocale, useOnDndLeave } from '~/hooks';
|
||||
import { hardwareModelToIcon } from '~/util/hardware';
|
||||
import { usePlatform } from '~/util/Platform';
|
||||
|
||||
import { TOP_BAR_ICON_STYLE } from '../TopBar/TopBarOptions';
|
||||
import { useIncomingSpacedropToast, useSpacedropProgressToast } from './toast';
|
||||
import { hardwareModelToIcon } from '~/util/hardware';
|
||||
|
||||
// TODO: This is super hacky so should probs be rewritten but for now it works.
|
||||
const hackyState = proxy({
|
||||
|
@ -88,7 +94,7 @@ export function Spacedrop({ triggerClose }: { triggerClose: () => void }) {
|
|||
|
||||
const onDropped = (id: string, files: string[]) => {
|
||||
if (doSpacedrop.isLoading) {
|
||||
toast.warning(t("spacedrop_already_progress"));
|
||||
toast.warning(t('spacedrop_already_progress'));
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -107,21 +113,25 @@ export function Spacedrop({ triggerClose }: { triggerClose: () => void }) {
|
|||
<span className="text-lg font-bold">Spacedrop</span>
|
||||
|
||||
<div className="flex flex-col space-y-4 pt-2">
|
||||
<p className="text-center text-ink-dull">
|
||||
{t("spacedrop_description")}
|
||||
</p>
|
||||
{discoveredPeers.size === 0 && <div className={clsx(
|
||||
'flex items-center justify-center gap-3 rounded-md border border-dashed border-app-line bg-app-darkBox px-3 py-2 font-medium text-ink',
|
||||
|
||||
)}>
|
||||
<p className="text-center text-ink-faint">
|
||||
{t("no_nodes_found")}
|
||||
</p>
|
||||
</div>}
|
||||
<div className='flex flex-col space-y-2'>
|
||||
{Array.from(discoveredPeers).map(([id, meta]) => (
|
||||
<Node key={id} id={id} name={meta.name as HardwareModel} onDropped={onDropped} />
|
||||
))}
|
||||
<p className="text-center text-ink-dull">{t('spacedrop_description')}</p>
|
||||
{discoveredPeers.size === 0 && (
|
||||
<div
|
||||
className={clsx(
|
||||
'flex items-center justify-center gap-3 rounded-md border border-dashed border-app-line bg-app-darkBox px-3 py-2 font-medium text-ink'
|
||||
)}
|
||||
>
|
||||
<p className="text-center text-ink-faint">{t('no_nodes_found')}</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col space-y-2">
|
||||
{Array.from(discoveredPeers).map(([id, meta]) => (
|
||||
<Node
|
||||
key={id}
|
||||
id={id}
|
||||
name={meta.name as HardwareModel}
|
||||
onDropped={onDropped}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -151,7 +161,9 @@ function Node({
|
|||
ref={ref}
|
||||
className={clsx(
|
||||
'flex items-center justify-start gap-2 rounded-md border bg-app-darkBox px-3 py-2 font-medium text-ink',
|
||||
state === 'hovered' ? 'border-solid border-accent-deep' : 'border-dashed border-app-line'
|
||||
state === 'hovered'
|
||||
? 'border-solid border-accent-deep'
|
||||
: 'border-dashed border-app-line'
|
||||
)}
|
||||
onClick={() => {
|
||||
if (!platform.openFilePickerDialog) {
|
||||
|
|
|
@ -96,10 +96,10 @@ function ToolGroup({
|
|||
const roundingCondition = individual
|
||||
? 'both'
|
||||
: index === 0
|
||||
? 'left'
|
||||
: index === group.length - 1
|
||||
? 'right'
|
||||
: 'none';
|
||||
? 'left'
|
||||
: index === group.length - 1
|
||||
? 'right'
|
||||
: 'none';
|
||||
|
||||
const popover = usePopover();
|
||||
const os = useOperatingSystem();
|
||||
|
@ -130,7 +130,7 @@ function ToolGroup({
|
|||
{typeof icon === 'function'
|
||||
? icon({
|
||||
triggerOpen: () => popover.setOpen(true)
|
||||
})
|
||||
})
|
||||
: icon}
|
||||
</Tooltip>
|
||||
</TopBarButton>
|
||||
|
|
|
@ -36,7 +36,7 @@ export const Component = () => {
|
|||
? {
|
||||
type: 'Node',
|
||||
node: nodeState.data
|
||||
}
|
||||
}
|
||||
: undefined,
|
||||
settings: explorerSettings,
|
||||
showPathBar: false,
|
||||
|
|
|
@ -62,7 +62,7 @@ export const AppliedFilters = ({ allowRemove = true }: { allowRemove?: boolean }
|
|||
return dyanmicFilters;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
|
|
|
@ -138,7 +138,7 @@ const FilterOptionList = ({
|
|||
{option.name}
|
||||
</SearchOptionItem>
|
||||
);
|
||||
})}
|
||||
})}
|
||||
</SearchOptionSubMenu>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -35,7 +35,7 @@ const LANGUAGE_OPTIONS = [
|
|||
{ value: 'ru', label: 'Русский' },
|
||||
{ value: 'zh-CN', label: '中文(简体)' },
|
||||
{ value: 'zh-TW', label: '中文(繁體)' },
|
||||
{ value: 'it', label: "Italiano"}
|
||||
{ value: 'it', label: 'Italiano' }
|
||||
];
|
||||
|
||||
// Sort the languages by their label
|
||||
|
|
|
@ -42,8 +42,8 @@ export const validateInput = (
|
|||
const regex = isWeb
|
||||
? null // Non web plataforms use the native file picker, so there is no need to validate
|
||||
: os === 'windows'
|
||||
? /^[^<>:"/|?*\u0000-\u0031]+$/
|
||||
: /^[^\0]+$/;
|
||||
? /^[^<>:"/|?*\u0000-\u0031]+$/
|
||||
: /^[^\0]+$/;
|
||||
return {
|
||||
value: regex?.test(value) || false,
|
||||
message: value ? 'Invalid path' : 'Value required'
|
||||
|
@ -116,8 +116,8 @@ export const RuleInput = memo(
|
|||
(os === 'windows'
|
||||
? 'C:\\Users\\john\\Downloads'
|
||||
: os === 'macOS'
|
||||
? '/Users/clara/Pictures'
|
||||
: '/home/emily/Documents') +
|
||||
? '/Users/clara/Pictures'
|
||||
: '/home/emily/Documents') +
|
||||
')'
|
||||
}
|
||||
onClick={async () => {
|
||||
|
|
|
@ -114,7 +114,7 @@ export default function IndexerRuleEditor<T extends IndexerRuleIdFieldType>({
|
|||
setSelectedRule(
|
||||
selectedRule === rule ? undefined : rule
|
||||
);
|
||||
}
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
className={clsx(
|
||||
|
|
|
@ -18,8 +18,8 @@ export const Component = () => {
|
|||
|
||||
const filteredLocations = useMemo(
|
||||
() =>
|
||||
locations?.filter(
|
||||
(location) => location.name?.toLowerCase().includes(debouncedSearch.toLowerCase())
|
||||
locations?.filter((location) =>
|
||||
location.name?.toLowerCase().includes(debouncedSearch.toLowerCase())
|
||||
) ?? [],
|
||||
[debouncedSearch, locations]
|
||||
);
|
||||
|
|
|
@ -399,4 +399,3 @@ body {
|
|||
.wiggle {
|
||||
animation: wiggle 200ms infinite;
|
||||
}
|
||||
|
||||
|
|
|
@ -23,8 +23,8 @@ export const useKeybind = (
|
|||
typeof options === 'object' && 'repeatable' in options
|
||||
? options.repeatable
|
||||
: typeof dependencies === 'object' && 'repeatable' in dependencies
|
||||
? dependencies.repeatable
|
||||
: false;
|
||||
? dependencies.repeatable
|
||||
: false;
|
||||
|
||||
return useHotkeys(
|
||||
keyCombination,
|
||||
|
|
|
@ -106,8 +106,8 @@ function parseParams(o: any, schema: any, key: string, value: any) {
|
|||
shape instanceof z.ZodObject
|
||||
? shape.shape
|
||||
: shape instanceof z.ZodEffects
|
||||
? shape._def.schema
|
||||
: null;
|
||||
? shape._def.schema
|
||||
: null;
|
||||
if (shape === null) {
|
||||
throw new Error(`Could not find shape for key ${key}`);
|
||||
}
|
||||
|
|
|
@ -52,13 +52,13 @@ export const getIcon = (
|
|||
(extension && extension in icons
|
||||
? extension
|
||||
: // 2. If in light mode, check if the specific kind in light exists.
|
||||
!isDark && lightKind in icons
|
||||
? lightKind
|
||||
: // 3. Check if a general kind icon exists.
|
||||
kind in icons
|
||||
? kind
|
||||
: // 4. Default to the document (or document light) icon.
|
||||
document) as keyof typeof icons
|
||||
!isDark && lightKind in icons
|
||||
? lightKind
|
||||
: // 3. Check if a general kind icon exists.
|
||||
kind in icons
|
||||
? kind
|
||||
: // 4. Default to the document (or document light) icon.
|
||||
document) as keyof typeof icons
|
||||
];
|
||||
};
|
||||
|
||||
|
|
|
@ -236,10 +236,10 @@ function specialMerge(copy: Record<any, any>, original: unknown) {
|
|||
export type UseCacheResult<T> = T extends (infer A)[]
|
||||
? UseCacheResult<A>[]
|
||||
: T extends object
|
||||
? T extends { '__type': any; '__id': string; '#type': infer U }
|
||||
? UseCacheResult<U>
|
||||
: { [K in keyof T]: UseCacheResult<T[K]> }
|
||||
: { [K in keyof T]: UseCacheResult<T[K]> };
|
||||
? T extends { '__type': any; '__id': string; '#type': infer U }
|
||||
? UseCacheResult<U>
|
||||
: { [K in keyof T]: UseCacheResult<T[K]> }
|
||||
: { [K in keyof T]: UseCacheResult<T[K]> };
|
||||
|
||||
export function useCache<T>(data: T | undefined) {
|
||||
const cache = useCacheContext();
|
||||
|
|
|
@ -312,7 +312,7 @@ export type GenerateThumbsForLocationArgs = { id: number; path: string; regenera
|
|||
|
||||
export type GetAll = { backups: Backup[]; directory: string }
|
||||
|
||||
export type HardwareModel = "Other" | "MacStudio" | "MacBookAir" | "MacBookPro" | "MacBook" | "MacMini" | "MacPro" | "IMac" | "IMacPro" | "IPad" | "IPhone"
|
||||
export type HardwareModel = "Other" | "MacStudio" | "MacBookAir" | "MacBookPro" | "MacBook" | "MacMini" | "MacPro" | "IMac" | "IMacPro" | "IPad" | "IPhone" | "Simulator" | "Android"
|
||||
|
||||
export type IdentifyUniqueFilesArgs = { id: number; path: string }
|
||||
|
||||
|
|
|
@ -213,7 +213,7 @@ const submitPlausibleEvent = async ({ event, debugState, ...props }: SubmitEvent
|
|||
? () => {
|
||||
const { callback: _, ...event } = fullEvent;
|
||||
console.log(event);
|
||||
}
|
||||
}
|
||||
: undefined
|
||||
};
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ import { useObserverWithOwner } from './useObserver';
|
|||
type AllowReactiveScope<T> = T extends object
|
||||
? {
|
||||
[P in keyof T]: AllowReactiveScope<T[P]>;
|
||||
}
|
||||
}
|
||||
: T | (() => T);
|
||||
|
||||
type Props<T> =
|
||||
|
|
|
@ -77,7 +77,7 @@ export function toggleFeatureFlag(flags: FeatureFlag | FeatureFlag[]) {
|
|||
? true
|
||||
: await confirm(
|
||||
'This feature will render your database broken and it WILL need to be reset! Use at your own risk!'
|
||||
);
|
||||
);
|
||||
|
||||
if (result) {
|
||||
nonLibraryClient.mutation(['toggleFeatureFlag', f as any]);
|
||||
|
|
|
@ -83,8 +83,8 @@ export function getIndexedItemFilePath(data: ExplorerItem) {
|
|||
return data.type === 'Path'
|
||||
? data.item
|
||||
: data.type === 'Object'
|
||||
? data.item.file_paths[0] ?? null
|
||||
: null;
|
||||
? data.item.file_paths[0] ?? null
|
||||
: null;
|
||||
}
|
||||
|
||||
export function getItemLocation(data: ExplorerItem) {
|
||||
|
@ -118,11 +118,10 @@ export type UnionToIntersection<U> = (U extends never ? never : (arg: U) => neve
|
|||
? I
|
||||
: never;
|
||||
|
||||
export type UnionToTuple<T> = UnionToIntersection<T extends never ? never : (t: T) => T> extends (
|
||||
_: never
|
||||
) => infer W
|
||||
? [...UnionToTuple<Exclude<T, W>>, W]
|
||||
: [];
|
||||
export type UnionToTuple<T> =
|
||||
UnionToIntersection<T extends never ? never : (t: T) => T> extends (_: never) => infer W
|
||||
? [...UnionToTuple<Exclude<T, W>>, W]
|
||||
: [];
|
||||
|
||||
export function formatNumber(n: number) {
|
||||
if (!n) return '0';
|
||||
|
|
|
@ -51,11 +51,11 @@ export function useJobInfo(job: JobReport, realtimeUpdate: JobProgressEvent | nu
|
|||
text: isPaused
|
||||
? job.message
|
||||
: isRunning && realtimeUpdate?.message
|
||||
? realtimeUpdate.message
|
||||
: `${formatNumber(output?.total_paths)} ${plural(
|
||||
output?.total_paths,
|
||||
'path'
|
||||
)} discovered`
|
||||
? realtimeUpdate.message
|
||||
: `${formatNumber(output?.total_paths)} ${plural(
|
||||
output?.total_paths,
|
||||
'path'
|
||||
)} discovered`
|
||||
}
|
||||
]
|
||||
]
|
||||
|
@ -127,7 +127,7 @@ export function useJobInfo(job: JobReport, realtimeUpdate: JobProgressEvent | nu
|
|||
'thumb'
|
||||
)}`
|
||||
}
|
||||
];
|
||||
];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -169,7 +169,7 @@ export function useJobInfo(job: JobReport, realtimeUpdate: JobProgressEvent | nu
|
|||
output?.total_objects_linked
|
||||
)} ${plural(output?.total_objects_linked, 'Object')} linked`
|
||||
}
|
||||
]
|
||||
]
|
||||
: [{ text: addCommasToNumbersInMessage(realtimeUpdate?.message) }]
|
||||
]
|
||||
};
|
||||
|
|
|
@ -185,9 +185,9 @@ export function Dialog<S extends FieldValues>({
|
|||
);
|
||||
const disableCheck = props.errorMessageException
|
||||
? !form.formState.isValid &&
|
||||
!form.formState.errors.root?.serverError?.message?.startsWith(
|
||||
!form.formState.errors.root?.serverError?.message?.startsWith(
|
||||
props.errorMessageException as string
|
||||
)
|
||||
)
|
||||
: !form.formState.isValid;
|
||||
|
||||
const submitButton = (
|
||||
|
|
|
@ -81,7 +81,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
|
|||
: createElement<IconProps>(icon as Icon, {
|
||||
size: 18,
|
||||
className: 'text-gray-350'
|
||||
})}
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
|
@ -20,8 +20,8 @@ export const ProgressBar = memo((props: ProgressBarProps) => {
|
|||
const percentage = props.pending
|
||||
? 0
|
||||
: 'percent' in props
|
||||
? props.percent
|
||||
: Math.round((props.value / props.total) * 100);
|
||||
? props.percent
|
||||
: Math.round((props.value / props.total) * 100);
|
||||
|
||||
if (props.pending) {
|
||||
return (
|
||||
|
|
|
@ -382,13 +382,13 @@ importers:
|
|||
specifier: ^0.0.3
|
||||
version: 0.0.3
|
||||
expo:
|
||||
specifier: ~50.0.6
|
||||
specifier: ~50.0.7
|
||||
version: 50.0.7(@babel/core@7.24.0)(@react-native/babel-preset@0.73.21)
|
||||
expo-av:
|
||||
specifier: ^13.10.5
|
||||
version: 13.10.5(expo@50.0.7)
|
||||
expo-blur:
|
||||
specifier: ^12.9.1
|
||||
specifier: ^12.9.2
|
||||
version: 12.9.2(expo@50.0.7)
|
||||
expo-build-properties:
|
||||
specifier: ~0.11.1
|
||||
|
@ -429,6 +429,9 @@ importers:
|
|||
react-native-circular-progress:
|
||||
specifier: ^1.3.9
|
||||
version: 1.3.9(react-native-svg@14.1.0)(react-native@0.73.4)(react@18.2.0)
|
||||
react-native-device-info:
|
||||
specifier: ^10.13.1
|
||||
version: 10.13.1(react-native@0.73.4)
|
||||
react-native-document-picker:
|
||||
specifier: ^9.0.1
|
||||
version: 9.1.1(react-native-windows@0.73.8)(react-native@0.73.4)(react@18.2.0)
|
||||
|
@ -4493,7 +4496,7 @@ packages:
|
|||
resolution: {integrity: sha512-bOhuFnlRaS7CU33+rFFIWdcET/Vkyn1vsN8BYFwCDEF5P1fVVvYN7bFOsQLTMD3nvi35C1AGmtqUr/Wfv8Xaow==}
|
||||
engines: {node: '>=12'}
|
||||
dependencies:
|
||||
'@expo/spawn-async': 1.5.0
|
||||
'@expo/spawn-async': 1.7.2
|
||||
exec-async: 2.2.0
|
||||
dev: false
|
||||
|
||||
|
@ -4501,7 +4504,7 @@ packages:
|
|||
resolution: {integrity: sha512-LKdo/6y4W7llZ6ghsg1kdx2CeH/qR/c6QI/JI8oPUvppsZoeIYjSkdflce978fAMfR8IXoi0wt0jA2w0kWpwbg==}
|
||||
dependencies:
|
||||
'@expo/json-file': 8.3.0
|
||||
'@expo/spawn-async': 1.5.0
|
||||
'@expo/spawn-async': 1.7.2
|
||||
ansi-regex: 5.0.1
|
||||
chalk: 4.1.2
|
||||
find-up: 5.0.0
|
||||
|
@ -20414,6 +20417,14 @@ packages:
|
|||
react-native-svg: 14.1.0(react-native@0.73.4)(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/react-native-device-info@10.13.1(react-native@0.73.4):
|
||||
resolution: {integrity: sha512-j/7Z+Yl9Cesjp8vKaVzbuJQKJSVs4ojXATt5WjwipZ0Ss0mBJjqtbc4x5dfZLmQ4y55VVa7c0v8KHca1iqY/TQ==}
|
||||
peerDependencies:
|
||||
react-native: '*'
|
||||
dependencies:
|
||||
react-native: 0.73.4(@babel/core@7.24.0)(@babel/preset-env@7.24.0)(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/react-native-document-picker@9.1.1(react-native-windows@0.73.8)(react-native@0.73.4)(react@18.2.0):
|
||||
resolution: {integrity: sha512-BW+7DbsILuFThlBm7NUFVUmKKf6Awkcf9R0q8wiCU2DlGGtAKQTt2iHpO5+Dn/7WMPB+rqNv3X1HsmJQ0t5R3g==}
|
||||
peerDependencies:
|
||||
|
|
|
@ -34,8 +34,8 @@ export async function which(progName) {
|
|||
Array.from(new Set(env.PATH?.split(':'))).map(dir =>
|
||||
fs.access(path.join(dir, progName), fs.constants.X_OK)
|
||||
)
|
||||
).then(
|
||||
).then(
|
||||
() => true,
|
||||
() => false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue