mirror of
https://github.com/linkwarden/linkwarden.git
synced 2026-03-03 02:57:02 +00:00
46
.github/pull_request_template.md
vendored
Normal file
46
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
## What does this PR do?
|
||||||
|
|
||||||
|
<!-- Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. -->
|
||||||
|
|
||||||
|
- Fixes #XXXX (GitHub issue number)
|
||||||
|
|
||||||
|
## Visual Demo
|
||||||
|
|
||||||
|
A visual demonstration is strongly recommended, for both the original and new change **(video / image)**.
|
||||||
|
|
||||||
|
#### Video Demo (if applicable):
|
||||||
|
|
||||||
|
- Show screen recordings of the issue or feature.
|
||||||
|
- Demonstrate how to reproduce the issue, the behavior before and after the change.
|
||||||
|
|
||||||
|
#### Image Demo (if applicable):
|
||||||
|
|
||||||
|
- Add side-by-side screenshots of the original and updated change.
|
||||||
|
- Highlight any significant change(s).
|
||||||
|
|
||||||
|
## AI Assistance (Required)
|
||||||
|
|
||||||
|
We allow AI-assisted development, but reviewers need transparency to assess risk, maintainability, and correctness.
|
||||||
|
|
||||||
|
#### AI usage level (check one)
|
||||||
|
|
||||||
|
- [ ] None (no AI used)
|
||||||
|
- [ ] Light (spellcheck/rewording/comments/docs only)
|
||||||
|
- [ ] Medium (AI suggested small code changes/snippets that I adapted)
|
||||||
|
- [ ] Heavy (AI significantly shaped the implementation or architecture)
|
||||||
|
|
||||||
|
#### Which tool(s) where used?
|
||||||
|
|
||||||
|
- e.g., ChatGPT, Copilot, Cursor, etc.
|
||||||
|
|
||||||
|
## What was verified by the author?
|
||||||
|
|
||||||
|
<!-- Add what you personally checked to ensure correctness and safety. -->
|
||||||
|
|
||||||
|
- [ ] I reviewed **and** understood all AI/human generated code
|
||||||
|
- [ ] I validated behavior locally (tests/manual verification)
|
||||||
|
- [ ] I checked edge cases and failure modes
|
||||||
|
|
||||||
|
## Submission Acknowledgement
|
||||||
|
|
||||||
|
- [ ] I acknowledge that a decent size PR without self-review might be rejected
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
"expo": {
|
"expo": {
|
||||||
"name": "Linkwarden",
|
"name": "Linkwarden",
|
||||||
"slug": "linkwarden",
|
"slug": "linkwarden",
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"orientation": "portrait",
|
"orientation": "portrait",
|
||||||
"icon": "./assets/images/icon.png",
|
"icon": "./assets/images/icon.png",
|
||||||
"scheme": "linkwarden",
|
"scheme": "linkwarden",
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export default function CollectionsScreen() {
|
|||||||
collapsableChildren={false}
|
collapsableChildren={false}
|
||||||
>
|
>
|
||||||
{collections.isLoading ? (
|
{collections.isLoading ? (
|
||||||
<View className="flex justify-center h-full items-center">
|
<View className="flex justify-center h-screen items-center">
|
||||||
<ActivityIndicator size="large" />
|
<ActivityIndicator size="large" />
|
||||||
<Text className="text-base mt-2.5 text-neutral">Loading...</Text>
|
<Text className="text-base mt-2.5 text-neutral">Loading...</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ export default function Layout() {
|
|||||||
Platform.OS === "ios"
|
Platform.OS === "ios"
|
||||||
? "transparent"
|
? "transparent"
|
||||||
: colorScheme === "dark"
|
: colorScheme === "dark"
|
||||||
? rawTheme["dark"]["base-100"]
|
? rawTheme["dark"]["base-100"]
|
||||||
: "white",
|
: "white",
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
import { Platform, ScrollView, StyleSheet } from "react-native";
|
import {
|
||||||
|
ActivityIndicator,
|
||||||
|
Platform,
|
||||||
|
ScrollView,
|
||||||
|
StyleSheet,
|
||||||
|
Text,
|
||||||
|
View,
|
||||||
|
} from "react-native";
|
||||||
import React, { useEffect, useMemo, useState } from "react";
|
import React, { useEffect, useMemo, useState } from "react";
|
||||||
import { useDashboardData } from "@linkwarden/router/dashboardData";
|
import { useDashboardData } from "@linkwarden/router/dashboardData";
|
||||||
import useAuthStore from "@/store/auth";
|
import useAuthStore from "@/store/auth";
|
||||||
@@ -53,22 +60,36 @@ export default function DashboardScreen() {
|
|||||||
});
|
});
|
||||||
}, [dashboardSections]);
|
}, [dashboardSections]);
|
||||||
|
|
||||||
|
const [pullRefreshing, setPullRefreshing] = useState(false);
|
||||||
|
|
||||||
|
const onRefresh = async () => {
|
||||||
|
setPullRefreshing(true);
|
||||||
|
try {
|
||||||
|
await Promise.all([
|
||||||
|
dashboardData.refetch(),
|
||||||
|
userData.refetch(),
|
||||||
|
collectionsData.refetch(),
|
||||||
|
tagsData.refetch(),
|
||||||
|
]);
|
||||||
|
} finally {
|
||||||
|
setPullRefreshing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (orderedSections.length === 0 && dashboardData.isLoading)
|
||||||
|
return (
|
||||||
|
<View className="flex justify-center h-screen items-center bg-base-100">
|
||||||
|
<ActivityIndicator size="large" />
|
||||||
|
<Text className="text-base mt-2.5 text-neutral">Loading...</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView
|
<ScrollView
|
||||||
refreshControl={
|
refreshControl={
|
||||||
<Spinner
|
<Spinner
|
||||||
refreshing={
|
refreshing={pullRefreshing}
|
||||||
dashboardData.isRefetching ||
|
onRefresh={onRefresh}
|
||||||
userData.isRefetching ||
|
|
||||||
collectionsData.isRefetching ||
|
|
||||||
tagsData.isRefetching
|
|
||||||
}
|
|
||||||
onRefresh={() => {
|
|
||||||
dashboardData.refetch();
|
|
||||||
userData.refetch();
|
|
||||||
collectionsData.refetch();
|
|
||||||
tagsData.refetch();
|
|
||||||
}}
|
|
||||||
progressBackgroundColor={
|
progressBackgroundColor={
|
||||||
rawTheme[colorScheme as ThemeName]["base-200"]
|
rawTheme[colorScheme as ThemeName]["base-200"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export default function TagsScreen() {
|
|||||||
collapsableChildren={false}
|
collapsableChildren={false}
|
||||||
>
|
>
|
||||||
{tags.isLoading ? (
|
{tags.isLoading ? (
|
||||||
<View className="flex justify-center h-full items-center">
|
<View className="flex justify-center h-screen items-center">
|
||||||
<ActivityIndicator size="large" />
|
<ActivityIndicator size="large" />
|
||||||
<Text className="text-base mt-2.5 text-neutral">Loading...</Text>
|
<Text className="text-base mt-2.5 text-neutral">Loading...</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -20,56 +20,58 @@ export default function HomeScreen() {
|
|||||||
return (
|
return (
|
||||||
<Animated.View
|
<Animated.View
|
||||||
entering={SlideInDown.springify().damping(100).stiffness(300)}
|
entering={SlideInDown.springify().damping(100).stiffness(300)}
|
||||||
className="flex-col justify-end h-full bg-primary relative"
|
className="flex-col justify-end h-full"
|
||||||
>
|
>
|
||||||
<View className="my-auto">
|
<View className="h-full bg-primary relative">
|
||||||
<Image
|
<View className="my-auto">
|
||||||
source={require("@/assets/images/linkwarden.png")}
|
<Image
|
||||||
className="w-[120px] h-[120px] mx-auto"
|
source={require("@/assets/images/linkwarden.png")}
|
||||||
/>
|
className="w-[120px] h-[120px] mx-auto"
|
||||||
<Text className="text-base-100 text-4xl font-semibold mt-7 mx-auto">
|
/>
|
||||||
Linkwarden
|
<Text className="text-base-100 text-4xl font-semibold mt-7 mx-auto">
|
||||||
</Text>
|
Linkwarden
|
||||||
</View>
|
</Text>
|
||||||
<View>
|
</View>
|
||||||
<Text className="text-base-100 text-xl text-center font-semibold mx-4 mt-3">
|
<View>
|
||||||
Welcome to the official mobile app for Linkwarden!
|
<Text className="text-base-100 text-xl text-center font-semibold mx-4 mt-3">
|
||||||
</Text>
|
Welcome to the official mobile app for Linkwarden!
|
||||||
|
</Text>
|
||||||
|
|
||||||
<Text className="text-base-100 text-xl text-center mx-4 mt-3">
|
<Text className="text-base-100 text-xl text-center mx-4 mt-3">
|
||||||
Expect regular improvements and new features as we continue refining
|
Expect regular improvements and new features as we continue refining
|
||||||
the experience.
|
the experience.
|
||||||
</Text>
|
</Text>
|
||||||
|
</View>
|
||||||
|
<Svg
|
||||||
|
viewBox="0 0 1440 320"
|
||||||
|
width={Dimensions.get("screen").width}
|
||||||
|
height={Dimensions.get("screen").width * (320 / 1440) + 2}
|
||||||
|
>
|
||||||
|
<Path
|
||||||
|
fill={rawTheme[colorScheme as ThemeName]["base-100"]}
|
||||||
|
fill-opacity="1"
|
||||||
|
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>
|
||||||
|
<SafeAreaView
|
||||||
|
edges={["bottom"]}
|
||||||
|
className="flex-col justify-end h-auto duration-100 pt-10 bg-base-100 -mt-2 pb-10 gap-4 w-full px-4"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="accent"
|
||||||
|
size="lg"
|
||||||
|
onPress={() => router.navigate("/login")}
|
||||||
|
>
|
||||||
|
<Text className="text-white text-xl">Get Started</Text>
|
||||||
|
</Button>
|
||||||
|
<TouchableOpacity
|
||||||
|
className="w-fit mx-auto"
|
||||||
|
onPress={() => SheetManager.show("support-sheet")}
|
||||||
|
>
|
||||||
|
<Text className="text-neutral text-center w-fit">Need help?</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</SafeAreaView>
|
||||||
</View>
|
</View>
|
||||||
<Svg
|
|
||||||
viewBox="0 0 1440 320"
|
|
||||||
width={Dimensions.get("screen").width}
|
|
||||||
height={Dimensions.get("screen").width * (320 / 1440) + 2}
|
|
||||||
>
|
|
||||||
<Path
|
|
||||||
fill={rawTheme[colorScheme as ThemeName]["base-100"]}
|
|
||||||
fill-opacity="1"
|
|
||||||
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>
|
|
||||||
<SafeAreaView
|
|
||||||
edges={["bottom"]}
|
|
||||||
className="flex-col justify-end h-auto duration-100 pt-10 bg-base-100 -mt-2 pb-10 gap-4 w-full px-4"
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="accent"
|
|
||||||
size="lg"
|
|
||||||
onPress={() => router.navigate("/login")}
|
|
||||||
>
|
|
||||||
<Text className="text-white text-xl">Get Started</Text>
|
|
||||||
</Button>
|
|
||||||
<TouchableOpacity
|
|
||||||
className="w-fit mx-auto"
|
|
||||||
onPress={() => SheetManager.show("support-sheet")}
|
|
||||||
>
|
|
||||||
<Text className="text-neutral text-center w-fit">Need help?</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</SafeAreaView>
|
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Alert, Platform, Text, View } from "react-native";
|
import { Alert, Text, View } from "react-native";
|
||||||
import { useRef, useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
import ActionSheet, { ActionSheetRef } from "react-native-actions-sheet";
|
import ActionSheet, { ActionSheetRef } from "react-native-actions-sheet";
|
||||||
import Input from "@/components/ui/Input";
|
import Input from "@/components/ui/Input";
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { View, Text, Alert, Platform } from "react-native";
|
import { View, Text, Alert } from "react-native";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import ActionSheet, {
|
import ActionSheet, {
|
||||||
FlatList,
|
FlatList,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Alert, Platform, Text, View } from "react-native";
|
import { Alert, Text, View } from "react-native";
|
||||||
import { useRef, useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
import ActionSheet, { ActionSheetRef } from "react-native-actions-sheet";
|
import ActionSheet, { ActionSheetRef } from "react-native-actions-sheet";
|
||||||
import Input from "@/components/ui/Input";
|
import Input from "@/components/ui/Input";
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export default function Links({ links, data }: Props) {
|
|||||||
const [promptedRefetch, setPromptedRefetch] = useState(false);
|
const [promptedRefetch, setPromptedRefetch] = useState(false);
|
||||||
|
|
||||||
return data.isLoading ? (
|
return data.isLoading ? (
|
||||||
<View className="flex justify-center h-full items-center">
|
<View className="flex justify-center h-screen items-center">
|
||||||
<ActivityIndicator size="large" />
|
<ActivityIndicator size="large" />
|
||||||
<Text className="text-base mt-2.5 text-neutral">Loading...</Text>
|
<Text className="text-base mt-2.5 text-neutral">Loading...</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
import { PropsWithChildren } from "react";
|
|
||||||
import { IconSymbol } from "../ui/IconSymbol";
|
|
||||||
import ModalBase from "../ModalBase";
|
|
||||||
import { Text } from "react-native";
|
|
||||||
|
|
||||||
type Props = PropsWithChildren<{
|
|
||||||
isVisible: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
export default function AddLink({ isVisible, onClose }: Props) {
|
|
||||||
return (
|
|
||||||
// <ModalBase isVisible={isVisible} onClose={onClose}>
|
|
||||||
<Text>Hi</Text>
|
|
||||||
// </ModalBase>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"production": {
|
"production": {
|
||||||
|
"corepack": true,
|
||||||
"distribution": "store",
|
"distribution": "store",
|
||||||
"autoIncrement": true,
|
"autoIncrement": true,
|
||||||
"channel": "production"
|
"channel": "production"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@linkwarden/mobile",
|
"name": "@linkwarden/mobile",
|
||||||
"main": "expo-router/entry",
|
"main": "expo-router/entry",
|
||||||
"version": "1.0.0",
|
"version": "0.0.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "expo start",
|
"start": "expo start",
|
||||||
"android": "expo run:android",
|
"android": "expo run:android",
|
||||||
|
|||||||
@@ -3,13 +3,10 @@ import * as SecureStore from "expo-secure-store";
|
|||||||
import { router } from "expo-router";
|
import { router } from "expo-router";
|
||||||
import { MobileAuth } from "@linkwarden/types";
|
import { MobileAuth } from "@linkwarden/types";
|
||||||
import { Alert } from "react-native";
|
import { Alert } from "react-native";
|
||||||
import * as FileSystem from "expo-file-system";
|
|
||||||
import { queryClient } from "@/lib/queryClient";
|
import { queryClient } from "@/lib/queryClient";
|
||||||
import { mmkvPersister } from "@/lib/queryPersister";
|
import { mmkvPersister } from "@/lib/queryPersister";
|
||||||
import { clearCache } from "@/lib/cache";
|
import { clearCache } from "@/lib/cache";
|
||||||
|
|
||||||
const CACHE_DIR = FileSystem.documentDirectory + "archivedData/";
|
|
||||||
|
|
||||||
type AuthStore = {
|
type AuthStore = {
|
||||||
auth: MobileAuth;
|
auth: MobileAuth;
|
||||||
signIn: (
|
signIn: (
|
||||||
@@ -78,31 +75,42 @@ const useAuthStore = create<AuthStore>((set) => ({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await fetch(instance + "/api/v1/session", {
|
try {
|
||||||
method: "POST",
|
const res = await Promise.race([
|
||||||
body: JSON.stringify({ username, password }),
|
fetch(`${instance}/api/v1/session`, {
|
||||||
headers: {
|
method: "POST",
|
||||||
"Content-Type": "application/json",
|
body: JSON.stringify({ username, password }),
|
||||||
},
|
headers: { "Content-Type": "application/json" },
|
||||||
}).then(async (res) => {
|
}),
|
||||||
|
new Promise<Response>((_, reject) =>
|
||||||
|
setTimeout(() => reject(new Error("TIMEOUT")), 30000)
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
const session = (data as any).response.token;
|
const session = (data as any).response.token;
|
||||||
|
|
||||||
await SecureStore.setItemAsync("TOKEN", session);
|
await SecureStore.setItemAsync("TOKEN", session);
|
||||||
await SecureStore.setItemAsync("INSTANCE", instance);
|
await SecureStore.setItemAsync("INSTANCE", instance);
|
||||||
set({
|
set({ auth: { session, instance, status: "authenticated" } });
|
||||||
auth: {
|
|
||||||
session,
|
|
||||||
instance,
|
|
||||||
status: "authenticated",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
router.replace("/(tabs)/dashboard");
|
router.replace("/(tabs)/dashboard");
|
||||||
} else {
|
} else {
|
||||||
Alert.alert("Error", "Invalid credentials");
|
Alert.alert("Error", "Invalid credentials");
|
||||||
}
|
}
|
||||||
});
|
} catch (err: any) {
|
||||||
|
if (err?.message === "TIMEOUT") {
|
||||||
|
Alert.alert(
|
||||||
|
"Request timed out",
|
||||||
|
"Unable to reach the server in time. Please check your network configuration and try again."
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
Alert.alert(
|
||||||
|
"Network error",
|
||||||
|
"Could not connect to the server. Please check your network configuration and try again."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
signOut: async () => {
|
signOut: async () => {
|
||||||
|
|||||||
@@ -15,13 +15,13 @@ const useDataStore = create<DataStore>((set, get) => ({
|
|||||||
hasShareIntent: false,
|
hasShareIntent: false,
|
||||||
url: "",
|
url: "",
|
||||||
},
|
},
|
||||||
theme: "light",
|
theme: "system",
|
||||||
preferredBrowser: "app",
|
preferredBrowser: "app",
|
||||||
},
|
},
|
||||||
setData: async () => {
|
setData: async () => {
|
||||||
const dataString = JSON.parse((await AsyncStorage.getItem("data")) || "{}");
|
const dataString = JSON.parse((await AsyncStorage.getItem("data")) || "{}");
|
||||||
|
|
||||||
colorScheme.set(dataString.theme || "light");
|
colorScheme.set(dataString.theme || "system");
|
||||||
|
|
||||||
if (dataString)
|
if (dataString)
|
||||||
set((state) => ({ data: { ...state.data, ...dataString } }));
|
set((state) => ({ data: { ...state.data, ...dataString } }));
|
||||||
|
|||||||
Reference in New Issue
Block a user