feat(mobile): improvements

This commit is contained in:
daniel31x13
2025-08-28 20:53:57 -04:00
parent 36be3d8772
commit 07665cee7e
10 changed files with 127 additions and 57 deletions

View File

@@ -32,7 +32,7 @@
"image": "./assets/images/splash-icon.png",
"imageWidth": 200,
"resizeMode": "contain",
"backgroundColor": "#ffffff"
"backgroundColor": "#020609"
}
],
"expo-secure-store",

View File

@@ -4,8 +4,8 @@ import { rawTheme, ThemeName } from "@/lib/colors";
import useAuthStore from "@/store/auth";
import { Redirect } from "expo-router";
import { useColorScheme } from "nativewind";
import { useState } from "react";
import { View, Text, Dimensions, TouchableOpacity } from "react-native";
import { useEffect, useState } from "react";
import { View, Text, Dimensions, TouchableOpacity, Image } from "react-native";
import Svg, { Path } from "react-native-svg";
export default function HomeScreen() {
@@ -17,16 +17,67 @@ export default function HomeScreen() {
user: "",
password: "",
token: "",
instance: "",
instance: auth.instance || "https://cloud.linkwarden.app",
});
const [showInstanceField, setShowInstanceField] = useState(
form.instance !== "https://cloud.linkwarden.app"
);
useEffect(() => {
setForm((prev) => ({
...prev,
instance: auth.instance || "https://cloud.linkwarden.app",
}));
}, [auth.instance]);
useEffect(() => {
setShowInstanceField(form.instance !== "https://cloud.linkwarden.app");
}, [form.instance]);
useEffect(() => {
setForm((prev) => ({
...prev,
token: "",
user: "",
password: "",
}));
}, [method]);
if (auth.status === "authenticated") {
return <Redirect href="/dashboard" />;
}
return (
<View className="flex-col justify-end h-full bg-primary">
<View className="flex-col justify-end h-full bg-primary relative">
<View className="my-auto">
<Image
source={require("@/assets/images/linkwarden.png")}
className="w-[120px] h-[120px] mx-auto"
/>
</View>
<Text className="text-base-100 text-7xl font-bold ml-8">Login</Text>
<View>
<Text className="text-base-100 text-2xl mx-8 mt-3" numberOfLines={1}>
Login to{" "}
{form.instance === "https://cloud.linkwarden.app"
? "cloud.linkwarden.app"
: form.instance}
</Text>
<TouchableOpacity
onPress={() => {
if (showInstanceField) {
setForm({ ...form, instance: "https://cloud.linkwarden.app" });
}
setShowInstanceField(!showInstanceField);
}}
className="mx-8 mt-2 w-fit"
>
<Text className="text-neutral-content text-sm w-fit">
{!showInstanceField ? "Change host" : "Use default host"}
</Text>
</TouchableOpacity>
</View>
<Svg
viewBox="0 0 1440 320"
width={Dimensions.get("screen").width}
@@ -38,19 +89,28 @@ export default function HomeScreen() {
d="M0,256L48,234.7C96,213,192,171,288,176C384,181,480,235,576,266.7C672,299,768,309,864,277.3C960,245,1056,171,1152,122.7C1248,75,1344,53,1392,42.7L1440,32L1440,320L1392,320C1344,320,1248,320,1152,320C1056,320,960,320,864,320C768,320,672,320,576,320C480,320,384,320,288,320C192,320,96,320,48,320L0,320Z"
/>
</Svg>
<View className="flex-col justify-end h-1/3 bg-base-100 -mt-2 pb-10 gap-4 w-full px-4">
<View className="flex-col justify-end h-auto duration-100 pt-10 bg-base-100 -mt-2 pb-10 gap-4 w-full px-4">
{showInstanceField && (
<Input
className="w-full text-xl p-3 leading-tight h-12"
textAlignVertical="center"
placeholder="Instance URL"
selectTextOnFocus={false}
value={form.instance}
onChangeText={(text) => setForm({ ...form, instance: text })}
/>
)}
{method === "password" ? (
<>
<Input
className="w-full text-xl p-3 leading-tight"
className="w-full text-xl p-3 leading-tight h-12"
textAlignVertical="center"
placeholder="Email or Username"
value={form.user}
onChangeText={(text) => setForm({ ...form, user: text })}
/>
<Input
className="w-full text-xl p-3 leading-tight"
className="w-full text-xl p-3 leading-tight h-12"
textAlignVertical="center"
placeholder="Password"
secureTextEntry
@@ -60,7 +120,7 @@ export default function HomeScreen() {
</>
) : (
<Input
className="w-full text-xl p-3 leading-tight"
className="w-full text-xl p-3 leading-tight h-12"
textAlignVertical="center"
placeholder="Access Token"
secureTextEntry
@@ -77,8 +137,8 @@ export default function HomeScreen() {
>
<Text className="text-primary w-fit text-center">
{method === "password"
? "Login with Access Token instead"
: "Login with Username/Password instead"}
? "Login with Access Token"
: "Login with Username/Password"}
</Text>
</TouchableOpacity>
@@ -86,11 +146,7 @@ export default function HomeScreen() {
variant="accent"
size="lg"
onPress={() =>
signIn(
form.user,
form.password,
form.instance ? form.instance : undefined
)
signIn(form.user, form.password, form.instance, form.token)
}
>
<Text className="text-white">Login</Text>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 241 KiB

View File

@@ -5,7 +5,12 @@ import { MobileAuth } from "@linkwarden/types";
type AuthStore = {
auth: MobileAuth;
signIn: (username: string, password: string, instance?: string) => void;
signIn: (
username: string,
password: string,
instance: string,
token: string
) => void;
signOut: () => void;
setAuth: () => void;
};
@@ -31,60 +36,69 @@ const useAuthStore = create<AuthStore>((set) => ({
} else {
set({
auth: {
instance: "",
instance: instance || "https://cloud.linkwarden.app",
session: null,
status: "unauthenticated",
},
});
}
},
signIn: async (
username,
password,
instance = process.env.NODE_ENV === "production"
? "https://cloud.linkwarden.app"
: (process.env.EXPO_PUBLIC_LINKWARDEN_URL as string)
) => {
signIn: async (username, password, instance, token) => {
if (process.env.EXPO_PUBLIC_SHOW_LOGS === "true")
console.log("Signing into", instance);
await fetch(instance + "/api/v1/session", {
method: "POST",
body: JSON.stringify({ username, password }),
headers: {
"Content-Type": "application/json",
},
}).then(async (res) => {
if (res.ok) {
const data = await res.json();
const session = (data as any).response.token;
await SecureStore.setItemAsync("TOKEN", session);
await SecureStore.setItemAsync("INSTANCE", instance);
set({
auth: {
session,
instance,
status: "authenticated",
},
});
if (token) {
await SecureStore.setItemAsync("TOKEN", token);
// make a request to the API to validate the token (TODO)
router.replace("/(tabs)/dashboard");
set({
auth: {
session: token,
instance,
status: "authenticated",
},
});
} else {
await fetch(instance + "/api/v1/session", {
method: "POST",
body: JSON.stringify({ username, password }),
headers: {
"Content-Type": "application/json",
},
}).then(async (res) => {
if (res.ok) {
const data = await res.json();
const session = (data as any).response.token;
await SecureStore.setItemAsync("TOKEN", session);
await SecureStore.setItemAsync("INSTANCE", instance);
set({
auth: {
session,
instance,
status: "authenticated",
},
});
router.replace("/(tabs)/dashboard");
} else {
set({
auth: {
instance,
session: null,
status: "unauthenticated",
},
});
}
});
router.replace("/(tabs)/dashboard");
} else {
set({
auth: {
instance,
session: null,
status: "unauthenticated",
},
});
}
});
}
},
signOut: async () => {
await SecureStore.deleteItemAsync("TOKEN");
const instance = await SecureStore.getItemAsync("INSTANCE");
set({
auth: {
instance: "",
instance,
session: null,
status: "unauthenticated",
},