[MOB-1] Theme support for Mobile (#755)

* revert rspc changes and some theme stuff

* Run onboarding test first

* test adding a tag

* handle keyboard on Create Tag Modal

* listen system theme changes

* fix delete tag button

* wait add tag mutation

* remove duplicate assert

* fix edit location setting screen

* select theme & fix add tag test

* add how to run web app to contributing

* add note about how to use stores correctly

* use theme colors

* system theme

* remove metro-minify-terser

* final tweaks

* cleanup

* cleanup

---------

Co-authored-by: Oscar Beaumont <oscar@otbeaumont.me>
This commit is contained in:
Utku 2023-05-04 11:10:31 +03:00 committed by GitHub
parent 2571e3b275
commit 62f2c77a52
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
44 changed files with 663 additions and 313 deletions

View file

@ -48,8 +48,9 @@
"search.exclude": {
"**/node_modules": true,
"**/bower_components": true,
"**/*.code-search": true
"**/*.code-search": true,
// Hiding these folders bcs they create a lot of noise in the search results
"apps/mobile/ios/Pods": true
// "apps/mobile/android": true
// "apps/mobile/ios": true
},

View file

@ -54,9 +54,13 @@ To quickly run only the desktop app after `prep` you can use:
If necessary, react-devtools can be launched using `pnpm react-devtools`.
However, it must be executed before the desktop app for it to connect.
To run the web app:
- `cargo run -p server` - runs the server
- `pnpm web dev` - runs the web embed server
To run the landing page:
- `pnpm web dev` - runs the web app for the embed
- `pnpm landing dev`
If you are having issues ensure you are using the following versions of Rust and Node:
@ -78,6 +82,7 @@ To run mobile app
- You must also ensure [you must have NDK 23.1.7779620 and CMake](https://developer.android.com/studio/projects/install-ndk#default-version) in Android Studio
- `pnpm android` - runs on Android Emulator
- `pnpm ios` - runs on iOS Emulator
- `pnpm start` - runs the metro bundler
### Pull Request

View file

@ -18,11 +18,6 @@
"supportsTablet": false
},
"android": {},
"privacy": "hidden",
"extra": {
"eas": {
"projectId": "0cbf4456-87fb-499c-8dfa-554bfa5129f3"
}
}
"privacy": "hidden"
}
}

View file

@ -86,8 +86,6 @@ target 'Spacedrive' do
target_installation_result.resource_bundle_targets.each do |resource_bundle_target|
resource_bundle_target.build_configurations.each do |config|
config.build_settings['CODE_SIGNING_ALLOWED'] = 'NO'
# Spacedrive - Added to fix the issue with the deployment target
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = podfile_properties['ios.deploymentTarget']
end
end
end

View file

@ -603,8 +603,8 @@ EXTERNAL SOURCES:
:path: "../../../node_modules/react-native/ReactCommon/yoga"
SPEC CHECKSUMS:
boost: 57d2868c099736d80fcd648bf211b4431e51a558
DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54
boost: 20b6b3a53cb43939b6ffc2fd27d15ec7497167d0
DoubleConversion: 2d248a4174d78beaaa49735340c9bcc564091245
EXApplication: d8f53a7eee90a870a75656280e8d4b85726ea903
EXConstants: f348da07e21b23d2b085e270d7b74f282df1a7d9
EXFileSystem: 844e86ca9b5375486ecc4ef06d3838d5597d895d
@ -617,12 +617,12 @@ SPEC CHECKSUMS:
FBLazyVector: 60195509584153283780abdac5569feffb8f08cc
FBReactNativeSpec: c5a5c4f1b95ae42a17cd22c8c89c482a7b327fe3
fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9
glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b
hermes-engine: 38bfe887e456b33b697187570a08de33969f5db7
glog: 5b8834f2f3f7d36c1c73e5a9837f53f03cfe84eb
hermes-engine: 2670551c21c6838c04f837c7c3db0ce55f5da525
libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
lottie-ios: 8f97d3271e155c2d688875c29cd3c74908aef5f8
lottie-react-native: b702fab740cdb952a8e2354713d3beda63ff97b0
RCT-Folly: 424b8c9a7a0b9ab2886ffe9c3b041ef628fd4fb1
RCT-Folly: 30d165d2e977f66793419c3314296392cb793f38
RCTRequired: bec48f07daf7bcdc2655a0cde84e07d24d2a9e2a
RCTTypeSafety: 171394eebacf71e1cfad79dbfae7ee8fc16ca80a
React: d7433ccb6a8c36e4cbed59a73c0700fc83c3e98a

View file

@ -350,7 +350,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = H7MGS2DHHJ;
DEVELOPMENT_TEAM = XF923AZS22;
ENABLE_BITCODE = NO;
GCC_PREPROCESSOR_DEFINITIONS = (
"$(inherited)",
@ -411,6 +411,7 @@
/usr/lib/swift,
../../../target,
);
MARKETING_VERSION = 0.0.1;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@ -451,7 +452,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = H7MGS2DHHJ;
DEVELOPMENT_TEAM = XF923AZS22;
INFOPLIST_FILE = Spacedrive/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Spacedrive;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity";
@ -507,6 +508,7 @@
/usr/lib/swift,
../../../target,
);
MARKETING_VERSION = 0.0.1;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",

View file

@ -83,5 +83,7 @@
<string>Automatic</string>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false />
<key>ITSAppUsesNonExemptEncryption</key>
<false />
</dict>
</plist>

View file

@ -1,8 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>aps-environment</key>
<string>development</string>
</dict>
<dict>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.developer.icloud-container-identifiers</key>
<array />
<key>com.apple.developer.icloud-services</key>
<array>
<string>CloudDocuments</string>
</array>
<key>com.apple.developer.ubiquity-container-identifiers</key>
<array />
</dict>
</plist>

View file

