experiments in a post-browser web
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(ios): make share extension content and OCR text editable

+183 -51
+6
backend/tauri-mobile/peek-core/src/ffi.rs
··· 244 244 Ok(()) 245 245 } 246 246 247 + /// Get item content text (e.g. OCR text for images). 248 + pub fn get_item_content(&self, item_id: String) -> Result<Option<String>, FfiError> { 249 + let content = self.store.get_item_content(&item_id)?; 250 + Ok(content) 251 + } 252 + 247 253 /// Update item content text (e.g. OCR text for images). 248 254 pub fn update_item_content(&self, item_id: String, content: String) -> Result<(), FfiError> { 249 255 self.store.update_item_content(&item_id, &content)?;
+12
backend/tauri-mobile/peek-core/src/items.rs
··· 604 604 605 605 /// Update item metadata by merging new JSON into existing metadata. 606 606 /// Update item content text (e.g. OCR text for images). 607 + pub fn get_item_content( 608 + conn: &Connection, 609 + id: &str, 610 + ) -> Result<Option<String>, String> { 611 + conn.query_row( 612 + "SELECT content FROM items WHERE id = ? AND deleted_at IS NULL", 613 + params![id], 614 + |row| row.get(0), 615 + ) 616 + .map_err(|e| format!("Failed to get item content: {}", e)) 617 + } 618 + 607 619 pub fn update_item_content( 608 620 conn: &Connection, 609 621 id: &str,
+6
backend/tauri-mobile/peek-core/src/lib.rs
··· 179 179 items::update_image_tags(&conn, id, tags, &self.device_id) 180 180 } 181 181 182 + /// Get item content text (e.g. OCR text for images). 183 + pub fn get_item_content(&self, id: &str) -> Result<Option<String>, String> { 184 + let conn = self.conn.lock().map_err(|e| format!("Lock error: {}", e))?; 185 + items::get_item_content(&conn, id) 186 + } 187 + 182 188 /// Update item content text (e.g. OCR text for images). 183 189 pub fn update_item_content(&self, id: &str, content: &str) -> Result<(), String> { 184 190 let conn = self.conn.lock().map_err(|e| format!("Lock error: {}", e))?;
+2
backend/tauri-mobile/src-tauri/gen/apple/Peek/Info.plist
··· 30 30 <integer>20</integer> 31 31 <key>NSExtensionActivationSupportsWebURLWithMaxCount</key> 32 32 <integer>1</integer> 33 + <key>NSExtensionActivationSupportsText</key> 34 + <true/> 33 35 </dict> 34 36 </dict> 35 37 <key>NSExtensionMainStoryboard</key>
+115 -34
backend/tauri-mobile/src-tauri/gen/apple/Peek/ShareViewController.swift
··· 283 283 } 284 284 285 285 /// Save OCR/vision text as item content so it's visible in the main app. 286 + func getItemContent(itemId: String) -> String? { 287 + do { 288 + return try store?.getItemContent(itemId: itemId) 289 + } catch { 290 + debugLog("getItemContent error: \(error)") 291 + return nil 292 + } 293 + } 294 + 286 295 func updateItemContent(itemId: String, content: String) { 287 296 do { 288 297 try store?.updateItemContent(itemId: itemId, content: content) ··· 726 735 } 727 736 728 737 // MARK: - ShareViewController 729 - class ShareViewController: UIViewController { 738 + class ShareViewController: UIViewController, UITextViewDelegate { 730 739 var sharedURL: String? 731 740 var sharedText: String? 732 741 var sharedImageData: Data? ··· 746 755 747 756 let scrollView = UIScrollView() 748 757 let contentStackView = UIStackView() 749 - let contentLabel = UILabel() // Shows URL or text content 758 + let contentTextView = UITextView() // Editable content (URL, text, or image description) 750 759 let imagePreviewView = UIImageView() // Shows image preview 751 760 752 761 // Selected tags at top - self-sizing ··· 787 796 let analyzeButton = UIButton(type: .system) 788 797 let analyzeSpinner = UIActivityIndicatorView(style: .medium) 789 798 let visionResultsContainer = UIStackView() 790 - let ocrTextLabel = UILabel() 799 + let ocrTextView = UITextView() 791 800 let ocrShowMoreButton = UIButton(type: .system) 792 801 let barcodesLabel = UILabel() 793 802 let suggestedTagsCollectionView: SelfSizingCollectionView = { ··· 853 862 if traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) { 854 863 // Update cgColor-based borders which don't respond to dynamic colors automatically 855 864 newTagTextField.layer.borderColor = PeekTheme.border.cgColor 865 + contentTextView.layer.borderColor = PeekTheme.border.cgColor 866 + ocrTextView.layer.borderColor = PeekTheme.border.cgColor 856 867 } 857 868 } 858 869 ··· 991 1002 visionResultsContainer.translatesAutoresizingMaskIntoConstraints = false 992 1003 contentStackView.addArrangedSubview(visionResultsContainer) 993 1004 994 - // OCR text label 995 - ocrTextLabel.font = PeekTheme.caption 996 - ocrTextLabel.textColor = PeekTheme.textSecondary 997 - ocrTextLabel.numberOfLines = 4 998 - ocrTextLabel.lineBreakMode = .byTruncatingTail 999 - ocrTextLabel.isHidden = true 1000 - visionResultsContainer.addArrangedSubview(ocrTextLabel) 1005 + // OCR text view (editable) 1006 + ocrTextView.font = PeekTheme.caption 1007 + ocrTextView.textColor = PeekTheme.textSecondary 1008 + ocrTextView.backgroundColor = PeekTheme.bgSecondary 1009 + ocrTextView.layer.cornerRadius = PeekTheme.inputCornerRadius 1010 + ocrTextView.layer.borderWidth = 1 1011 + ocrTextView.layer.borderColor = PeekTheme.border.cgColor 1012 + ocrTextView.textContainerInset = UIEdgeInsets(top: 6, left: 4, bottom: 6, right: 4) 1013 + ocrTextView.isScrollEnabled = false // Let it grow with content 1014 + ocrTextView.isEditable = true 1015 + ocrTextView.delegate = self 1016 + ocrTextView.isHidden = true 1017 + ocrTextView.translatesAutoresizingMaskIntoConstraints = false 1018 + visionResultsContainer.addArrangedSubview(ocrTextView) 1019 + // Constrain to ~4 lines initially, expanded via show more 1020 + let ocrMaxHeight = ocrTextView.heightAnchor.constraint(lessThanOrEqualToConstant: 80) 1021 + ocrMaxHeight.priority = .defaultHigh 1022 + ocrMaxHeight.isActive = true 1023 + ocrTextView.tag = 100 // Tag to find the height constraint later 1001 1024 1002 1025 // "Show more" button for OCR text 1003 1026 ocrShowMoreButton.setTitle("Show more", for: .normal) ··· 1034 1057 suggestedTagsCollectionView.isHidden = true 1035 1058 visionResultsContainer.addArrangedSubview(suggestedTagsCollectionView) 1036 1059 1037 - // URL/Text Label — base05 (primary text) to stay visible against share sheet bg 1038 - contentLabel.font = PeekTheme.body 1039 - contentLabel.numberOfLines = 1 1040 - contentLabel.textColor = PeekTheme.text 1041 - contentLabel.lineBreakMode = .byTruncatingTail 1042 - contentStackView.addArrangedSubview(contentLabel) 1043 - // Extra space below URL since it's the focal point 1044 - contentStackView.setCustomSpacing(PeekTheme.spacingMD * 3, after: contentLabel) 1060 + // Editable content area — base05 (primary text) to stay visible against share sheet bg 1061 + contentTextView.font = PeekTheme.body 1062 + contentTextView.textColor = PeekTheme.text 1063 + contentTextView.backgroundColor = PeekTheme.bgSecondary 1064 + contentTextView.layer.cornerRadius = PeekTheme.inputCornerRadius 1065 + contentTextView.layer.borderWidth = 1 1066 + contentTextView.layer.borderColor = PeekTheme.border.cgColor 1067 + contentTextView.textContainerInset = UIEdgeInsets(top: 8, left: 4, bottom: 8, right: 4) 1068 + contentTextView.isScrollEnabled = false // Let it grow with content 1069 + contentTextView.isEditable = true 1070 + contentTextView.delegate = self 1071 + contentTextView.translatesAutoresizingMaskIntoConstraints = false 1072 + contentStackView.addArrangedSubview(contentTextView) 1073 + // Constrain max height so it doesn't grow unbounded 1074 + let contentTextViewMaxHeight = contentTextView.heightAnchor.constraint(lessThanOrEqualToConstant: 120) 1075 + contentTextViewMaxHeight.isActive = true 1076 + // Extra space below content since it's the focal point 1077 + contentStackView.setCustomSpacing(PeekTheme.spacingMD * 3, after: contentTextView) 1045 1078 1046 1079 // Selected Tags Collection View - self-sizing 1047 1080 selectedTagsCollectionView.backgroundColor = .clear ··· 1445 1478 os_log("Setting shared URL: %{public}@ (normalized: %{public}@)", log: shareLog, type: .info, urlString, normalized) 1446 1479 sharedURL = normalized 1447 1480 sharedItemType = .page 1448 - contentLabel.text = urlString 1449 - contentLabel.numberOfLines = 2 1481 + contentTextView.text = urlString 1450 1482 checkIfURLExists() 1451 1483 } 1452 1484 ··· 1458 1490 os_log("Setting shared text: %{public}@", log: shareLog, type: .info, String(text.prefix(50))) 1459 1491 sharedText = text 1460 1492 sharedItemType = .text 1461 - contentLabel.text = text 1462 - contentLabel.numberOfLines = 4 // Show more lines for text 1493 + contentTextView.text = text 1463 1494 1464 1495 // Parse hashtags from text and add them as tags 1465 1496 let hashtags = parseHashtags(from: text) ··· 1534 1565 print("[ShareExt] Generated title: \(title ?? "nil") from filename: \(effectiveFilename ?? "nil")") 1535 1566 if let title = title { 1536 1567 sharedMetadata["title"] = title 1537 - contentLabel.text = title 1568 + contentTextView.text = title 1538 1569 print("[ShareExt] Set title in metadata and label: \(title)") 1539 1570 } else if let sourceUrl = sourceUrl { 1540 1571 // Fallback: try to get title from source URL 1541 1572 if let urlTitle = titleFromFilename(URL(string: sourceUrl)?.lastPathComponent) { 1542 1573 sharedMetadata["title"] = urlTitle 1543 - contentLabel.text = urlTitle 1574 + contentTextView.text = urlTitle 1544 1575 } else { 1545 - contentLabel.text = "Image from: \(sourceUrl)" 1576 + contentTextView.text = "Image from: \(sourceUrl)" 1546 1577 } 1547 1578 } else { 1548 1579 let sizeKB = imageData.count / 1024 1549 - contentLabel.text = "Image (\(sizeKB) KB)" 1580 + contentTextView.text = "Image (\(sizeKB) KB)" 1550 1581 } 1551 - contentLabel.numberOfLines = 2 1552 1582 1553 1583 // Store source URL in metadata if present 1554 1584 if let sourceUrl = sourceUrl { ··· 1761 1791 /// Save the current state of selectedTags to the database (called instantly on tag changes) 1762 1792 func saveCurrentState() { 1763 1793 let finalTags = Array(selectedTags).sorted() 1794 + 1795 + // Include edited content from the text view in metadata 1796 + let editedContent = contentTextView.text?.trimmingCharacters(in: .whitespacesAndNewlines) 1797 + if let content = editedContent, !content.isEmpty { 1798 + sharedMetadata["content"] = content 1799 + } 1800 + 1801 + // Include edited OCR text if present 1802 + let editedOcr = ocrTextView.text?.trimmingCharacters(in: .whitespacesAndNewlines) 1803 + if let ocr = editedOcr, !ocr.isEmpty { 1804 + sharedMetadata["ocrText"] = ocr 1805 + } 1806 + 1764 1807 let metadata = sharedMetadata.isEmpty ? nil : sharedMetadata 1765 1808 print("[ShareExt] saveCurrentState - sharedMetadata: \(sharedMetadata)") 1766 1809 ··· 1782 1825 } 1783 1826 1784 1827 case .text: 1785 - guard let text = sharedText, !text.isEmpty else { return } 1828 + // Use edited text from the text view, falling back to original shared text 1829 + let textContent = editedContent ?? sharedText ?? "" 1830 + guard !textContent.isEmpty else { return } 1786 1831 1787 1832 PeekCoreManager.shared.saveText( 1788 - content: text, 1833 + content: textContent, 1789 1834 tags: finalTags, 1790 1835 metadata: metadata 1791 1836 ) { } ··· 1839 1884 PeekCoreManager.shared.debugLog("saveImage completion: imageId=\(imageId ?? "nil")") 1840 1885 if let imageId = imageId { 1841 1886 self.savedImageId = imageId 1887 + // If this was a dedup (image already existed), load and show existing OCR content 1888 + if let existingContent = PeekCoreManager.shared.getItemContent(itemId: imageId), 1889 + !existingContent.isEmpty { 1890 + DispatchQueue.main.async { 1891 + self.ocrTextView.text = existingContent 1892 + self.ocrTextView.isHidden = false 1893 + self.visionResultsContainer.isHidden = false 1894 + self.showStatus("\u{2713} Saved", color: .secondaryLabel) 1895 + } 1896 + } 1842 1897 } 1843 1898 self.isSavingImage = false 1844 1899 } ··· 1933 1988 1934 1989 // Show OCR text 1935 1990 if let text = result.ocrText, !text.isEmpty { 1936 - ocrTextLabel.text = text 1937 - ocrTextLabel.isHidden = false 1991 + ocrTextView.text = text 1992 + ocrTextView.isHidden = false 1938 1993 // Show "show more" if text is long 1939 1994 let lineCount = text.components(separatedBy: "\n").count 1940 1995 ocrShowMoreButton.isHidden = lineCount <= 4 ··· 1950 2005 1951 2006 @objc func toggleOcrText() { 1952 2007 isOcrExpanded.toggle() 2008 + // Find the max height constraint (tag 100) and toggle it 2009 + if let maxConstraint = ocrTextView.constraints.first(where: { $0.firstAttribute == .height && $0.relation == .lessThanOrEqual }) { 2010 + maxConstraint.isActive = !isOcrExpanded 2011 + } 2012 + ocrTextView.isScrollEnabled = isOcrExpanded // Scroll when expanded 1953 2013 if isOcrExpanded { 1954 - ocrTextLabel.numberOfLines = 0 1955 2014 ocrShowMoreButton.setTitle("Show less", for: .normal) 2015 + // Set a taller max when expanded 2016 + let expandedHeight = ocrTextView.heightAnchor.constraint(lessThanOrEqualToConstant: 300) 2017 + expandedHeight.priority = .defaultHigh 2018 + expandedHeight.isActive = true 1956 2019 } else { 1957 - ocrTextLabel.numberOfLines = 4 2020 + ocrTextView.isScrollEnabled = false 1958 2021 ocrShowMoreButton.setTitle("Show more", for: .normal) 2022 + // Re-enable the compact constraint 2023 + if let maxConstraint = ocrTextView.constraints.first(where: { $0.firstAttribute == .height && $0.relation == .lessThanOrEqual }) { 2024 + maxConstraint.isActive = true 2025 + } 1959 2026 } 1960 2027 view.layoutIfNeeded() 1961 2028 updatePreferredContentSize() ··· 2053 2120 } 2054 2121 } 2055 2122 2123 + // MARK: - UITextViewDelegate methods 2124 + extension ShareViewController { 2125 + func textViewDidChange(_ textView: UITextView) { 2126 + // Resize the text view and update layout 2127 + view.layoutIfNeeded() 2128 + updatePreferredContentSize() 2129 + } 2130 + 2131 + func textViewDidEndEditing(_ textView: UITextView) { 2132 + // Auto-save when user finishes editing 2133 + saveCurrentState() 2134 + } 2135 + } 2136 + 2056 2137 // MARK: - UIGestureRecognizerDelegate 2057 2138 extension ShareViewController: UIGestureRecognizerDelegate { 2058 2139 func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { 2059 2140 // Don't intercept touches on interactive elements 2060 2141 let touchedView = touch.view 2061 - if touchedView is UIButton || touchedView is UITextField || touchedView is UICollectionViewCell { 2142 + if touchedView is UIButton || touchedView is UITextField || touchedView is UITextView || touchedView is UICollectionViewCell { 2062 2143 return false 2063 2144 } 2064 2145 // Check if touch is inside a collection view cell
+19
backend/tauri-mobile/src-tauri/gen/apple/Peek/peek_core.swift
··· 532 532 func findExistingUrl(url: String) throws -> FfiSavedItem? 533 533 534 534 /** 535 + * Get item content text (e.g. OCR text for images). 536 + */ 537 + func getItemContent(itemId: String) throws -> String? 538 + 539 + /** 535 540 * Get all tags ordered by frecency score. 536 541 */ 537 542 func getTagsByFrecency() throws -> [FfiTagStats] ··· 661 666 return try FfiConverterOptionTypeFfiSavedItem.lift(try rustCallWithError(FfiConverterTypeFfiError.lift) { 662 667 uniffi_peek_core_fn_method_peekcoreffi_find_existing_url(self.uniffiClonePointer(), 663 668 FfiConverterString.lower(url),$0 669 + ) 670 + }) 671 + } 672 + 673 + /** 674 + * Get item content text (e.g. OCR text for images). 675 + */ 676 + open func getItemContent(itemId: String)throws -> String? { 677 + return try FfiConverterOptionString.lift(try rustCallWithError(FfiConverterTypeFfiError.lift) { 678 + uniffi_peek_core_fn_method_peekcoreffi_get_item_content(self.uniffiClonePointer(), 679 + FfiConverterString.lower(itemId),$0 664 680 ) 665 681 }) 666 682 } ··· 1639 1655 return InitializationResult.apiChecksumMismatch 1640 1656 } 1641 1657 if (uniffi_peek_core_checksum_method_peekcoreffi_find_existing_url() != 16518) { 1658 + return InitializationResult.apiChecksumMismatch 1659 + } 1660 + if (uniffi_peek_core_checksum_method_peekcoreffi_get_item_content() != 62284) { 1642 1661 return InitializationResult.apiChecksumMismatch 1643 1662 } 1644 1663 if (uniffi_peek_core_checksum_method_peekcoreffi_get_tags_by_frecency() != 27194) {
+23 -17
backend/tauri-mobile/src/App.tsx
··· 2279 2279 if (!item) return null; 2280 2280 const metadata = item.metadata as Record<string, unknown> | undefined; 2281 2281 const title = metadata?.title as string | undefined; 2282 + console.log('[DEBUG EDITOR IMAGE]', JSON.stringify({ id: item.id, hasContent: !!item.content, contentLength: item.content?.length, contentPreview: item.content?.substring(0, 100) })); 2282 2283 2283 2284 return ( 2284 2285 <EditorOverlay onDismiss={requestCancelEditingImage} keyboardHeight={keyboardHeight} className="text-editor-overlay"> 2285 - <div className="editor-image-preview"> 2286 - {item.thumbnail ? ( 2287 - <img 2288 - src={`data:image/jpeg;base64,${item.thumbnail}`} 2289 - alt={title || "Preview"} 2290 - className="edit-modal-image" 2291 - /> 2292 - ) : ( 2293 - <div className="image-placeholder"> 2294 - <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> 2295 - <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect> 2296 - <circle cx="8.5" cy="8.5" r="1.5"></circle> 2297 - <polyline points="21 15 16 10 5 21"></polyline> 2298 - </svg> 2299 - </div> 2300 - )} 2301 - {title && <div className="edit-image-title">{title}</div>} 2286 + <div style={{ flex: "1 1 auto", minHeight: 0, overflowY: "auto" }}> 2287 + <div className="editor-image-preview"> 2288 + {item.thumbnail ? ( 2289 + <img 2290 + src={`data:image/jpeg;base64,${item.thumbnail}`} 2291 + alt={title || "Preview"} 2292 + className="edit-modal-image" 2293 + /> 2294 + ) : ( 2295 + <div className="image-placeholder"> 2296 + <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> 2297 + <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect> 2298 + <circle cx="8.5" cy="8.5" r="1.5"></circle> 2299 + <polyline points="21 15 16 10 5 21"></polyline> 2300 + </svg> 2301 + </div> 2302 + )} 2303 + {title && <div className="edit-image-title">{title}</div>} 2304 + </div> 2305 + <div className="edit-image-content" style={{ padding: "0 16px 12px", fontSize: "14px", opacity: 0.85, lineHeight: "1.5", whiteSpace: "pre-wrap" }}> 2306 + {item.content || `[no content — keys: ${Object.keys(item).join(', ')}]`} 2307 + </div> 2302 2308 </div> 2303 2309 <TagsSection 2304 2310 selectedTags={editingImageTags}