diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml
index ddad224..9aa87a3 100644
--- a/.gitea/workflows/ci.yml
+++ b/.gitea/workflows/ci.yml
@@ -7,12 +7,12 @@
# `docker compose` simply builds and (re)creates the `wisp` container on the host.
#
# Triggers:
-# push to master -> test, then deploy-web
+# push to master -> test, then deploy-web, then build-apk
# pull_request -> test only
#
-# Required repo secrets (Settings -> Actions -> Secrets) — used by the APK job
-# once it is enabled (see TODO at the bottom): ANDROID_KEYSTORE_BASE64,
-# ANDROID_KEYSTORE_PASSWORD, ANDROID_KEY_ALIAS, ANDROID_KEY_PASSWORD.
+# Required repo secrets (Settings -> Actions -> Secrets) — used by the build-apk
+# job: ANDROID_KEYSTORE_BASE64, ANDROID_KEYSTORE_PASSWORD, ANDROID_KEY_ALIAS,
+# ANDROID_KEY_PASSWORD.
# ---------------------------------------------------------------------------
name: CI
@@ -72,11 +72,11 @@ jobs:
# with docker create/cp and copy it into the running `wisp` web container so
# it is downloadable at https://wisp.briggen.dev/wisp.apk.
build-apk:
- # ONLY on version tags (v*). The native build is heavy (NDK/whisper.cpp) and
- # slow on the shared prod runner; building it on every push would peg the box
- # (and degrade the live web apps). Cut a release with: git tag v1.2.3 && git push --tags
- needs: [test]
- if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
+ # Auto-build the APK on every push to master (arm64-only keeps it ~minutes).
+ # Runs after deploy-web so the web updates promptly, then the APK follows and
+ # is published to /srv/wisp/wisp.apk (served at /wisp.apk via the compose mount).
+ needs: [test, deploy-web]
+ if: github.event_name == 'push' && github.ref == 'refs/heads/master'
runs-on: ubuntu-latest
timeout-minutes: 120
steps:
diff --git a/src/app/(tabs)/_layout.tsx b/src/app/(tabs)/_layout.tsx
new file mode 100644
index 0000000..baac9c8
--- /dev/null
+++ b/src/app/(tabs)/_layout.tsx
@@ -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 {glyph};
+}
+
+export default function TabsLayout() {
+ const theme = useTheme();
+ return (
+
+ }}
+ />
+ }}
+ />
+ }}
+ />
+ }}
+ />
+ }}
+ />
+
+ );
+}
diff --git a/src/app/ask.tsx b/src/app/(tabs)/ask.tsx
similarity index 98%
rename from src/app/ask.tsx
rename to src/app/(tabs)/ask.tsx
index cf18e5c..f0a67b5 100644
--- a/src/app/ask.tsx
+++ b/src/app/(tabs)/ask.tsx
@@ -1,4 +1,4 @@
-import { Stack, useFocusEffect, useRouter } from 'expo-router';
+import { useFocusEffect, useRouter } from 'expo-router';
import { useCallback, useState } from 'react';
import {
ActivityIndicator,
@@ -81,7 +81,6 @@ export default function AskScreen() {
return (
-
Ask a question and get an answer grounded in your lectures — every claim links back to the
diff --git a/src/app/index.tsx b/src/app/(tabs)/index.tsx
similarity index 69%
rename from src/app/index.tsx
rename to src/app/(tabs)/index.tsx
index 573d63c..04e39ef 100644
--- a/src/app/index.tsx
+++ b/src/app/(tabs)/index.tsx
@@ -14,13 +14,12 @@ import { ThemedText } from '@/components/themed-text';
import { ThemedView } from '@/components/themed-view';
import { MaxContentWidth, Spacing } from '@/constants/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 { MODELS } from '@/lib/models/catalog';
import { pickAudio, type PickedAudio } from '@/lib/pickAudio';
import { useCourses } from '@/stores/coursesStore';
import { useTranscribe } from '@/stores/transcribeStore';
-import { useTranscripts, type CourseFilter } from '@/stores/transcriptsStore';
+import { useTranscripts } from '@/stores/transcriptsStore';
export default function LibraryScreen() {
const theme = useTheme();
@@ -31,21 +30,11 @@ export default function LibraryScreen() {
const refreshCourses = useCourses((s) => s.refresh);
const job = useTranscribe();
const [picked, setPicked] = useState(null);
- const [dueCount, setDueCount] = useState(0);
useFocusEffect(
useCallback(() => {
void refresh();
void refreshCourses();
- let alive = true;
- void getRepo()
- .flashcardCounts()
- .then((c) => {
- if (alive) setDueCount(c.due);
- });
- return () => {
- alive = false;
- };
}, [refresh, refreshCourses]),
);
@@ -54,7 +43,7 @@ export default function LibraryScreen() {
const onNew = useCallback(async () => {
if (busy) return;
const p = await pickAudio();
- if (p) setPicked(p); // opens the pre-capture sheet
+ if (p) setPicked(p);
}, [busy]);
const onStart = useCallback(
@@ -74,42 +63,15 @@ export default function LibraryScreen() {
return (
-
-
- Wisp
-
- Private transcription — runs on your device, nothing uploaded.
-
-
-
-
-
- Search
-
-
-
-
- Ask
-
-
-
-
- Courses
-
-
-
-
-
- {dueCount > 0 ? `Study (${dueCount})` : 'Study'}
-
-
-
-
-
- ⚙ Settings
-
-
-
+
+
+ On your device — nothing uploaded.
+
+
+
+ Courses ›
+
+
@@ -120,7 +82,7 @@ export default function LibraryScreen() {
styles.captureBtn,
{ backgroundColor: '#e5484d', opacity: busy ? 0.5 : pressed ? 0.85 : 1 },
]}>
- 🎙 Record
+ 🎙 Record
- + Import file
+ + Import
@@ -142,18 +104,20 @@ export default function LibraryScreen() {
)}
-
- void setCourseFilter('all')} />
- void setCourseFilter(null)} />
- {courses.map((c) => (
- void setCourseFilter(c.id)} />
- ))}
-
+ {courses.length > 0 && (
+
+ void setCourseFilter('all')} />
+ void setCourseFilter(null)} />
+ {courses.map((c) => (
+ void setCourseFilter(c.id)} />
+ ))}
+
+ )}
void setQuery(t)}
- placeholder="Search transcripts…"
+ placeholder="Filter by title…"
placeholderTextColor={theme.textSecondary}
style={[styles.search, { color: theme.text, backgroundColor: theme.backgroundElement }]}
/>
@@ -162,7 +126,7 @@ export default function LibraryScreen() {
) : items.length === 0 ? (
- {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.'}
) : (
items.map((t) => (
@@ -245,9 +209,7 @@ function TranscriptRow({
[pressed && styles.pressed]}>
-
- {item.title}
-
+ {item.title}
✕
@@ -269,20 +231,19 @@ const styles = StyleSheet.create({
maxWidth: MaxContentWidth,
width: '100%',
alignSelf: 'center',
+ paddingBottom: Spacing.six,
},
flex: { flex: 1 },
- headerRow: { flexDirection: 'row', alignItems: 'flex-start', 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 },
+ subHeader: { flexDirection: 'row', alignItems: 'center', gap: Spacing.two },
captureRow: { flexDirection: 'row', gap: Spacing.two },
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 },
rowBetween: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', gap: Spacing.two },
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' },
- 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' },
bar: { height: 6, borderRadius: 3, backgroundColor: '#3c87f7' },
pad: { paddingVertical: Spacing.four, textAlign: 'center' },
diff --git a/src/app/search.tsx b/src/app/(tabs)/search.tsx
similarity index 98%
rename from src/app/search.tsx
rename to src/app/(tabs)/search.tsx
index 808d139..ce8dd99 100644
--- a/src/app/search.tsx
+++ b/src/app/(tabs)/search.tsx
@@ -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 { ActivityIndicator, Pressable, ScrollView, StyleSheet, TextInput, View } from 'react-native';
@@ -104,7 +104,6 @@ export default function SearchScreen() {
return (
-
Semantic search across your lectures — runs entirely on your device.
diff --git a/src/app/settings.tsx b/src/app/(tabs)/settings.tsx
similarity index 100%
rename from src/app/settings.tsx
rename to src/app/(tabs)/settings.tsx
diff --git a/src/app/study.tsx b/src/app/(tabs)/study.tsx
similarity index 95%
rename from src/app/study.tsx
rename to src/app/(tabs)/study.tsx
index 14f9fc0..fbabd75 100644
--- a/src/app/study.tsx
+++ b/src/app/(tabs)/study.tsx
@@ -1,4 +1,4 @@
-import { Stack, useFocusEffect, useRouter } from 'expo-router';
+import { useFocusEffect, useRouter } from 'expo-router';
import { useCallback, useState } from 'react';
import { ActivityIndicator, Pressable, ScrollView, StyleSheet, View } from 'react-native';
@@ -80,17 +80,12 @@ export default function StudyScreen() {
return (
- (
- void exportAnki()} hitSlop={8}>
- Export Anki (.csv)
-
- ),
- }}
- />
+
+ void exportAnki()} hitSlop={8}>
+ Export Anki (.csv)
+
+
pickScope('all')} />
pickScope(null)} />
@@ -192,6 +187,7 @@ const styles = StyleSheet.create({
},
filterBar: { gap: Spacing.two, paddingVertical: Spacing.one, paddingRight: Spacing.three },
filterChip: { paddingHorizontal: Spacing.three, paddingVertical: Spacing.one, borderRadius: 999 },
+ topActions: { flexDirection: 'row', justifyContent: 'flex-end' },
chipActive: { color: '#fff', fontWeight: '700' },
card: { padding: Spacing.four, borderRadius: Spacing.three, gap: Spacing.three },
front: { fontSize: 24, lineHeight: 32 },
diff --git a/src/app/_layout.tsx b/src/app/_layout.tsx
index ba03e6c..fb20bb1 100644
--- a/src/app/_layout.tsx
+++ b/src/app/_layout.tsx
@@ -14,16 +14,12 @@ export default function RootLayout() {
return (
-
+
-
-
-
-