@ -59,22 +59,9 @@ const metroConfig = makeMetroConfig({
},
transformer: {
...expoDefaultConfig.transformer,
// Metro default is "uglify-es" but terser should be faster and has better defaults.
minifierPath: 'metro-minify-terser',
minifierConfig: {
compress: {
drop_console: true,
// Sometimes improves performance?
reduce_funcs: false
},
format: {
ascii_only: true,
wrap_iife: true,
quote_style: 3
}
},
getTransformOptions: async () => ({
transform: {
// What does this do?
experimentalImportSupport: false,
inlineRequires: true
}

View file

@ -18,7 +18,7 @@
"dependencies": {
"@gorhom/bottom-sheet": "^4.4.5",
"@hookform/resolvers": "^3.1.0",
"@react-native-async-storage/async-storage": "~1.17.11",
"@react-native-async-storage/async-storage": "~1.17.12",
"@react-native-masked-view/masked-view": "0.2.8",
"@react-navigation/bottom-tabs": "^6.5.7",
"@react-navigation/drawer": "^6.6.2",
@ -28,12 +28,12 @@
"@rspc/react": "=0.0.0-main-a312a505",
"@sd/assets": "workspace:*",
"@sd/client": "workspace:*",
"@shopify/flash-list": "1.4.0",
"@tanstack/react-query": "^4.26.1",
"@shopify/flash-list": "1.4.2",
"@tanstack/react-query": "^4.29.1",
"byte-size": "^8.1.0",
"class-variance-authority": "^0.4.0",
"class-variance-authority": "^0.5.2",
"dayjs": "^1.11.5",
"expo": "~48.0.6",
"expo": "~48.0.10",
"expo-linking": "~4.0.1",
"expo-media-library": "~15.2.3",
"expo-splash-screen": "~0.18.1",
@ -45,30 +45,28 @@
"react": "18.2.0",
"react-hook-form": "^7.43.9",
"react-native": "0.71.3",
"react-native-document-picker": "^8.1.1",
"react-native-document-picker": "^8.2.0",
"react-native-fs": "^2.20.0",
"react-native-gesture-handler": "~2.9.0",
"react-native-popup-menu": "^0.16.1",
"react-native-reanimated": "~2.14.4",
"react-native-safe-area-context": "4.5.0",
"react-native-safe-area-context": "4.5.1",
"react-native-screens": "~3.20.0",
"react-native-svg": "13.4.0",
"react-native-wheel-color-picker": "^1.2.0",
"twrnc": "^3.6.0",
"use-count-up": "^3.0.1",
"use-debounce": "^9.0.2",
"valtio": "^1.10.3",
"use-debounce": "^9.0.4",
"valtio": "^1.10.4",
"zod": "^3.21.4"
},
"devDependencies": {
"@babel/core": "^7.21.0",
"@babel/core": "^7.21.4",
"@rnx-kit/metro-config": "^1.3.5",
"@sd/config": "workspace:*",
"@types/react": "~18.0.27",
"babel-plugin-module-resolver": "^5.0.0",
"eslint-plugin-react-native": "^4.0.0",
"metro-minify-terser": "0.76.0",
"react-native-svg-transformer": "^1.0.0",
"typescript": "^4.9.5"
"react-native-svg-transformer": "^1.0.0"
}
}

View file

@ -22,9 +22,36 @@ else
testFiles=$(ls apps/mobile/tests/*.yml apps/mobile/tests/android-only/*.yml)
fi
# Run onboarding first
onboardingFile="apps/mobile/tests/onboarding.yml"
if ! maestro test "$onboardingFile"
then
echo "Onboarding test failed. Retrying in 30 seconds..."
sleep 30
if ! maestro test "$onboardingFile"
then
echo "Onboarding test failed again. Retrying for the last time in 120 seconds..."
sleep 120
if ! maestro test "$onboardingFile"
then
echo "Onboarding test failed again. Exiting..."
exit 1
fi
fi
fi
failedTests=()
# Run the rest of the files
for file in $testFiles
do
# Skip onboarding.yml since it has already been run
if [ "$file" == "$onboardingFile" ]
then
continue
fi
if ! maestro test "$file"
then
echo "Test ${file} failed. Retrying in 30 seconds..."

View file

@ -2,7 +2,6 @@ import { BottomSheetModalProvider } from '@gorhom/bottom-sheet';
import {
DefaultTheme,
NavigationContainer,
Theme,
useNavigationContainerRef
} from '@react-navigation/native';
import { QueryClient } from '@tanstack/react-query';
@ -16,7 +15,6 @@ import { useEffect, useRef, useState } from 'react';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { MenuProvider } from 'react-native-popup-menu';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { useDeviceContext } from 'twrnc';
import { useSnapshot } from 'valtio';
import {
ClientContextProvider,
@ -28,7 +26,8 @@ import {
usePlausiblePageViewMonitor
} from '@sd/client';
import { GlobalModals } from './components/modal/GlobalModals';
import { tw } from './lib/tailwind';
import { useTheme } from './hooks/useTheme';
import { changeTwTheme, tw } from './lib/tailwind';
import RootNavigator from './navigation';
import OnboardingNavigator from './navigation/OnboardingNavigator';
import { currentLibraryStore } from './utils/nav';
@ -37,16 +36,10 @@ dayjs.extend(advancedFormat);
dayjs.extend(relativeTime);
dayjs.extend(duration);
const NavigatorTheme: Theme = {
...DefaultTheme,
colors: {
...DefaultTheme.colors,
// Default screen background
background: tw.color('app')!
}
};
initPlausible({ platformType: 'mobile' });
// changeTwTheme(getThemeStore().theme);
// TODO: Use above when light theme is ready
changeTwTheme('dark');
function AppNavigation() {
const { library } = useClientContext();
@ -67,7 +60,14 @@ function AppNavigation() {
onReady={() => {
routeNameRef.current = navRef.getCurrentRoute()?.name;
}}
theme={NavigatorTheme}
theme={{
...DefaultTheme,
colors: {
...DefaultTheme.colors,
// Default screen background
background: tw.color('app')!
}
}}
onStateChange={async () => {
const previousRouteName = routeNameRef.current;
const currentRouteName = navRef.getCurrentRoute()?.name;
@ -78,7 +78,6 @@ function AppNavigation() {
if (navRef.getRootState().routeNames.includes('GetStarted')) {
return;
}
console.log(`Navigated from ${previousRouteName} to ${currentRouteName}`);
currentRouteName && setCurrentPath(currentRouteName);
}
}}
@ -96,9 +95,7 @@ function AppNavigation() {
}
function AppContainer() {
// Enables dark mode, and screen size breakpoints, etc. for tailwind
useDeviceContext(tw, { withDeviceColorScheme: false });
useTheme();
useInvalidateQuery();
const { id } = useSnapshot(currentLibraryStore);

View file

@ -17,7 +17,7 @@ type DrawerTagItemProps = {
const DrawerTagItem: React.FC<DrawerTagItemProps> = (props) => {
const { tagName, tagColor, onPress } = props;
return (
<Pressable onPress={onPress}>
<Pressable onPress={onPress} testID="drawer-tag">
<View style={twStyle('mb-[4px] flex flex-row items-center rounded px-1 py-2')}>
<View style={twStyle('h-3.5 w-3.5 rounded-full', { backgroundColor: tagColor })} />
<Text style={twStyle('ml-2 text-sm font-medium text-gray-300')} numberOfLines={1}>

View file

@ -80,7 +80,7 @@ export default function FileThumb({ data, size = 1 }: FileThumbProps) {
icon = icons[kind];
}
// TODO: Handle video thumbnails
// TODO: Handle video thumbnails (do we have ffmpeg on mobile?)
// // 10 percent of the size
// const videoBarsHeight = Math.floor(size / 10);

View file

@ -1,3 +1,4 @@
import { BottomSheetTextInput } from '@gorhom/bottom-sheet';
import { VariantProps, cva } from 'class-variance-authority';
import { Eye, EyeSlash } from 'phosphor-react-native';
import { useState } from 'react';
@ -33,7 +34,19 @@ export const Input = ({ variant, size, ...props }: InputProps) => {
);
};
// Same as above but configured with password props & show/hide password button
// To use in modals (for keyboard handling)
export const ModalInput = ({ variant, size, ...props }: InputProps) => {
const { style, ...otherProps } = props;
return (
<BottomSheetTextInput
placeholderTextColor={tw.color('ink-faint')}
style={twStyle(input({ variant, size }), style as string)}
{...otherProps}
/>
);
};
// Same as Input but configured with password props & show/hide password button
type PasswordInputProps = InputProps & {
isNewPassword?: boolean;

View file

@ -110,7 +110,7 @@ export const ConfirmModal = forwardRef<ModalRef, ConfirmModalProps>((props, ref)
handleComponent={(props) =>
ModalHandle({ modalRef, showCloseButton: false, ...props })
}
snapPoints={props.snapPoints ?? ['25%']}
snapPoints={props.snapPoints ?? ['25']}
>
{/* Title */}
{props.title && (

View file

@ -131,7 +131,7 @@ const ImportModal = forwardRef<ModalRef, unknown>((_, ref) => {
// }, []);
return (
<Modal ref={modalRef} snapPoints={['25%']}>
<Modal ref={modalRef} snapPoints={['25']}>
<View style={tw`flex-1 px-8 pb-2 pt-8`}>
{/* <Button variant="accent" style={tw`my-2`} onPress={testFN}>
<Text>TEST</Text>

View file

@ -65,7 +65,7 @@ export const ActionsModal = () => {
return (
<>
<Modal ref={modalRef} snapPoints={['60%', '90%']}>
<Modal ref={modalRef} snapPoints={['60', '90']}>
{data && (
<View style={tw`flex-1 px-4`}>
<View style={tw`flex flex-row items-center`}>

View file

@ -64,7 +64,7 @@ const FileInfoModal = forwardRef<ModalRef, FileInfoModalProps>((props, ref) => {
ref={modalRef}
enableContentPanningGesture={false}
enablePanDownToClose={false}
snapPoints={['70%']}
snapPoints={['70']}
>
{data && (
<ModalScrollView style={tw`flex-1 p-4`}>

View file

@ -4,10 +4,11 @@ import { Pressable, Text, View } from 'react-native';
import ColorPicker from 'react-native-wheel-color-picker';
import { useLibraryMutation, usePlausibleEvent } from '@sd/client';
import { FadeInAnimation } from '~/components/animation/layout';
import { Input } from '~/components/form/Input';
import { ModalInput } from '~/components/form/Input';
import { Modal, ModalRef } from '~/components/layout/Modal';
import { Button } from '~/components/primitive/Button';
import useForwardedRef from '~/hooks/useForwardedRef';
import { useKeyboard } from '~/hooks/useKeyboard';
import { tw, twStyle } from '~/lib/tailwind';
const CreateTagModal = forwardRef<ModalRef, unknown>((_, ref) => {
@ -39,14 +40,20 @@ const CreateTagModal = forwardRef<ModalRef, unknown>((_, ref) => {
}
});
const { keyboardShown } = useKeyboard();
useEffect(() => {
modalRef.current?.snapToIndex(showPicker ? 1 : 0);
}, [modalRef, showPicker]);
if (!keyboardShown && showPicker) {
modalRef.current?.snapToPosition('58');
} else if (keyboardShown && showPicker) {
modalRef.current?.snapToPosition('94');
}
}, [keyboardShown, modalRef, showPicker]);
return (
<Modal
ref={modalRef}
snapPoints={['30%', '60%']}
snapPoints={['25']}
title="Create Tag"
onDismiss={() => {
// Resets form onDismiss
@ -54,18 +61,19 @@ const CreateTagModal = forwardRef<ModalRef, unknown>((_, ref) => {
setTagColor('#A717D9');
setShowPicker(false);
}}
showCloseButton
// Disable panning gestures
enableHandlePanningGesture={false}
enableContentPanningGesture={false}
showCloseButton
>
<View style={tw`p-4`}>
<View style={tw`mt-4 flex flex-row items-center`}>
<Pressable
onPress={() => setShowPicker((v) => !v)}
onPress={() => setShowPicker(true)}
style={twStyle({ backgroundColor: tagColor }, 'h-6 w-6 rounded-full')}
/>
<Input
<ModalInput
testID="create-tag-name"
style={tw`ml-2 flex-1`}
value={tagName}
onChangeText={(text) => setTagName(text)}
@ -75,7 +83,7 @@ const CreateTagModal = forwardRef<ModalRef, unknown>((_, ref) => {
{/* Color Picker */}
{showPicker && (
<FadeInAnimation>
<View style={tw`mt-4 h-64`}>
<View style={tw`my-4 h-64`}>
<ColorPicker
color={tagColor}
onColorChangeComplete={(color) => setTagColor(color)}

View file

@ -44,7 +44,7 @@ const UpdateTagModal = forwardRef<ModalRef, Props>((props, ref) => {
return (
<Modal
ref={modalRef}
snapPoints={['35%', '65%']}
snapPoints={['35', '65']}
onDismiss={() => {
// Resets form onDismiss
setShowPicker(false);

View file

@ -13,6 +13,20 @@ const StatItemNames: Partial<Record<keyof Statistics, string>> = {
total_bytes_free: 'Free space'
};
const displayableStatItems = Object.keys(StatItemNames) as unknown as keyof typeof StatItemNames;
const EMPTY_STATISTICS = {
id: 0,
date_captured: '',
total_bytes_capacity: '0',
preview_media_bytes: '0',
library_db_size: '0',
total_object_count: 0,
total_bytes_free: '0',
total_bytes_used: '0',
total_unique_bytes: '0'
};
const StatItem: FC<{ title: string; bytes: bigint }> = ({ title, bytes }) => {
const { value, unit } = byteSize(Number(bytes)); // TODO: This BigInt to Number conversion will truncate the number if the number is too large. `byteSize` doesn't support BigInt so we are gonna need to come up with a longer term solution at some point.
@ -30,9 +44,9 @@ const StatItem: FC<{ title: string; bytes: bigint }> = ({ title, bytes }) => {
};
const OverviewStats = () => {
// TODO: Add loading state
const { data: libraryStatistics } = useLibraryQuery(['library.getStatistics']);
const { data: libraryStatistics } = useLibraryQuery(['library.getStatistics'], {
initialData: { ...EMPTY_STATISTICS }
});
const displayableStatItems = Object.keys(
StatItemNames
@ -51,7 +65,9 @@ const OverviewStats = () => {
});
}, []);
return libraryStatistics ? (
if (!libraryStatistics) return null;
return (
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
{Object.entries(libraryStatistics).map(([key, bytesRaw]) => {
if (!displayableStatItems.includes(key)) return null;
@ -70,10 +86,6 @@ const OverviewStats = () => {
);
})}
</ScrollView>
) : (
<View>
<Text style={tw`text-center font-bold text-red-600`}>No library found...</Text>
</View>
);
};

View file

@ -19,5 +19,5 @@ export function SettingsContainer({ children, title, description }: SettingsCont
);
}
export const SettingsInputTitle = styled(Text, 'text-ink mb-1.5 ml-1 text-sm font-medium');
export const SettingsTitle = styled(Text, 'text-ink mb-1.5 ml-1 text-sm font-medium');
export const SettingsInputInfo = styled(Text, 'mt-2 text-xs text-ink-faint');

View file

@ -1,6 +1,6 @@
import { CaretRight, Icon } from 'phosphor-react-native';
import { Pressable, Text, View } from 'react-native';
import { tw } from '~/lib/tailwind';
import { Pressable, Text, View, ViewStyle } from 'react-native';
import { tw, twStyle } from '~/lib/tailwind';
type SettingsItemProps = {
title: string;
@ -28,9 +28,9 @@ export function SettingsItem(props: SettingsItemProps) {
);
}
export function SettingsItemDivider() {
export function SettingsItemDivider(props: { style?: ViewStyle }) {
return (
<View style={tw`bg-app-overlay`}>
<View style={twStyle('bg-app-overlay', props.style)}>
<View style={tw`mx-3 border-b border-b-app-line`} />
</View>
);

View file

@ -0,0 +1,120 @@
// We can override these values if needed (like desktop).
const LIGHT_HUE = 235;
const DARK_HUE = 235;
const ALPHA = 1;
const nonThemeColors = {
black: `hsla(0, 0%, 0%, ${ALPHA})`,
white: `hsla(0, 0%, 100%, ${ALPHA})`,
gray: {
DEFAULT: '#505468',
50: '#F1F1F4',
100: '#E8E9ED',
150: '#E0E1E6',
200: '#D8DAE3',
250: '#D2D4DC',
300: '#C0C2CE',
350: '#A6AABF',
400: '#9196A8',
450: '#71758A',
500: '#303544',
550: '#20222d',
600: '#171720',
650: '#121219',
700: '#121317',
750: '#0D0E11',
800: '#0C0C0F',
850: '#08090D',
900: '#060609',
950: '#030303'
}
};
module.exports = {
dark: {
...nonThemeColors,
// accent theme colors
accent: {
DEFAULT: `hsla(208, 100%, 57%, ${ALPHA})`,
faint: `hsla(208, 100%, 64%, ${ALPHA})`,
deep: `hsla(208, 100%, 47%, ${ALPHA})`
},
// text
ink: {
DEFAULT: `hsla(${DARK_HUE}, 0%, 100%, ${ALPHA})`,
light: `hsla(${DARK_HUE}, 0%, 82%, ${ALPHA})`,
dull: `hsla(${DARK_HUE}, 10%, 70%, ${ALPHA})`,
faint: `hsla(${DARK_HUE}, 10%, 55%, ${ALPHA})`
},
app: {
DEFAULT: `hsla(${DARK_HUE}, 15%, 13%, ${ALPHA})`,
// background (dark)
box: `hsla(${DARK_HUE}, 15%, 18%, ${ALPHA})`,
darkBox: `hsla(${DARK_HUE}, 15%, 7%, ${ALPHA})`,
// foreground (light)
overlay: `hsla(${DARK_HUE}, 15%, 17%, ${ALPHA})`,
// border
line: `hsla(${DARK_HUE}, 15%, 25%, ${ALPHA})`,
darkLine: `hsla(${DARK_HUE}, 15%, 7%, ${ALPHA})`,
// `selected` on desktop
highlight: `hsla(${DARK_HUE}, 15%, 26%, ${ALPHA})`,
// shadow
shade: `hsla(${DARK_HUE}, 15%, 0%, ${ALPHA})`,
// button
button: `hsla(${DARK_HUE}, 15%, 23%, ${ALPHA})`,
// menu
menu: `hsla(${DARK_HUE}, 25%, 5%, ${ALPHA})`,
// input
input: `hsla(${DARK_HUE}, 15%, 20%, ${ALPHA})`
},
sidebar: {
box: `hsla(${DARK_HUE}, 15%, 16%, ${ALPHA})`,
line: `hsla(${DARK_HUE}, 15%, 23%, ${ALPHA})`,
button: `hsla(${DARK_HUE}, 15%, 18%, ${ALPHA})`
}
},
vanilla: {
...nonThemeColors,
// accent theme colors
accent: {
DEFAULT: `hsla(208, 100%, 57%, ${ALPHA})`,
faint: `hsla(208, 100%, 67%, ${ALPHA})`,
deep: `hsla(208, 100%, 47%, ${ALPHA})`
},
// text
ink: {
DEFAULT: `hsla(${LIGHT_HUE}, 5%, 20%, ${ALPHA})`,
dull: `hsla(${LIGHT_HUE}, 5%, 30%, ${ALPHA})`,
faint: `hsla(${LIGHT_HUE}, 5%, 40%, ${ALPHA})`,
// TODO:
light: `hsla(${LIGHT_HUE}, 0%, 82%, ${ALPHA})`
},
app: {
DEFAULT: `hsla(${LIGHT_HUE}, 5%, 100%, ${ALPHA})`,
// background (dark)
box: `hsla(${LIGHT_HUE}, 5%, 98%, ${ALPHA})`,
darkBox: `hsla(${LIGHT_HUE}, 5%, 100%, ${ALPHA})`,
// foreground (light)
overlay: `hsla(${LIGHT_HUE}, 5%, 98%, ${ALPHA})`,
// border
line: `hsla(${LIGHT_HUE}, 5%, 90%, ${ALPHA})`,
// TODO:
darkLine: `hsla(${LIGHT_HUE}, 15%, 7%, ${ALPHA})`,
// `selected` on desktop
highlight: `hsla(${LIGHT_HUE}, 5%, 93%, ${ALPHA})`,
// shadow
shade: `hsla(${LIGHT_HUE}, 15%, 50%, ${ALPHA})`,
// button
button: `hsla(${LIGHT_HUE}, 5%, 100%, ${ALPHA})`,
// menu
menu: `hsla(${LIGHT_HUE}, 5%, 100%, ${ALPHA})`,
// input
input: `hsla(${LIGHT_HUE}, 5%, 100%, ${ALPHA})`
},
sidebar: {
box: `hsla(${LIGHT_HUE}, 5%, 100%, ${ALPHA})`,
line: `hsla(${LIGHT_HUE}, 10%, 85%, ${ALPHA})`,
button: `hsla(${LIGHT_HUE}, 15%, 100%, ${ALPHA})`
}
}
};

View file

@ -0,0 +1,16 @@
const COLORS = require('./Colors');
module.exports = function (theme) {
return {
content: ['./src/screens/**/*.{ts,tsx}', './src/components/**/*.{ts,tsx}', 'App.tsx'],
theme: {
extend: {
colors: theme ? COLORS[theme] : COLORS.dark
}
},
variants: {
extend: {}
},
plugins: []
};
};

View file

@ -0,0 +1,62 @@
import { useEffect, useState } from 'react';
import { Keyboard, KeyboardEventListener, KeyboardMetrics } from 'react-native';
const emptyCoordinates = Object.freeze({
screenX: 0,
screenY: 0,
width: 0,
height: 0
});
const initialValue = {
start: emptyCoordinates,
end: emptyCoordinates
};
export function useKeyboard() {
const [shown, setShown] = useState(false);
const [coordinates, setCoordinates] = useState<{
start: undefined | KeyboardMetrics;
end: KeyboardMetrics;
}>(initialValue);
const [keyboardHeight, setKeyboardHeight] = useState<number>(0);
const handleKeyboardWillShow: KeyboardEventListener = (e) => {
setCoordinates({ start: e.startCoordinates, end: e.endCoordinates });
};
const handleKeyboardDidShow: KeyboardEventListener = (e) => {
setShown(true);
setCoordinates({ start: e.startCoordinates, end: e.endCoordinates });
setKeyboardHeight(e.endCoordinates.height);
};
const handleKeyboardWillHide: KeyboardEventListener = (e) => {
setCoordinates({ start: e.startCoordinates, end: e.endCoordinates });
};
const handleKeyboardDidHide: KeyboardEventListener = (e) => {
setShown(false);
if (e) {
setCoordinates({ start: e.startCoordinates, end: e.endCoordinates });
} else {
setCoordinates(initialValue);
setKeyboardHeight(0);
}
};
useEffect(() => {
const subscriptions = [
Keyboard.addListener('keyboardWillShow', handleKeyboardWillShow),
Keyboard.addListener('keyboardDidShow', handleKeyboardDidShow),
Keyboard.addListener('keyboardWillHide', handleKeyboardWillHide),
Keyboard.addListener('keyboardDidHide', handleKeyboardDidHide)
];
return () => {
subscriptions.forEach((subscription) => subscription.remove());
};
}, []);
return {
keyboardShown: shown,
coordinates,
keyboardHeight
};
}

View file

@ -0,0 +1,38 @@
import { useEffect, useReducer } from 'react';
import { Appearance, NativeEventSubscription } from 'react-native';
import { useDeviceContext } from 'twrnc';
import { subscribe } from 'valtio';
import { getThemeStore } from '@sd/client';
import { changeTwTheme, tw } from '~/lib/tailwind';
export function useTheme() {
// Enables screen size breakpoints, etc. for tailwind
useDeviceContext(tw, { withDeviceColorScheme: false });
const [_, forceUpdate] = useReducer((x) => x + 1, 0);
useEffect(() => {
const unsubscribe = subscribe(getThemeStore(), () => {
changeTwTheme(getThemeStore().theme);
forceUpdate();
});
return () => {
unsubscribe();
};
}, []);
useEffect(() => {
let systemThemeListener: NativeEventSubscription | undefined;
if (getThemeStore().syncThemeWithSystem === true) {
systemThemeListener = Appearance.addChangeListener(({ colorScheme }) => {
changeTwTheme(colorScheme === 'dark' ? 'dark' : 'vanilla');
forceUpdate();
});
}
return () => {
systemThemeListener?.remove();
};
}, []);
}

View file

@ -1,11 +1,16 @@
import React, { ComponentType } from 'react';
import { ComponentType, createElement, forwardRef } from 'react';
import { create } from 'twrnc';
import { Themes } from '@sd/client';
const tw = create(require(`../../tailwind.config.js`));
let tw = create(require('../constants/style/tailwind.js')());
function styled<P>(Component: ComponentType<P>, baseStyles?: string) {
return React.forwardRef<ComponentType<P>, P>(({ style, ...props }: any, ref) =>
React.createElement(Component as any, {
export function changeTwTheme(theme: Themes) {
tw = create(require('../constants/style/tailwind.js')(theme));
}
export function styled<P>(Component: ComponentType<P>, baseStyles?: string) {
return forwardRef<ComponentType<P>, P>(({ style, ...props }: any, ref) =>
createElement(Component as any, {
...props,
style: twStyle(baseStyles, style),
ref
@ -14,10 +19,10 @@ function styled<P>(Component: ComponentType<P>, baseStyles?: string) {
}
// Same as clsx, this works with the eslint plugin (tailwindcss/classnames-order).
const twStyle = tw.style;
export const twStyle = tw.style;
tw.style = () => {
throw new Error('Use twStyle instead of tw.style');
};
export { tw, twStyle, styled };
export { tw };

View file

@ -1,7 +1,6 @@
import { BottomTabScreenProps, createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { CompositeScreenProps, NavigatorScreenParams } from '@react-navigation/native';
import { Broadcast, CirclesFour, Planet, ShareNetwork } from 'phosphor-react-native';
import React from 'react';
import { Broadcast, CirclesFour, Planet } from 'phosphor-react-native';
import { tw } from '~/lib/tailwind';
import type { HomeDrawerScreenProps } from './DrawerNavigator';
import OverviewStack, { OverviewStackParamList } from './tabs/OverviewStack';

View file

@ -64,7 +64,10 @@ const PrivacyScreen = ({ navigation }: OnboardingStackScreenProps<'Privacy'>) =>
style={tw`mb-3 mt-4`}
/>
</Pressable>
<Pressable onPress={() => setShareTelemetry('no-share-telemetry')}>
<Pressable
testID="share-nothing"
onPress={() => setShareTelemetry('no-share-telemetry')}
>
<RadioButton
title="Share nothing"
description="Do not share any telemetry data with the developers"

View file

@ -1,14 +1,156 @@
import React from 'react';
import { Text, View } from 'react-native';
import { tw } from '~/lib/tailwind';
import { CheckCircle } from 'phosphor-react-native';
import React, { useState } from 'react';
import { ColorValue, Pressable, ScrollView, Text, View, ViewStyle } from 'react-native';
import { Themes, useThemeStore } from '@sd/client';
import { SettingsTitle } from '~/components/settings/SettingsContainer';
import Colors from '~/constants/style/Colors';
import { tw, twStyle } from '~/lib/tailwind';
import { SettingsStackScreenProps } from '~/navigation/SettingsNavigator';
type Theme = {
insideColor: ColorValue;
outsideColor: ColorValue;
textColor: ColorValue;
highlightColor: ColorValue;
themeName: string;
themeValue: Themes | 'system';
};
// TODO: Once theming is fixed, use theme values for Light theme too.
const themes: Theme[] = [
{
insideColor: Colors.vanilla.app.DEFAULT,
outsideColor: '#F0F0F0',
textColor: Colors.vanilla.ink.DEFAULT,
highlightColor: '#E6E6E6',
themeName: 'Light',
themeValue: 'vanilla'
},
{
insideColor: Colors.dark.app.DEFAULT,
outsideColor: Colors.dark.app.darkBox,
textColor: Colors.dark.ink.DEFAULT,
highlightColor: Colors.dark.app.line,
themeName: 'Dark',
themeValue: 'dark'
},
{
insideColor: '#000000',
outsideColor: '#000000',
textColor: '#000000',
highlightColor: '#000000',
themeName: 'System',
themeValue: 'system'
}
];
type ThemeProps = Theme & { isSelected?: boolean; containerStyle?: ViewStyle };
function Theme(props: ThemeProps) {
return (
<View style={twStyle(props.containerStyle)}>
<View
style={twStyle(
{ backgroundColor: props.outsideColor },
'relative h-[80px] w-[100px] overflow-hidden rounded-xl'
)}
>
<View
style={twStyle(
{ backgroundColor: props.insideColor, borderColor: props.highlightColor },
'absolute bottom-[-1px] right-[-1px] h-[60px] w-[75px] rounded-tl-xl border'
)}
>
<Text
style={twStyle({ color: props.textColor }, 'ml-3 mt-1 text-lg font-medium')}
>
Aa
</Text>
</View>
{/* Checkmark */}
{props.isSelected && (
<CheckCircle
color={props.textColor as string}
weight="fill"
size={24}
style={tw`absolute bottom-1.5 right-1.5`}
/>
)}
</View>
</View>
);
}
function SystemTheme(props: { isSelected: boolean }) {
return (
<View style={tw`h-[90px] w-[110px] flex-1 flex-row overflow-hidden rounded-xl`}>
<View
style={twStyle('flex-1 overflow-hidden', {
backgroundColor: themes[1]!.outsideColor
})}
>
<View style={tw`absolute`}>
<Theme {...themes[1]!} containerStyle={tw`right-3`} />
</View>
</View>
<View
style={twStyle(' flex-1 overflow-hidden', {
backgroundColor: themes[0]!.outsideColor
})}
>
<Theme {...themes[0]!} containerStyle={tw`right-3`} />
</View>
{/* Checkmark */}
{props.isSelected && (
<CheckCircle
color={'black'}
weight="fill"
size={24}
style={tw`absolute bottom-1.5 right-1.5`}
/>
)}
</View>
);
}
const AppearanceSettingsScreen = ({
navigation
}: SettingsStackScreenProps<'AppearanceSettings'>) => {
const themeStore = useThemeStore();
const [selectedTheme, setSelectedTheme] = useState<Theme['themeValue']>(
themeStore.syncThemeWithSystem === true ? 'system' : themeStore.theme
);
// TODO: Hook this up to the theme store once light theme is fixed.
return (
<View>
<Text style={tw`text-ink`}>TODO: Theme Switch</Text>
<View style={tw`flex-1 pt-4`}>
<View style={tw`px-4`}>
<SettingsTitle>Theme</SettingsTitle>
<View style={tw`mb-4 border-b border-b-app-line`} />
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={tw`gap-x-3`}
>
{themes.map((theme) => (
<Pressable
key={theme.themeValue}
onPress={() => setSelectedTheme(theme.themeValue)}
>
{theme.themeValue === 'system' ? (
<SystemTheme isSelected={selectedTheme === 'system'} />
) : (
<Theme {...theme} isSelected={selectedTheme === theme.themeValue} />
)}
<Text style={tw`mt-1.5 text-center font-medium text-white`}>
{theme.themeName}
</Text>
</Pressable>
))}
</ScrollView>
</View>
</View>
);
};

View file

@ -3,7 +3,7 @@ import { useBridgeQuery } from '@sd/client';
import { Input } from '~/components/form/Input';
import Card from '~/components/layout/Card';
import { Divider } from '~/components/primitive/Divider';
import { SettingsInputTitle } from '~/components/settings/SettingsContainer';
import { SettingsTitle } from '~/components/settings/SettingsContainer';
import { tw } from '~/lib/tailwind';
import { SettingsStackScreenProps } from '~/navigation/SettingsNavigator';
@ -32,9 +32,9 @@ const GeneralSettingsScreen = ({ navigation }: SettingsStackScreenProps<'General
{/* Divider */}
<Divider style={tw`mb-4 mt-2`} />
{/* Node Name and Port */}
<SettingsInputTitle>Node Name</SettingsInputTitle>
<SettingsTitle>Node Name</SettingsTitle>
<Input value={node.name} />
<SettingsInputTitle style={tw`mt-3`}>Node Port</SettingsInputTitle>
<SettingsTitle style={tw`mt-3`}>Node Port</SettingsTitle>
<Input value={node.p2p_port?.toString() ?? '5795'} keyboardType="numeric" />
</Card>
</View>

View file

@ -1,22 +1,22 @@
import { useQueryClient } from '@tanstack/react-query';
import { Archive, ArrowsClockwise, Trash } from 'phosphor-react-native';
import React from 'react';
import { useEffect } from 'react';
import { Controller } from 'react-hook-form';
import { Alert, ScrollView, Text, View } from 'react-native';
import { useLibraryMutation, useLibraryQuery } from '@sd/client';
import { Input } from '~/components/form/Input';
import { Switch } from '~/components/form/Switch';
import DeleteLocationModal from '~/components/modal/confirm-modals/DeleteLocationModal';
import { AnimatedButton, Button, FakeButton } from '~/components/primitive/Button';
import { AnimatedButton, FakeButton } from '~/components/primitive/Button';
import { Divider } from '~/components/primitive/Divider';
import {
SettingsContainer,
SettingsInputInfo,
SettingsInputTitle
SettingsTitle
} from '~/components/settings/SettingsContainer';
import { SettingsItem } from '~/components/settings/SettingsItem';
import { useZodForm, z } from '~/hooks/useZodForm';
import { tw } from '~/lib/tailwind';
import { tw, twStyle } from '~/lib/tailwind';
import { SettingsStackScreenProps } from '~/navigation/SettingsNavigator';
const schema = z.object({
@ -58,28 +58,37 @@ const EditLocationSettingsScreen = ({
})
);
navigation.setOptions({
headerRight: () => (
<View style={tw`mr-1 flex flex-row gap-x-1`}>
{form.formState.isDirty && (
useEffect(() => {
navigation.setOptions({
headerRight: () => (
<View style={tw`mr-1 flex flex-row gap-x-1`}>
{form.formState.isDirty && (
<AnimatedButton
variant="outline"
onPress={() => form.reset()}
disabled={!form.formState.isDirty}
>
<Text style={tw`text-white`}>Reset</Text>
</AnimatedButton>
)}
<AnimatedButton
variant="outline"
onPress={() => form.reset()}
disabled={!form.formState.isDirty}
onPress={onSubmit}
disabled={!form.formState.isDirty || form.formState.isSubmitting}
variant={form.formState.isDirty ? 'accent' : 'outline'}
>
<Text style={tw`text-white`}>Reset</Text>
<Text
style={twStyle(
'font-medium',
form.formState.isDirty ? 'text-white' : ' text-ink-faint'
)}
>
Save
</Text>
</AnimatedButton>
)}
<AnimatedButton
onPress={onSubmit}
disabled={!form.formState.isDirty || form.formState.isSubmitting}
variant={form.formState.isDirty ? 'accent' : 'outline'}
>
<Text style={tw`font-bold text-white`}>Save</Text>
</AnimatedButton>
</View>
)
});
</View>
)
});
}, [form, navigation, onSubmit]);
useLibraryQuery(['locations.getById', id], {
onSuccess: (data) => {
@ -101,7 +110,7 @@ const EditLocationSettingsScreen = ({
<ScrollView contentContainerStyle={tw`gap-y-6 pb-12 pt-4`}>
{/* Inputs */}
<View style={tw`px-2`}>
<SettingsInputTitle>Display Name</SettingsInputTitle>
<SettingsTitle>Display Name</SettingsTitle>
<Controller
name="displayName"
control={form.control}
@ -114,7 +123,7 @@ const EditLocationSettingsScreen = ({
not rename the actual folder on disk.
</SettingsInputInfo>
<SettingsInputTitle style={tw`mt-3`}>Local Path</SettingsInputTitle>
<SettingsTitle style={tw`mt-3`}>Local Path</SettingsTitle>
<Controller
name="localPath"
control={form.control}

View file

@ -8,7 +8,7 @@ import { Switch } from '~/components/form/Switch';
import DeleteLibraryModal from '~/components/modal/confirm-modals/DeleteLibraryModal';
import { FakeButton } from '~/components/primitive/Button';
import { Divider } from '~/components/primitive/Divider';
import { SettingsContainer, SettingsInputTitle } from '~/components/settings/SettingsContainer';
import { SettingsContainer, SettingsTitle } from '~/components/settings/SettingsContainer';
import { SettingsItem } from '~/components/settings/SettingsItem';
import { useAutoForm } from '~/hooks/useAutoForm';
import { useZodForm, z } from '~/hooks/useZodForm';
@ -38,7 +38,7 @@ const LibraryGeneralSettingsScreen = ({
return (
<View style={tw`gap-4`}>
<View style={tw`mt-4 px-2`}>
<SettingsInputTitle>Name</SettingsInputTitle>
<SettingsTitle>Name</SettingsTitle>
<Controller
name="name"
control={form.control}
@ -47,7 +47,7 @@ const LibraryGeneralSettingsScreen = ({
)}
/>
{/* Description */}
<SettingsInputTitle style={tw`mt-4`}>Description</SettingsInputTitle>
<SettingsTitle style={tw`mt-4`}>Description</SettingsTitle>
<Controller
name="description"
control={form.control}

View file

@ -6,7 +6,7 @@ import { Tag, useLibraryQuery } from '@sd/client';
import { ModalRef } from '~/components/layout/Modal';
import DeleteTagModal from '~/components/modal/confirm-modals/DeleteTagModal';
import UpdateTagModal from '~/components/modal/tag/UpdateTagModal';
import { AnimatedButton } from '~/components/primitive/Button';
import { AnimatedButton, FakeButton } from '~/components/primitive/Button';
import { tw, twStyle } from '~/lib/tailwind';
import { SettingsStackScreenProps } from '~/navigation/SettingsNavigator';
@ -39,9 +39,9 @@ function TagItem({ tag, index }: { tag: Tag; index: number }) {
<DeleteTagModal
tagId={tag.id}
trigger={
<AnimatedButton style={tw`mx-2`}>
<FakeButton style={tw`mx-2`}>
<Trash size={18} color="white" />
</AnimatedButton>
</FakeButton>
}
/>
</Animated.View>

View file

@ -1,96 +1 @@
// Extended colors are copied from packages/ui/style/colors.scss
module.exports = {
content: ['./screens/**/*.{js,ts,jsx}', './components/**/*.{js,ts,jsx}', 'App.tsx'],
theme: {
extend: {
colors: {
// Brand blue
accent: {
DEFAULT: 'hsla(208, 100%, 47%, 1)',
faint: 'hsla(208, 100%, 64%, 1)',
deep: 'hsla(208, 100%, 47%, 1)'
},
ink: {
DEFAULT: 'hsla(230, 0%, 100%, 1)',
light: 'hsla(230, 0%, 82%, 1)',
dull: 'hsla(230, 10%, 70%, 1)',
faint: 'hsla(230, 10%, 55%, 1)'
},
// Brand gray
app: {
DEFAULT: 'hsla(230, 15%, 13%, 1)',
// background (dark)
box: 'hsla(230, 15%, 17%, 1)',
darkBox: 'hsla(230, 15%, 7%, 1)',
// foreground (light)
overlay: 'hsla(230, 15%, 19%, 1)',
// border
line: 'hsla(230, 15%, 25%, 1)',
darkLine: 'hsla(230, 15%, 7%, 1)',
// 'selected' on desktop
highlight: 'hsla(230, 15%, 27%, 1)',
// shadow
shade: 'hsla(230, 15%, 0%, 1)',
// button
button: 'hsla(230, 15%, 23%, 1)',
// menu
menu: 'hsla(230, 25%, 5%, 1)',
// input
input: 'hsla(230, 15%, 20%, 1)',
50: 'hsla(230, 15%, 5%, 1)',
100: 'hsla(230, 15%, 10%, 1)',
150: 'hsla(230, 15%, 15%, 1)',
200: 'hsla(230, 15%, 20%, 1)',
250: 'hsla(230, 15%, 30%, 1)',
300: 'hsla(230, 15%, 35%, 1)',
350: 'hsla(230, 15%, 40%, 1)',
450: 'hsla(230, 15%, 45%, 1)',
500: 'hsla(230, 15%, 50%, 1)',
550: 'hsla(230, 15%, 55%, 1)',
600: 'hsla(230, 15%, 60%, 1)',
650: 'hsla(230, 15%, 65%, 1)',
700: 'hsla(230, 15%, 70%, 1)',
750: 'hsla(230, 15%, 75%, 1)',
800: 'hsla(230, 15%, 80%, 1)',
850: 'hsla(230, 15%, 85%, 1)',
900: 'hsla(230, 15%, 90%, 1)',
950: 'hsla(230, 15%, 95%, 1)',
1000: 'hsla(230, 15%, 100%, 1)'
},
sidebar: {
box: 'hsla(230, 15%, 16%, 1)',
line: 'hsla(230, 15%, 23%, 1)',
button: 'hsla(230, 15%, 18%, 1)'
},
gray: {
DEFAULT: '#505468',
50: '#F1F1F4',
100: '#E8E9ED',
150: '#E0E1E6',
200: '#D8DAE3',
250: '#D2D4DC',
300: '#C0C2CE',
350: '#A6AABF',
400: '#9196A8',
450: '#71758A',
500: '#303544',
550: '#20222d',
600: '#171720',
650: '#121219',
700: '#121317',
750: '#0D0E11',
800: '#0C0C0F',
850: '#08090D',
900: '#060609',
950: '#030303'
}
},
extend: {}
}
},
variants: {
extend: {}
},
plugins: []
};
module.exports = require('./src/constants/style/tailwind.js')();

View file

@ -0,0 +1,12 @@
appId: com.spacedrive.app
---
- launchApp
- tapOn:
id: 'drawer-toggle'
- tapOn: 'Add Tag'
- tapOn:
id: 'create-tag-name'
- inputText: 'MyTag'
- tapOn: Create
- assertVisible:
id: 'drawer-tag'

View file

@ -7,12 +7,9 @@ appId: com.spacedrive.app
id: 'library-name'
- inputText: 'TestLib'
- tapOn: 'New Library'
- tapOn:
id: 'share-nothing'
- tapOn: 'Continue'
# Library creation can take a while...
- extendedWaitUntil:
visible:
id: 'drawer-toggle'
timeout: 180000 # 3 minutes
- tapOn:
id: 'drawer-toggle'
- assertVisible: 'TestLib'

View file

@ -211,15 +211,6 @@ const submitPlausibleEvent = async ({ event, debugState, ...props }: SubmitEvent
);
};
interface UsePlausibleEventProps {
/**
* The current platform type. This should be the output of `usePlatform().platform`
*
* @see {@link PlausiblePlatformType}
*/
platformType: PlausiblePlatformType;
}
interface EventSubmissionCallbackProps {
/**
* The plausible event to submit.

View file

@ -3,3 +3,5 @@ export * from './themeStore';
export * from './util';
export * from './onboardingStore';
export * from './telemetryState';
// NOTE: Rule of thumb: read from snapshots, mutate the source.

View file

@ -1,17 +1,18 @@
import { useSnapshot } from 'valtio';
import { valtioPersist } from './util';
const appThemeStore = valtioPersist('appTheme', {
themeName: 'vanilla',
themeMode: 'light' as 'light' | 'dark',
export type Themes = 'vanilla' | 'dark';
const themeStore = valtioPersist('sd-theme', {
theme: 'vanilla' as Themes,
syncThemeWithSystem: false,
hueValue: null as number | null
});
export function useThemeStore() {
return useSnapshot(appThemeStore);
return useSnapshot(themeStore);
}
export function getThemeStore() {
return appThemeStore;
return themeStore;
}

View file

@ -9,9 +9,6 @@
"scripts": {
"lint": "eslint . --cache"
},
"files": [
"eslint-react.js"
],
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^5.58.0",
"@typescript-eslint/parser": "^5.58.0",

View file

@ -179,10 +179,10 @@ importers:
apps/mobile:
specifiers:
'@babel/core': ^7.21.0
'@babel/core': ^7.21.4
'@gorhom/bottom-sheet': ^4.4.5
'@hookform/resolvers': ^3.1.0
'@react-native-async-storage/async-storage': ~1.17.11
'@react-native-async-storage/async-storage': ~1.17.12
'@react-native-masked-view/masked-view': 0.2.8
'@react-navigation/bottom-tabs': ^6.5.7
'@react-navigation/drawer': ^6.6.2
@ -194,60 +194,58 @@ importers:
'@sd/assets': workspace:*
'@sd/client': workspace:*
'@sd/config': workspace:*
'@shopify/flash-list': 1.4.0
'@tanstack/react-query': ^4.26.1
'@shopify/flash-list': 1.4.2
'@tanstack/react-query': ^4.29.1
'@types/react': ~18.0.27
babel-plugin-module-resolver: ^5.0.0
byte-size: ^8.1.0
class-variance-authority: ^0.4.0
class-variance-authority: ^0.5.2
dayjs: ^1.11.5
eslint-plugin-react-native: ^4.0.0
expo: ~48.0.6
expo: ~48.0.10
expo-linking: ~4.0.1
expo-media-library: ~15.2.3
expo-splash-screen: ~0.18.1
expo-status-bar: ~1.4.4
intl: ^1.2.5
lottie-react-native: 5.1.4
metro-minify-terser: 0.76.0
moti: ^0.24.2
phosphor-react-native: ^1.1.2
react: 18.2.0
react-hook-form: ^7.43.9
react-native: 0.71.3
react-native-document-picker: ^8.1.1
react-native-document-picker: ^8.2.0
react-native-fs: ^2.20.0
react-native-gesture-handler: ~2.9.0
react-native-popup-menu: ^0.16.1
react-native-reanimated: ~2.14.4
react-native-safe-area-context: 4.5.0
react-native-safe-area-context: 4.5.1
react-native-screens: ~3.20.0
react-native-svg: 13.4.0
react-native-svg-transformer: ^1.0.0
react-native-wheel-color-picker: ^1.2.0
twrnc: ^3.6.0
typescript: ^4.9.5
use-count-up: ^3.0.1
use-debounce: ^9.0.2
valtio: ^1.10.3
use-debounce: ^9.0.4
valtio: ^1.10.4
zod: ^3.21.4
dependencies:
'@gorhom/bottom-sheet': 4.4.6_jr3t7femuevp5e62naouyiqwum
'@hookform/resolvers': 3.1.0_react-hook-form@7.43.9
'@react-native-async-storage/async-storage': 1.17.12_react-native@0.71.3
'@react-native-masked-view/masked-view': 0.2.8_yqouayos4dnow7nnkhah4yzuzq
'@react-navigation/bottom-tabs': 6.5.7_lzeolizq5fdjozeb3mtrlbatoi
'@react-navigation/drawer': 6.6.2_7vhdhx5mqw5r775ehb5p4uprq4
'@react-navigation/bottom-tabs': 6.5.7_5y26adjgtb7dnfz3snlqtvb734
'@react-navigation/drawer': 6.6.2_r3wrakhng3tz4ogutvz6753mze
'@react-navigation/native': 6.1.6_yqouayos4dnow7nnkhah4yzuzq
'@react-navigation/stack': 6.3.16_pf4j4f7edywtcmcmtmj6rq4iri
'@react-navigation/stack': 6.3.16_azgz2xa2gctylhtvozsxdprjtq
'@rspc/client': 0.0.0-main-a312a505
'@rspc/react': 0.0.0-main-a312a505_ae5ckqy7cfj5x44ydgdrnj6j7q
'@sd/assets': link:../../packages/assets
'@sd/client': link:../../packages/client
'@shopify/flash-list': 1.4.0_aim4q6r6ykgo64vi4fl5665p4i
'@shopify/flash-list': 1.4.2_aim4q6r6ykgo64vi4fl5665p4i
'@tanstack/react-query': 4.29.5_bblof3d6od7kdqr2urb3dykbey
byte-size: 8.1.1
class-variance-authority: 0.4.0_typescript@4.9.5
class-variance-authority: 0.5.3
dayjs: 1.11.7
expo: 48.0.16_@babel+core@7.21.8
expo-linking: 4.0.1_expo@48.0.16
@ -266,7 +264,7 @@ importers:
react-native-gesture-handler: 2.9.0_yqouayos4dnow7nnkhah4yzuzq
react-native-popup-menu: 0.16.1
react-native-reanimated: 2.14.4_4bda5deopy4tuaohhmpt3ilfxa
react-native-safe-area-context: 4.5.0_yqouayos4dnow7nnkhah4yzuzq
react-native-safe-area-context: 4.5.1_yqouayos4dnow7nnkhah4yzuzq
react-native-screens: 3.20.0_yqouayos4dnow7nnkhah4yzuzq
react-native-svg: 13.4.0_yqouayos4dnow7nnkhah4yzuzq
react-native-wheel-color-picker: 1.2.0
@ -282,9 +280,7 @@ importers:
'@types/react': 18.0.38
babel-plugin-module-resolver: 5.0.0
eslint-plugin-react-native: 4.0.0_eslint@8.39.0
metro-minify-terser: 0.76.0
react-native-svg-transformer: 1.0.0_csdbj5z546ts5ngzc62z27kx4u
typescript: 4.9.5
apps/releases:
specifiers:
@ -676,7 +672,7 @@ importers:
react-dom: 18.2.0_react@18.2.0
react-loading-icons: 1.1.0
react-router-dom: 6.9.0_biqbaboplfbrettd7655fr4n2y
react-spring: 9.7.1_xvilhl2zf3rx3r4v7bihziaory
react-spring: 9.7.1_xu7fsxwv7ced4b5547wygkrwiy
tailwindcss-radix: 2.8.0
use-debounce: 9.0.4_react@18.2.0
zod: 3.21.4
@ -4701,7 +4697,7 @@ packages:
/@react-native/polyfills/2.0.0:
resolution: {integrity: sha512-K0aGNn1TjalKj+65D7ycc1//H9roAQ51GJVk5ZJQFb2teECGmzd86bYDC0aYdbRf7gtovescq4Zt6FR0tgXiHQ==}
/@react-navigation/bottom-tabs/6.5.7_lzeolizq5fdjozeb3mtrlbatoi:
/@react-navigation/bottom-tabs/6.5.7_5y26adjgtb7dnfz3snlqtvb734:
resolution: {integrity: sha512-9oZYyRu2z7+1pr2dX5V54rHFPmlj4ztwQxFe85zwpnGcPtGIsXj7VCIdlHnjRHJBBFCszvJGQpYY6/G2+DfD+A==}
peerDependencies:
'@react-navigation/native': ^6.0.0
@ -4710,12 +4706,12 @@ packages:
react-native-safe-area-context: '>= 3.0.0'
react-native-screens: '>= 3.0.0'
dependencies:
'@react-navigation/elements': 1.3.17_3q57rrzrqjmhzcnf6o45kky2ma
'@react-navigation/elements': 1.3.17_242f5xi7fk7ywkpkaltx3no7gi
'@react-navigation/native': 6.1.6_yqouayos4dnow7nnkhah4yzuzq
color: 4.2.3
react: 18.2.0
react-native: 0.71.3_3ap3zwt6eikbjp3skxdjmrinky
react-native-safe-area-context: 4.5.0_yqouayos4dnow7nnkhah4yzuzq
react-native-safe-area-context: 4.5.1_yqouayos4dnow7nnkhah4yzuzq
react-native-screens: 3.20.0_yqouayos4dnow7nnkhah4yzuzq
warn-once: 0.1.1
dev: false
@ -4734,7 +4730,7 @@ packages:
use-latest-callback: 0.1.6_react@18.2.0
dev: false
/@react-navigation/drawer/6.6.2_7vhdhx5mqw5r775ehb5p4uprq4:
/@react-navigation/drawer/6.6.2_r3wrakhng3tz4ogutvz6753mze:
resolution: {integrity: sha512-6qt4guBdz7bkdo/8BLSCcFNdQdSPYyNn05D9cD+VCY3mGThSiD8bRiP9ju+64im7LsSU+bNWXaP8RxA/FtTVQg==}
peerDependencies:
'@react-navigation/native': ^6.0.0
@ -4745,19 +4741,19 @@ packages:
react-native-safe-area-context: '>= 3.0.0'
react-native-screens: '>= 3.0.0'
dependencies:
'@react-navigation/elements': 1.3.17_3q57rrzrqjmhzcnf6o45kky2ma
'@react-navigation/elements': 1.3.17_242f5xi7fk7ywkpkaltx3no7gi
'@react-navigation/native': 6.1.6_yqouayos4dnow7nnkhah4yzuzq
color: 4.2.3
react: 18.2.0
react-native: 0.71.3_3ap3zwt6eikbjp3skxdjmrinky
react-native-gesture-handler: 2.9.0_yqouayos4dnow7nnkhah4yzuzq
react-native-reanimated: 2.14.4_4bda5deopy4tuaohhmpt3ilfxa
react-native-safe-area-context: 4.5.0_yqouayos4dnow7nnkhah4yzuzq
react-native-safe-area-context: 4.5.1_yqouayos4dnow7nnkhah4yzuzq
react-native-screens: 3.20.0_yqouayos4dnow7nnkhah4yzuzq
warn-once: 0.1.1
dev: false
/@react-navigation/elements/1.3.17_3q57rrzrqjmhzcnf6o45kky2ma:
/@react-navigation/elements/1.3.17_242f5xi7fk7ywkpkaltx3no7gi:
resolution: {integrity: sha512-sui8AzHm6TxeEvWT/NEXlz3egYvCUog4tlXA4Xlb2Vxvy3purVXDq/XsM56lJl344U5Aj/jDzkVanOTMWyk4UA==}
peerDependencies:
'@react-navigation/native': ^6.0.0
@ -4768,7 +4764,7 @@ packages:
'@react-navigation/native': 6.1.6_yqouayos4dnow7nnkhah4yzuzq
react: 18.2.0
react-native: 0.71.3_3ap3zwt6eikbjp3skxdjmrinky
react-native-safe-area-context: 4.5.0_yqouayos4dnow7nnkhah4yzuzq
react-native-safe-area-context: 4.5.1_yqouayos4dnow7nnkhah4yzuzq
dev: false
/@react-navigation/native/6.1.6_yqouayos4dnow7nnkhah4yzuzq:
@ -4791,7 +4787,7 @@ packages:
nanoid: 3.3.6
dev: false
/@react-navigation/stack/6.3.16_pf4j4f7edywtcmcmtmj6rq4iri:
/@react-navigation/stack/6.3.16_azgz2xa2gctylhtvozsxdprjtq:
resolution: {integrity: sha512-KTOn9cNuZ6p154Htbl2DiR95Wl+c7niLPRiGs7gjOkyVDGiaGQF9ODNQTYBDE1OxZGHe/EyYc6T2CbmiItLWDg==}
peerDependencies:
'@react-navigation/native': ^6.0.0
@ -4801,13 +4797,13 @@ packages:
react-native-safe-area-context: '>= 3.0.0'
react-native-screens: '>= 3.0.0'
dependencies:
'@react-navigation/elements': 1.3.17_3q57rrzrqjmhzcnf6o45kky2ma
'@react-navigation/elements': 1.3.17_242f5xi7fk7ywkpkaltx3no7gi
'@react-navigation/native': 6.1.6_yqouayos4dnow7nnkhah4yzuzq
color: 4.2.3
react: 18.2.0
react-native: 0.71.3_3ap3zwt6eikbjp3skxdjmrinky
react-native-gesture-handler: 2.9.0_yqouayos4dnow7nnkhah4yzuzq
react-native-safe-area-context: 4.5.0_yqouayos4dnow7nnkhah4yzuzq
react-native-safe-area-context: 4.5.1_yqouayos4dnow7nnkhah4yzuzq
react-native-screens: 3.20.0_yqouayos4dnow7nnkhah4yzuzq
warn-once: 0.1.1
dev: false
@ -4834,7 +4830,7 @@ packages:
react: 18.2.0
dev: false
/@react-spring/konva/9.7.2_fwynnzbz4ntjdutyr5mh7pkgwe:
/@react-spring/konva/9.7.2_m3igf56nx7ldc6sopza7vgxzfy:
resolution: {integrity: sha512-xMA0XkIyv02euso8BGlLA4iXVJ76p6WCjABMTjdvcp//KvIub596vLwoLzmqHYjqRhH7BP4UQKxzhHvM7AylQA==}
peerDependencies:
konva: '>=2.6'
@ -4845,9 +4841,9 @@ packages:
'@react-spring/core': 9.7.2_react@18.2.0
'@react-spring/shared': 9.7.2_react@18.2.0
'@react-spring/types': 9.7.2
konva: 9.0.1
konva: 8.4.3
react: 18.2.0
react-konva: 18.2.7_uqe3zrqiwmgwvxxbpngihu5cam
react-konva: 18.2.7_tbagvnph2utfj5xyzoveciwt3a
dev: false
/@react-spring/native/9.7.2_sen2ivbtrxhzzk6vdoh233h3yi:
@ -5158,8 +5154,8 @@ packages:
tslib: 1.14.1
dev: false
/@shopify/flash-list/1.4.0_aim4q6r6ykgo64vi4fl5665p4i:
resolution: {integrity: sha512-PvPOyk353LuETFnNA038+QaJsAFlCQ2TYC7DHP3YnYqTX72g2BM6qLoLsPaptXKuoXX+dinOo0MbEm7HDjTy1g==}
/@shopify/flash-list/1.4.2_aim4q6r6ykgo64vi4fl5665p4i:
resolution: {integrity: sha512-MX3vyiHdyCoveqrv+0LufQVlLpoWMZ/bpn+4v6RKfW6ZE0+z8S7WdZTU5Gdj7IFPlkulJAtdFn4Jl0V7tDvd6A==}
peerDependencies:
'@babel/runtime': '*'
react: '*'
@ -8462,6 +8458,15 @@ packages:
typescript: 4.9.5
dev: false
/class-variance-authority/0.5.3:
resolution: {integrity: sha512-vcMxnya/xxQSvTuXO9FWAOnmLihbzX82fPkQpMUwPHZ4tdopmFlM4X818fMgCPt2L6CirOIbN91vybifdScUVw==}
peerDependencies:
typescript: '>= 4.5.5 < 6'
peerDependenciesMeta:
typescript:
optional: true
dev: false
/classnames/2.3.2:
resolution: {integrity: sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==}
dev: false
@ -12491,8 +12496,8 @@ packages:
engines: {node: '>= 8'}
dev: true
/konva/9.0.1:
resolution: {integrity: sha512-wzpkprJ8idE42TDF9Lu9RNjVVYNXrj0apvTK3pujdHQhX1iNV+MUquSxYN8HqjYSG95QQ51jhFzRLWhnhf44Mw==}
/konva/8.4.3:
resolution: {integrity: sha512-ARqdgAbdNIougRlOKvkQwHlGhXPRBV4KvhCP+qoPpGoVQwwiJe4Hkdu4HHdRPb9rGUp04jDTAxBzEwBsE272pg==}
dev: false
/language-subtag-registry/0.3.22:
@ -13088,13 +13093,6 @@ packages:
dependencies:
terser: 5.17.1
/metro-minify-terser/0.76.0:
resolution: {integrity: sha512-dxaE/pvFDFEvXoNHuiXbA2Tw/jT1MD3B4a9AM+aYPWJBh3PdT9XM1HdzumyJldtZpCn5yka4maYSrtuebKgOyw==}
engines: {node: '>=16'}
dependencies:
terser: 5.17.1
dev: true
/metro-minify-terser/0.76.3:
resolution: {integrity: sha512-5A+vQq80j3b4ulAG99l0tIQk9CsxR4b9iUC97HQMTUndF+VvCU/eEb628wewh3s0NbADcgGtAbKOylrpyWgJjw==}
engines: {node: '>=16'}
@ -15216,7 +15214,7 @@ packages:
- encoding
dev: false
/react-konva/18.2.7_uqe3zrqiwmgwvxxbpngihu5cam:
/react-konva/18.2.7_tbagvnph2utfj5xyzoveciwt3a:
resolution: {integrity: sha512-Q52ghaIR+2g3x14V5aIyt7VWNqPnWRotILo0Nj4YWVbNQisozrTJJ3/w68qb5Ar2fsYo7yXb4T6Nt4yZcGd2yg==}
peerDependencies:
konva: ^8.0.1 || ^7.2.5 || ^9.0.0
@ -15225,7 +15223,7 @@ packages:
dependencies:
'@types/react-reconciler': 0.28.2
its-fine: 1.1.1_react@18.2.0
konva: 9.0.1
konva: 8.4.3
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
react-reconciler: 0.29.0_react@18.2.0
@ -15340,8 +15338,8 @@ packages:
- supports-color
dev: false
/react-native-safe-area-context/4.5.0_yqouayos4dnow7nnkhah4yzuzq:
resolution: {integrity: sha512-0WORnk9SkREGUg2V7jHZbuN5x4vcxj/1B0QOcXJjdYWrzZHgLcUzYWWIUecUPJh747Mwjt/42RZDOaFn3L8kPQ==}
/react-native-safe-area-context/4.5.1_yqouayos4dnow7nnkhah4yzuzq:
resolution: {integrity: sha512-bKcwk6zZvyz+VLoG6Uia1oiYU1jSbv1ysjEKSRLsLtPcDsbixsTc0UgfrPqjZxNTPzvYLMcr8ufA90UQauN4mw==}
peerDependencies:
react: '*'
react-native: '*'
@ -15668,14 +15666,14 @@ packages:
react: 18.2.0
dev: false
/react-spring/9.7.1_xvilhl2zf3rx3r4v7bihziaory:
/react-spring/9.7.1_xu7fsxwv7ced4b5547wygkrwiy:
resolution: {integrity: sha512-o2+r2DNQDVEuefiz33ZF76DPd/gLq3kbdObJmllGF2IUfv2W6x+ZP0gR97QYCSR4QLbmOl1mPKUBbI+FJdys2Q==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
dependencies:
'@react-spring/core': 9.7.2_react@18.2.0
'@react-spring/konva': 9.7.2_fwynnzbz4ntjdutyr5mh7pkgwe
'@react-spring/konva': 9.7.2_m3igf56nx7ldc6sopza7vgxzfy
'@react-spring/native': 9.7.2_sen2ivbtrxhzzk6vdoh233h3yi
'@react-spring/three': 9.7.2_ttqqn6qwqy3fbu5kr3upuddewu
'@react-spring/web': 9.7.2_biqbaboplfbrettd7655fr4n2y