Merge pull request #130 from donetick/add-task-photo-fix

fix: update version codes and enhance document scanning functionality
This commit is contained in:
Mohamad Tarbin
2026-07-07 23:17:17 -04:00
committed by GitHub
10 changed files with 86 additions and 33 deletions

View File

@@ -1,5 +1,11 @@
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
def keystorePropertiesFile = rootProject.file("keystore.properties")
def keystoreProperties = new Properties()
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
android { android {
namespace "com.donetick.app" namespace "com.donetick.app"
compileSdk rootProject.ext.compileSdkVersion compileSdk rootProject.ext.compileSdkVersion
@@ -7,8 +13,8 @@ android {
applicationId "com.donetick.app" applicationId "com.donetick.app"
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 25 versionCode 26
versionName "1.2.3" versionName "1.2.4"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions { aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
@@ -16,8 +22,19 @@ android {
ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~' ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
} }
} }
signingConfigs {
release {
if (keystorePropertiesFile.exists()) {
storeFile file(keystoreProperties['storeFile'])
storePassword keystoreProperties['storePassword']
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
}
}
}
buildTypes { buildTypes {
release { release {
signingConfig keystorePropertiesFile.exists() ? signingConfigs.release : signingConfigs.debug
minifyEnabled false minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
} }

View File

@@ -11,8 +11,8 @@
"type": "SINGLE", "type": "SINGLE",
"filters": [], "filters": [],
"attributes": [], "attributes": [],
"versionCode": 20, "versionCode": 25,
"versionName": "1.0.20", "versionName": "1.2.3",
"outputFile": "app-release.apk" "outputFile": "app-release.apk"
} }
], ],
@@ -33,5 +33,5 @@
] ]
} }
], ],
"minSdkVersionForDexing": 24 "minSdkVersionForDexing": 28
} }

View File

@@ -357,15 +357,16 @@
baseConfigurationReference = FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */; baseConfigurationReference = FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */;
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ALLOW_ENTITLEMENTS_MODIFICATION = YES;
CODE_SIGN_ENTITLEMENTS = App/App.entitlements; CODE_SIGN_ENTITLEMENTS = App/App.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 25; CURRENT_PROJECT_VERSION = 26;
DEVELOPMENT_TEAM = 6UJJ78R3BS; DEVELOPMENT_TEAM = 6UJJ78R3BS;
INFOPLIST_FILE = App/Info.plist; INFOPLIST_FILE = App/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.0; IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = com.donetick.app; PRODUCT_BUNDLE_IDENTIFIER = com.donetick.app;
MARKETING_VERSION = 1.2.3; MARKETING_VERSION = 1.2.4;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2";
@@ -377,14 +378,15 @@
baseConfigurationReference = AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */; baseConfigurationReference = AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */;
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ALLOW_ENTITLEMENTS_MODIFICATION = YES;
CODE_SIGN_ENTITLEMENTS = App/App.entitlements; CODE_SIGN_ENTITLEMENTS = App/App.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 25; CURRENT_PROJECT_VERSION = 26;
DEVELOPMENT_TEAM = 6UJJ78R3BS; DEVELOPMENT_TEAM = 6UJJ78R3BS;
INFOPLIST_FILE = App/Info.plist; INFOPLIST_FILE = App/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.0; IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 1.2.3; MARKETING_VERSION = 1.2.4;
PRODUCT_BUNDLE_IDENTIFIER = com.donetick.app; PRODUCT_BUNDLE_IDENTIFIER = com.donetick.app;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";

View File

