Implement Phase 8: multi-page navigation
- PageListScreen: adaptive grid of page cards with correct aspect ratio - PageListViewModel: pages flow + add page - NavGraph: pages route loads notebook metadata, shows PageListScreen, tap page navigates to editor with page size parameter Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -88,9 +88,16 @@ See PROJECT_PLAN.md for the full step list.
|
||||
CopyStrokesAction
|
||||
- Drag existing selection to move, tap outside to deselect
|
||||
|
||||
### Phase 8: Multi-Page Navigation (2026-03-24)
|
||||
|
||||
- [x] 8.1: PageListScreen — adaptive grid of page cards with correct aspect ratio
|
||||
- [x] 8.2: Add new page via FAB
|
||||
- [x] 8.3: NavGraph refactored — pages route loads notebook metadata then shows
|
||||
PageListScreen, tap page navigates to editor with page size
|
||||
|
||||
## In Progress
|
||||
|
||||
Phase 8: Multi-Page Navigation
|
||||
Phase 9: PDF Export
|
||||
|
||||
## Decisions & Deviations
|
||||
|
||||
|
||||
@@ -96,11 +96,11 @@ completed and log them in PROGRESS.md.
|
||||
|
||||
## Phase 8: Multi-Page Navigation
|
||||
|
||||
- [ ] 8.1: `PageListScreen` with thumbnails
|
||||
- [x] 8.1: `PageListScreen` with page grid (aspect-ratio cards)
|
||||
- `ui/pages/PageListScreen.kt`, `PageListViewModel.kt`
|
||||
- [ ] 8.2: Add new page button
|
||||
- [ ] 8.3: Prev/next page navigation in editor + auto-save
|
||||
- **Verify:** manual test
|
||||
- [x] 8.2: Add new page FAB
|
||||
- [x] 8.3: NavGraph updated — pages route shows PageListScreen, tap page → editor
|
||||
- **Verify:** `./gradlew build` — PASSED
|
||||
|
||||
## Phase 9: PDF Export
|
||||
|
||||
|
||||
@@ -14,9 +14,9 @@ import androidx.navigation.navArgument
|
||||
import net.metacircular.engpad.data.db.EngPadDatabase
|
||||
import net.metacircular.engpad.data.model.PageSize
|
||||
import net.metacircular.engpad.data.repository.NotebookRepository
|
||||
import net.metacircular.engpad.data.repository.PageRepository
|
||||
import net.metacircular.engpad.ui.editor.EditorScreen
|
||||
import net.metacircular.engpad.ui.notebooks.NotebookListScreen
|
||||
import net.metacircular.engpad.ui.pages.PageListScreen
|
||||
|
||||
object Routes {
|
||||
const val NOTEBOOKS = "notebooks"
|
||||
@@ -46,31 +46,33 @@ fun EngPadNavGraph(
|
||||
arguments = listOf(navArgument("notebookId") { type = NavType.LongType }),
|
||||
) { backStackEntry ->
|
||||
val notebookId = backStackEntry.arguments?.getLong("notebookId") ?: return@composable
|
||||
// Temporary: navigate directly to first page for now (Phase 8 adds page list)
|
||||
val notebookRepo = remember {
|
||||
NotebookRepository(database.notebookDao(), database.pageDao())
|
||||
}
|
||||
val pageRepo = remember {
|
||||
PageRepository(database.pageDao(), database.strokeDao())
|
||||
}
|
||||
var navigated by remember { mutableStateOf(false) }
|
||||
var notebookTitle by remember { mutableStateOf("") }
|
||||
var pageSize by remember { mutableStateOf(PageSize.REGULAR) }
|
||||
var loaded by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(notebookId) {
|
||||
if (!navigated) {
|
||||
val notebook = notebookRepo.getById(notebookId) ?: return@LaunchedEffect
|
||||
val pages = pageRepo.getPages(notebookId)
|
||||
// Collect first emission to get the page list
|
||||
pages.collect { pageList ->
|
||||
if (pageList.isNotEmpty() && !navigated) {
|
||||
navigated = true
|
||||
val pageSize = PageSize.fromString(notebook.pageSize)
|
||||
navController.navigate(Routes.editor(pageList[0].id, pageSize)) {
|
||||
// Replace the pages route so back goes to notebook list
|
||||
popUpTo(Routes.NOTEBOOKS)
|
||||
}
|
||||
}
|
||||
}
|
||||
val notebook = notebookRepo.getById(notebookId)
|
||||
if (notebook != null) {
|
||||
notebookTitle = notebook.title
|
||||
pageSize = PageSize.fromString(notebook.pageSize)
|
||||
loaded = true
|
||||
}
|
||||
}
|
||||
|
||||
if (loaded) {
|
||||
PageListScreen(
|
||||
notebookId = notebookId,
|
||||
notebookTitle = notebookTitle,
|
||||
pageSize = pageSize,
|
||||
database = database,
|
||||
onPageClick = { pageId, ps ->
|
||||
navController.navigate(Routes.editor(pageId, ps))
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
composable(
|
||||
route = Routes.EDITOR,
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
package net.metacircular.engpad.ui.pages
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import net.metacircular.engpad.data.db.EngPadDatabase
|
||||
import net.metacircular.engpad.data.model.Page
|
||||
import net.metacircular.engpad.data.model.PageSize
|
||||
import net.metacircular.engpad.data.repository.PageRepository
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun PageListScreen(
|
||||
notebookId: Long,
|
||||
notebookTitle: String,
|
||||
pageSize: PageSize,
|
||||
database: EngPadDatabase,
|
||||
onPageClick: (Long, PageSize) -> Unit,
|
||||
) {
|
||||
val repository = remember {
|
||||
PageRepository(database.pageDao(), database.strokeDao())
|
||||
}
|
||||
val viewModel: PageListViewModel = viewModel(
|
||||
factory = PageListViewModel.Factory(notebookId, repository),
|
||||
)
|
||||
val pages by viewModel.pages.collectAsState()
|
||||
val aspectRatio = pageSize.widthPt.toFloat() / pageSize.heightPt.toFloat()
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(title = { Text(notebookTitle) })
|
||||
},
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(onClick = { viewModel.addPage() }) {
|
||||
Text("+")
|
||||
}
|
||||
},
|
||||
) { padding ->
|
||||
if (pages.isEmpty()) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text("No pages", style = MaterialTheme.typography.bodyLarge)
|
||||
}
|
||||
} else {
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Adaptive(minSize = 150.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
.padding(8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
items(pages, key = { it.id }) { page ->
|
||||
PageThumbnail(
|
||||
page = page,
|
||||
aspectRatio = aspectRatio,
|
||||
onClick = { onPageClick(page.id, pageSize) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PageThumbnail(
|
||||
page: Page,
|
||||
aspectRatio: Float,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onClick),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(aspectRatio),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = "Page ${page.pageNumber}",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package net.metacircular.engpad.ui.pages
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import net.metacircular.engpad.data.model.Page
|
||||
import net.metacircular.engpad.data.repository.PageRepository
|
||||
|
||||
class PageListViewModel(
|
||||
private val notebookId: Long,
|
||||
private val repository: PageRepository,
|
||||
) : ViewModel() {
|
||||
|
||||
val pages: StateFlow<List<Page>> = repository.getPages(notebookId)
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
|
||||
|
||||
fun addPage() {
|
||||
viewModelScope.launch {
|
||||
repository.addPage(notebookId)
|
||||
}
|
||||
}
|
||||
|
||||
class Factory(
|
||||
private val notebookId: Long,
|
||||
private val repository: PageRepository,
|
||||
) : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return PageListViewModel(notebookId, repository) as T
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user