Mobile UI: bottom tab navigation + auto-build APK on push
The Library crammed five text nav-links into a non-wrapping row with no bottom navigation — unusable on phones. Restructure the primary screens into a bottom tab bar: - Add src/app/(tabs)/_layout.tsx: <Tabs> with 5 tabs (Library, Search, Study, Ask, Settings) with emoji icons and the accent active tint. - Move index/search/study/ask/settings into the (tabs) route group; the parens keep URLs unchanged (/, /search, /study, /ask, /settings). - Root _layout becomes a Stack hosting (tabs) (headerless) plus the secondary pushed screens (record, transcript/[id], courses, quiz, bibliography), each with a back-button header. - Drop the per-screen <Stack.Screen> headers from the moved tabs (titles now come from Tabs); relocate Study's "Export Anki" action into the body. - Library: remove the cramped header link-row and duplicate title; add a "nothing uploaded" subheader and a Courses link. Verified at 375px: tab bar pinned to the bottom, 5 even tabs, navigation between tabs works. tsc clean, web export OK, 279 tests pass. CI: build-apk now runs automatically on every push to master (after deploy-web), so the APK at /wisp.apk tracks master instead of being frozen at a stale tag. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -7,12 +7,12 @@
|
|||||||
# `docker compose` simply builds and (re)creates the `wisp` container on the host.
|
# `docker compose` simply builds and (re)creates the `wisp` container on the host.
|
||||||
#
|
#
|
||||||
# Triggers:
|
# Triggers:
|
||||||
# push to master -> test, then deploy-web
|
# push to master -> test, then deploy-web, then build-apk
|
||||||
# pull_request -> test only
|
# pull_request -> test only
|
||||||
#
|
#
|
||||||
# Required repo secrets (Settings -> Actions -> Secrets) — used by the APK job
|
# Required repo secrets (Settings -> Actions -> Secrets) — used by the build-apk
|
||||||
# once it is enabled (see TODO at the bottom): ANDROID_KEYSTORE_BASE64,
|
# job: ANDROID_KEYSTORE_BASE64, ANDROID_KEYSTORE_PASSWORD, ANDROID_KEY_ALIAS,
|
||||||
# ANDROID_KEYSTORE_PASSWORD, ANDROID_KEY_ALIAS, ANDROID_KEY_PASSWORD.
|
# ANDROID_KEY_PASSWORD.
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
name: CI
|
name: CI
|
||||||
@@ -72,11 +72,11 @@ jobs:
|
|||||||
# with docker create/cp and copy it into the running `wisp` web container so
|
# with docker create/cp and copy it into the running `wisp` web container so
|
||||||
# it is downloadable at https://wisp.briggen.dev/wisp.apk.
|
# it is downloadable at https://wisp.briggen.dev/wisp.apk.
|
||||||
build-apk:
|
build-apk:
|
||||||
# ONLY on version tags (v*). The native build is heavy (NDK/whisper.cpp) and
|
# Auto-build the APK on every push to master (arm64-only keeps it ~minutes).
|
||||||
# slow on the shared prod runner; building it on every push would peg the box
|
# Runs after deploy-web so the web updates promptly, then the APK follows and
|
||||||
# (and degrade the live web apps). Cut a release with: git tag v1.2.3 && git push --tags
|
# is published to /srv/wisp/wisp.apk (served at /wisp.apk via the compose mount).
|
||||||
needs: [test]
|
needs: [test, deploy-web]
|
||||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
|
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 120
|
timeout-minutes: 120
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
// Bottom tab navigation for the primary destinations (mobile-first). Secondary
|
||||||
|
// screens (Record, Transcript, Courses, Quiz, Bibliography) are pushed on the
|
||||||
|
// root Stack from within these tabs. Route-group parens keep URLs unchanged
|
||||||
|
// (/, /search, /study, /ask, /settings).
|
||||||
|
import { Tabs } from 'expo-router';
|
||||||
|
import { Text, type ColorValue } from 'react-native';
|
||||||
|
|
||||||
|
import { useTheme } from '@/hooks/use-theme';
|
||||||
|
|
||||||
|
const ACCENT = '#3c87f7';
|
||||||
|
|
||||||
|
function TabIcon({ glyph, color }: { glyph: string; color: ColorValue }) {
|
||||||
|
return <Text style={{ fontSize: 20, color }}>{glyph}</Text>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TabsLayout() {
|
||||||
|
const theme = useTheme();
|
||||||
|
return (
|
||||||
|
<Tabs
|
||||||
|
screenOptions={{
|
||||||
|
tabBarActiveTintColor: ACCENT,
|
||||||
|
tabBarInactiveTintColor: theme.textSecondary,
|
||||||
|
tabBarStyle: { backgroundColor: theme.background, borderTopColor: theme.backgroundElement },
|
||||||
|
headerStyle: { backgroundColor: theme.background },
|
||||||
|
headerTitleStyle: { color: theme.text },
|
||||||
|
headerTintColor: theme.text,
|
||||||
|
headerShadowVisible: false,
|
||||||
|
}}>
|
||||||
|
<Tabs.Screen
|
||||||
|
name="index"
|
||||||
|
options={{ title: 'Wisp', tabBarLabel: 'Library', tabBarIcon: ({ color }) => <TabIcon glyph="📚" color={color} /> }}
|
||||||
|
/>
|
||||||
|
<Tabs.Screen
|
||||||
|
name="search"
|
||||||
|
options={{ title: 'Search', tabBarIcon: ({ color }) => <TabIcon glyph="🔎" color={color} /> }}
|
||||||
|
/>
|
||||||
|
<Tabs.Screen
|
||||||
|
name="study"
|
||||||
|
options={{ title: 'Study', tabBarIcon: ({ color }) => <TabIcon glyph="🎴" color={color} /> }}
|
||||||
|
/>
|
||||||
|
<Tabs.Screen
|
||||||
|
name="ask"
|
||||||
|
options={{ title: 'Ask', tabBarIcon: ({ color }) => <TabIcon glyph="✨" color={color} /> }}
|
||||||
|
/>
|
||||||
|
<Tabs.Screen
|
||||||
|
name="settings"
|
||||||
|
options={{ title: 'Settings', tabBarIcon: ({ color }) => <TabIcon glyph="⚙️" color={color} /> }}
|
||||||
|
/>
|
||||||
|
</Tabs>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Stack, useFocusEffect, useRouter } from 'expo-router';
|
import { useFocusEffect, useRouter } from 'expo-router';
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
@@ -81,7 +81,6 @@ export default function AskScreen() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemedView style={styles.fill}>
|
<ThemedView style={styles.fill}>
|
||||||
<Stack.Screen options={{ title: 'Ask' }} />
|
|
||||||
<ScrollView contentContainerStyle={styles.content} keyboardShouldPersistTaps="handled">
|
<ScrollView contentContainerStyle={styles.content} keyboardShouldPersistTaps="handled">
|
||||||
<ThemedText type="small" themeColor="textSecondary">
|
<ThemedText type="small" themeColor="textSecondary">
|
||||||
Ask a question and get an answer grounded in your lectures — every claim links back to the
|
Ask a question and get an answer grounded in your lectures — every claim links back to the
|
||||||
@@ -14,13 +14,12 @@ import { ThemedText } from '@/components/themed-text';
|
|||||||
import { ThemedView } from '@/components/themed-view';
|
import { ThemedView } from '@/components/themed-view';
|
||||||
import { MaxContentWidth, Spacing } from '@/constants/theme';
|
import { MaxContentWidth, Spacing } from '@/constants/theme';
|
||||||
import { useTheme } from '@/hooks/use-theme';
|
import { useTheme } from '@/hooks/use-theme';
|
||||||
import { getRepo, type TranscriptMeta } from '@/lib/db';
|
import type { TranscriptMeta } from '@/lib/db';
|
||||||
import { formatClock } from '@/lib/format';
|
import { formatClock } from '@/lib/format';
|
||||||
import { MODELS } from '@/lib/models/catalog';
|
|
||||||
import { pickAudio, type PickedAudio } from '@/lib/pickAudio';
|
import { pickAudio, type PickedAudio } from '@/lib/pickAudio';
|
||||||
import { useCourses } from '@/stores/coursesStore';
|
import { useCourses } from '@/stores/coursesStore';
|
||||||
import { useTranscribe } from '@/stores/transcribeStore';
|
import { useTranscribe } from '@/stores/transcribeStore';
|
||||||
import { useTranscripts, type CourseFilter } from '@/stores/transcriptsStore';
|
import { useTranscripts } from '@/stores/transcriptsStore';
|
||||||
|
|
||||||
export default function LibraryScreen() {
|
export default function LibraryScreen() {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
@@ -31,21 +30,11 @@ export default function LibraryScreen() {
|
|||||||
const refreshCourses = useCourses((s) => s.refresh);
|
const refreshCourses = useCourses((s) => s.refresh);
|
||||||
const job = useTranscribe();
|
const job = useTranscribe();
|
||||||
const [picked, setPicked] = useState<PickedAudio | null>(null);
|
const [picked, setPicked] = useState<PickedAudio | null>(null);
|
||||||
const [dueCount, setDueCount] = useState(0);
|
|
||||||
|
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
void refresh();
|
void refresh();
|
||||||
void refreshCourses();
|
void refreshCourses();
|
||||||
let alive = true;
|
|
||||||
void getRepo()
|
|
||||||
.flashcardCounts()
|
|
||||||
.then((c) => {
|
|
||||||
if (alive) setDueCount(c.due);
|
|
||||||
});
|
|
||||||
return () => {
|
|
||||||
alive = false;
|
|
||||||
};
|
|
||||||
}, [refresh, refreshCourses]),
|
}, [refresh, refreshCourses]),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -54,7 +43,7 @@ export default function LibraryScreen() {
|
|||||||
const onNew = useCallback(async () => {
|
const onNew = useCallback(async () => {
|
||||||
if (busy) return;
|
if (busy) return;
|
||||||
const p = await pickAudio();
|
const p = await pickAudio();
|
||||||
if (p) setPicked(p); // opens the pre-capture sheet
|
if (p) setPicked(p);
|
||||||
}, [busy]);
|
}, [busy]);
|
||||||
|
|
||||||
const onStart = useCallback(
|
const onStart = useCallback(
|
||||||
@@ -74,42 +63,15 @@ export default function LibraryScreen() {
|
|||||||
return (
|
return (
|
||||||
<ThemedView style={styles.fill}>
|
<ThemedView style={styles.fill}>
|
||||||
<ScrollView contentContainerStyle={styles.content}>
|
<ScrollView contentContainerStyle={styles.content}>
|
||||||
<View style={styles.headerRow}>
|
<View style={styles.subHeader}>
|
||||||
<View style={styles.flex}>
|
<ThemedText type="small" themeColor="textSecondary" style={styles.flex}>
|
||||||
<ThemedText type="subtitle">Wisp</ThemedText>
|
On your device — nothing uploaded.
|
||||||
<ThemedText type="small" themeColor="textSecondary">
|
</ThemedText>
|
||||||
Private transcription — runs on your device, nothing uploaded.
|
<Link href="/courses" asChild>
|
||||||
</ThemedText>
|
<Pressable hitSlop={8}>
|
||||||
</View>
|
<ThemedText type="link" themeColor="textSecondary">Courses ›</ThemedText>
|
||||||
<View style={styles.headerLinks}>
|
</Pressable>
|
||||||
<Link href="/search" asChild>
|
</Link>
|
||||||
<Pressable hitSlop={8}>
|
|
||||||
<ThemedText type="link" themeColor="textSecondary">Search</ThemedText>
|
|
||||||
</Pressable>
|
|
||||||
</Link>
|
|
||||||
<Link href="/ask" asChild>
|
|
||||||
<Pressable hitSlop={8}>
|
|
||||||
<ThemedText type="link" themeColor="textSecondary">Ask</ThemedText>
|
|
||||||
</Pressable>
|
|
||||||
</Link>
|
|
||||||
<Link href="/courses" asChild>
|
|
||||||
<Pressable hitSlop={8}>
|
|
||||||
<ThemedText type="link" themeColor="textSecondary">Courses</ThemedText>
|
|
||||||
</Pressable>
|
|
||||||
</Link>
|
|
||||||
<Link href="/study" asChild>
|
|
||||||
<Pressable hitSlop={8}>
|
|
||||||
<ThemedText type="link" themeColor="textSecondary">
|
|
||||||
{dueCount > 0 ? `Study (${dueCount})` : 'Study'}
|
|
||||||
</ThemedText>
|
|
||||||
</Pressable>
|
|
||||||
</Link>
|
|
||||||
<Link href="/settings" asChild>
|
|
||||||
<Pressable hitSlop={8}>
|
|
||||||
<ThemedText type="link" themeColor="textSecondary">⚙ Settings</ThemedText>
|
|
||||||
</Pressable>
|
|
||||||
</Link>
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.captureRow}>
|
<View style={styles.captureRow}>
|
||||||
@@ -120,7 +82,7 @@ export default function LibraryScreen() {
|
|||||||
styles.captureBtn,
|
styles.captureBtn,
|
||||||
{ backgroundColor: '#e5484d', opacity: busy ? 0.5 : pressed ? 0.85 : 1 },
|
{ backgroundColor: '#e5484d', opacity: busy ? 0.5 : pressed ? 0.85 : 1 },
|
||||||
]}>
|
]}>
|
||||||
<ThemedText style={styles.newButtonText}>🎙 Record</ThemedText>
|
<ThemedText style={styles.captureText}>🎙 Record</ThemedText>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</Link>
|
</Link>
|
||||||
<Pressable
|
<Pressable
|
||||||
@@ -130,7 +92,7 @@ export default function LibraryScreen() {
|
|||||||
styles.captureBtn,
|
styles.captureBtn,
|
||||||
{ backgroundColor: '#3c87f7', opacity: busy ? 0.5 : pressed ? 0.85 : 1 },
|
{ backgroundColor: '#3c87f7', opacity: busy ? 0.5 : pressed ? 0.85 : 1 },
|
||||||
]}>
|
]}>
|
||||||
<ThemedText style={styles.newButtonText}>+ Import file</ThemedText>
|
<ThemedText style={styles.captureText}>+ Import</ThemedText>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@@ -142,18 +104,20 @@ export default function LibraryScreen() {
|
|||||||
</ThemedView>
|
</ThemedView>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.filterBar}>
|
{courses.length > 0 && (
|
||||||
<FilterChip label="All" active={courseFilter === 'all'} onPress={() => void setCourseFilter('all')} />
|
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.filterBar}>
|
||||||
<FilterChip label="Unsorted" active={courseFilter === null} onPress={() => void setCourseFilter(null)} />
|
<FilterChip label="All" active={courseFilter === 'all'} onPress={() => void setCourseFilter('all')} />
|
||||||
{courses.map((c) => (
|
<FilterChip label="Unsorted" active={courseFilter === null} onPress={() => void setCourseFilter(null)} />
|
||||||
<FilterChip key={c.id} label={c.name} active={courseFilter === c.id} onPress={() => void setCourseFilter(c.id)} />
|
{courses.map((c) => (
|
||||||
))}
|
<FilterChip key={c.id} label={c.name} active={courseFilter === c.id} onPress={() => void setCourseFilter(c.id)} />
|
||||||
</ScrollView>
|
))}
|
||||||
|
</ScrollView>
|
||||||
|
)}
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
value={query}
|
value={query}
|
||||||
onChangeText={(t) => void setQuery(t)}
|
onChangeText={(t) => void setQuery(t)}
|
||||||
placeholder="Search transcripts…"
|
placeholder="Filter by title…"
|
||||||
placeholderTextColor={theme.textSecondary}
|
placeholderTextColor={theme.textSecondary}
|
||||||
style={[styles.search, { color: theme.text, backgroundColor: theme.backgroundElement }]}
|
style={[styles.search, { color: theme.text, backgroundColor: theme.backgroundElement }]}
|
||||||
/>
|
/>
|
||||||
@@ -162,7 +126,7 @@ export default function LibraryScreen() {
|
|||||||
<ActivityIndicator style={styles.pad} />
|
<ActivityIndicator style={styles.pad} />
|
||||||
) : items.length === 0 ? (
|
) : items.length === 0 ? (
|
||||||
<ThemedText type="small" themeColor="textSecondary" style={styles.pad}>
|
<ThemedText type="small" themeColor="textSecondary" style={styles.pad}>
|
||||||
{query ? 'No matches.' : 'No transcripts here yet. Pick an audio or video file to begin.'}
|
{query ? 'No matches.' : 'No lectures yet. Record one or import an audio/video file to begin.'}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
) : (
|
) : (
|
||||||
items.map((t) => (
|
items.map((t) => (
|
||||||
@@ -245,9 +209,7 @@ function TranscriptRow({
|
|||||||
<Pressable onPress={onOpen} style={({ pressed }) => [pressed && styles.pressed]}>
|
<Pressable onPress={onOpen} style={({ pressed }) => [pressed && styles.pressed]}>
|
||||||
<ThemedView type="backgroundElement" style={styles.card}>
|
<ThemedView type="backgroundElement" style={styles.card}>
|
||||||
<View style={styles.rowBetween}>
|
<View style={styles.rowBetween}>
|
||||||
<ThemedText type="smallBold" numberOfLines={1} style={styles.flex}>
|
<ThemedText type="smallBold" numberOfLines={1} style={styles.flex}>{item.title}</ThemedText>
|
||||||
{item.title}
|
|
||||||
</ThemedText>
|
|
||||||
<Pressable onPress={onDelete} hitSlop={8}>
|
<Pressable onPress={onDelete} hitSlop={8}>
|
||||||
<ThemedText type="small" themeColor="textSecondary">✕</ThemedText>
|
<ThemedText type="small" themeColor="textSecondary">✕</ThemedText>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
@@ -269,20 +231,19 @@ const styles = StyleSheet.create({
|
|||||||
maxWidth: MaxContentWidth,
|
maxWidth: MaxContentWidth,
|
||||||
width: '100%',
|
width: '100%',
|
||||||
alignSelf: 'center',
|
alignSelf: 'center',
|
||||||
|
paddingBottom: Spacing.six,
|
||||||
},
|
},
|
||||||
flex: { flex: 1 },
|
flex: { flex: 1 },
|
||||||
headerRow: { flexDirection: 'row', alignItems: 'flex-start', gap: Spacing.two },
|
subHeader: { flexDirection: 'row', alignItems: 'center', gap: Spacing.two },
|
||||||
headerLinks: { flexDirection: 'row', gap: Spacing.three, alignItems: 'center' },
|
|
||||||
newButton: { paddingVertical: Spacing.three, borderRadius: Spacing.three, alignItems: 'center' },
|
|
||||||
newButtonText: { color: '#fff', fontWeight: '700', fontSize: 16 },
|
|
||||||
captureRow: { flexDirection: 'row', gap: Spacing.two },
|
captureRow: { flexDirection: 'row', gap: Spacing.two },
|
||||||
captureBtn: { flex: 1, paddingVertical: Spacing.three, borderRadius: Spacing.three, alignItems: 'center' },
|
captureBtn: { flex: 1, paddingVertical: Spacing.three, borderRadius: Spacing.three, alignItems: 'center' },
|
||||||
|
captureText: { color: '#fff', fontWeight: '700', fontSize: 16 },
|
||||||
card: { padding: Spacing.three, borderRadius: Spacing.three, gap: Spacing.two },
|
card: { padding: Spacing.three, borderRadius: Spacing.three, gap: Spacing.two },
|
||||||
rowBetween: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', gap: Spacing.two },
|
rowBetween: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', gap: Spacing.two },
|
||||||
filterBar: { gap: Spacing.two, paddingVertical: Spacing.one, paddingRight: Spacing.three },
|
filterBar: { gap: Spacing.two, paddingVertical: Spacing.one, paddingRight: Spacing.three },
|
||||||
filterChip: { paddingHorizontal: Spacing.three, paddingVertical: Spacing.one, borderRadius: 999 },
|
filterChip: { paddingHorizontal: Spacing.three, paddingVertical: Spacing.two, borderRadius: 999 },
|
||||||
chipActive: { color: '#fff', fontWeight: '700' },
|
chipActive: { color: '#fff', fontWeight: '700' },
|
||||||
search: { borderRadius: Spacing.two, paddingHorizontal: Spacing.three, paddingVertical: Spacing.two, fontSize: 15 },
|
search: { borderRadius: Spacing.two, paddingHorizontal: Spacing.three, paddingVertical: Spacing.three, fontSize: 15 },
|
||||||
track: { height: 6, borderRadius: 3, backgroundColor: '#88888833', overflow: 'hidden' },
|
track: { height: 6, borderRadius: 3, backgroundColor: '#88888833', overflow: 'hidden' },
|
||||||
bar: { height: 6, borderRadius: 3, backgroundColor: '#3c87f7' },
|
bar: { height: 6, borderRadius: 3, backgroundColor: '#3c87f7' },
|
||||||
pad: { paddingVertical: Spacing.four, textAlign: 'center' },
|
pad: { paddingVertical: Spacing.four, textAlign: 'center' },
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Stack, useFocusEffect, useRouter } from 'expo-router';
|
import { useFocusEffect, useRouter } from 'expo-router';
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { ActivityIndicator, Pressable, ScrollView, StyleSheet, TextInput, View } from 'react-native';
|
import { ActivityIndicator, Pressable, ScrollView, StyleSheet, TextInput, View } from 'react-native';
|
||||||
|
|
||||||
@@ -104,7 +104,6 @@ export default function SearchScreen() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemedView style={styles.fill}>
|
<ThemedView style={styles.fill}>
|
||||||
<Stack.Screen options={{ title: 'Search' }} />
|
|
||||||
<ScrollView contentContainerStyle={styles.content} keyboardShouldPersistTaps="handled">
|
<ScrollView contentContainerStyle={styles.content} keyboardShouldPersistTaps="handled">
|
||||||
<ThemedText type="small" themeColor="textSecondary">
|
<ThemedText type="small" themeColor="textSecondary">
|
||||||
Semantic search across your lectures — runs entirely on your device.
|
Semantic search across your lectures — runs entirely on your device.
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Stack, useFocusEffect, useRouter } from 'expo-router';
|
import { useFocusEffect, useRouter } from 'expo-router';
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import { ActivityIndicator, Pressable, ScrollView, StyleSheet, View } from 'react-native';
|
import { ActivityIndicator, Pressable, ScrollView, StyleSheet, View } from 'react-native';
|
||||||
|
|
||||||
@@ -80,17 +80,12 @@ export default function StudyScreen() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemedView style={styles.fill}>
|
<ThemedView style={styles.fill}>
|
||||||
<Stack.Screen
|
|
||||||
options={{
|
|
||||||
title: 'Study',
|
|
||||||
headerRight: () => (
|
|
||||||
<Pressable onPress={() => void exportAnki()} hitSlop={8}>
|
|
||||||
<ThemedText type="link" themeColor="textSecondary">Export Anki (.csv)</ThemedText>
|
|
||||||
</Pressable>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ScrollView contentContainerStyle={styles.content}>
|
<ScrollView contentContainerStyle={styles.content}>
|
||||||
|
<View style={styles.topActions}>
|
||||||
|
<Pressable onPress={() => void exportAnki()} hitSlop={8}>
|
||||||
|
<ThemedText type="link" themeColor="textSecondary">Export Anki (.csv)</ThemedText>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.filterBar}>
|
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.filterBar}>
|
||||||
<FilterChip label="All" active={scope === 'all'} onPress={() => pickScope('all')} />
|
<FilterChip label="All" active={scope === 'all'} onPress={() => pickScope('all')} />
|
||||||
<FilterChip label="Unsorted" active={scope === null} onPress={() => pickScope(null)} />
|
<FilterChip label="Unsorted" active={scope === null} onPress={() => pickScope(null)} />
|
||||||
@@ -192,6 +187,7 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
filterBar: { gap: Spacing.two, paddingVertical: Spacing.one, paddingRight: Spacing.three },
|
filterBar: { gap: Spacing.two, paddingVertical: Spacing.one, paddingRight: Spacing.three },
|
||||||
filterChip: { paddingHorizontal: Spacing.three, paddingVertical: Spacing.one, borderRadius: 999 },
|
filterChip: { paddingHorizontal: Spacing.three, paddingVertical: Spacing.one, borderRadius: 999 },
|
||||||
|
topActions: { flexDirection: 'row', justifyContent: 'flex-end' },
|
||||||
chipActive: { color: '#fff', fontWeight: '700' },
|
chipActive: { color: '#fff', fontWeight: '700' },
|
||||||
card: { padding: Spacing.four, borderRadius: Spacing.three, gap: Spacing.three },
|
card: { padding: Spacing.four, borderRadius: Spacing.three, gap: Spacing.three },
|
||||||
front: { fontSize: 24, lineHeight: 32 },
|
front: { fontSize: 24, lineHeight: 32 },
|
||||||
+1
-5
@@ -14,16 +14,12 @@ export default function RootLayout() {
|
|||||||
return (
|
return (
|
||||||
<ThemeProvider value={scheme === 'dark' ? DarkTheme : DefaultTheme}>
|
<ThemeProvider value={scheme === 'dark' ? DarkTheme : DefaultTheme}>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Stack.Screen name="index" options={{ title: 'Wisp' }} />
|
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
|
||||||
<Stack.Screen name="record" options={{ title: 'Record' }} />
|
<Stack.Screen name="record" options={{ title: 'Record' }} />
|
||||||
<Stack.Screen name="transcript/[id]" options={{ title: 'Transcript' }} />
|
<Stack.Screen name="transcript/[id]" options={{ title: 'Transcript' }} />
|
||||||
<Stack.Screen name="search" options={{ title: 'Search' }} />
|
|
||||||
<Stack.Screen name="ask" options={{ title: 'Ask' }} />
|
|
||||||
<Stack.Screen name="courses" options={{ title: 'Courses' }} />
|
<Stack.Screen name="courses" options={{ title: 'Courses' }} />
|
||||||
<Stack.Screen name="study" options={{ title: 'Study' }} />
|
|
||||||
<Stack.Screen name="quiz" options={{ title: 'Quiz' }} />
|
<Stack.Screen name="quiz" options={{ title: 'Quiz' }} />
|
||||||
<Stack.Screen name="bibliography" options={{ title: 'Bibliography' }} />
|
<Stack.Screen name="bibliography" options={{ title: 'Bibliography' }} />
|
||||||
<Stack.Screen name="settings" options={{ title: 'Settings' }} />
|
|
||||||
</Stack>
|
</Stack>
|
||||||
<StatusBar style="auto" />
|
<StatusBar style="auto" />
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
|||||||
Reference in New Issue
Block a user