[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:
Arnab Chakraborty 2024-03-06 01:46:22 -05:00 committed by GitHub
parent 55d2ec7a6a
commit 3bd1622e93
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
59 changed files with 518 additions and 258 deletions

53
Cargo.lock generated
View file

@ -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",

View file

@ -98,7 +98,7 @@ export const Document = defineDocumentType(() => ({
.replace(/^.+?(\/)/, '')
.split('/')
.slice(-1)[0]
)
)
},
section: {
type: 'string',

View file

@ -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} />;

View file

@ -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"

View file

@ -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",

View file

@ -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 = () => (

View file

@ -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`}

View file

@ -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)}>

View file

@ -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`}>

View file

@ -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>
);
};

View file

@ -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

View file

@ -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)}>

View file

@ -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"

View file

@ -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 {}
}
}

View file

@ -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} />;
}

View file

@ -118,7 +118,7 @@ const sections: (debugState: DebugState) => SectionType[] = (debugState) => [
title: 'Debug',
rounded: 'bottom'
}
] as const)
] as const)
: [])
]
}

View file

@ -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.

View file

@ -1,10 +1,7 @@
import Tags from '~/screens/Tags';
const TagsSettingsScreen = () => {
return (
<Tags viewStyle="list" />
);
return <Tags viewStyle="list" />;
};
export default TagsSettingsScreen;

View file

@ -58,7 +58,7 @@ const ScreenshotWrapper = ({
margin: '0 auto',
position: 'relative',
overflow: 'hidden'
}
}
: {}
}
>

View file

@ -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"

View file

@ -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

View file

@ -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,

View file

@ -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;

View file

@ -136,7 +136,7 @@ export function ErrorPage({
? sendReportBtn()
: sentryBrowserLazy.then(({ captureException }) =>
captureException(message)
)
)
}
>
Send report

View file

@ -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
);

View file

@ -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']);

View file

@ -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

View file

@ -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()}

View file

@ -80,7 +80,7 @@ const InnerCell = memo(
: flexRender(props.cell.column.columnDef.cell, {
...props.cell.getContext(),
selected: props.selected
})}
})}
</div>
);
}

View file

@ -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;
}

View file

@ -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;

View file

@ -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 {

View file

@ -150,7 +150,7 @@ export const useExplorerDroppable = ({
z.ZodLiteral<ExplorerItemType>,
...z.ZodLiteral<ExplorerItemType>[]
]
)
)
: z.literal(allowedType)
});

View file

@ -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>

View file

@ -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) {

View file

@ -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>

View file

@ -36,7 +36,7 @@ export const Component = () => {
? {
type: 'Node',
node: nodeState.data
}
}
: undefined,
settings: explorerSettings,
showPathBar: false,

View file

@ -62,7 +62,7 @@ export const AppliedFilters = ({ allowRemove = true }: { allowRemove?: boolean }
return dyanmicFilters;
}
);
}
}
: undefined
}
/>

View file

@ -138,7 +138,7 @@ const FilterOptionList = ({
{option.name}
</SearchOptionItem>
);
})}
})}
</SearchOptionSubMenu>
);
};

View file

@ -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

View file

@ -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 () => {

View file

@ -114,7 +114,7 @@ export default function IndexerRuleEditor<T extends IndexerRuleIdFieldType>({
setSelectedRule(
selectedRule === rule ? undefined : rule
);
}
}
: undefined
}
className={clsx(

View file

@ -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]
);

View file

@ -399,4 +399,3 @@ body {
.wiggle {
animation: wiggle 200ms infinite;
}

View file

@ -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,

View file

@ -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}`);
}

View file

@ -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
];
};

View file

@ -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();

View file

@ -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 }

View file

@ -213,7 +213,7 @@ const submitPlausibleEvent = async ({ event, debugState, ...props }: SubmitEvent
? () => {
const { callback: _, ...event } = fullEvent;
console.log(event);
}
}
: undefined
};

View file

@ -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> =

View file

@ -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]);

View file

@ -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';

View file

@ -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) }]
]
};

View file

@ -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 = (

View file

@ -81,7 +81,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
: createElement<IconProps>(icon as Icon, {
size: 18,
className: 'text-gray-350'
})}
})}
</div>
)}

View file

@ -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 (

View file

@ -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:

View file

@ -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
)
)
}