@@ -15,13 +15,16 @@ def capacitor_pods
pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app' pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app'
pod 'CapacitorBrowser', :path => '../../node_modules/@capacitor/browser' pod 'CapacitorBrowser', :path => '../../node_modules/@capacitor/browser'
pod 'CapacitorDevice', :path => '../../node_modules/@capacitor/device' pod 'CapacitorDevice', :path => '../../node_modules/@capacitor/device'
pod 'CapacitorLocalLlm', :path => '../../node_modules/@capacitor/local-llm'
pod 'CapacitorLocalNotifications', :path => '../../node_modules/@capacitor/local-notifications' pod 'CapacitorLocalNotifications', :path => '../../node_modules/@capacitor/local-notifications'
pod 'CapacitorNetwork', :path => '../../node_modules/@capacitor/network' pod 'CapacitorNetwork', :path => '../../node_modules/@capacitor/network'
pod 'CapacitorPreferences', :path => '../../node_modules/@capacitor/preferences' pod 'CapacitorPreferences', :path => '../../node_modules/@capacitor/preferences'
pod 'CapacitorPushNotifications', :path => '../../node_modules/@capacitor/push-notifications' pod 'CapacitorPushNotifications', :path => '../../node_modules/@capacitor/push-notifications'
pod 'CapacitorStatusBar', :path => '../../node_modules/@capacitor/status-bar' pod 'CapacitorStatusBar', :path => '../../node_modules/@capacitor/status-bar'
pod 'CapgoCapacitorDocumentScanner', :path => '../../node_modules/@capgo/capacitor-document-scanner'
pod 'CapgoCapacitorNfc', :path => '../../node_modules/@capgo/capacitor-nfc' pod 'CapgoCapacitorNfc', :path => '../../node_modules/@capgo/capacitor-nfc'
pod 'CapgoCapacitorSocialLogin', :path => '../../node_modules/@capgo/capacitor-social-login' pod 'CapgoCapacitorSocialLogin', :path => '../../node_modules/@capgo/capacitor-social-login'
pod 'JcesarmobileCapacitorOcr', :path => '../../node_modules/@jcesarmobile/capacitor-ocr'
pod 'RevenuecatPurchasesCapacitor', :path => '../../node_modules/@revenuecat/purchases-capacitor' pod 'RevenuecatPurchasesCapacitor', :path => '../../node_modules/@revenuecat/purchases-capacitor'
pod 'RevenuecatPurchasesCapacitorUi', :path => '../../node_modules/@revenuecat/purchases-capacitor-ui' pod 'RevenuecatPurchasesCapacitorUi', :path => '../../node_modules/@revenuecat/purchases-capacitor-ui'
pod 'CapacitorPluginSafeArea', :path => '../../node_modules/capacitor-plugin-safe-area' pod 'CapacitorPluginSafeArea', :path => '../../node_modules/capacitor-plugin-safe-area'

View File

@@ -1,7 +1,7 @@
{ {
"name": "donetick", "name": "donetick",
"private": true, "private": true,
"version": "1.2.3", "version": "1.2.4",
"type": "module", "type": "module",
"engines": { "engines": {
"node": ">=20.0.0", "node": ">=20.0.0",
@@ -30,8 +30,9 @@
"ionic:build": "npm run build", "ionic:build": "npm run build",
"ionic:serve": "npm run start", "ionic:serve": "npm run start",
"android": "npm run build && npx cap sync android", "android": "npm run build && npx cap sync android",
"android:dev": "npx cap run android --live-reload",
"cap": "npm run build && npx cap sync", "cap": "npm run build && npx cap sync",
"bump": "node bump-version.js", "bump": "node bump-version.js patch",
"bump:minor": "node bump-version.js minor", "bump:minor": "node bump-version.js minor",
"bump:major": "node bump-version.js major", "bump:major": "node bump-version.js major",
"bump:patch": "node bump-version.js patch" "bump:patch": "node bump-version.js patch"

View File

@@ -25,7 +25,7 @@ function normalizeScannedImage(raw) {
export function useDocumentScanner() { export function useDocumentScanner() {
const isNativeScanner = Capacitor.isNativePlatform() const isNativeScanner = Capacitor.isNativePlatform()
const scanDocument = async ({ maxDocuments = 1, quality = 90, letUserAdjustCrop = true } = {}) => { const scanDocument = async ({ maxDocuments = 1, quality = 90, letUserAdjustCrop = false } = {}) => {
if (!isNativeScanner) return { image: null, cancelled: false } if (!isNativeScanner) return { image: null, cancelled: false }
try { try {

View File

@@ -30,6 +30,7 @@ import LearnMoreButton from './LearnMore'
import NotificationPickerField from './NotificationPickerField' import NotificationPickerField from './NotificationPickerField'
import ScanPanel from './ScanToTask/ScanPanel' import ScanPanel from './ScanToTask/ScanPanel'
import { useDocumentScanner } from '../../hooks/useDocumentScanner' import { useDocumentScanner } from '../../hooks/useDocumentScanner'
import { localAIService } from '../../service/LocalAIService'
import PriorityPickerField from './PriorityPickerField' import PriorityPickerField from './PriorityPickerField'
import RepeatPickerField from './RepeatPickerField' import RepeatPickerField from './RepeatPickerField'
import RichTextEditor from './RichTextEditor' import RichTextEditor from './RichTextEditor'
@@ -107,9 +108,15 @@ const TaskInput = ({ onChoreUpdate, isModalOpen, onClose }) => {
const [attachments, setAttachments] = useState([]) const [attachments, setAttachments] = useState([])
const [draftId, setDraftId] = useState(() => crypto.randomUUID()) const [draftId, setDraftId] = useState(() => crypto.randomUUID())
const [showScan, setShowScan] = useState(false) const [showScan, setShowScan] = useState(false)
const [scanAutoCapture, setScanAutoCapture] = useState(false)
const [pendingPhotoUrl, setPendingPhotoUrl] = useState(null) const [pendingPhotoUrl, setPendingPhotoUrl] = useState(null)
const [llmAvailable, setLlmAvailable] = useState(false)
const { isNativeScanner } = useDocumentScanner() const { isNativeScanner } = useDocumentScanner()
useEffect(() => {
localAIService.isAvailable().then(setLlmAvailable)
}, [])
// Priority colors // Priority colors
const priorityColors = { const priorityColors = {
0: TASK_COLOR.NO_PRIORITY, 0: TASK_COLOR.NO_PRIORITY,
@@ -793,11 +800,15 @@ const TaskInput = ({ onChoreUpdate, isModalOpen, onClose }) => {
autoFocus autoFocus
value={taskText} value={taskText}
isNativeScanner={isNativeScanner} isNativeScanner={isNativeScanner}
onScanClick={() => setShowScan(true)} onScanClick={llmAvailable ? () => {
onPhotoSelected={dataUrl => { setScanAutoCapture(true)
setShowScan(true)
} : undefined}
onPhotoSelected={llmAvailable ? dataUrl => {
setScanAutoCapture(false)
setPendingPhotoUrl(dataUrl) setPendingPhotoUrl(dataUrl)
setShowScan(true) setShowScan(true)
}} } : undefined}
placeholder='Type your task...' placeholder='Type your task...'
onChange={text => { onChange={text => {
setTaskText(text) setTaskText(text)
@@ -984,10 +995,12 @@ const TaskInput = ({ onChoreUpdate, isModalOpen, onClose }) => {
{showScan && ( {showScan && (
<ScanPanel <ScanPanel
open open
autoCapture={scanAutoCapture}
onTaskExtracted={handleTaskExtracted} onTaskExtracted={handleTaskExtracted}
initialImageUrl={pendingPhotoUrl} initialImageUrl={pendingPhotoUrl}
onClose={() => { onClose={() => {
setShowScan(false) setShowScan(false)
setScanAutoCapture(false)
setPendingPhotoUrl(null) setPendingPhotoUrl(null)
}} }}
/> />

View File

@@ -70,10 +70,16 @@ Output:
async function runNativeOCR(imageSource) { async function runNativeOCR(imageSource) {
const { Ocr } = await import('@jcesarmobile/capacitor-ocr') const { Ocr } = await import('@jcesarmobile/capacitor-ocr')
// Convert Capacitor WebView file URL → native file:// URL the plugin can read let image = imageSource
const image = imageSource.includes('/_capacitor_file_/') if (imageSource.includes('/_capacitor_file_/')) {
? 'file://' + imageSource.replace(/^https?:\/\/localhost\/_capacitor_file_/, '') // Convert Capacitor WebView file URL → native file:// URL the plugin can read
: imageSource image =
'file://' +
imageSource.replace(/^https?:\/\/localhost\/_capacitor_file_/, '')
} else if (imageSource.startsWith('data:')) {
// iOS document scanner returns base64; strip the data URI prefix for the native plugin
image = imageSource.split(',')[1] || imageSource
}
const result = await Ocr.process({ image }) const result = await Ocr.process({ image })
return result.results.map(r => r.text).join('\n').trim() return result.results.map(r => r.text).join('\n').trim()
@@ -512,15 +518,17 @@ const PhotoTaskModal = ({ open, onClose, onTaskExtracted }) => {
> >
Retake Retake
</Button> </Button>
{isNativeScanner && ( {isNativeScanner &&
<Button capturedImage &&
variant='outlined' !capturedImage.startsWith('data:') && (
color='primary' <Button
onClick={() => handleProcess('native')} variant='outlined'
> color='primary'
Process Natively onClick={() => handleProcess('native')}
</Button> >
)} Process Natively
</Button>
)}
<Button <Button
variant='solid' variant='solid'
color='primary' color='primary'

View File

@@ -21,7 +21,7 @@ import { useScanToTask } from './useScanToTask'
* Flow: capture → (auto) processing → done [calls onTaskExtracted + onClose] * Flow: capture → (auto) processing → done [calls onTaskExtracted + onClose]
* → error [retake or cancel] * → error [retake or cancel]
*/ */
const ScanPanel = ({ open, onTaskExtracted, onClose, initialImageUrl }) => { const ScanPanel = ({ open, onTaskExtracted, onClose, initialImageUrl, autoCapture }) => {
const { const {
isNativeScanner, isNativeScanner,
phase, phase,
@@ -47,6 +47,9 @@ const ScanPanel = ({ open, onTaskExtracted, onClose, initialImageUrl }) => {
useEffect(() => { useEffect(() => {
if (open) { if (open) {
activate(initialImageUrl) activate(initialImageUrl)
if (autoCapture && isNativeScanner && !initialImageUrl) {
handleNativeScan()
}
} else { } else {
reset() reset()
} }

View File

@@ -50,9 +50,13 @@ Output:
async function runNativeOCR(imageSource) { async function runNativeOCR(imageSource) {
const { Ocr } = await import('@jcesarmobile/capacitor-ocr') const { Ocr } = await import('@jcesarmobile/capacitor-ocr')
const image = imageSource.includes('/_capacitor_file_/') let image = imageSource
? 'file://' + imageSource.replace(/^https?:\/\/localhost\/_capacitor_file_/, '') if (imageSource.includes('/_capacitor_file_/')) {
: imageSource image = 'file://' + imageSource.replace(/^https?:\/\/localhost\/_capacitor_file_/, '')
} else if (imageSource.startsWith('data:')) {
// iOS document scanner returns base64; strip the data URI prefix for the native plugin
image = imageSource.split(',')[1] || imageSource
}
const result = await Ocr.process({ image }) const result = await Ocr.process({ image })
return result.results.map(r => r.text).join('\n').trim() return result.results.map(r => r.text).join('\n').trim()
} }
@@ -196,7 +200,9 @@ export function useScanToTask() {
return return
} }
setCapturedImage(image) setCapturedImage(image)
processImage(image, 'native') // Native scanner already applied edge detection + crop; use browser OCR
// since the native OCR plugin requires a file path, not a base64 data URI
processImage(image, 'browser')
}, [scanDocument, processImage]) }, [scanDocument, processImage])
const retake = useCallback(() => { const retake = useCallback(() => {