diff --git a/android/app/build.gradle b/android/app/build.gradle index b90cbe7..729845e 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -1,5 +1,11 @@ apply plugin: 'com.android.application' +def keystorePropertiesFile = rootProject.file("keystore.properties") +def keystoreProperties = new Properties() +if (keystorePropertiesFile.exists()) { + keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) +} + android { namespace "com.donetick.app" compileSdk rootProject.ext.compileSdkVersion @@ -7,8 +13,8 @@ android { applicationId "com.donetick.app" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 25 - versionName "1.2.3" + versionCode 26 + versionName "1.2.4" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { // 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:!*~' } } + signingConfigs { + release { + if (keystorePropertiesFile.exists()) { + storeFile file(keystoreProperties['storeFile']) + storePassword keystoreProperties['storePassword'] + keyAlias keystoreProperties['keyAlias'] + keyPassword keystoreProperties['keyPassword'] + } + } + } buildTypes { release { + signingConfig keystorePropertiesFile.exists() ? signingConfigs.release : signingConfigs.debug minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } diff --git a/android/app/release/output-metadata.json b/android/app/release/output-metadata.json index 0683b46..2b10993 100644 --- a/android/app/release/output-metadata.json +++ b/android/app/release/output-metadata.json @@ -11,8 +11,8 @@ "type": "SINGLE", "filters": [], "attributes": [], - "versionCode": 20, - "versionName": "1.0.20", + "versionCode": 25, + "versionName": "1.2.3", "outputFile": "app-release.apk" } ], @@ -33,5 +33,5 @@ ] } ], - "minSdkVersionForDexing": 24 + "minSdkVersionForDexing": 28 } \ No newline at end of file diff --git a/ios/App/App.xcodeproj/project.pbxproj b/ios/App/App.xcodeproj/project.pbxproj index 9230fec..cbc2233 100644 --- a/ios/App/App.xcodeproj/project.pbxproj +++ b/ios/App/App.xcodeproj/project.pbxproj @@ -357,15 +357,16 @@ baseConfigurationReference = FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ALLOW_ENTITLEMENTS_MODIFICATION = YES; CODE_SIGN_ENTITLEMENTS = App/App.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 25; + CURRENT_PROJECT_VERSION = 26; DEVELOPMENT_TEAM = 6UJJ78R3BS; INFOPLIST_FILE = App/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.donetick.app; - MARKETING_VERSION = 1.2.3; + MARKETING_VERSION = 1.2.4; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -377,14 +378,15 @@ baseConfigurationReference = AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ALLOW_ENTITLEMENTS_MODIFICATION = YES; CODE_SIGN_ENTITLEMENTS = App/App.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 25; + CURRENT_PROJECT_VERSION = 26; DEVELOPMENT_TEAM = 6UJJ78R3BS; INFOPLIST_FILE = App/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.0; 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_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; diff --git a/ios/App/Podfile b/ios/App/Podfile index e5d54c4..30b894a 100644 --- a/ios/App/Podfile +++ b/ios/App/Podfile @@ -15,13 +15,16 @@ def capacitor_pods pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app' pod 'CapacitorBrowser', :path => '../../node_modules/@capacitor/browser' pod 'CapacitorDevice', :path => '../../node_modules/@capacitor/device' + pod 'CapacitorLocalLlm', :path => '../../node_modules/@capacitor/local-llm' pod 'CapacitorLocalNotifications', :path => '../../node_modules/@capacitor/local-notifications' pod 'CapacitorNetwork', :path => '../../node_modules/@capacitor/network' pod 'CapacitorPreferences', :path => '../../node_modules/@capacitor/preferences' pod 'CapacitorPushNotifications', :path => '../../node_modules/@capacitor/push-notifications' 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 '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 'RevenuecatPurchasesCapacitorUi', :path => '../../node_modules/@revenuecat/purchases-capacitor-ui' pod 'CapacitorPluginSafeArea', :path => '../../node_modules/capacitor-plugin-safe-area' diff --git a/package.json b/package.json index 2410be7..b73685f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "donetick", "private": true, - "version": "1.2.3", + "version": "1.2.4", "type": "module", "engines": { "node": ">=20.0.0", @@ -30,8 +30,9 @@ "ionic:build": "npm run build", "ionic:serve": "npm run start", "android": "npm run build && npx cap sync android", + "android:dev": "npx cap run android --live-reload", "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:major": "node bump-version.js major", "bump:patch": "node bump-version.js patch" diff --git a/src/hooks/useDocumentScanner.js b/src/hooks/useDocumentScanner.js index ff66e9b..1b3c827 100644 --- a/src/hooks/useDocumentScanner.js +++ b/src/hooks/useDocumentScanner.js @@ -25,7 +25,7 @@ function normalizeScannedImage(raw) { export function useDocumentScanner() { 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 } try { diff --git a/src/views/components/AddTaskModal.jsx b/src/views/components/AddTaskModal.jsx index be8a3a6..e73c121 100644 --- a/src/views/components/AddTaskModal.jsx +++ b/src/views/components/AddTaskModal.jsx @@ -30,6 +30,7 @@ import LearnMoreButton from './LearnMore' import NotificationPickerField from './NotificationPickerField' import ScanPanel from './ScanToTask/ScanPanel' import { useDocumentScanner } from '../../hooks/useDocumentScanner' +import { localAIService } from '../../service/LocalAIService' import PriorityPickerField from './PriorityPickerField' import RepeatPickerField from './RepeatPickerField' import RichTextEditor from './RichTextEditor' @@ -107,9 +108,15 @@ const TaskInput = ({ onChoreUpdate, isModalOpen, onClose }) => { const [attachments, setAttachments] = useState([]) const [draftId, setDraftId] = useState(() => crypto.randomUUID()) const [showScan, setShowScan] = useState(false) + const [scanAutoCapture, setScanAutoCapture] = useState(false) const [pendingPhotoUrl, setPendingPhotoUrl] = useState(null) + const [llmAvailable, setLlmAvailable] = useState(false) const { isNativeScanner } = useDocumentScanner() + useEffect(() => { + localAIService.isAvailable().then(setLlmAvailable) + }, []) + // Priority colors const priorityColors = { 0: TASK_COLOR.NO_PRIORITY, @@ -793,11 +800,15 @@ const TaskInput = ({ onChoreUpdate, isModalOpen, onClose }) => { autoFocus value={taskText} isNativeScanner={isNativeScanner} - onScanClick={() => setShowScan(true)} - onPhotoSelected={dataUrl => { + onScanClick={llmAvailable ? () => { + setScanAutoCapture(true) + setShowScan(true) + } : undefined} + onPhotoSelected={llmAvailable ? dataUrl => { + setScanAutoCapture(false) setPendingPhotoUrl(dataUrl) setShowScan(true) - }} + } : undefined} placeholder='Type your task...' onChange={text => { setTaskText(text) @@ -984,10 +995,12 @@ const TaskInput = ({ onChoreUpdate, isModalOpen, onClose }) => { {showScan && ( { setShowScan(false) + setScanAutoCapture(false) setPendingPhotoUrl(null) }} /> diff --git a/src/views/components/PhotoTaskModal.jsx b/src/views/components/PhotoTaskModal.jsx index 100fbf6..bc63010 100644 --- a/src/views/components/PhotoTaskModal.jsx +++ b/src/views/components/PhotoTaskModal.jsx @@ -70,10 +70,16 @@ Output: async function runNativeOCR(imageSource) { const { Ocr } = await import('@jcesarmobile/capacitor-ocr') - // Convert Capacitor WebView file URL → native file:// URL the plugin can read - const image = imageSource.includes('/_capacitor_file_/') - ? 'file://' + imageSource.replace(/^https?:\/\/localhost\/_capacitor_file_/, '') - : imageSource + let image = imageSource + if (imageSource.includes('/_capacitor_file_/')) { + // Convert Capacitor WebView file URL → native file:// URL the plugin can read + 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 }) return result.results.map(r => r.text).join('\n').trim() @@ -512,15 +518,17 @@ const PhotoTaskModal = ({ open, onClose, onTaskExtracted }) => { > Retake - {isNativeScanner && ( - - )} + {isNativeScanner && + capturedImage && + !capturedImage.startsWith('data:') && ( + + )}