mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-04 13:23:28 +00:00
[MOB-55] Video animation for onboarding on mobile and desktop (#2065)
* video animation for onboarding on mobile and desktop run assets gen cleanup declare mp4 type * update metro config to transform video files from sd assets * test ci without native video exclude * casing? * remove to add back again due to github * add videos back * versions * no need to transform --------- Co-authored-by: Utku Bakir <74243531+utkubakir@users.noreply.github.com>
This commit is contained in:
parent
da2841b37a
commit
bda9a1b6ee
BIN
apps/mobile/assets/sd-intro.mp4
Normal file
BIN
apps/mobile/assets/sd-intro.mp4
Normal file
Binary file not shown.
|
@ -21,7 +21,7 @@ const metroConfig = makeMetroConfig({
|
||||||
resolver: {
|
resolver: {
|
||||||
...expoDefaultConfig.resolver,
|
...expoDefaultConfig.resolver,
|
||||||
extraNodeModules: {
|
extraNodeModules: {
|
||||||
'react-native-svg': reactSVGPath
|
'react-native-svg': reactSVGPath,
|
||||||
},
|
},
|
||||||
blockList: exclusionList([reactSVGExclude, rspcClientExclude, rspcReactExclude]),
|
blockList: exclusionList([reactSVGExclude, rspcClientExclude, rspcReactExclude]),
|
||||||
sourceExts: [...expoDefaultConfig.resolver.sourceExts, 'svg'],
|
sourceExts: [...expoDefaultConfig.resolver.sourceExts, 'svg'],
|
||||||
|
|
|
@ -35,7 +35,8 @@
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
"event-target-polyfill": "^0.0.3",
|
"event-target-polyfill": "^0.0.3",
|
||||||
"expo": "~50.0.5",
|
"expo": "~50.0.6",
|
||||||
|
"expo-av": "^13.10.5",
|
||||||
"expo-blur": "^12.9.1",
|
"expo-blur": "^12.9.1",
|
||||||
"expo-build-properties": "~0.11.1",
|
"expo-build-properties": "~0.11.1",
|
||||||
"expo-linking": "~6.2.2",
|
"expo-linking": "~6.2.2",
|
||||||
|
@ -49,7 +50,7 @@
|
||||||
"phosphor-react-native": "^2.0.0",
|
"phosphor-react-native": "^2.0.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-hook-form": "^7.47.0",
|
"react-hook-form": "^7.47.0",
|
||||||
"react-native": "0.73.2",
|
"react-native": "0.73.4",
|
||||||
"react-native-circular-progress": "^1.3.9",
|
"react-native-circular-progress": "^1.3.9",
|
||||||
"react-native-document-picker": "^9.0.1",
|
"react-native-document-picker": "^9.0.1",
|
||||||
"react-native-fs": "^2.20.0",
|
"react-native-fs": "^2.20.0",
|
||||||
|
|
|
@ -77,8 +77,8 @@ export default function Header({
|
||||||
Platform.OS === 'android' ? 'pt-5' : 'pt-10'
|
Platform.OS === 'android' ? 'pt-5' : 'pt-10'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<View style={tw`mx-auto mt-5 h-auto w-full justify-center px-7 pb-5`}>
|
<View style={tw`justify-center w-full h-auto pb-5 mx-auto mt-5 px-7`}>
|
||||||
<View style={tw`w-full flex-row items-center justify-between`}>
|
<View style={tw`flex-row items-center justify-between w-full`}>
|
||||||
<View style={tw`flex-row items-center gap-5`}>
|
<View style={tw`flex-row items-center gap-5`}>
|
||||||
{navBack && (
|
{navBack && (
|
||||||
<Pressable
|
<Pressable
|
||||||
|
|
|
@ -24,10 +24,10 @@ export function SettingsItem(props: SettingsItemProps) {
|
||||||
return (
|
return (
|
||||||
<Pressable onPress={props.onPress}>
|
<Pressable onPress={props.onPress}>
|
||||||
<View style={twStyle(' border-app-input bg-sidebar-box', borderRounded, border)}>
|
<View style={twStyle(' border-app-input bg-sidebar-box', borderRounded, border)}>
|
||||||
<View style={tw`h-auto flex-row items-center`}>
|
<View style={tw`flex-row items-center h-auto`}>
|
||||||
{props.leftIcon && (
|
{props.leftIcon && (
|
||||||
<View
|
<View
|
||||||
style={tw`ml-4 mr-5 h-8 w-8 items-center justify-center rounded-full bg-app-input`}
|
style={tw`items-center justify-center w-8 h-8 ml-4 mr-5 rounded-full bg-app-input`}
|
||||||
>
|
>
|
||||||
{props.leftIcon({ size: 20, color: tw.color('ink-dull') })}
|
{props.leftIcon({ size: 20, color: tw.color('ink-dull') })}
|
||||||
</View>
|
</View>
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
import { createStackNavigator, StackScreenProps } from '@react-navigation/stack';
|
import { createStackNavigator, StackScreenProps } from '@react-navigation/stack';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useOnboardingStore } from '@sd/client';
|
||||||
import { OnboardingContext, useContextValue } from '~/screens/onboarding/context';
|
import { OnboardingContext, useContextValue } from '~/screens/onboarding/context';
|
||||||
import CreatingLibraryScreen from '~/screens/onboarding/CreatingLibrary';
|
import CreatingLibraryScreen from '~/screens/onboarding/CreatingLibrary';
|
||||||
import GetStartedScreen from '~/screens/onboarding/GetStarted';
|
import GetStartedScreen from '~/screens/onboarding/GetStarted';
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
|
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
|
||||||
import { CheckCircle } from 'phosphor-react-native';
|
import { CheckCircle } from 'phosphor-react-native';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ScrollView, View } from 'react-native';
|
import { ScrollView, View } from 'react-native';
|
||||||
import { useLibraryQuery } from '@sd/client';
|
import { resetOnboardingStore, useLibraryQuery } from '@sd/client';
|
||||||
import { PulseAnimation } from '~/components/animation/lottie';
|
import { PulseAnimation } from '~/components/animation/lottie';
|
||||||
import BrowseLocations from '~/components/browse/BrowseLocations';
|
import BrowseLocations from '~/components/browse/BrowseLocations';
|
||||||
import BrowseTags from '~/components/browse/BrowseTags';
|
import BrowseTags from '~/components/browse/BrowseTags';
|
||||||
|
|
|
@ -1,10 +1,14 @@
|
||||||
import { useNavigation, useRoute } from '@react-navigation/native';
|
import { useNavigation, useRoute } from '@react-navigation/native';
|
||||||
import { AppLogo, BloomOne } from '@sd/assets/images';
|
import { AppLogo, BloomOne } from '@sd/assets/images';
|
||||||
|
import { sdintro } from '@sd/assets/videos';
|
||||||
|
import { ResizeMode, Video } from 'expo-av';
|
||||||
import { MotiView } from 'moti';
|
import { MotiView } from 'moti';
|
||||||
import { CaretLeft } from 'phosphor-react-native';
|
import { CaretLeft } from 'phosphor-react-native';
|
||||||
|
import { useEffect } from 'react';
|
||||||
import { Image, KeyboardAvoidingView, Platform, Pressable, Text, View } from 'react-native';
|
import { Image, KeyboardAvoidingView, Platform, Pressable, Text, View } from 'react-native';
|
||||||
import Animated from 'react-native-reanimated';
|
import Animated from 'react-native-reanimated';
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
|
import { useOnboardingStore } from '@sd/client';
|
||||||
import { FadeInUpAnimation, LogoAnimation } from '~/components/animation/layout';
|
import { FadeInUpAnimation, LogoAnimation } from '~/components/animation/layout';
|
||||||
import { AnimatedButton } from '~/components/primitive/Button';
|
import { AnimatedButton } from '~/components/primitive/Button';
|
||||||
import { styled, tw, twStyle } from '~/lib/tailwind';
|
import { styled, tw, twStyle } from '~/lib/tailwind';
|
||||||
|
@ -13,11 +17,31 @@ import { OnboardingStackScreenProps } from '~/navigation/OnboardingNavigator';
|
||||||
export function OnboardingContainer({ children }: React.PropsWithChildren) {
|
export function OnboardingContainer({ children }: React.PropsWithChildren) {
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|
||||||
const { top, bottom } = useSafeAreaInsets();
|
const { top, bottom } = useSafeAreaInsets();
|
||||||
|
const store = useOnboardingStore();
|
||||||
return (
|
return (
|
||||||
<View style={tw`flex-1`}>
|
<View style={tw`relative flex-1`}>
|
||||||
|
{store.showIntro && (
|
||||||
|
<View
|
||||||
|
style={twStyle(
|
||||||
|
'absolute z-50 mx-auto h-full w-full flex-1 items-center justify-center',
|
||||||
|
Platform.OS === 'ios' ? 'bg-[#1C1E27]' : 'bg-[#1E1D28]'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Video
|
||||||
|
style={tw`w-[700px] h-[700px]`}
|
||||||
|
shouldPlay
|
||||||
|
onPlaybackStatusUpdate={(status) => {
|
||||||
|
if (status.isLoaded && status.didJustFinish) {
|
||||||
|
store.showIntro = false;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
source={sdintro}
|
||||||
|
isMuted
|
||||||
|
resizeMode={ResizeMode.CONTAIN}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
{route.name !== 'GetStarted' && route.name !== 'CreatingLibrary' && (
|
{route.name !== 'GetStarted' && route.name !== 'CreatingLibrary' && (
|
||||||
<Pressable
|
<Pressable
|
||||||
style={twStyle('absolute left-6 z-50', { top: top + 16 })}
|
style={twStyle('absolute left-6 z-50', { top: top + 16 })}
|
||||||
|
@ -26,22 +50,22 @@ export function OnboardingContainer({ children }: React.PropsWithChildren) {
|
||||||
<CaretLeft size={24} weight="bold" color="white" />
|
<CaretLeft size={24} weight="bold" color="white" />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
)}
|
)}
|
||||||
<View style={tw`z-10 flex-1 items-center justify-center`}>
|
<View style={tw`z-10 items-center justify-center flex-1`}>
|
||||||
<KeyboardAvoidingView
|
<KeyboardAvoidingView
|
||||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||||
keyboardVerticalOffset={bottom}
|
keyboardVerticalOffset={bottom}
|
||||||
style={tw`w-full flex-1 items-center justify-center`}
|
style={tw`items-center justify-center flex-1 w-full`}
|
||||||
>
|
>
|
||||||
<MotiView style={tw`w-full items-center justify-center px-4`}>
|
<MotiView style={tw`items-center justify-center w-full px-4`}>
|
||||||
{children}
|
{children}
|
||||||
</MotiView>
|
</MotiView>
|
||||||
</KeyboardAvoidingView>
|
</KeyboardAvoidingView>
|
||||||
<Text style={tw`absolute bottom-8 text-xs text-ink-dull/50`}>
|
<Text style={tw`absolute text-xs bottom-8 text-ink-dull/50`}>
|
||||||
© {new Date().getFullYear()} Spacedrive Technology Inc.
|
© {new Date().getFullYear()} Spacedrive Technology Inc.
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
{/* Bloom */}
|
{/* Bloom */}
|
||||||
<Image source={BloomOne} style={tw`top-100 absolute h-screen w-screen opacity-20`} />
|
<Image source={BloomOne} style={tw`absolute w-screen h-screen top-100 opacity-20`} />
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -57,6 +81,11 @@ export const OnboardingDescription = styled(
|
||||||
);
|
);
|
||||||
|
|
||||||
const GetStartedScreen = ({ navigation }: OnboardingStackScreenProps<'GetStarted'>) => {
|
const GetStartedScreen = ({ navigation }: OnboardingStackScreenProps<'GetStarted'>) => {
|
||||||
|
//initial render - reset video intro value
|
||||||
|
const store = useOnboardingStore();
|
||||||
|
useEffect(() => {
|
||||||
|
store.showIntro = true;
|
||||||
|
}, []);
|
||||||
return (
|
return (
|
||||||
<OnboardingContainer>
|
<OnboardingContainer>
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
|
@ -76,7 +105,7 @@ const GetStartedScreen = ({ navigation }: OnboardingStackScreenProps<'GetStarted
|
||||||
{/* Get Started Button */}
|
{/* Get Started Button */}
|
||||||
<FadeInUpAnimation delay={1200} style={tw`mt-8`}>
|
<FadeInUpAnimation delay={1200} style={tw`mt-8`}>
|
||||||
<AnimatedButton variant="accent" onPress={() => navigation.push('NewLibrary')}>
|
<AnimatedButton variant="accent" onPress={() => navigation.push('NewLibrary')}>
|
||||||
<Text style={tw`text-center text-base font-medium text-ink`}>Get Started</Text>
|
<Text style={tw`text-base font-medium text-center text-ink`}>Get Started</Text>
|
||||||
</AnimatedButton>
|
</AnimatedButton>
|
||||||
</FadeInUpAnimation>
|
</FadeInUpAnimation>
|
||||||
</OnboardingContainer>
|
</OnboardingContainer>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Trash } from 'phosphor-react-native';
|
import { Trash } from 'phosphor-react-native';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Controller } from 'react-hook-form';
|
import { Controller } from 'react-hook-form';
|
||||||
import { Alert, View } from 'react-native';
|
import { Alert, Text, View } from 'react-native';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { useBridgeMutation, useLibraryContext, useZodForm } from '@sd/client';
|
import { useBridgeMutation, useLibraryContext, useZodForm } from '@sd/client';
|
||||||
import { Input } from '~/components/form/Input';
|
import { Input } from '~/components/form/Input';
|
||||||
|
@ -38,7 +38,7 @@ const LibraryGeneralSettingsScreen = (_: SettingsStackScreenProps<'LibraryGenera
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={tw`gap-4`}>
|
<View style={tw`gap-4`}>
|
||||||
<View style={tw`mt-4 px-2`}>
|
<View style={tw`px-2 mt-4`}>
|
||||||
<SettingsTitle>Name</SettingsTitle>
|
<SettingsTitle>Name</SettingsTitle>
|
||||||
<Controller
|
<Controller
|
||||||
name="name"
|
name="name"
|
||||||
|
@ -66,21 +66,7 @@ const LibraryGeneralSettingsScreen = (_: SettingsStackScreenProps<'LibraryGenera
|
||||||
{/* Export */}
|
{/* Export */}
|
||||||
<SettingsItem title="Export Library" onPress={() => Alert.alert('TODO')} />
|
<SettingsItem title="Export Library" onPress={() => Alert.alert('TODO')} />
|
||||||
{/* Delete Library */}
|
{/* Delete Library */}
|
||||||
<SettingsContainer description="This is permanent, your files will not be deleted, only the Spacedrive library.">
|
<DeleteLibraryModal trigger={<Text>Delete</Text>} libraryUuid={library.uuid} />
|
||||||
<SettingsItem
|
|
||||||
title="Delete Library"
|
|
||||||
rightArea={
|
|
||||||
<DeleteLibraryModal
|
|
||||||
libraryUuid={library.uuid}
|
|
||||||
trigger={
|
|
||||||
<FakeButton size="sm" variant="danger">
|
|
||||||
<Trash color={tw.color('ink')} size={20} />
|
|
||||||
</FakeButton>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</SettingsContainer>
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
5
apps/mobile/src/types/declarations.d.ts
vendored
5
apps/mobile/src/types/declarations.d.ts
vendored
|
@ -9,3 +9,8 @@ declare module '*.png' {
|
||||||
const content: any;
|
const content: any;
|
||||||
export default content;
|
export default content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare module '*.mp4' {
|
||||||
|
const content: any;
|
||||||
|
export default content;
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import { BloomOne } from '@sd/assets/images';
|
import { BloomOne } from '@sd/assets/images';
|
||||||
|
import { sdintro } from '@sd/assets/videos';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
import { useState } from 'react';
|
||||||
import { Navigate, Outlet } from 'react-router';
|
import { Navigate, Outlet } from 'react-router';
|
||||||
import { useDebugState } from '@sd/client';
|
import { useDebugState } from '@sd/client';
|
||||||
import DragRegion from '~/components/DragRegion';
|
import DragRegion from '~/components/DragRegion';
|
||||||
|
@ -13,7 +15,7 @@ import Progress from './Progress';
|
||||||
export const Component = () => {
|
export const Component = () => {
|
||||||
const os = useOperatingSystem();
|
const os = useOperatingSystem();
|
||||||
const debugState = useDebugState();
|
const debugState = useDebugState();
|
||||||
|
const [showIntro, setShowIntro] = useState(true);
|
||||||
const ctx = useContextValue();
|
const ctx = useContextValue();
|
||||||
|
|
||||||
if (ctx.libraries.isLoading) return null;
|
if (ctx.libraries.isLoading) return null;
|
||||||
|
@ -27,20 +29,35 @@ export const Component = () => {
|
||||||
'flex h-screen flex-col bg-sidebar text-ink'
|
'flex h-screen flex-col bg-sidebar text-ink'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
{showIntro && (
|
||||||
|
<div className="absolute left-0 top-0 z-50 flex h-screen w-screen items-center justify-center bg-[#1F212C]">
|
||||||
|
<video
|
||||||
|
width={700}
|
||||||
|
className="mx-auto"
|
||||||
|
autoPlay
|
||||||
|
onEnded={() => {
|
||||||
|
setShowIntro(false);
|
||||||
|
}}
|
||||||
|
muted
|
||||||
|
controls={false}
|
||||||
|
src={sdintro}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<DragRegion className="z-50 h-9" />
|
<DragRegion className="z-50 h-9" />
|
||||||
<div className="-mt-5 flex grow flex-col gap-8 p-10">
|
<div className="flex flex-col gap-8 p-10 -mt-5 grow">
|
||||||
<div className="flex grow flex-col items-center justify-center">
|
<div className="flex flex-col items-center justify-center grow">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
<Progress />
|
<Progress />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-center p-4">
|
<div className="flex justify-center p-4">
|
||||||
<p className="text-xs text-ink-dull opacity-50">
|
<p className="text-xs opacity-50 text-ink-dull">
|
||||||
© {new Date().getFullYear()} Spacedrive Technology Inc.
|
© {new Date().getFullYear()} Spacedrive Technology Inc.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute -z-10">
|
<div className="absolute -z-10">
|
||||||
<div className="relative h-screen w-screen">
|
<div className="relative w-screen h-screen">
|
||||||
<img src={BloomOne} className="absolute h-[2000px] w-[2000px]" />
|
<img src={BloomOne} className="absolute h-[2000px] w-[2000px]" />
|
||||||
{/* <img src={BloomThree} className="absolute w-[2000px] h-[2000px] -right-[200px]" /> */}
|
{/* <img src={BloomThree} className="absolute w-[2000px] h-[2000px] -right-[200px]" /> */}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -4,5 +4,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import fda from './fda.mp4';
|
import fda from './fda.mp4';
|
||||||
|
import sdintro from './sdintro.mp4';
|
||||||
|
|
||||||
export { fda };
|
export { fda, sdintro };
|
||||||
|
|
BIN
packages/assets/videos/sdintro.mp4
Normal file
BIN
packages/assets/videos/sdintro.mp4
Normal file
Binary file not shown.
|
@ -15,7 +15,8 @@ const onboardingStoreDefaults = () => ({
|
||||||
lastActiveScreen: null as string | null,
|
lastActiveScreen: null as string | null,
|
||||||
useCases: [] as UseCase[],
|
useCases: [] as UseCase[],
|
||||||
grantedFullDiskAccess: false,
|
grantedFullDiskAccess: false,
|
||||||
data: {} as Record<string, any> | undefined
|
data: {} as Record<string, any> | undefined,
|
||||||
|
showIntro: true
|
||||||
});
|
});
|
||||||
|
|
||||||
export const onboardingStore = createPersistedMutable(
|
export const onboardingStore = createPersistedMutable(
|
||||||
|
|
587
pnpm-lock.yaml
587
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue