mirror of
https://github.com/Stef-00012/Zipline-Android-App.git
synced 2025-05-11 18:35:58 +02:00
first commit :D
This commit is contained in:
parent
40b7eabefe
commit
ed5c91435d
41 changed files with 724 additions and 907 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -35,4 +35,4 @@ yarn-error.*
|
|||
# typescript
|
||||
*.tsbuildinfo
|
||||
|
||||
app-example
|
||||
/android
|
||||
|
|
88
app.json
88
app.json
|
@ -1,41 +1,51 @@
|
|||
{
|
||||
"expo": {
|
||||
"name": "test-share-target",
|
||||
"slug": "test-share-target",
|
||||
"version": "1.0.0",
|
||||
"orientation": "portrait",
|
||||
"icon": "./assets/images/icon.png",
|
||||
"scheme": "myapp",
|
||||
"userInterfaceStyle": "automatic",
|
||||
"newArchEnabled": true,
|
||||
"ios": {
|
||||
"supportsTablet": true
|
||||
},
|
||||
"android": {
|
||||
"adaptiveIcon": {
|
||||
"foregroundImage": "./assets/images/adaptive-icon.png",
|
||||
"backgroundColor": "#ffffff"
|
||||
}
|
||||
},
|
||||
"web": {
|
||||
"bundler": "metro",
|
||||
"output": "static",
|
||||
"favicon": "./assets/images/favicon.png"
|
||||
},
|
||||
"plugins": [
|
||||
"expo-router",
|
||||
[
|
||||
"expo-splash-screen",
|
||||
{
|
||||
"image": "./assets/images/splash-icon.png",
|
||||
"imageWidth": 200,
|
||||
"resizeMode": "contain",
|
||||
"backgroundColor": "#ffffff"
|
||||
}
|
||||
]
|
||||
],
|
||||
"experiments": {
|
||||
"typedRoutes": true
|
||||
}
|
||||
}
|
||||
"expo": {
|
||||
"name": "Zipline",
|
||||
"slug": "zipline",
|
||||
"version": "1.0.0",
|
||||
"orientation": "portrait",
|
||||
"icon": "./assets/images/icon.png",
|
||||
"scheme": "zipline",
|
||||
"userInterfaceStyle": "automatic",
|
||||
"newArchEnabled": true,
|
||||
"ios": {
|
||||
"supportsTablet": true,
|
||||
"bundleIdentifier": "com.stefdp.zipline"
|
||||
},
|
||||
"android": {
|
||||
"adaptiveIcon": {
|
||||
"foregroundImage": "./assets/images/adaptive-icon.png",
|
||||
"backgroundColor": "#ffffff"
|
||||
},
|
||||
"package": "com.stefdp.zipline"
|
||||
},
|
||||
"web": {
|
||||
"bundler": "metro",
|
||||
"output": "static",
|
||||
"favicon": "./assets/images/favicon.png"
|
||||
},
|
||||
"plugins": [
|
||||
"expo-router",
|
||||
[
|
||||
"expo-splash-screen",
|
||||
{
|
||||
"image": "./assets/images/splash-icon.png",
|
||||
"imageWidth": 200,
|
||||
"resizeMode": "contain",
|
||||
"backgroundColor": "#ffffff"
|
||||
}
|
||||
],
|
||||
[
|
||||
"expo-share-intent",
|
||||
{
|
||||
"androidIntentFilters": ["*/*"],
|
||||
"androidMultiIntentFilters": ["*/*"],
|
||||
"disableIOS": true
|
||||
}
|
||||
]
|
||||
],
|
||||
"experiments": {
|
||||
"typedRoutes": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,45 +0,0 @@
|
|||
import { Tabs } from 'expo-router';
|
||||
import React from 'react';
|
||||
import { Platform } from 'react-native';
|
||||
|
||||
import { HapticTab } from '@/components/HapticTab';
|
||||
import { IconSymbol } from '@/components/ui/IconSymbol';
|
||||
import TabBarBackground from '@/components/ui/TabBarBackground';
|
||||
import { Colors } from '@/constants/Colors';
|
||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||
|
||||
export default function TabLayout() {
|
||||
const colorScheme = useColorScheme();
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
screenOptions={{
|
||||
tabBarActiveTintColor: Colors[colorScheme ?? 'light'].tint,
|
||||
headerShown: false,
|
||||
tabBarButton: HapticTab,
|
||||
tabBarBackground: TabBarBackground,
|
||||
tabBarStyle: Platform.select({
|
||||
ios: {
|
||||
// Use a transparent background on iOS to show the blur effect
|
||||
position: 'absolute',
|
||||
},
|
||||
default: {},
|
||||
}),
|
||||
}}>
|
||||
<Tabs.Screen
|
||||
name="index"
|
||||
options={{
|
||||
title: 'Home',
|
||||
tabBarIcon: ({ color }) => <IconSymbol size={28} name="house.fill" color={color} />,
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="explore"
|
||||
options={{
|
||||
title: 'Explore',
|
||||
tabBarIcon: ({ color }) => <IconSymbol size={28} name="paperplane.fill" color={color} />,
|
||||
}}
|
||||
/>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
|
@ -1,109 +0,0 @@
|
|||
import { StyleSheet, Image, Platform } from 'react-native';
|
||||
|
||||
import { Collapsible } from '@/components/Collapsible';
|
||||
import { ExternalLink } from '@/components/ExternalLink';
|
||||
import ParallaxScrollView from '@/components/ParallaxScrollView';
|
||||
import { ThemedText } from '@/components/ThemedText';
|
||||
import { ThemedView } from '@/components/ThemedView';
|
||||
import { IconSymbol } from '@/components/ui/IconSymbol';
|
||||
|
||||
export default function TabTwoScreen() {
|
||||
return (
|
||||
<ParallaxScrollView
|
||||
headerBackgroundColor={{ light: '#D0D0D0', dark: '#353636' }}
|
||||
headerImage={
|
||||
<IconSymbol
|
||||
size={310}
|
||||
color="#808080"
|
||||
name="chevron.left.forwardslash.chevron.right"
|
||||
style={styles.headerImage}
|
||||
/>
|
||||
}>
|
||||
<ThemedView style={styles.titleContainer}>
|
||||
<ThemedText type="title">Explore</ThemedText>
|
||||
</ThemedView>
|
||||
<ThemedText>This app includes example code to help you get started.</ThemedText>
|
||||
<Collapsible title="File-based routing">
|
||||
<ThemedText>
|
||||
This app has two screens:{' '}
|
||||
<ThemedText type="defaultSemiBold">app/(tabs)/index.tsx</ThemedText> and{' '}
|
||||
<ThemedText type="defaultSemiBold">app/(tabs)/explore.tsx</ThemedText>
|
||||
</ThemedText>
|
||||
<ThemedText>
|
||||
The layout file in <ThemedText type="defaultSemiBold">app/(tabs)/_layout.tsx</ThemedText>{' '}
|
||||
sets up the tab navigator.
|
||||
</ThemedText>
|
||||
<ExternalLink href="https://docs.expo.dev/router/introduction">
|
||||
<ThemedText type="link">Learn more</ThemedText>
|
||||
</ExternalLink>
|
||||
</Collapsible>
|
||||
<Collapsible title="Android, iOS, and web support">
|
||||
<ThemedText>
|
||||
You can open this project on Android, iOS, and the web. To open the web version, press{' '}
|
||||
<ThemedText type="defaultSemiBold">w</ThemedText> in the terminal running this project.
|
||||
</ThemedText>
|
||||
</Collapsible>
|
||||
<Collapsible title="Images">
|
||||
<ThemedText>
|
||||
For static images, you can use the <ThemedText type="defaultSemiBold">@2x</ThemedText> and{' '}
|
||||
<ThemedText type="defaultSemiBold">@3x</ThemedText> suffixes to provide files for
|
||||
different screen densities
|
||||
</ThemedText>
|
||||
<Image source={require('@/assets/images/react-logo.png')} style={{ alignSelf: 'center' }} />
|
||||
<ExternalLink href="https://reactnative.dev/docs/images">
|
||||
<ThemedText type="link">Learn more</ThemedText>
|
||||
</ExternalLink>
|
||||
</Collapsible>
|
||||
<Collapsible title="Custom fonts">
|
||||
<ThemedText>
|
||||
Open <ThemedText type="defaultSemiBold">app/_layout.tsx</ThemedText> to see how to load{' '}
|
||||
<ThemedText style={{ fontFamily: 'SpaceMono' }}>
|
||||
custom fonts such as this one.
|
||||
</ThemedText>
|
||||
</ThemedText>
|
||||
<ExternalLink href="https://docs.expo.dev/versions/latest/sdk/font">
|
||||
<ThemedText type="link">Learn more</ThemedText>
|
||||
</ExternalLink>
|
||||
</Collapsible>
|
||||
<Collapsible title="Light and dark mode components">
|
||||
<ThemedText>
|
||||
This template has light and dark mode support. The{' '}
|
||||
<ThemedText type="defaultSemiBold">useColorScheme()</ThemedText> hook lets you inspect
|
||||
what the user's current color scheme is, and so you can adjust UI colors accordingly.
|
||||
</ThemedText>
|
||||
<ExternalLink href="https://docs.expo.dev/develop/user-interface/color-themes/">
|
||||
<ThemedText type="link">Learn more</ThemedText>
|
||||
</ExternalLink>
|
||||
</Collapsible>
|
||||
<Collapsible title="Animations">
|
||||
<ThemedText>
|
||||
This template includes an example of an animated component. The{' '}
|
||||
<ThemedText type="defaultSemiBold">components/HelloWave.tsx</ThemedText> component uses
|
||||
the powerful <ThemedText type="defaultSemiBold">react-native-reanimated</ThemedText>{' '}
|
||||
library to create a waving hand animation.
|
||||
</ThemedText>
|
||||
{Platform.select({
|
||||
ios: (
|
||||
<ThemedText>
|
||||
The <ThemedText type="defaultSemiBold">components/ParallaxScrollView.tsx</ThemedText>{' '}
|
||||
component provides a parallax effect for the header image.
|
||||
</ThemedText>
|
||||
),
|
||||
})}
|
||||
</Collapsible>
|
||||
</ParallaxScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
headerImage: {
|
||||
color: '#808080',
|
||||
bottom: -90,
|
||||
left: -35,
|
||||
position: 'absolute',
|
||||
},
|
||||
titleContainer: {
|
||||
flexDirection: 'row',
|
||||
gap: 8,
|
||||
},
|
||||
});
|
|
@ -1,74 +0,0 @@
|
|||
import { Image, StyleSheet, Platform } from 'react-native';
|
||||
|
||||
import { HelloWave } from '@/components/HelloWave';
|
||||
import ParallaxScrollView from '@/components/ParallaxScrollView';
|
||||
import { ThemedText } from '@/components/ThemedText';
|
||||
import { ThemedView } from '@/components/ThemedView';
|
||||
|
||||
export default function HomeScreen() {
|
||||
return (
|
||||
<ParallaxScrollView
|
||||
headerBackgroundColor={{ light: '#A1CEDC', dark: '#1D3D47' }}
|
||||
headerImage={
|
||||
<Image
|
||||
source={require('@/assets/images/partial-react-logo.png')}
|
||||
style={styles.reactLogo}
|
||||
/>
|
||||
}>
|
||||
<ThemedView style={styles.titleContainer}>
|
||||
<ThemedText type="title">Welcome!</ThemedText>
|
||||
<HelloWave />
|
||||
</ThemedView>
|
||||
<ThemedView style={styles.stepContainer}>
|
||||
<ThemedText type="subtitle">Step 1: Try it</ThemedText>
|
||||
<ThemedText>
|
||||
Edit <ThemedText type="defaultSemiBold">app/(tabs)/index.tsx</ThemedText> to see changes.
|
||||
Press{' '}
|
||||
<ThemedText type="defaultSemiBold">
|
||||
{Platform.select({
|
||||
ios: 'cmd + d',
|
||||
android: 'cmd + m',
|
||||
web: 'F12'
|
||||
})}
|
||||
</ThemedText>{' '}
|
||||
to open developer tools.
|
||||
</ThemedText>
|
||||
</ThemedView>
|
||||
<ThemedView style={styles.stepContainer}>
|
||||
<ThemedText type="subtitle">Step 2: Explore</ThemedText>
|
||||
<ThemedText>
|
||||
Tap the Explore tab to learn more about what's included in this starter app.
|
||||
</ThemedText>
|
||||
</ThemedView>
|
||||
<ThemedView style={styles.stepContainer}>
|
||||
<ThemedText type="subtitle">Step 3: Get a fresh start</ThemedText>
|
||||
<ThemedText>
|
||||
When you're ready, run{' '}
|
||||
<ThemedText type="defaultSemiBold">npm run reset-project</ThemedText> to get a fresh{' '}
|
||||
<ThemedText type="defaultSemiBold">app</ThemedText> directory. This will move the current{' '}
|
||||
<ThemedText type="defaultSemiBold">app</ThemedText> to{' '}
|
||||
<ThemedText type="defaultSemiBold">app-example</ThemedText>.
|
||||
</ThemedText>
|
||||
</ThemedView>
|
||||
</ParallaxScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
titleContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
},
|
||||
stepContainer: {
|
||||
gap: 8,
|
||||
marginBottom: 8,
|
||||
},
|
||||
reactLogo: {
|
||||
height: 178,
|
||||
width: 290,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
position: 'absolute',
|
||||
},
|
||||
});
|
|
@ -1,32 +1,26 @@
|
|||
import { Link, Stack } from 'expo-router';
|
||||
import { StyleSheet } from 'react-native';
|
||||
import { useRouter } from "expo-router";
|
||||
import { Pressable, Text, View } from "react-native";
|
||||
|
||||
import { ThemedText } from '@/components/ThemedText';
|
||||
import { ThemedView } from '@/components/ThemedView';
|
||||
import { styles } from "@/styles/not-found";
|
||||
|
||||
export default function NotFoundScreen() {
|
||||
return (
|
||||
<>
|
||||
<Stack.Screen options={{ title: 'Oops!' }} />
|
||||
<ThemedView style={styles.container}>
|
||||
<ThemedText type="title">This screen doesn't exist.</ThemedText>
|
||||
<Link href="/" style={styles.link}>
|
||||
<ThemedText type="link">Go to home screen!</ThemedText>
|
||||
</Link>
|
||||
</ThemedView>
|
||||
</>
|
||||
);
|
||||
}
|
||||
const router = useRouter();
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 20,
|
||||
},
|
||||
link: {
|
||||
marginTop: 15,
|
||||
paddingVertical: 15,
|
||||
},
|
||||
});
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.code}>404</Text>
|
||||
<Text style={styles.text}>This page doesn't exist</Text>
|
||||
|
||||
<Pressable
|
||||
style={styles.button}
|
||||
onPress={() => {
|
||||
router.replace({
|
||||
pathname: "/",
|
||||
});
|
||||
}}
|
||||
>
|
||||
Head to the Dashboard
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
}
|
|
@ -1,39 +1,26 @@
|
|||
import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native';
|
||||
import { useFonts } from 'expo-font';
|
||||
import { Stack } from 'expo-router';
|
||||
import * as SplashScreen from 'expo-splash-screen';
|
||||
import { StatusBar } from 'expo-status-bar';
|
||||
import { useEffect } from 'react';
|
||||
import 'react-native-reanimated';
|
||||
import Header from "@/components/Header";
|
||||
import { Slot, useRouter } from "expo-router";
|
||||
|
||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||
import { ShareIntentProvider } from "expo-share-intent";
|
||||
import { Text } from "react-native";
|
||||
|
||||
// Prevent the splash screen from auto-hiding before asset loading is complete.
|
||||
SplashScreen.preventAutoHideAsync();
|
||||
export default function Layout() {
|
||||
const router = useRouter();
|
||||
|
||||
export default function RootLayout() {
|
||||
const colorScheme = useColorScheme();
|
||||
const [loaded] = useFonts({
|
||||
SpaceMono: require('../assets/fonts/SpaceMono-Regular.ttf'),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (loaded) {
|
||||
SplashScreen.hideAsync();
|
||||
}
|
||||
}, [loaded]);
|
||||
|
||||
if (!loaded) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
|
||||
<Stack>
|
||||
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="+not-found" />
|
||||
</Stack>
|
||||
<StatusBar style="auto" />
|
||||
</ThemeProvider>
|
||||
);
|
||||
return (
|
||||
<ShareIntentProvider
|
||||
options={{
|
||||
scheme: "zipline",
|
||||
debug: true,
|
||||
resetOnBackground: true,
|
||||
onResetShareIntent: () =>
|
||||
router.replace({
|
||||
pathname: "/",
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<Header/>
|
||||
<Slot />
|
||||
</ShareIntentProvider>
|
||||
);
|
||||
}
|
||||
|
|
115
app/index.tsx
Normal file
115
app/index.tsx
Normal file
|
@ -0,0 +1,115 @@
|
|||
import { useRouter } from "expo-router";
|
||||
import { useShareIntentContext } from "expo-share-intent";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Text, View, TextInput, Pressable } from "react-native";
|
||||
import { styles } from "@/styles/home";
|
||||
import { getRecentFiles, getStats, getUser } from "@/functions/zipline";
|
||||
import type { APIRecentFiles, APIStats, APIUser } from "@/types/zipline";
|
||||
import * as db from "@/functions/database";
|
||||
|
||||
export default function Home() {
|
||||
const router = useRouter();
|
||||
const { hasShareIntent } = useShareIntentContext();
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
|
||||
useEffect(() => {
|
||||
if (hasShareIntent) {
|
||||
router.replace({
|
||||
pathname: "/shareintent",
|
||||
});
|
||||
}
|
||||
}, [hasShareIntent]);
|
||||
|
||||
const [inputtedUrl, setInputtedUrl] = useState<string | null>(null);
|
||||
const [inputtedToken, setInputtedToken] = useState<string | null>(null);
|
||||
|
||||
const [token, setToken] = useState<string | null>(db.get("token"));
|
||||
const [url, setUrl] = useState<string | null>(db.get("url"));
|
||||
const [user, setUser] = useState<APIUser | null>(null);
|
||||
const [recentFiles, setRecentFiles] = useState<APIRecentFiles | null>();
|
||||
const [stats, setStats] = useState<APIStats | null>();
|
||||
|
||||
const mainContainerStyles = user ? {
|
||||
...styles.mainContainer
|
||||
} : {
|
||||
...styles.mainContainer,
|
||||
marginTop: 0
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const user = await getUser();
|
||||
const recentFiles = await getRecentFiles();
|
||||
const stats = await getStats();
|
||||
|
||||
setUser(user);
|
||||
setRecentFiles(recentFiles);
|
||||
setStats(stats);
|
||||
})();
|
||||
});
|
||||
|
||||
async function handleLogin() {
|
||||
const user = await getUser();
|
||||
const recentFiles = await getRecentFiles();
|
||||
const stats = await getStats();
|
||||
|
||||
setUser(user);
|
||||
setRecentFiles(recentFiles);
|
||||
setStats(stats);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={mainContainerStyles}>
|
||||
{url && token ? (
|
||||
<View style={{flex:1}}>
|
||||
{user ? (
|
||||
<View>
|
||||
<Text style={{color: "#fff"}}>{user.username}</Text>
|
||||
</View>
|
||||
) : (
|
||||
<View style={styles.loadingContainer}>
|
||||
<Text style={styles.loadingText}>Loading...</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
) : (
|
||||
<View style={styles.loginContainer}>
|
||||
<View style={styles.loginBox}>
|
||||
<TextInput
|
||||
style={styles.textInput}
|
||||
onChangeText={(content) => {
|
||||
setInputtedUrl(content.length > 0 ? content : null);
|
||||
}}
|
||||
placeholder="Zipline URL"
|
||||
placeholderTextColor="#222c47"
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
style={styles.textInput}
|
||||
secureTextEntry={true}
|
||||
onChangeText={(content) => {
|
||||
setInputtedToken(content.length > 0 ? content : null);
|
||||
}}
|
||||
placeholder="Zipline Token"
|
||||
placeholderTextColor="#222c47"
|
||||
/>
|
||||
<Pressable
|
||||
style={styles.button}
|
||||
onPress={async (event) => {
|
||||
setUrl(inputtedUrl);
|
||||
db.set("url", inputtedUrl || "");
|
||||
|
||||
setToken(inputtedToken);
|
||||
db.set("token", inputtedToken || "");
|
||||
|
||||
await handleLogin();
|
||||
}}
|
||||
>
|
||||
<Text style={styles.buttonText}>Login</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
114
app/shareintent.tsx
Normal file
114
app/shareintent.tsx
Normal file
|
@ -0,0 +1,114 @@
|
|||
import { Button, Image, StyleSheet, Text, View } from "react-native";
|
||||
|
||||
import { useRouter } from "expo-router";
|
||||
import {
|
||||
type ShareIntent as ShareIntentType,
|
||||
useShareIntentContext,
|
||||
} from "expo-share-intent";
|
||||
|
||||
const WebUrlComponent = ({ shareIntent }: { shareIntent: ShareIntentType }) => {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.gap,
|
||||
styles.row,
|
||||
{ borderWidth: 1, borderRadius: 5, height: 102 },
|
||||
]}
|
||||
>
|
||||
<Image
|
||||
source={
|
||||
shareIntent.meta?.["og:image"]
|
||||
? { uri: shareIntent.meta?.["og:image"] }
|
||||
: undefined
|
||||
}
|
||||
style={[styles.icon, styles.gap, { borderRadius: 5 }]}
|
||||
/>
|
||||
<View style={{ flexShrink: 1, padding: 5 }}>
|
||||
<Text style={[styles.gap]}>
|
||||
{shareIntent.meta?.title || "<NO TITLE>"}
|
||||
</Text>
|
||||
<Text style={styles.gap}>{shareIntent.webUrl}</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default function ShareIntent() {
|
||||
const router = useRouter();
|
||||
const { hasShareIntent, shareIntent, error, resetShareIntent } =
|
||||
useShareIntentContext();
|
||||
|
||||
console.log({
|
||||
hasShareIntent,
|
||||
shareIntent,
|
||||
error,
|
||||
resetShareIntent
|
||||
})
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{/* <Image
|
||||
source={require("../assets/images/icon.png")}
|
||||
style={[styles.logo, styles.gap]}
|
||||
/> */}
|
||||
{!hasShareIntent && <Text>No Share intent detected</Text>}
|
||||
{hasShareIntent && (
|
||||
<Text style={[styles.gap, { fontSize: 20 }]}>
|
||||
Congratz, a share intent value is available
|
||||
</Text>
|
||||
)}
|
||||
{!!shareIntent.text && <Text style={styles.gap}>{shareIntent.text}</Text>}
|
||||
{shareIntent?.type === "weburl" && (
|
||||
<WebUrlComponent shareIntent={shareIntent} />
|
||||
)}
|
||||
{shareIntent?.files?.map((file) => (
|
||||
<Image
|
||||
key={file.path}
|
||||
source={{ uri: file.path }}
|
||||
style={[styles.image, styles.gap]}
|
||||
/>
|
||||
))}
|
||||
{hasShareIntent && (
|
||||
<Button onPress={() => resetShareIntent()} title="Reset" />
|
||||
)}
|
||||
<Text style={[styles.error]}>{error}</Text>
|
||||
<Button onPress={() => router.replace("/")} title="Go home" />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: "#fff",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
paddingHorizontal: 10,
|
||||
},
|
||||
logo: {
|
||||
width: 75,
|
||||
height: 75,
|
||||
resizeMode: "contain",
|
||||
},
|
||||
image: {
|
||||
width: 200,
|
||||
height: 200,
|
||||
resizeMode: "contain",
|
||||
},
|
||||
icon: {
|
||||
width: 100,
|
||||
height: 100,
|
||||
resizeMode: "contain",
|
||||
backgroundColor: "lightgray",
|
||||
},
|
||||
row: {
|
||||
flexDirection: "row",
|
||||
gap: 10,
|
||||
},
|
||||
gap: {
|
||||
marginBottom: 20,
|
||||
},
|
||||
error: {
|
||||
color: "red",
|
||||
},
|
||||
});
|
Binary file not shown.
Binary file not shown.
Before Width: | Height: | Size: 5 KiB |
Binary file not shown.
Before Width: | Height: | Size: 6.2 KiB |
Binary file not shown.
Before Width: | Height: | Size: 14 KiB |
Binary file not shown.
Before Width: | Height: | Size: 21 KiB |
BIN
bun.lockb
BIN
bun.lockb
Binary file not shown.
|
@ -1,45 +0,0 @@
|
|||
import { PropsWithChildren, useState } from 'react';
|
||||
import { StyleSheet, TouchableOpacity } from 'react-native';
|
||||
|
||||
import { ThemedText } from '@/components/ThemedText';
|
||||
import { ThemedView } from '@/components/ThemedView';
|
||||
import { IconSymbol } from '@/components/ui/IconSymbol';
|
||||
import { Colors } from '@/constants/Colors';
|
||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||
|
||||
export function Collapsible({ children, title }: PropsWithChildren & { title: string }) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const theme = useColorScheme() ?? 'light';
|
||||
|
||||
return (
|
||||
<ThemedView>
|
||||
<TouchableOpacity
|
||||
style={styles.heading}
|
||||
onPress={() => setIsOpen((value) => !value)}
|
||||
activeOpacity={0.8}>
|
||||
<IconSymbol
|
||||
name="chevron.right"
|
||||
size={18}
|
||||
weight="medium"
|
||||
color={theme === 'light' ? Colors.light.icon : Colors.dark.icon}
|
||||
style={{ transform: [{ rotate: isOpen ? '90deg' : '0deg' }] }}
|
||||
/>
|
||||
|
||||
<ThemedText type="defaultSemiBold">{title}</ThemedText>
|
||||
</TouchableOpacity>
|
||||
{isOpen && <ThemedView style={styles.content}>{children}</ThemedView>}
|
||||
</ThemedView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
heading: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
},
|
||||
content: {
|
||||
marginTop: 6,
|
||||
marginLeft: 24,
|
||||
},
|
||||
});
|
|
@ -1,24 +0,0 @@
|
|||
import { Link } from 'expo-router';
|
||||
import { openBrowserAsync } from 'expo-web-browser';
|
||||
import { type ComponentProps } from 'react';
|
||||
import { Platform } from 'react-native';
|
||||
|
||||
type Props = Omit<ComponentProps<typeof Link>, 'href'> & { href: string };
|
||||
|
||||
export function ExternalLink({ href, ...rest }: Props) {
|
||||
return (
|
||||
<Link
|
||||
target="_blank"
|
||||
{...rest}
|
||||
href={href}
|
||||
onPress={async (event) => {
|
||||
if (Platform.OS !== 'web') {
|
||||
// Prevent the default behavior of linking to the default browser on native.
|
||||
event.preventDefault();
|
||||
// Open the link in an in-app browser.
|
||||
await openBrowserAsync(href);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
import { BottomTabBarButtonProps } from '@react-navigation/bottom-tabs';
|
||||
import { PlatformPressable } from '@react-navigation/elements';
|
||||
import * as Haptics from 'expo-haptics';
|
||||
|
||||
export function HapticTab(props: BottomTabBarButtonProps) {
|
||||
return (
|
||||
<PlatformPressable
|
||||
{...props}
|
||||
onPressIn={(ev) => {
|
||||
if (process.env.EXPO_OS === 'ios') {
|
||||
// Add a soft haptic feedback when pressing down on the tabs.
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
}
|
||||
props.onPressIn?.(ev);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
66
components/Header.tsx
Normal file
66
components/Header.tsx
Normal file
|
@ -0,0 +1,66 @@
|
|||
import { Stack, IconButton } from "@react-native-material/core";
|
||||
import MaterialIcons from "@expo/vector-icons/MaterialIcons";
|
||||
import { View, Text, Pressable, Image } from "react-native";
|
||||
import type React from "react";
|
||||
import { styles } from "@/styles/header";
|
||||
import { type PropsWithChildren, useEffect, useState } from "react";
|
||||
import { getUser, getUserAvatar } from "@/functions/zipline";
|
||||
import type { APIUser } from "@/types/zipline";
|
||||
|
||||
export default function Header() {
|
||||
const [avatar, setAvatar] = useState<string | null>(null);
|
||||
const [user, setUser] = useState<APIUser | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const avatar = await getUserAvatar();
|
||||
const user = await getUser();
|
||||
|
||||
setAvatar(avatar);
|
||||
setUser(user);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<View>
|
||||
{avatar && user ? (
|
||||
<View>
|
||||
<View style={styles.header}>
|
||||
<View style={styles.headerLeft}>
|
||||
<IconButton
|
||||
icon={() => (
|
||||
<MaterialIcons name="menu" color={"#fff"} size={40} />
|
||||
)}
|
||||
onPress={() => {
|
||||
console.log("menu pressed");
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<Stack>
|
||||
<View>
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
console.log("user menu pressed");
|
||||
}}
|
||||
>
|
||||
<View style={styles.userMenuContainer}>
|
||||
<Image
|
||||
source={{ uri: avatar }}
|
||||
width={35}
|
||||
height={35}
|
||||
style={styles.userMenuAvatar}
|
||||
/>
|
||||
<Text style={styles.userMenuText}>{user.username}</Text>
|
||||
</View>
|
||||
</Pressable>
|
||||
</View>
|
||||
</Stack>
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
<View />
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
import { useEffect } from 'react';
|
||||
import { StyleSheet } from 'react-native';
|
||||
import Animated, {
|
||||
useSharedValue,
|
||||
useAnimatedStyle,
|
||||
withTiming,
|
||||
withRepeat,
|
||||
withSequence,
|
||||
} from 'react-native-reanimated';
|
||||
|
||||
import { ThemedText } from '@/components/ThemedText';
|
||||
|
||||
export function HelloWave() {
|
||||
const rotationAnimation = useSharedValue(0);
|
||||
|
||||
useEffect(() => {
|
||||
rotationAnimation.value = withRepeat(
|
||||
withSequence(withTiming(25, { duration: 150 }), withTiming(0, { duration: 150 })),
|
||||
4 // Run the animation 4 times
|
||||
);
|
||||
}, []);
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => ({
|
||||
transform: [{ rotate: `${rotationAnimation.value}deg` }],
|
||||
}));
|
||||
|
||||
return (
|
||||
<Animated.View style={animatedStyle}>
|
||||
<ThemedText style={styles.text}>👋</ThemedText>
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
text: {
|
||||
fontSize: 28,
|
||||
lineHeight: 32,
|
||||
marginTop: -6,
|
||||
},
|
||||
});
|
|
@ -1,82 +0,0 @@
|
|||
import type { PropsWithChildren, ReactElement } from 'react';
|
||||
import { StyleSheet } from 'react-native';
|
||||
import Animated, {
|
||||
interpolate,
|
||||
useAnimatedRef,
|
||||
useAnimatedStyle,
|
||||
useScrollViewOffset,
|
||||
} from 'react-native-reanimated';
|
||||
|
||||
import { ThemedView } from '@/components/ThemedView';
|
||||
import { useBottomTabOverflow } from '@/components/ui/TabBarBackground';
|
||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||
|
||||
const HEADER_HEIGHT = 250;
|
||||
|
||||
type Props = PropsWithChildren<{
|
||||
headerImage: ReactElement;
|
||||
headerBackgroundColor: { dark: string; light: string };
|
||||
}>;
|
||||
|
||||
export default function ParallaxScrollView({
|
||||
children,
|
||||
headerImage,
|
||||
headerBackgroundColor,
|
||||
}: Props) {
|
||||
const colorScheme = useColorScheme() ?? 'light';
|
||||
const scrollRef = useAnimatedRef<Animated.ScrollView>();
|
||||
const scrollOffset = useScrollViewOffset(scrollRef);
|
||||
const bottom = useBottomTabOverflow();
|
||||
const headerAnimatedStyle = useAnimatedStyle(() => {
|
||||
return {
|
||||
transform: [
|
||||
{
|
||||
translateY: interpolate(
|
||||
scrollOffset.value,
|
||||
[-HEADER_HEIGHT, 0, HEADER_HEIGHT],
|
||||
[-HEADER_HEIGHT / 2, 0, HEADER_HEIGHT * 0.75]
|
||||
),
|
||||
},
|
||||
{
|
||||
scale: interpolate(scrollOffset.value, [-HEADER_HEIGHT, 0, HEADER_HEIGHT], [2, 1, 1]),
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<ThemedView style={styles.container}>
|
||||
<Animated.ScrollView
|
||||
ref={scrollRef}
|
||||
scrollEventThrottle={16}
|
||||
scrollIndicatorInsets={{ bottom }}
|
||||
contentContainerStyle={{ paddingBottom: bottom }}>
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.header,
|
||||
{ backgroundColor: headerBackgroundColor[colorScheme] },
|
||||
headerAnimatedStyle,
|
||||
]}>
|
||||
{headerImage}
|
||||
</Animated.View>
|
||||
<ThemedView style={styles.content}>{children}</ThemedView>
|
||||
</Animated.ScrollView>
|
||||
</ThemedView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
header: {
|
||||
height: HEADER_HEIGHT,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
padding: 32,
|
||||
gap: 16,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
});
|
|
@ -1,60 +0,0 @@
|
|||
import { Text, type TextProps, StyleSheet } from 'react-native';
|
||||
|
||||
import { useThemeColor } from '@/hooks/useThemeColor';
|
||||
|
||||
export type ThemedTextProps = TextProps & {
|
||||
lightColor?: string;
|
||||
darkColor?: string;
|
||||
type?: 'default' | 'title' | 'defaultSemiBold' | 'subtitle' | 'link';
|
||||
};
|
||||
|
||||
export function ThemedText({
|
||||
style,
|
||||
lightColor,
|
||||
darkColor,
|
||||
type = 'default',
|
||||
...rest
|
||||
}: ThemedTextProps) {
|
||||
const color = useThemeColor({ light: lightColor, dark: darkColor }, 'text');
|
||||
|
||||
return (
|
||||
<Text
|
||||
style={[
|
||||
{ color },
|
||||
type === 'default' ? styles.default : undefined,
|
||||
type === 'title' ? styles.title : undefined,
|
||||
type === 'defaultSemiBold' ? styles.defaultSemiBold : undefined,
|
||||
type === 'subtitle' ? styles.subtitle : undefined,
|
||||
type === 'link' ? styles.link : undefined,
|
||||
style,
|
||||
]}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
default: {
|
||||
fontSize: 16,
|
||||
lineHeight: 24,
|
||||
},
|
||||
defaultSemiBold: {
|
||||
fontSize: 16,
|
||||
lineHeight: 24,
|
||||
fontWeight: '600',
|
||||
},
|
||||
title: {
|
||||
fontSize: 32,
|
||||
fontWeight: 'bold',
|
||||
lineHeight: 32,
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: 20,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
link: {
|
||||
lineHeight: 30,
|
||||
fontSize: 16,
|
||||
color: '#0a7ea4',
|
||||
},
|
||||
});
|
|
@ -1,14 +0,0 @@
|
|||
import { View, type ViewProps } from 'react-native';
|
||||
|
||||
import { useThemeColor } from '@/hooks/useThemeColor';
|
||||
|
||||
export type ThemedViewProps = ViewProps & {
|
||||
lightColor?: string;
|
||||
darkColor?: string;
|
||||
};
|
||||
|
||||
export function ThemedView({ style, lightColor, darkColor, ...otherProps }: ThemedViewProps) {
|
||||
const backgroundColor = useThemeColor({ light: lightColor, dark: darkColor }, 'background');
|
||||
|
||||
return <View style={[{ backgroundColor }, style]} {...otherProps} />;
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import renderer from 'react-test-renderer';
|
||||
|
||||
import { ThemedText } from '../ThemedText';
|
||||
|
||||
it(`renders correctly`, () => {
|
||||
const tree = renderer.create(<ThemedText>Snapshot test!</ThemedText>).toJSON();
|
||||
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
|
@ -1,24 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders correctly 1`] = `
|
||||
<Text
|
||||
style={
|
||||
[
|
||||
{
|
||||
"color": "#11181C",
|
||||
},
|
||||
{
|
||||
"fontSize": 16,
|
||||
"lineHeight": 24,
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
]
|
||||
}
|
||||
>
|
||||
Snapshot test!
|
||||
</Text>
|
||||
`;
|
|
@ -1,32 +0,0 @@
|
|||
import { SymbolView, SymbolViewProps, SymbolWeight } from 'expo-symbols';
|
||||
import { StyleProp, ViewStyle } from 'react-native';
|
||||
|
||||
export function IconSymbol({
|
||||
name,
|
||||
size = 24,
|
||||
color,
|
||||
style,
|
||||
weight = 'regular',
|
||||
}: {
|
||||
name: SymbolViewProps['name'];
|
||||
size?: number;
|
||||
color: string;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
weight?: SymbolWeight;
|
||||
}) {
|
||||
return (
|
||||
<SymbolView
|
||||
weight={weight}
|
||||
tintColor={color}
|
||||
resizeMode="scaleAspectFit"
|
||||
name={name}
|
||||
style={[
|
||||
{
|
||||
width: size,
|
||||
height: size,
|
||||
},
|
||||
style,
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
// This file is a fallback for using MaterialIcons on Android and web.
|
||||
|
||||
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||
import { SymbolWeight } from 'expo-symbols';
|
||||
import React from 'react';
|
||||
import { OpaqueColorValue, StyleProp, ViewStyle } from 'react-native';
|
||||
|
||||
// Add your SFSymbol to MaterialIcons mappings here.
|
||||
const MAPPING = {
|
||||
// See MaterialIcons here: https://icons.expo.fyi
|
||||
// See SF Symbols in the SF Symbols app on Mac.
|
||||
'house.fill': 'home',
|
||||
'paperplane.fill': 'send',
|
||||
'chevron.left.forwardslash.chevron.right': 'code',
|
||||
'chevron.right': 'chevron-right',
|
||||
} as Partial<
|
||||
Record<
|
||||
import('expo-symbols').SymbolViewProps['name'],
|
||||
React.ComponentProps<typeof MaterialIcons>['name']
|
||||
>
|
||||
>;
|
||||
|
||||
export type IconSymbolName = keyof typeof MAPPING;
|
||||
|
||||
/**
|
||||
* An icon component that uses native SFSymbols on iOS, and MaterialIcons on Android and web. This ensures a consistent look across platforms, and optimal resource usage.
|
||||
*
|
||||
* Icon `name`s are based on SFSymbols and require manual mapping to MaterialIcons.
|
||||
*/
|
||||
export function IconSymbol({
|
||||
name,
|
||||
size = 24,
|
||||
color,
|
||||
style,
|
||||
}: {
|
||||
name: IconSymbolName;
|
||||
size?: number;
|
||||
color: string | OpaqueColorValue;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
weight?: SymbolWeight;
|
||||
}) {
|
||||
return <MaterialIcons color={color} size={size} name={MAPPING[name]} style={style} />;
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
|
||||
import { BlurView } from 'expo-blur';
|
||||
import { StyleSheet } from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
|
||||
export default function BlurTabBarBackground() {
|
||||
return (
|
||||
<BlurView
|
||||
// System chrome material automatically adapts to the system's theme
|
||||
// and matches the native tab bar appearance on iOS.
|
||||
tint="systemChromeMaterial"
|
||||
intensity={100}
|
||||
style={StyleSheet.absoluteFill}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function useBottomTabOverflow() {
|
||||
const tabHeight = useBottomTabBarHeight();
|
||||
const { bottom } = useSafeAreaInsets();
|
||||
return tabHeight - bottom;
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
// This is a shim for web and Android where the tab bar is generally opaque.
|
||||
export default undefined;
|
||||
|
||||
export function useBottomTabOverflow() {
|
||||
return 0;
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
/**
|
||||
* Below are the colors that are used in the app. The colors are defined in the light and dark mode.
|
||||
* There are many other ways to style your app. For example, [Nativewind](https://www.nativewind.dev/), [Tamagui](https://tamagui.dev/), [unistyles](https://reactnativeunistyles.vercel.app), etc.
|
||||
*/
|
||||
|
||||
const tintColorLight = '#0a7ea4';
|
||||
const tintColorDark = '#fff';
|
||||
|
||||
export const Colors = {
|
||||
light: {
|
||||
text: '#11181C',
|
||||
background: '#fff',
|
||||
tint: tintColorLight,
|
||||
icon: '#687076',
|
||||
tabIconDefault: '#687076',
|
||||
tabIconSelected: tintColorLight,
|
||||
},
|
||||
dark: {
|
||||
text: '#ECEDEE',
|
||||
background: '#151718',
|
||||
tint: tintColorDark,
|
||||
icon: '#9BA1A6',
|
||||
tabIconDefault: '#9BA1A6',
|
||||
tabIconSelected: tintColorDark,
|
||||
},
|
||||
};
|
15
functions/database.ts
Normal file
15
functions/database.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import * as SecureStore from "expo-secure-store";
|
||||
|
||||
export function set(key: string, value: string) {
|
||||
SecureStore.setItem(key, value);
|
||||
}
|
||||
|
||||
export function get(key: string) {
|
||||
const result = SecureStore.getItem(key);
|
||||
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
79
functions/zipline.ts
Normal file
79
functions/zipline.ts
Normal file
|
@ -0,0 +1,79 @@
|
|||
import * as db from "@/functions/database";
|
||||
import type { APIRecentFiles, APIStats, APIUser } from "@/types/zipline";
|
||||
import axios from "axios";
|
||||
|
||||
export async function getUser() {
|
||||
const token = db.get("token")
|
||||
const url = db.get("url")
|
||||
|
||||
if (!url || !token) return null;
|
||||
|
||||
try {
|
||||
const res = await axios.get(`${url}/api/user`, {
|
||||
headers: {
|
||||
Authorization: token
|
||||
}
|
||||
})
|
||||
|
||||
return res.data.user as APIUser;
|
||||
} catch(e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getRecentFiles() {
|
||||
const token = db.get("token")
|
||||
const url = db.get("url")
|
||||
|
||||
if (!url || !token) return null;
|
||||
|
||||
try {
|
||||
const res = await axios.get(`${url}/api/user/recent`, {
|
||||
headers: {
|
||||
Authorization: token
|
||||
}
|
||||
})
|
||||
|
||||
return res.data as APIRecentFiles;
|
||||
} catch(e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getStats() {
|
||||
const token = db.get("token")
|
||||
const url = db.get("url")
|
||||
|
||||
if (!url || !token) return null;
|
||||
|
||||
try {
|
||||
const res = await axios.get(`${url}/api/user/stats`, {
|
||||
headers: {
|
||||
Authorization: token
|
||||
}
|
||||
})
|
||||
|
||||
return res.data as APIStats;
|
||||
} catch(e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getUserAvatar() {
|
||||
const token = db.get("token")
|
||||
const url = db.get("url")
|
||||
|
||||
if (!url || !token) return null;
|
||||
|
||||
try {
|
||||
const res = await axios.get(`${url}/api/user/avatar`, {
|
||||
headers: {
|
||||
Authorization: token
|
||||
}
|
||||
})
|
||||
|
||||
return res.data as string;
|
||||
} catch(e) {
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
export { useColorScheme } from 'react-native';
|
|
@ -1,21 +0,0 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import { useColorScheme as useRNColorScheme } from 'react-native';
|
||||
|
||||
/**
|
||||
* To support static rendering, this value needs to be re-calculated on the client side for web
|
||||
*/
|
||||
export function useColorScheme() {
|
||||
const [hasHydrated, setHasHydrated] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setHasHydrated(true);
|
||||
}, []);
|
||||
|
||||
const colorScheme = useRNColorScheme();
|
||||
|
||||
if (hasHydrated) {
|
||||
return colorScheme;
|
||||
}
|
||||
|
||||
return 'light';
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
/**
|
||||
* Learn more about light and dark modes:
|
||||
* https://docs.expo.dev/guides/color-schemes/
|
||||
*/
|
||||
|
||||
import { Colors } from '@/constants/Colors';
|
||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||
|
||||
export function useThemeColor(
|
||||
props: { light?: string; dark?: string },
|
||||
colorName: keyof typeof Colors.light & keyof typeof Colors.dark
|
||||
) {
|
||||
const theme = useColorScheme() ?? 'light';
|
||||
const colorFromProps = props[theme];
|
||||
|
||||
if (colorFromProps) {
|
||||
return colorFromProps;
|
||||
} else {
|
||||
return Colors[theme][colorName];
|
||||
}
|
||||
}
|
10
package.json
10
package.json
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "test-share-target",
|
||||
"name": "zipline",
|
||||
"main": "expo-router/entry",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"start": "expo start",
|
||||
"reset-project": "node ./scripts/reset-project.js",
|
||||
"android": "expo start --android",
|
||||
"ios": "expo start --ios",
|
||||
"android": "expo run:android",
|
||||
"ios": "expo run:ios",
|
||||
"web": "expo start --web",
|
||||
"test": "jest --watchAll",
|
||||
"lint": "expo lint"
|
||||
|
@ -16,8 +16,10 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@expo/vector-icons": "^14.0.2",
|
||||
"@react-native-material/core": "^1.3.7",
|
||||
"@react-navigation/bottom-tabs": "^7.2.0",
|
||||
"@react-navigation/native": "^7.0.14",
|
||||
"axios": "^1.7.9",
|
||||
"expo": "~52.0.26",
|
||||
"expo-blur": "~14.0.2",
|
||||
"expo-constants": "~17.0.4",
|
||||
|
@ -25,6 +27,8 @@
|
|||
"expo-haptics": "~14.0.1",
|
||||
"expo-linking": "~7.0.4",
|
||||
"expo-router": "~4.0.17",
|
||||
"expo-secure-store": "^14.0.1",
|
||||
"expo-share-intent": "^3.2.0",
|
||||
"expo-splash-screen": "~0.29.21",
|
||||
"expo-status-bar": "~2.0.1",
|
||||
"expo-symbols": "~0.2.1",
|
||||
|
|
|
@ -1,84 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* This script is used to reset the project to a blank state.
|
||||
* It moves the /app, /components, /hooks, /scripts, and /constants directories to /app-example and creates a new /app directory with an index.tsx and _layout.tsx file.
|
||||
* You can remove the `reset-project` script from package.json and safely delete this file after running it.
|
||||
*/
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const root = process.cwd();
|
||||
const oldDirs = ["app", "components", "hooks", "constants", "scripts"];
|
||||
const newDir = "app-example";
|
||||
const newAppDir = "app";
|
||||
const newDirPath = path.join(root, newDir);
|
||||
|
||||
const indexContent = `import { Text, View } from "react-native";
|
||||
|
||||
export default function Index() {
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Text>Edit app/index.tsx to edit this screen.</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
`;
|
||||
|
||||
const layoutContent = `import { Stack } from "expo-router";
|
||||
|
||||
export default function RootLayout() {
|
||||
return <Stack />;
|
||||
}
|
||||
`;
|
||||
|
||||
const moveDirectories = async () => {
|
||||
try {
|
||||
// Create the app-example directory
|
||||
await fs.promises.mkdir(newDirPath, { recursive: true });
|
||||
console.log(`📁 /${newDir} directory created.`);
|
||||
|
||||
// Move old directories to new app-example directory
|
||||
for (const dir of oldDirs) {
|
||||
const oldDirPath = path.join(root, dir);
|
||||
const newDirPath = path.join(root, newDir, dir);
|
||||
if (fs.existsSync(oldDirPath)) {
|
||||
await fs.promises.rename(oldDirPath, newDirPath);
|
||||
console.log(`➡️ /${dir} moved to /${newDir}/${dir}.`);
|
||||
} else {
|
||||
console.log(`➡️ /${dir} does not exist, skipping.`);
|
||||
}
|
||||
}
|
||||
|
||||
// Create new /app directory
|
||||
const newAppDirPath = path.join(root, newAppDir);
|
||||
await fs.promises.mkdir(newAppDirPath, { recursive: true });
|
||||
console.log("\n📁 New /app directory created.");
|
||||
|
||||
// Create index.tsx
|
||||
const indexPath = path.join(newAppDirPath, "index.tsx");
|
||||
await fs.promises.writeFile(indexPath, indexContent);
|
||||
console.log("📄 app/index.tsx created.");
|
||||
|
||||
// Create _layout.tsx
|
||||
const layoutPath = path.join(newAppDirPath, "_layout.tsx");
|
||||
await fs.promises.writeFile(layoutPath, layoutContent);
|
||||
console.log("📄 app/_layout.tsx created.");
|
||||
|
||||
console.log("\n✅ Project reset complete. Next steps:");
|
||||
console.log(
|
||||
"1. Run `npx expo start` to start a development server.\n2. Edit app/index.tsx to edit the main screen.\n3. Delete the /app-example directory when you're done referencing it."
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(`Error during script execution: ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
moveDirectories();
|
53
styles/header.ts
Normal file
53
styles/header.ts
Normal file
|
@ -0,0 +1,53 @@
|
|||
import { StyleSheet } from "react-native";
|
||||
|
||||
export const styles = StyleSheet.create({
|
||||
header: {
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
padding: 10,
|
||||
backgroundColor: "#0c101c",
|
||||
zIndex: 99999,
|
||||
borderBottomWidth: 2,
|
||||
borderBottomColor: "#222c47",
|
||||
height: 70
|
||||
},
|
||||
|
||||
headerLeft: {
|
||||
flexDirection: "column",
|
||||
},
|
||||
|
||||
text: {
|
||||
fontFamily: "Arimo-Nerd-Font",
|
||||
color: "#fff",
|
||||
},
|
||||
|
||||
settings: {
|
||||
width: 30,
|
||||
height: 30,
|
||||
marginRight: 10,
|
||||
},
|
||||
|
||||
settingsIcon: {
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
},
|
||||
userMenuContainer: {
|
||||
flexWrap: "wrap",
|
||||
flexDirection: "row",
|
||||
},
|
||||
userMenuAvatar: {
|
||||
borderRadius: 8
|
||||
},
|
||||
userMenuText: {
|
||||
color: "white",
|
||||
fontWeight: "bold",
|
||||
marginLeft: 10,
|
||||
marginTop: 5,
|
||||
fontSize: 18
|
||||
}
|
||||
})
|
59
styles/home.ts
Normal file
59
styles/home.ts
Normal file
|
@ -0,0 +1,59 @@
|
|||
import { StyleSheet } from "react-native";
|
||||
|
||||
export const styles = StyleSheet.create({
|
||||
mainContainer: {
|
||||
backgroundColor: "#0c101c",
|
||||
flex: 1,
|
||||
marginTop: 50
|
||||
},
|
||||
loadingContainer: {
|
||||
flex: 1,
|
||||
display: "flex"
|
||||
},
|
||||
loadingText: {
|
||||
fontSize: 40,
|
||||
fontWeight: "bold",
|
||||
margin: "auto",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
color: "#304270",
|
||||
},
|
||||
loginContainer: {
|
||||
display: "flex",
|
||||
flex: 1,
|
||||
backgroundColor: "#181c28",
|
||||
justifyContent: "center",
|
||||
alignContent: "center",
|
||||
},
|
||||
loginBox: {
|
||||
width: "80%",
|
||||
borderWidth: 4,
|
||||
borderColor: "#222c47",
|
||||
borderStyle: "solid",
|
||||
padding: 10,
|
||||
borderRadius: 10,
|
||||
margin: "auto"
|
||||
},
|
||||
textInput: {
|
||||
width: "100%",
|
||||
borderWidth: 2,
|
||||
borderColor: "#222c47",
|
||||
marginBottom: 5,
|
||||
marginTop: 5,
|
||||
color: "white",
|
||||
padding: 3,
|
||||
borderRadius: 6
|
||||
},
|
||||
button: {
|
||||
width: "100%",
|
||||
backgroundColor: "#323ea8",
|
||||
padding: 10,
|
||||
marginTop: 5,
|
||||
borderRadius: 6
|
||||
},
|
||||
buttonText: {
|
||||
textAlign: "center",
|
||||
fontWeight: "bold",
|
||||
color: "white"
|
||||
}
|
||||
});
|
21
styles/not-found.ts
Normal file
21
styles/not-found.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { StyleSheet } from "react-native";
|
||||
|
||||
export const styles = StyleSheet.create({
|
||||
container: {
|
||||
display: "flex",
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
},
|
||||
code: {
|
||||
color: "white",
|
||||
fontSize: 40
|
||||
},
|
||||
text: {
|
||||
color: "#6c7a8d",
|
||||
fontSize: 20,
|
||||
},
|
||||
button: {
|
||||
color: "white",
|
||||
backgroundColor: "#323ea8"
|
||||
}
|
||||
});
|
101
types/zipline.ts
Normal file
101
types/zipline.ts
Normal file
|
@ -0,0 +1,101 @@
|
|||
// /api/user
|
||||
|
||||
export interface APIUser {
|
||||
id: string;
|
||||
username: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
role: "SUPERADMIN" | "ADMIN" | "USER";
|
||||
view: APIUserView;
|
||||
oauthProviders: Array<OAuthProvider>;
|
||||
totpSecret: string;
|
||||
passkeys: Array<Passkey>;
|
||||
quota: null;
|
||||
sessions: Array<string>;
|
||||
}
|
||||
|
||||
export interface APIUserView {
|
||||
enabled: boolean;
|
||||
align: "left" | "center" | "right";
|
||||
showMimetype: boolean;
|
||||
content: string;
|
||||
embed: boolean;
|
||||
embedTitle: string;
|
||||
embedDescription: string;
|
||||
embedColor: string;
|
||||
embedSiteName: string;
|
||||
}
|
||||
|
||||
export interface OAuthProvider {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
userId: string;
|
||||
provider: string;
|
||||
username: string;
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
oauthId: string;
|
||||
}
|
||||
|
||||
export interface Passkey {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
lastUsed: string;
|
||||
name: string;
|
||||
reg: PasskeyReg;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
export interface PasskeyReg {
|
||||
id: string;
|
||||
type: string;
|
||||
rawId: string;
|
||||
response: PasskeyRegResponse;
|
||||
clientExtensionResults: PasskeyRegClientExtensionResults;
|
||||
authenticatorAttachment: string;
|
||||
}
|
||||
|
||||
export interface PasskeyRegResponse {
|
||||
transports: Array<string>;
|
||||
clientDataJSON: string;
|
||||
attestationObject: string;
|
||||
}
|
||||
|
||||
export interface PasskeyRegClientExtensionResults {
|
||||
credProps: null;
|
||||
}
|
||||
|
||||
export type APIRecentFiles = Array<APIFile>;
|
||||
|
||||
export interface APIFile {
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
deletesAt: string | null;
|
||||
favorite: boolean;
|
||||
id: string;
|
||||
originalName: string | null;
|
||||
size: number;
|
||||
type: string;
|
||||
views: number;
|
||||
maxViews: number | null;
|
||||
thumbnail: string | null;
|
||||
tags: Array<string>;
|
||||
password: string | null;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface APIStats {
|
||||
filesUploaded: number;
|
||||
favoriteFiles: number;
|
||||
views: number;
|
||||
avgViews: number;
|
||||
storageUsed: number;
|
||||
avgStorageUsed: number;
|
||||
urlsCreated: number;
|
||||
urlViews: number;
|
||||
sortTypeCount: {
|
||||
[key: string]: number;
|
||||
};
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue