feat(mobile): Add user setting for default bookmark view mode (#1723)

* feat(mobile): add user setting for default bookmark view mode

* regen db migration script

* clean up implementation

* Update docs/docs/07-Development/01-setup.md

* Update GEMINI.md

* use local setting instead of storing value in db

* improve start-dev.sh to also handle for db migration

* rename mobileBookmarkClickDefaultViewMode to defaultBookmarkView for consistency
This commit is contained in:
xuatz
2025-07-17 17:24:33 +09:00
committed by GitHub
parent a362756946
commit fe69ca8ce8
11 changed files with 214 additions and 10 deletions

9
.claude/settings.json Normal file
View File

@@ -0,0 +1,9 @@
{
"permissions": {
"allow": [
"Bash(pnpm typecheck:*)",
"Bash(pnpm lint:*)"
],
"deny": []
}
}

1
.gitignore vendored
View File

@@ -61,3 +61,4 @@ data
# VS-Code
.vscode
auth_failures.log
.claude/settings.local.json

View File

@@ -62,6 +62,7 @@ The project is organized into `apps` and `packages`:
- `pnpm format`: Format the codebase.
- `pnpm format:fix`: Fix formatting issues.
- `pnpm test`: Run tests.
- `pnpm db:generate --name description_of_schema_change`: db migration after making schema changes
Starting services:
- `pnpm web`: Start the web application (this doesn't return, unless you kill it).

View File

@@ -1,5 +1,5 @@
import { useEffect } from "react";
import { Pressable, Text, View } from "react-native";
import { ActivityIndicator, Pressable, Text, View } from "react-native";
import { Slider } from "react-native-awesome-slider";
import { useSharedValue } from "react-native-reanimated";
import { Link } from "expo-router";
@@ -67,6 +67,31 @@ export default function Dashboard() {
</Pressable>
</Link>
</View>
<View className="flex w-full flex-row items-center justify-between gap-8 rounded-lg bg-white px-4 py-2 dark:bg-accent">
<Link
asChild
href="/dashboard/settings/bookmark-default-view"
className="flex-1"
>
<Pressable className="flex flex-row justify-between">
<Text className="text-lg text-accent-foreground">
Default Bookmark View
</Text>
<View className="flex flex-row items-center gap-2">
{isSettingsLoading ? (
<ActivityIndicator size="small" />
) : (
<Text className="text-lg text-muted-foreground">
{settings.defaultBookmarkView === "reader"
? "Reader"
: "Browser"}
</Text>
)}
<ChevronRight color="rgb(0, 122, 255)" />
</View>
</Pressable>
</Link>
</View>
<Text className="w-full p-1 text-2xl font-bold text-foreground">
Upload Settings
</Text>

View File

@@ -136,6 +136,14 @@ export default function Dashboard() {
headerBackTitle: "Back",
}}
/>
<Stack.Screen
name="settings/bookmark-default-view"
options={{
title: "Bookmark View Mode",
headerTitle: "Bookmark View Mode",
headerBackTitle: "Back",
}}
/>
</StyledStack>
);
}

View File

@@ -1,4 +1,4 @@
import React, { useState } from "react";
import { useState } from "react";
import {
Alert,
Keyboard,
@@ -27,6 +27,7 @@ import FullPageSpinner from "@/components/ui/FullPageSpinner";
import { Input } from "@/components/ui/Input";
import { useToast } from "@/components/ui/Toast";
import { useAssetUrl } from "@/lib/hooks";
import useAppSettings from "@/lib/settings";
import { api } from "@/lib/trpc";
import { MenuView } from "@react-native-menu/menu";
import {
@@ -378,9 +379,11 @@ export default function ListView() {
const { slug } = useLocalSearchParams();
const { colorScheme } = useColorScheme();
const isDark = colorScheme === "dark";
const { settings } = useAppSettings();
const [bookmarkLinkType, setBookmarkLinkType] =
useState<BookmarkLinkType>("browser");
const [bookmarkLinkType, setBookmarkLinkType] = useState<BookmarkLinkType>(
settings.defaultBookmarkView,
);
if (typeof slug !== "string") {
throw new Error("Unexpected param type");

View File

@@ -0,0 +1,68 @@
import { Pressable, Text, View } from "react-native";
import { useRouter } from "expo-router";
import CustomSafeAreaView from "@/components/ui/CustomSafeAreaView";
import { Divider } from "@/components/ui/Divider";
import { useToast } from "@/components/ui/Toast";
import useAppSettings from "@/lib/settings";
import { Check } from "lucide-react-native";
export default function BookmarkDefaultViewSettings() {
const router = useRouter();
const { toast } = useToast();
const { settings, setSettings } = useAppSettings();
const handleUpdate = async (mode: "reader" | "browser") => {
try {
await setSettings({
...settings,
defaultBookmarkView: mode,
});
toast({
message: "Default Bookmark View updated!",
showProgress: false,
});
router.back();
} catch {
toast({
message: "Something went wrong",
variant: "destructive",
showProgress: false,
});
}
};
const options = (["reader", "browser"] as const)
.map((mode) => {
const currentMode = settings.defaultBookmarkView;
const isChecked = currentMode === mode;
return [
<Pressable
onPress={() => handleUpdate(mode)}
className="flex flex-row justify-between"
key={mode}
>
<Text className="text-lg text-accent-foreground">
{{ browser: "Browser", reader: "Reader" }[mode]}
</Text>
{isChecked && <Check color="rgb(0, 122, 255)" />}
</Pressable>,
<Divider
key={mode + "-divider"}
orientation="horizontal"
className="my-3 h-0.5 w-full"
/>,
];
})
.flat();
options.pop();
return (
<CustomSafeAreaView>
<View className="flex h-full w-full items-center px-4 py-2">
<View className="w-full rounded-lg bg-white px-4 py-2 dark:bg-accent">
{options}
</View>
</View>
</CustomSafeAreaView>
);
}

View File

@@ -10,6 +10,10 @@ const zSettingsSchema = z.object({
address: z.string(),
imageQuality: z.number().optional().default(0.2),
theme: z.enum(["light", "dark", "system"]).optional().default("system"),
defaultBookmarkView: z
.enum(["reader", "browser"])
.optional()
.default("reader"),
});
export type Settings = z.infer<typeof zSettingsSchema>;
@@ -23,7 +27,12 @@ interface AppSettingsState {
const useSettings = create<AppSettingsState>((set, get) => ({
settings: {
isLoading: true,
settings: { address: "", imageQuality: 0.2, theme: "system" },
settings: {
address: "",
imageQuality: 0.2,
theme: "system",
defaultBookmarkView: "reader",
},
},
setSettings: async (settings) => {
await SecureStore.setItemAsync(SETTING_NAME, JSON.stringify(settings));

View File

@@ -1,5 +1,31 @@
# Setup
## Quick Start
For the fastest way to get started with development, use the one-command setup script:
```bash
./start-dev.sh
```
This script will automatically:
- Start Meilisearch in Docker (on port 7700)
- Start headless Chrome in Docker (on port 9222)
- Install dependencies with `pnpm install` if needed
- Start both the web app and workers in parallel
- Provide cleanup when you stop with Ctrl+C
**Prerequisites:**
- Docker installed and running
- pnpm installed (see manual setup below for installation instructions)
The script will output the running services:
- Web app: http://localhost:3000
- Meilisearch: http://localhost:7700
- Chrome debugger: http://localhost:9222
Press Ctrl+C to stop all services and clean up Docker containers.
## Manual Setup
Karakeep uses `node` version 22. To install it, you can use `nvm` [^1]
@@ -88,14 +114,44 @@ The worker app will automatically start headless chrome on startup for crawling
- Run `pnpm workers` in the root of the repo.
### iOS Mobile App
### Mobile App (iOS & Android)
#### Prerequisites
To build and run the mobile app locally, you'll need:
- **For iOS development**:
- macOS computer
- Xcode installed from the App Store
- iOS Simulator (comes with Xcode)
- **For Android development**:
- Android Studio installed
- Android SDK configured
- Android Emulator or physical device
For detailed setup instructions, refer to the [Expo documentation](https://docs.expo.dev/guides/local-app-development/).
#### Running the app
- `cd apps/mobile`
- `pnpm exec expo prebuild --no-install` to build the app.
- Start the ios simulator.
**For iOS:**
- `pnpm exec expo run:ios`
- The app will be installed and started in the simulator.
**Troubleshooting iOS Setup:**
If you encounter an error like `xcrun: error: SDK "iphoneos" cannot be located`, you may need to set the correct Xcode developer directory:
```bash
sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
```
**For Android:**
- Start the Android emulator or connect a physical device.
- `pnpm exec expo run:android`
- The app will be installed and started on the emulator/device.
Changing the code will hot reload the app. However, installing new packages requires restarting the expo server.
### Browser Extension

View File

@@ -2,7 +2,7 @@
- The database schema lives in `packages/db/schema.ts`.
- Changing the schema, requires a migration.
- You can generate the migration by running `pnpm run db:generate` in the root dir.
- You can generate the migration by running `pnpm run db:generate --name description_of_schema_change` in the root dir.
- You can then apply the migration by running `pnpm run db:migrate`.
## Drizzle Studio

View File

@@ -50,9 +50,33 @@ if [ ! -d "node_modules" ]; then
pnpm install
fi
# Start the web app and workers in parallel
echo "Starting web app and workers..."
# Get DATA_DIR from environment or .env file
if [ -z "$DATA_DIR" ] && [ -f ".env" ]; then
DATA_DIR=$(grep "^DATA_DIR=" .env | cut -d'=' -f2)
fi
# Create DATA_DIR if it doesn't exist
if [ -n "$DATA_DIR" ] && [ ! -d "$DATA_DIR" ]; then
echo "Creating DATA_DIR at $DATA_DIR..."
mkdir -p "$DATA_DIR"
fi
# Start the web app
echo "Starting web app..."
pnpm web & WEB_PID=$!
# Wait for web app to be ready
echo "Waiting for web app to start..."
until curl -s http://localhost:3000 > /dev/null 2>&1; do
sleep 1
done
# Run database migrations
echo "Running database migrations..."
pnpm run db:migrate
# Start workers
echo "Starting workers..."
pnpm workers & WORKERS_PID=$!
# Function to handle script termination