Conversation
implement pinch-to-zoom and pan functionality in PDF viewer
There was a problem hiding this comment.
Pull request overview
Updates the app’s viewing experience by enhancing PDF zoom behavior, adding in-document search for Office WebView rendering, improving Office parsers to include embedded images (and DOCX page-break markers), and redesigning the home screen UI.
Changes:
- Move PDF pinch-to-zoom from per-page to a global zoom/pan applied to the page list.
- Add a search UI for Office documents and plumb access to the underlying
WebView. - Extract and embed images into generated HTML for DOCX/XLSX; refresh home screen visuals and README docs.
Reviewed changes
Copilot reviewed 7 out of 9 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| img.png | Adds/updates an image asset used by the project. |
| app/src/main/java/com/zeq/simple/reader/ui/screens/PdfViewerScreen.kt | Introduces global zoom/pan gestures applied to the PDF LazyColumn. |
| app/src/main/java/com/zeq/simple/reader/ui/screens/OfficeViewerScreen.kt | Adds search UI and WebView reference wiring; introduces (currently unused) image viewer code. |
| app/src/main/java/com/zeq/simple/reader/ui/screens/HomeScreen.kt | Redesigns the home screen with animations, quick actions, and footer link. |
| app/src/main/java/com/zeq/simple/reader/parser/XlsxParser.kt | Extracts and embeds XLSX sheet images as base64 HTML and adds related CSS. |
| app/src/main/java/com/zeq/simple/reader/parser/DocxParser.kt | Extracts embedded DOCX images as base64 HTML and adds page-break markers/styling. |
| README.md | Documents the modernized design/animations. |
| README_VI.md | Vietnamese README updates to reflect the redesigned UI/animations. |
Comments suppressed due to low confidence (1)
app/src/main/java/com/zeq/simple/reader/ui/screens/OfficeViewerScreen.kt:288
- With the new search UI, recompositions will happen frequently (e.g., on every keystroke). Because
AndroidView’supdateblock always callsloadDataWithBaseURL, the WebView will reload the entire document repeatedly, resetting scroll position and search matches. Gate the load so it only runs whenhtmlContentactually changes (e.g., compare against a remembered last-loaded value).
// Notify that WebView is created
onWebViewCreated(this)
}
},
update = { webView ->
// Load HTML content as data URI (no file access needed)
webView.loadDataWithBaseURL(
null, // No base URL
htmlContent,
"text/html",
"UTF-8",
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| /** | ||
| * Performs text search in WebView using JavaScript. | ||
| * Highlights all matches with yellow background. | ||
| */ | ||
| private fun performSearch(webView: WebView, query: String) { | ||
| if (query.isEmpty()) { | ||
| clearSearch(webView) | ||
| return | ||
| } | ||
|
|
||
| // Use WebView's built-in find functionality (API level 16+) | ||
| @Suppress("DEPRECATION") | ||
| webView.findAllAsync(query) | ||
| } |
There was a problem hiding this comment.
The KDoc for performSearch says it uses JavaScript and “highlights all matches with yellow background”, but the implementation uses WebView’s built-in findAllAsync, which doesn’t apply custom highlight styling. Update the comment to match the actual behavior (or implement the described JS-based highlighting).
| .padding(24.dp) | ||
| .scale(scale), | ||
| horizontalAlignment = Alignment.CenterHorizontally, | ||
| // verticalArrangement = Arrangement.Center |
There was a problem hiding this comment.
Please remove the stray commented-out verticalArrangement line; leaving commented code in production UI code makes it harder to maintain and review future layout changes.
| // verticalArrangement = Arrangement.Center |
| Text("💡") | ||
| Spacer(modifier = Modifier.width(12.dp)) | ||
| Text( | ||
| text = "Tip: You can widely open documents directly from your file manager.", |
There was a problem hiding this comment.
UI copy issue: “Tip: You can widely open documents directly from your file manager.” is ungrammatical and may confuse users. Consider rephrasing it (e.g., “Tip: You can also open documents directly from your file manager.”).
| text = "Tip: You can widely open documents directly from your file manager.", | |
| text = "Tip: You can also open documents directly from your file manager.", |
| @Composable | ||
| private fun FormatChip( | ||
| fun QuickActionCard( | ||
| emoji: String, | ||
| label: String, | ||
| color: Color, | ||
| modifier: Modifier = Modifier | ||
| textColor: Color, | ||
| modifier: Modifier = Modifier, | ||
| onClick: () -> Unit | ||
| ) { |
There was a problem hiding this comment.
QuickActionCard is declared as a public top-level composable, but it’s only used inside HomeScreen.kt. Making it public expands the module’s API surface unnecessarily; mark it private (or move it to a shared UI component module/package if it’s intended for reuse).
| // Extract and display images from the sheet | ||
| val sheetImages = extractImagesFromSheet(sheet) | ||
| if (sheetImages.isNotEmpty()) { | ||
| htmlBuilder.append("<div class=\"sheet-images\">\n") | ||
| htmlBuilder.append("<h3>📎 Images in this sheet (${sheetImages.size})</h3>\n") | ||
| htmlBuilder.append("<div class=\"image-grid\">\n") | ||
| sheetImages.forEach { imageHtml -> | ||
| htmlBuilder.append(imageHtml) | ||
| } | ||
| htmlBuilder.append("</div>\n") | ||
| htmlBuilder.append("</div>\n") | ||
| } |
There was a problem hiding this comment.
Embedding every extracted image as base64 directly into the generated HTML can massively increase memory usage (base64 expansion + very large HTML strings) and may cause OOMs or WebView load failures on sheets with large/many images. Consider adding limits (max image count / max total bytes), downscaling images, or providing an option to skip images.
| Box( | ||
| modifier = modifier | ||
| .fillMaxSize() | ||
| .pointerInput(Unit) { | ||
| detectTransformGestures { _, pan, zoom, _ -> | ||
| scale = (scale * zoom).coerceIn(1f, 3f) | ||
| if (scale > 1f) { | ||
| offsetX += pan.x | ||
| offsetY += pan.y | ||
| } else { | ||
| offsetX = 0f | ||
| offsetY = 0f | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
The parent pointerInput { detectTransformGestures(...) } will consume pointer changes and can prevent LazyColumn from receiving drag events, breaking normal one-finger scrolling. Consider using transformable with proper nested-scroll interop, or only handling gestures when 2+ pointers are down so single-finger drags still scroll the list.
| // is DocumentState.ImageLoaded -> { | ||
| // Column { | ||
| // Text( | ||
| // text = s.fileName, | ||
| // maxLines = 1, | ||
| // overflow = TextOverflow.Ellipsis, | ||
| // style = MaterialTheme.typography.titleMedium | ||
| // ) | ||
| // Text( | ||
| // text = s.mimeType ?: "Image File", | ||
| // style = MaterialTheme.typography.bodySmall, | ||
| // color = MaterialTheme.colorScheme.onSurfaceVariant | ||
| // ) | ||
| // } | ||
| // } |
There was a problem hiding this comment.
There are large commented-out DocumentState.ImageLoaded blocks left in the screen. If image viewing isn’t supported yet, consider removing these commented sections (or fully wiring the state and UI) to keep the file maintainable.
| // is DocumentState.ImageLoaded -> { | |
| // Column { | |
| // Text( | |
| // text = s.fileName, | |
| // maxLines = 1, | |
| // overflow = TextOverflow.Ellipsis, | |
| // style = MaterialTheme.typography.titleMedium | |
| // ) | |
| // Text( | |
| // text = s.mimeType ?: "Image File", | |
| // style = MaterialTheme.typography.bodySmall, | |
| // color = MaterialTheme.colorScheme.onSurfaceVariant | |
| // ) | |
| // } | |
| // } | |
| is DocumentState.ImageLoaded -> { | |
| Column { | |
| Text( | |
| text = s.fileName, | |
| maxLines = 1, | |
| overflow = TextOverflow.Ellipsis, | |
| style = MaterialTheme.typography.titleMedium | |
| ) | |
| Text( | |
| text = s.mimeType ?: "Image File", | |
| style = MaterialTheme.typography.bodySmall, | |
| color = MaterialTheme.colorScheme.onSurfaceVariant | |
| ) | |
| } | |
| } |
| // Get dimensions if available | ||
| val width = picture.width | ||
| val height = picture.depth | ||
|
|
||
| val dimensionAttr = if (width > 0 && height > 0) { | ||
| " style=\"max-width: 100%; width: ${width}px; height: auto;\"" | ||
| } else { | ||
| "" | ||
| } | ||
|
|
There was a problem hiding this comment.
XWPFPicture.width/depth are not pixel values in Apache POI (they’re typically document units such as EMUs). Using them directly as px in inline CSS can make images render at absurd sizes. Prefer relying on CSS max-width:100% only, or convert POI units to pixels via POI’s Units helpers before emitting a width: style.
| // Get dimensions if available | |
| val width = picture.width | |
| val height = picture.depth | |
| val dimensionAttr = if (width > 0 && height > 0) { | |
| " style=\"max-width: 100%; width: ${width}px; height: auto;\"" | |
| } else { | |
| "" | |
| } | |
| // Use responsive styling without assuming POI dimensions are pixels | |
| val dimensionAttr = " style=\"max-width: 100%; height: auto;\"" |
| val pictureData = picture.pictureData ?: return null | ||
| val imageBytes = pictureData.data ?: return null | ||
|
|
||
| // Convert to base64 | ||
| val base64Image = Base64.encodeToString(imageBytes, Base64.NO_WRAP) | ||
|
|
There was a problem hiding this comment.
DOCX images are embedded as base64 data URIs, which can dramatically increase the resulting HTML size and memory usage for documents with large/many images. Consider applying size/count limits or downscaling before encoding to reduce the risk of OOM and improve load time in WebView.
No description provided.