├── web ├── manifest.json ├── favicon.png └── icons │ ├── Icon-192.png │ ├── Icon-512.png │ ├── Icon-maskable-192.png │ └── Icon-maskable-512.png ├── fastlane └── metadata │ └── android │ └── en-US │ ├── title.txt │ ├── images │ ├── tvBanner.png │ ├── featureGraphic.png │ ├── promoGraphic.png │ ├── icon.png │ └── phoneScreenshots │ │ ├── 1.png │ │ ├── 10.png │ │ ├── 2.png │ │ ├── 3.png │ │ ├── 4.png │ │ ├── 5.png │ │ ├── 6.png │ │ ├── 7.png │ │ ├── 8.png │ │ └── 9.png │ ├── short_description.txt │ ├── changelogs │ ├── 7.txt │ ├── 8.txt │ ├── 9.txt │ ├── 3.txt │ ├── 14.txt │ ├── 5.txt │ ├── 13.txt │ ├── 4.txt │ ├── 6.txt │ ├── 11.txt │ └── 10.txt │ └── full_description.txt ├── android ├── app │ ├── src │ │ ├── main │ │ │ ├── java │ │ │ │ └── com │ │ │ │ │ ├── example │ │ │ │ │ └── todoflutter │ │ │ │ │ │ ├── AlarmReceiver.java │ │ │ │ │ │ ├── MainActivity.java │ │ │ │ │ │ └── NotificationActionReceiver.java │ │ │ │ │ └── todoapp │ │ │ │ │ └── todoflutter │ │ │ │ │ ├── AlarmReceiver.java │ │ │ │ │ ├── MainActivity.java │ │ │ │ │ └── NotificationActionReceiver.java │ │ │ ├── kotlin │ │ │ │ └── com │ │ │ │ │ ├── trudido │ │ │ │ │ └── app │ │ │ │ │ │ ├── TodoWidgetService.kt │ │ │ │ │ │ ├── TaskStatusStore.kt │ │ │ │ │ │ ├── ShowNotificationReceiver.kt │ │ │ │ │ │ ├── BootCompletedReceiver.kt │ │ │ │ │ │ ├── MissedReminderCatchUp.kt │ │ │ │ │ │ ├── PendingActionStore.kt │ │ │ │ │ │ ├── DeferredReminderWork.kt │ │ │ │ │ │ ├── NotificationActionReceiver.kt │ │ │ │ │ │ ├── DeferredReminderWorker.kt │ │ │ │ │ │ ├── LateAlarmTracker.kt │ │ │ │ │ │ ├── ScheduledNotificationsStore.kt │ │ │ │ │ │ ├── TaskFileHandler.kt │ │ │ │ │ │ └── PermissionsHelper.kt │ │ │ │ │ └── todoapp │ │ │ │ │ └── todoflutter │ │ │ │ │ ├── MainActivity.kt │ │ │ │ │ ├── PendingActionStore.kt │ │ │ │ │ ├── NotificationActionReceiver.kt │ │ │ │ │ ├── PermissionsHelper.kt │ │ │ │ │ ├── README_LEGACY.txt │ │ │ │ │ ├── ShowNotificationReceiver.kt │ │ │ │ │ ├── NotificationScheduler.kt │ │ │ │ │ ├── TaskStatusStore.kt │ │ │ │ │ └── ExactAlarmPermissionHelper.kt │ │ │ └── res │ │ │ │ ├── mipmap-hdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ ├── ic_launcher_background.png │ │ │ │ ├── ic_launcher_foreground.png │ │ │ │ └── ic_launcher_monochrome.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ ├── ic_launcher_background.png │ │ │ │ ├── ic_launcher_foreground.png │ │ │ │ └── ic_launcher_monochrome.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ ├── ic_launcher_background.png │ │ │ │ ├── ic_launcher_foreground.png │ │ │ │ └── ic_launcher_monochrome.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ ├── ic_launcher_background.png │ │ │ │ ├── ic_launcher_foreground.png │ │ │ │ └── ic_launcher_monochrome.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ ├── ic_launcher_background.png │ │ │ │ ├── ic_launcher_foreground.png │ │ │ │ └── ic_launcher_monochrome.png │ │ │ │ ├── values │ │ │ │ ├── colors.xml │ │ │ │ └── styles.xml │ │ │ │ ├── values-night │ │ │ │ ├── colors.xml │ │ │ │ └── styles.xml │ │ │ │ ├── layout │ │ │ │ └── activity_main.xml │ │ │ │ ├── xml │ │ │ │ └── file_paths.xml │ │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ └── ic_launcher.xml │ │ │ │ └── drawable │ │ │ │ └── launch_background.xml │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ └── profile │ │ │ └── AndroidManifest.xml │ ├── proguard-rules.pro │ └── build.gradle.kts ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── .gitignore ├── gradle.properties ├── build.gradle.kts └── settings.gradle.kts ├── assets ├── icon │ ├── 1.png │ └── 2.png ├── imagefiles │ ├── 1.png │ ├── 2.png │ ├── 3.png │ ├── 4.png │ ├── 5.png │ ├── 6.png │ ├── 7.png │ ├── 8.png │ ├── 9.png │ ├── 10.png │ ├── trudicon.png │ └── trudiconRound.png └── getitongithub.png ├── analysis_options.yaml ├── .gitmodules ├── android_icon_backups ├── mipmap-hdpi-20251126131522 │ ├── ic_launcher.png │ └── launcher_icon.png ├── mipmap-mdpi-20251126131522 │ ├── ic_launcher.png │ └── launcher_icon.png ├── mipmap-xhdpi-20251126131522 │ ├── ic_launcher.png │ └── launcher_icon.png ├── mipmap-xxhdpi-20251126131522 │ ├── ic_launcher.png │ └── launcher_icon.png ├── mipmap-xxxhdpi-20251126131522 │ ├── ic_launcher.png │ └── launcher_icon.png ├── drawable-hdpi-20251126131522 │ └── ic_launcher_foreground.png ├── drawable-mdpi-20251126131522 │ └── ic_launcher_foreground.png ├── drawable-xhdpi-20251126131522 │ └── ic_launcher_foreground.png ├── drawable-xxhdpi-20251126131522 │ └── ic_launcher_foreground.png ├── drawable-xxxhdpi-20251126131522 │ └── ic_launcher_foreground.png ├── drawable-20251126131522 │ ├── ic_launcher_background.xml │ ├── app_icon.xml │ ├── ic_check.xml │ ├── launch_background.xml │ ├── ic_notification.xml │ ├── ic_snooze.xml │ ├── ic_launcher_foreground.xml │ └── ic_launcher_monochrome.xml ├── mipmap-anydpi-v26-20251126131522 │ ├── launcher_icon.xml │ └── ic_launcher.xml └── drawable-v21-20251126131522 │ └── launch_background.xml ├── .github ├── FUNDING.yml └── workflows │ ├── build.yml │ └── crowdin-sync.yml ├── devtools_options.yaml ├── lib ├── models │ ├── duration_adapter.dart │ ├── statistics.dart │ ├── app_error.dart │ ├── note.g.dart │ ├── folder.g.dart │ ├── note_folder.g.dart │ ├── todo.g.dart │ ├── note.dart │ └── note_folder.dart ├── widgets │ ├── time_picker_form_field.dart │ ├── search_bar.dart │ ├── battery_optimization_nudge.dart │ ├── alarm_settings_watcher.dart │ ├── app_error_boundary.dart │ ├── reminder_components.dart │ ├── link_embed_builder.dart │ └── system_permission_dialogs.dart ├── services │ ├── new_notification_service.dart │ ├── task_import_export_service.dart │ ├── todo_provider.dart │ ├── battery_optimization_service.dart │ ├── late_alarm_nudge_service.dart │ ├── exact_alarm_permission.dart │ ├── lifecycle_sync_observer.dart │ ├── app_refresh_service.dart │ ├── text_scale_service.dart │ ├── haptic_feedback_service.dart │ ├── category_migration_service.dart │ ├── vault_password_service.dart │ ├── navigation_service.dart │ ├── permissions_channel.dart │ ├── template_provider.dart │ └── files_channel.dart ├── screens │ └── permissions_page.dart ├── providers │ ├── alarm_settings_providers.dart │ ├── clock.dart │ └── app_providers.dart ├── utils │ ├── formatters.dart │ ├── responsive_size.dart │ ├── encryption_helper.dart │ └── week_start_utils.dart ├── repositories │ ├── folder_repository.dart │ ├── folder_template_repository.dart │ └── task_repository.dart ├── theme │ └── solarized_colors.dart └── use_cases │ └── folder_template_use_cases.dart ├── BUILD_INFO.md ├── fdroid └── com.trudido.app.yml └── .gitignore /web/manifest.json: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/title.txt: -------------------------------------------------------------------------------- 1 | Trudido -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/tvBanner.png: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/featureGraphic.png: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/promoGraphic.png: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/example/todoflutter/AlarmReceiver.java: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/example/todoflutter/MainActivity.java: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/todoapp/todoflutter/AlarmReceiver.java: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/todoapp/todoflutter/MainActivity.java: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/trudido/app/TodoWidgetService.kt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/example/todoflutter/NotificationActionReceiver.java: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/todoapp/todoflutter/NotificationActionReceiver.java: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Empty proguard rules - let R8 handle optimization 2 | -------------------------------------------------------------------------------- /web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikmuellr/trudido/HEAD/web/favicon.png -------------------------------------------------------------------------------- /assets/icon/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikmuellr/trudido/HEAD/assets/icon/1.png -------------------------------------------------------------------------------- /assets/icon/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikmuellr/trudido/HEAD/assets/icon/2.png -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/todoapp/todoflutter/MainActivity.kt: -------------------------------------------------------------------------------- 1 | // (removed legacy content) 2 | -------------------------------------------------------------------------------- /assets/imagefiles/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikmuellr/trudido/HEAD/assets/imagefiles/1.png -------------------------------------------------------------------------------- /assets/imagefiles/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikmuellr/trudido/HEAD/assets/imagefiles/2.png -------------------------------------------------------------------------------- /assets/imagefiles/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikmuellr/trudido/HEAD/assets/imagefiles/3.png -------------------------------------------------------------------------------- /assets/imagefiles/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikmuellr/trudido/HEAD/assets/imagefiles/4.png -------------------------------------------------------------------------------- /assets/imagefiles/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikmuellr/trudido/HEAD/assets/imagefiles/5.png -------------------------------------------------------------------------------- /assets/imagefiles/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikmuellr/trudido/HEAD/assets/imagefiles/6.png -------------------------------------------------------------------------------- /assets/imagefiles/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikmuellr/trudido/HEAD/assets/imagefiles/7.png -------------------------------------------------------------------------------- /assets/imagefiles/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikmuellr/trudido/HEAD/assets/imagefiles/8.png -------------------------------------------------------------------------------- /assets/imagefiles/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikmuellr/trudido/HEAD/assets/imagefiles/9.png -------------------------------------------------------------------------------- /web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikmuellr/trudido/HEAD/web/icons/Icon-192.png -------------------------------------------------------------------------------- /web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikmuellr/trudido/HEAD/web/icons/Icon-512.png -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/todoapp/todoflutter/PendingActionStore.kt: -------------------------------------------------------------------------------- 1 | // (removed legacy content) 2 | -------------------------------------------------------------------------------- /assets/getitongithub.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikmuellr/trudido/HEAD/assets/getitongithub.png -------------------------------------------------------------------------------- /assets/imagefiles/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikmuellr/trudido/HEAD/assets/imagefiles/10.png -------------------------------------------------------------------------------- /assets/imagefiles/trudicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikmuellr/trudido/HEAD/assets/imagefiles/trudicon.png -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | analyzer: 2 | exclude: 3 | - submodules/** 4 | errors: 5 | deprecated_member_use: ignore -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/short_description.txt: -------------------------------------------------------------------------------- 1 | Privacy-friendly, open-source to-do list app. No ads, no tracking. -------------------------------------------------------------------------------- /web/icons/Icon-maskable-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikmuellr/trudido/HEAD/web/icons/Icon-maskable-192.png -------------------------------------------------------------------------------- /web/icons/Icon-maskable-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikmuellr/trudido/HEAD/web/icons/Icon-maskable-512.png -------------------------------------------------------------------------------- /assets/imagefiles/trudiconRound.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikmuellr/trudido/HEAD/assets/imagefiles/trudiconRound.png -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "submodules/flutter"] 2 | path = submodules/flutter 3 | url = https://github.com/flutter/flutter.git 4 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikmuellr/trudido/HEAD/fastlane/metadata/android/en-US/images/icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikmuellr/trudido/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikmuellr/trudido/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikmuellr/trudido/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikmuellr/trudido/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikmuellr/trudido/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #ffffff 4 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikmuellr/trudido/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikmuellr/trudido/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikmuellr/trudido/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikmuellr/trudido/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikmuellr/trudido/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikmuellr/trudido/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /android_icon_backups/mipmap-hdpi-20251126131522/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikmuellr/trudido/HEAD/android_icon_backups/mipmap-hdpi-20251126131522/ic_launcher.png -------------------------------------------------------------------------------- /android_icon_backups/mipmap-mdpi-20251126131522/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikmuellr/trudido/HEAD/android_icon_backups/mipmap-mdpi-20251126131522/ic_launcher.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikmuellr/trudido/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikmuellr/trudido/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/10.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikmuellr/trudido/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikmuellr/trudido/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikmuellr/trudido/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikmuellr/trudido/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikmuellr/trudido/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikmuellr/trudido/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/7.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikmuellr/trudido/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/8.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikmuellr/trudido/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/9.png -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/todoapp/todoflutter/NotificationActionReceiver.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | package com.todoapp.todoflutter 3 | 4 | class NotificationActionReceiver 5 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikmuellr/trudido/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikmuellr/trudido/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikmuellr/trudido/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikmuellr/trudido/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikmuellr/trudido/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikmuellr/trudido/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /android_icon_backups/mipmap-hdpi-20251126131522/launcher_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikmuellr/trudido/HEAD/android_icon_backups/mipmap-hdpi-20251126131522/launcher_icon.png -------------------------------------------------------------------------------- /android_icon_backups/mipmap-mdpi-20251126131522/launcher_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikmuellr/trudido/HEAD/android_icon_backups/mipmap-mdpi-20251126131522/launcher_icon.png -------------------------------------------------------------------------------- /android_icon_backups/mipmap-xhdpi-20251126131522/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikmuellr/trudido/HEAD/android_icon_backups/mipmap-xhdpi-20251126131522/ic_launcher.png -------------------------------------------------------------------------------- /android_icon_backups/mipmap-xxhdpi-20251126131522/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikmuellr/trudido/HEAD/android_icon_backups/mipmap-xxhdpi-20251126131522/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikmuellr/trudido/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikmuellr/trudido/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikmuellr/trudido/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /android_icon_backups/mipmap-xhdpi-20251126131522/launcher_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikmuellr/trudido/HEAD/android_icon_backups/mipmap-xhdpi-20251126131522/launcher_icon.png -------------------------------------------------------------------------------- /android_icon_backups/mipmap-xxhdpi-20251126131522/launcher_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikmuellr/trudido/HEAD/android_icon_backups/mipmap-xxhdpi-20251126131522/launcher_icon.png -------------------------------------------------------------------------------- /android_icon_backups/mipmap-xxxhdpi-20251126131522/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikmuellr/trudido/HEAD/android_icon_backups/mipmap-xxxhdpi-20251126131522/ic_launcher.png -------------------------------------------------------------------------------- /android_icon_backups/mipmap-xxxhdpi-20251126131522/launcher_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikmuellr/trudido/HEAD/android_icon_backups/mipmap-xxxhdpi-20251126131522/launcher_icon.png -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | liberapay: dominikmuellr 2 | 3 | github: [dominikmuellr] 4 | 5 | ko_fi: dominikmuellr 6 | 7 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 8 | -------------------------------------------------------------------------------- /android_icon_backups/drawable-hdpi-20251126131522/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikmuellr/trudido/HEAD/android_icon_backups/drawable-hdpi-20251126131522/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android_icon_backups/drawable-mdpi-20251126131522/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikmuellr/trudido/HEAD/android_icon_backups/drawable-mdpi-20251126131522/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android_icon_backups/drawable-xhdpi-20251126131522/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikmuellr/trudido/HEAD/android_icon_backups/drawable-xhdpi-20251126131522/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android_icon_backups/drawable-xxhdpi-20251126131522/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikmuellr/trudido/HEAD/android_icon_backups/drawable-xxhdpi-20251126131522/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/todoapp/todoflutter/PermissionsHelper.kt: -------------------------------------------------------------------------------- 1 | package com.todoapp.todoflutter 2 | 3 | // Legacy placeholder; real implementation moved to com.trudido.app 4 | object PermissionsHelper 5 | -------------------------------------------------------------------------------- /android_icon_backups/drawable-xxxhdpi-20251126131522/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominikmuellr/trudido/HEAD/android_icon_backups/drawable-xxxhdpi-20251126131522/ic_launcher_foreground.png -------------------------------------------------------------------------------- /devtools_options.yaml: -------------------------------------------------------------------------------- 1 | description: This file stores settings for Dart & Flutter DevTools. 2 | documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states 3 | extensions: 4 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/7.txt: -------------------------------------------------------------------------------- 1 | Version 1.0.7 (build 7): 2 | - Bumped app version from 1.0.6 to 1.0.7 (build 7) 3 | - Minor fixes and translations updates 4 | - See full changelog in app release notes 5 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/8.txt: -------------------------------------------------------------------------------- 1 | Version 1.0.8 (build 8): 2 | - Bumped app version from 1.0.7 to 1.0.8 (build 8) 3 | - Minor fixes and translations updates 4 | - See full changelog in app release notes 5 | -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | #121212 5 | 6 | -------------------------------------------------------------------------------- /android_icon_backups/drawable-20251126131522/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /android/app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /android/app/src/main/res/xml/file_paths.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip 6 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/todoapp/todoflutter/README_LEGACY.txt: -------------------------------------------------------------------------------- 1 | This package (com.todoapp.todoflutter) is deprecated and retained only as stubs. 2 | All active Android components migrated to com.trudido.app. 3 | Safe to delete once confirmed no lingering references in code or manifests. -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/todoapp/todoflutter/ShowNotificationReceiver.kt: -------------------------------------------------------------------------------- 1 | package com.todoapp.todoflutter 2 | 3 | import android.content.BroadcastReceiver 4 | import android.content.Context 5 | import android.content.Intent 6 | import androidx.core.app.NotificationManagerCompat 7 | 8 | // (removed legacy content) 9 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | .cxx/ 9 | 10 | # Remember to never publicly share your keystore. 11 | # See https://flutter.dev/to/reference-keystore 12 | key.properties 13 | **/*.keystore 14 | **/*.jks 15 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | 5 | # Disable Kotlin incremental compilation to avoid cache issues 6 | kotlin.incremental=false 7 | kotlin.incremental.android=false 8 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/todoapp/todoflutter/NotificationScheduler.kt: -------------------------------------------------------------------------------- 1 | // Deprecated duplicate legacy file retained temporarily. Not referenced by manifest. 2 | @file:Suppress("unused", "UNUSED_PARAMETER") 3 | package com.todoapp.todoflutter 4 | 5 | object NotificationScheduler { 6 | // Intentionally empty duplicate placeholder. 7 | } 8 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/9.txt: -------------------------------------------------------------------------------- 1 | Version 1.0.9 (build 9): 2 | - Added Material 3 expandable FAB menu with tab-specific actions 3 | - Implemented Vault Note creation directly from FAB menu 4 | - Enhanced animations with staggered pop-up effects 5 | - Improved user flow for vault folder creation 6 | - See full changelog in app release notes 7 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android_icon_backups/mipmap-anydpi-v26-20251126131522/launcher_icon.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /android_icon_backups/drawable-20251126131522/app_icon.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /android_icon_backups/drawable-20251126131522/ic_check.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /android_icon_backups/mipmap-anydpi-v26-20251126131522/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android_icon_backups/drawable-20251126131522/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/3.txt: -------------------------------------------------------------------------------- 1 | Visual Enhancements: 2 | - App name now uses primary theme color 3 | - Themed folder dropdown dividers 4 | - Updated filter icon (tune → filter_alt) 5 | - Greeting text uses theme primary/secondary colors 6 | - Consistent Material Design 3 theming 7 | 8 | Bug Fixes: 9 | - Language mapping corrections (French/German swap fixed) 10 | - Template popup menu crashes resolved 11 | - Name clearing behavior improved 12 | - Greeting hint text logic enhanced -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/14.txt: -------------------------------------------------------------------------------- 1 | Version 1.2.3 - Calendar & UI Improvements 2 | 3 | • Added "Week Starts On" setting - choose Sunday, Monday, Saturday, or any day as the first day of week for all calendars and date pickers 4 | • Fixed Android 3-button navigation bar overlapping note content in the Quill editor 5 | • Navigation bar now matches app theme color instead of staying blue 6 | • Added swipe-from-edge gesture to open navigation drawer (works with 3-button nav) 7 | • Improved FAB positioning on devices with 3-button navigation bar 8 | -------------------------------------------------------------------------------- /android_icon_backups/drawable-v21-20251126131522/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/5.txt: -------------------------------------------------------------------------------- 1 | Calendar Enhancements: 2 | - Long-press or double-tap any calendar day to create a task with that date 3 | - Date is now automatically prefilled when creating tasks from calendar 4 | - "Add task" button in empty calendar days 5 | - Optimized priority indicators (2 bars max to prevent overflow) 6 | - Individual task bars sorted by priority (high → medium → low → none) 7 | 8 | User Experience: 9 | - Improved calendar interaction with intuitive gestures 10 | - Better visual hierarchy in calendar view 11 | - Cleaner date selection workflow 12 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/13.txt: -------------------------------------------------------------------------------- 1 | Version 1.2.2 - Calendar Sync & Update Checker Fixes 2 | 3 | • Fixed calendar sync duplicate events - tasks are no longer exported multiple times 4 | • Fixed imported calendar events being re-exported (prevents duplicates) 5 | • Fixed all-day event timezone issue - dates now sync correctly 6 | • Added "Remove Duplicate Events" maintenance option 7 | • Added "Delete All Trudido Events" option for calendar cleanup 8 | • Switched update checker to GitHub Atom feed (unlimited checks, no rate limits) 9 | • Update dialog now opens browser to download instead of in-app download 10 | -------------------------------------------------------------------------------- /android/build.gradle.kts: -------------------------------------------------------------------------------- 1 | allprojects { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | } 7 | 8 | val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get() 9 | rootProject.layout.buildDirectory.value(newBuildDir) 10 | 11 | subprojects { 12 | val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) 13 | project.layout.buildDirectory.value(newSubprojectBuildDir) 14 | } 15 | subprojects { 16 | project.evaluationDependsOn(":app") 17 | } 18 | 19 | tasks.register("clean") { 20 | delete(rootProject.layout.buildDirectory) 21 | } 22 | -------------------------------------------------------------------------------- /android_icon_backups/drawable-20251126131522/ic_notification.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /android_icon_backups/drawable-20251126131522/ic_snooze.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /android_icon_backups/drawable-20251126131522/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 12 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/4.txt: -------------------------------------------------------------------------------- 1 | New Features: 2 | - Smart priority system with "None" option - cleaner default state 3 | - Beautiful priority chips with color coding (High/Medium/Low) 4 | - Time display on tasks - see exact due times, not just dates 5 | - Priority selector bottom sheet - no more cycling through options 6 | - Draggable theme & language selectors - expand to see all options 7 | 8 | Visual Improvements: 9 | - Priority chips only show when set (less clutter) 10 | - Overdue tasks highlighted in red 11 | - Time formatting with icons (🕒 for scheduled, 📅 for dates) 12 | - Theme-aware color chips adapt to your selected theme 13 | 14 | TASKS_DATA_KEYechnical: 15 | - Improved task display architecture 16 | - Better Material Design 3 integration 17 | - Enhanced bottom sheet interactions 18 | -------------------------------------------------------------------------------- /android_icon_backups/drawable-20251126131522/ic_launcher_monochrome.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /lib/models/duration_adapter.dart: -------------------------------------------------------------------------------- 1 | // Trudido - A privacy-focused todo and notes app 2 | // Copyright (C) 2025 Dominik Müller 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with this program. If not, see . 16 | 17 | 18 | -------------------------------------------------------------------------------- /lib/widgets/time_picker_form_field.dart: -------------------------------------------------------------------------------- 1 | // Trudido - A privacy-focused todo and notes app 2 | // Copyright (C) 2025 Dominik Müller 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with this program. If not, see . 16 | 17 | 18 | -------------------------------------------------------------------------------- /lib/services/new_notification_service.dart: -------------------------------------------------------------------------------- 1 | // Trudido - A privacy-focused todo and notes app 2 | // Copyright (C) 2025 Dominik Müller 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with this program. If not, see . 16 | 17 | 18 | -------------------------------------------------------------------------------- /lib/services/task_import_export_service.dart: -------------------------------------------------------------------------------- 1 | // Trudido - A privacy-focused todo and notes app 2 | // Copyright (C) 2025 Dominik Müller 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with this program. If not, see . 16 | 17 | 18 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/trudido/app/TaskStatusStore.kt: -------------------------------------------------------------------------------- 1 | package com.trudido.app 2 | 3 | import android.content.Context 4 | 5 | object TaskStatusStore { 6 | private const val PREFS = "task_status_store" 7 | private const val COMPLETED_SET = "completed_set" 8 | private fun prefs(ctx: Context) = ctx.getSharedPreferences(PREFS, Context.MODE_PRIVATE) 9 | fun isCompleted(ctx: Context, taskId: String): Boolean = (prefs(ctx).getString(COMPLETED_SET, "") ?: "").split('|').contains(taskId) 10 | fun markCompleted(ctx: Context, taskId: String) { 11 | val p = prefs(ctx) 12 | val raw = p.getString(COMPLETED_SET, "") ?: "" 13 | if (raw.split('|').contains(taskId)) return 14 | val updated = if (raw.isBlank()) taskId else "$raw|$taskId" 15 | p.edit().putString(COMPLETED_SET, updated).apply() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /android/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | val flutterSdkPath = run { 3 | val properties = java.util.Properties() 4 | file("local.properties").inputStream().use { properties.load(it) } 5 | val flutterSdkPath = properties.getProperty("flutter.sdk") 6 | require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } 7 | flutterSdkPath 8 | } 9 | 10 | includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") 11 | 12 | repositories { 13 | google() 14 | mavenCentral() 15 | gradlePluginPortal() 16 | } 17 | } 18 | 19 | plugins { 20 | id("dev.flutter.flutter-plugin-loader") version "1.0.0" 21 | id("com.android.application") version "8.9.1" apply false 22 | id("org.jetbrains.kotlin.android") version "2.1.0" apply false 23 | } 24 | 25 | include(":app") 26 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/trudido/app/ShowNotificationReceiver.kt: -------------------------------------------------------------------------------- 1 | package com.trudido.app 2 | 3 | import android.content.BroadcastReceiver 4 | import android.content.Context 5 | import android.content.Intent 6 | 7 | class ShowNotificationReceiver : BroadcastReceiver() { 8 | override fun onReceive(context: Context, intent: Intent) { 9 | val taskId = intent.getStringExtra("taskId") ?: return 10 | val title = intent.getStringExtra("title") ?: "Task Reminder" 11 | val body = intent.getStringExtra("body") ?: "" 12 | val scheduledAt = intent.getLongExtra("scheduledAt", 0L) 13 | if (scheduledAt > 0) LateAlarmTracker.recordFire(context, scheduledAt) 14 | val notif = NotificationScheduler.buildNotification(context, taskId, title, body) 15 | androidx.core.app.NotificationManagerCompat.from(context).notify(taskId.hashCode(), notif) 16 | // Update group summary after posting 17 | NotificationScheduler.updateGroupSummary(context) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/models/statistics.dart: -------------------------------------------------------------------------------- 1 | // Trudido - A privacy-focused todo and notes app 2 | // Copyright (C) 2025 Dominik Müller 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with this program. If not, see . 16 | 17 | // Deprecated: This file is retained only as an empty stub to avoid stale imports during 18 | // migration. All statistics logic moved to TaskStatistics in controllers/task_controller.dart. 19 | // Remove this file once no external references remain. 20 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/todoapp/todoflutter/TaskStatusStore.kt: -------------------------------------------------------------------------------- 1 | package com.todoapp.todoflutter 2 | 3 | import android.content.Context 4 | 5 | /** Stores minimal task status flags for idempotent native actions. */ 6 | object TaskStatusStore { 7 | private const val PREFS = "task_status_store" 8 | private const val COMPLETED_SET = "completed_set" // pipe-separated list 9 | 10 | private fun prefs(ctx: Context) = ctx.getSharedPreferences(PREFS, Context.MODE_PRIVATE) 11 | 12 | fun isCompleted(ctx: Context, taskId: String): Boolean { 13 | val raw = prefs(ctx).getString(COMPLETED_SET, "") ?: "" 14 | return raw.split('|').contains(taskId) 15 | } 16 | 17 | fun markCompleted(ctx: Context, taskId: String) { 18 | val p = prefs(ctx) 19 | val raw = p.getString(COMPLETED_SET, "") ?: "" 20 | if (raw.split('|').contains(taskId)) return 21 | val updated = if (raw.isBlank()) taskId else raw + "|" + taskId 22 | p.edit().putString(COMPLETED_SET, updated).apply() 23 | } 24 | // (removed legacy content) 25 | } 26 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/6.txt: -------------------------------------------------------------------------------- 1 | Repeatable Tasks Feature: 2 | - Create tasks that repeat daily, weekly, or monthly 3 | - Custom repeat patterns (e.g., every 2 days, every 3 weeks) 4 | - Select specific days for weekly repeats (Mon, Wed, Fri, etc.) 5 | - Optional end date for recurring tasks 6 | - Automatic creation of next occurrence when completing recurring tasks 7 | - Visual indicators showing which tasks are recurring 8 | - Calendar view displays recurring tasks on all relevant dates 9 | 10 | Calendar Enhancements: 11 | - Long-press or double-tap any calendar day to create a task with that date 12 | - Date is now automatically prefilled when creating tasks from calendar 13 | - "Add task" button in empty calendar days 14 | - Optimized priority indicators (2 bars max to prevent overflow) 15 | - Individual task bars sorted by priority (high → medium → low → none) 16 | 17 | User Experience: 18 | - Improved calendar interaction with intuitive gestures 19 | - Better visual hierarchy in calendar view 20 | - Cleaner date selection workflow 21 | - Enhanced task management with recurring patterns 22 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/full_description.txt: -------------------------------------------------------------------------------- 1 | Trudido is a privacy-friendly, open-source to-do list app built with Flutter. Organize your tasks, set reminders, and manage your daily life with a simple, intuitive interface. Trudido is designed for users who value transparency and control over their data—no ads, no tracking, and no unnecessary permissions.

2 | 3 | Features:
4 | - Add, edit, and delete tasks quickly
5 | - Organize tasks with categories and priorities
6 | - Set reminders to never miss important deadlines
7 | - Attach notes to tasks for extra details
8 | - Customizable themes including dark mode
9 | - Multilingual support for a global audience
10 | - Works offline—your data stays on your device

11 | 12 | Why Trudido?
13 | Trudido is lightweight, fast, and respects your privacy. All features are available without registration or cloud accounts. Perfect for students, professionals, and anyone who wants a reliable, distraction-free task manager.

14 | 15 | Trudido is free and open-source. Get organized and stay productive—your way! -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/11.txt: -------------------------------------------------------------------------------- 1 | Version 1.2.0 - Video & Media Enhancement Update 2 | 3 | 🎥 Video Features: 4 | - Added video recording capability via slash menu 5 | - Full video playback with tap-to-play/pause controls 6 | - Video progress bar with scrubbing support 7 | - Video thumbnails in note previews (first frame extraction) 8 | 9 | 📸 Photo Enhancements: 10 | - Tap photos to view fullscreen with pinch-to-zoom 11 | - Swipe to dismiss fullscreen viewer 12 | - Pan and zoom support (0.5x - 4x) 13 | 14 | 📅 Date Display Improvements: 15 | - More compact date format in note previews 16 | - European format (24-hour time, day before month) 17 | - Smart relative dates (Today, Yesterday, day names) 18 | 19 | 🎨 UI Improvements: 20 | - Small media thumbnails in note preview instead of emojis 21 | - Media-only notes now show "Media" as title instead of placeholder 22 | - Separated Photo and Video options in slash menu 23 | 24 | 🔧 Technical Updates: 25 | - Updated minSdk to 24 for better media support 26 | - Added video_player and video_thumbnail packages 27 | - Improved error handling for media operations 28 | -------------------------------------------------------------------------------- /BUILD_INFO.md: -------------------------------------------------------------------------------- 1 | # Build information 2 | 3 | ## Repository commit 4 | 5 | Full commit: 39c4217ce576487945d98f0013d055e1bf82b60e 6 | Short commit: 39c4217 7 | Commit summary: bumb up version number to v1.0.7 8 | Commit date: Thu Oct 23 13:20:38 2025 +0200 9 | Tag(s) pointing at commit: v1.0.7 10 | 11 | ## Flutter environment 12 | 13 | Flutter binary: /c/dev/flutter/bin/flutter 14 | Flutter version (on build machine): 15 | Flutter 3.35.5 • channel stable • https://github.com/flutter/flutter.git 16 | Framework • revision ac4e799d23 (4 weeks ago) • 2025-09-26 12:05:09 -0700 17 | Engine • hash 0274ead41f6265309f36e9d74bc8c559becd5345 (revision d3d45dcf25) (26 days ago) • 2025-09-26 16:45:18.000Z 18 | Tools • Dart 3.9.2 • DevTools 2.48.0 19 | 20 | ## Build command & artifacts 21 | 22 | Working directory: D:/projects/todoflutter 23 | Suggested build command used: flutter build apk --release --build-name=1.0.7 --build-number=7 24 | APK artifacts found: 25 | 26 | - build/app/outputs/flutter-apk/app-release.apk (26150396 bytes) 27 | - build/app/outputs/flutter-apk/app-release.apk.sha1 28 | 29 | ## Notes 30 | 31 | This file is auto-generated by a tooling step. If you want this metadata to be included in future builds, consider adding a `build_info` section to `pubspec.yaml` or committing this file at release time. 32 | -------------------------------------------------------------------------------- /lib/services/todo_provider.dart: -------------------------------------------------------------------------------- 1 | // Trudido - A privacy-focused todo and notes app 2 | // Copyright (C) 2025 Dominik Müller 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with this program. If not, see . 16 | 17 | // Deprecated legacy todo_provider.dart (stub) 18 | // All functionality migrated to: 19 | // - tasksProvider & TaskController (controllers/task_controller.dart) 20 | // - filter providers (providers/filter_providers.dart) 21 | // This file remains only to avoid broken imports during transition. Remove any 22 | // imports of this file; it will be deleted in a future cleanup. 23 | 24 | void deprecatedTodoProviderFileDoNotUse() { 25 | throw UnimplementedError( 26 | 'todo_provider.dart is deprecated. Use new providers.', 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /lib/screens/permissions_page.dart: -------------------------------------------------------------------------------- 1 | // Trudido - A privacy-focused todo and notes app 2 | // Copyright (C) 2025 Dominik Müller 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with this program. If not, see . 16 | 17 | import 'package:flutter/material.dart'; 18 | 19 | /// (Legacy stub) This page was replaced by UnifiedSettingsPage. 20 | @Deprecated('Use UnifiedSettingsPage instead. Will be removed after v1.1.0.') 21 | class PermissionsPage extends StatelessWidget { 22 | const PermissionsPage({super.key}); 23 | @override 24 | Widget build(BuildContext context) { 25 | assert(() { 26 | debugPrint( 27 | '[PermissionsPage] This legacy page is deprecated. Use UnifiedSettingsPage instead.', 28 | ); 29 | return true; 30 | }()); 31 | return const SizedBox.shrink(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/services/battery_optimization_service.dart: -------------------------------------------------------------------------------- 1 | // Trudido - A privacy-focused todo and notes app 2 | // Copyright (C) 2025 Dominik Müller 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with this program. If not, see . 16 | 17 | /// (Legacy stub) Use SystemSettingsService instead. 18 | @Deprecated('Replaced by SystemSettingsService. Will be removed after v1.1.0.') 19 | class BatteryOptimizationService { 20 | BatteryOptimizationService._(); 21 | static final instance = BatteryOptimizationService._(); 22 | Never _deprecated() => throw UnimplementedError( 23 | 'BatteryOptimizationService removed. Use SystemSettingsService.', 24 | ); 25 | Future isIgnoringOptimizations() async => _deprecated(); 26 | Future openSettings() async => _deprecated(); 27 | bool get hasAcknowledged => false; 28 | Future setAcknowledged() async => _deprecated(); 29 | } 30 | -------------------------------------------------------------------------------- /lib/models/app_error.dart: -------------------------------------------------------------------------------- 1 | // Trudido - A privacy-focused todo and notes app 2 | // Copyright (C) 2025 Dominik Müller 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with this program. If not, see . 16 | 17 | /// Unified application error types for consistent error handling & logging. 18 | enum AppErrorType { 19 | storageRead, 20 | storageWrite, 21 | serialization, 22 | deserialization, 23 | notFound, 24 | validation, 25 | unknown, 26 | } 27 | 28 | /// Simple wrapper exception carrying a type and context message. 29 | class AppError implements Exception { 30 | final AppErrorType type; 31 | final String message; 32 | final Object? cause; 33 | final StackTrace? stackTrace; 34 | 35 | const AppError(this.type, this.message, {this.cause, this.stackTrace}); 36 | 37 | @override 38 | String toString() => 39 | 'AppError(type: $type, message: $message, cause: $cause)'; 40 | } 41 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/trudido/app/BootCompletedReceiver.kt: -------------------------------------------------------------------------------- 1 | package com.trudido.app 2 | 3 | import android.content.BroadcastReceiver 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.util.Log 7 | 8 | /** Restores scheduled notifications after device reboot. */ 9 | class BootCompletedReceiver : BroadcastReceiver() { 10 | override fun onReceive(context: Context, intent: Intent?) { 11 | if (intent?.action != Intent.ACTION_BOOT_COMPLETED) return 12 | val now = System.currentTimeMillis() 13 | val items = ScheduledNotificationsStore.all(context) 14 | var restored = 0 15 | for (item in items) { 16 | // Skip past-due > 30 min; show immediately if within 30 min grace 17 | val delta = item.triggerTime - now 18 | if (delta <= 0) { 19 | if (now - item.triggerTime <= 30 * 60 * 1000) { 20 | NotificationScheduler.showNow(context, item.taskId, item.title, item.body) 21 | } else { 22 | // Drop very old reminder 23 | ScheduledNotificationsStore.remove(context, item.taskId) 24 | } 25 | } else { 26 | val requestCode = item.taskId.hashCode() 27 | NotificationScheduler.scheduleExact(context, item.taskId, item.title, item.body, item.triggerTime, requestCode) 28 | restored++ 29 | } 30 | } 31 | Log.i("BootCompletedReceiver", "Processed reboot restore items=${items.size} restored=$restored") 32 | } 33 | } -------------------------------------------------------------------------------- /lib/services/late_alarm_nudge_service.dart: -------------------------------------------------------------------------------- 1 | // Trudido - A privacy-focused todo and notes app 2 | // Copyright (C) 2025 Dominik Müller 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with this program. If not, see . 16 | 17 | import 'dart:async'; 18 | import 'package:flutter/foundation.dart'; 19 | import 'package:flutter/services.dart'; 20 | 21 | /// Bridges to native LateAlarmTracker to see if a battery optimization nudge should appear. 22 | class LateAlarmNudgeService { 23 | LateAlarmNudgeService._(); 24 | static final instance = LateAlarmNudgeService._(); 25 | 26 | static const _channel = MethodChannel('app.perms'); 27 | 28 | Future consumePromptIfNeeded() async { 29 | try { 30 | final r = await _channel.invokeMethod('consumeLateAlarmPrompt'); 31 | return r == true; 32 | } catch (e, st) { 33 | debugPrint( 34 | '[LateAlarmNudgeService] consumePromptIfNeeded error: $e\n$st', 35 | ); 36 | return false; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/trudido/app/MissedReminderCatchUp.kt: -------------------------------------------------------------------------------- 1 | package com.trudido.app 2 | 3 | import android.content.Context 4 | import android.util.Log 5 | 6 | /** Scans persisted scheduled notifications and reconciles any that are past-due. */ 7 | object MissedReminderCatchUp { 8 | private const val GRACE_MS = 30 * 60 * 1000L // 30 minutes show-now grace 9 | private const val MAX_STALE_MS = 12 * 60 * 60 * 1000L // drop if older than 12h 10 | 11 | fun run(context: Context) { 12 | val now = System.currentTimeMillis() 13 | val items = ScheduledNotificationsStore.all(context) 14 | var shown = 0 15 | var dropped = 0 16 | for (item in items) { 17 | val delta = item.triggerTime - now 18 | if (delta <= 0) { 19 | val age = now - item.triggerTime 20 | if (age <= GRACE_MS) { 21 | NotificationScheduler.showNow(context, item.taskId, item.title, item.body) 22 | ScheduledNotificationsStore.remove(context, item.taskId) 23 | shown++ 24 | } else if (age > MAX_STALE_MS) { 25 | ScheduledNotificationsStore.remove(context, item.taskId) 26 | dropped++ 27 | } else { 28 | // Keep for potential manual review (still future catch-up logic); no action 29 | } 30 | } 31 | } 32 | if (shown > 0 || dropped > 0) { 33 | Log.i("MissedReminderCatchUp", "shown=$shown dropped=$dropped total=${items.size}") 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/trudido/app/PendingActionStore.kt: -------------------------------------------------------------------------------- 1 | package com.trudido.app 2 | 3 | import android.content.Context 4 | import org.json.JSONArray 5 | import org.json.JSONObject 6 | import android.util.Log 7 | 8 | object PendingActionStore { 9 | private const val PREFS = "notification_actions" 10 | private const val KEY = "pending" 11 | fun addAction(context: Context, data: Map) { 12 | val prefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE) 13 | val existing = prefs.getString(KEY, null) 14 | val arr = if (existing != null) JSONArray(existing) else JSONArray() 15 | val obj = JSONObject() 16 | data.forEach { (k, v) -> obj.put(k, v) } 17 | arr.put(obj) 18 | prefs.edit().putString(KEY, arr.toString()).apply() 19 | Log.d("PendingActionStore", "Added action ${data["type"]} taskId=${data["taskId"]} newSize=${arr.length()}") 20 | } 21 | fun getPendingActions(context: Context): List> { 22 | val prefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE) 23 | val existing = prefs.getString(KEY, null) ?: return emptyList() 24 | val arr = JSONArray(existing) 25 | Log.d("PendingActionStore", "getPendingActions size=${arr.length()}") 26 | return (0 until arr.length()).map { i -> 27 | val obj = arr.getJSONObject(i) 28 | obj.keys().asSequence().associateWith { k -> obj.get(k) } 29 | } 30 | } 31 | fun clear(context: Context) { context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit().remove(KEY).apply() } 32 | } 33 | -------------------------------------------------------------------------------- /lib/providers/alarm_settings_providers.dart: -------------------------------------------------------------------------------- 1 | // Trudido - A privacy-focused todo and notes app 2 | // Copyright (C) 2025 Dominik Müller 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with this program. If not, see . 16 | 17 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 18 | import '../widgets/alarm_settings_watcher.dart'; 19 | 20 | /// Provides a singleton AlarmSettingsWatcher that starts automatically. 21 | final alarmSettingsWatcherProvider = 22 | ChangeNotifierProvider((ref) { 23 | final watcher = AlarmSettingsWatcher(); 24 | watcher.start(); 25 | ref.onDispose(() => watcher.disposeWatcher()); 26 | return watcher; 27 | }); 28 | 29 | /// Derived providers for convenience. 30 | final canExactAlarmsProvider = Provider( 31 | (ref) => ref.watch(alarmSettingsWatcherProvider).canExact, 32 | ); 33 | final ignoringBatteryOptimizationsProvider = Provider( 34 | (ref) => ref.watch(alarmSettingsWatcherProvider).ignoringBattery, 35 | ); 36 | -------------------------------------------------------------------------------- /lib/services/exact_alarm_permission.dart: -------------------------------------------------------------------------------- 1 | // Trudido - A privacy-focused todo and notes app 2 | // Copyright (C) 2025 Dominik Müller 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with this program. If not, see . 16 | 17 | /// (Legacy stub) This file is retained only to avoid import errors during migration. 18 | /// Use SystemSettingsService + AlarmSettingsWatcher + dialog helpers instead. 19 | @Deprecated( 20 | 'Replaced by SystemSettingsService + AlarmSettingsWatcher. Will be removed after v1.1.0.', 21 | ) 22 | class ExactAlarmPermissionService { 23 | ExactAlarmPermissionService._(); 24 | static final instance = ExactAlarmPermissionService._(); 25 | Never _deprecated() => throw UnimplementedError( 26 | 'ExactAlarmPermissionService removed. Use SystemSettingsService.', 27 | ); 28 | Future canScheduleExactAlarms() async => _deprecated(); 29 | Future openSettings() async => _deprecated(); 30 | bool get hasAcknowledged => false; 31 | Future setAcknowledged() async => _deprecated(); 32 | } 33 | -------------------------------------------------------------------------------- /lib/services/lifecycle_sync_observer.dart: -------------------------------------------------------------------------------- 1 | // Trudido - A privacy-focused todo and notes app 2 | // Copyright (C) 2025 Dominik Müller 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with this program. If not, see . 16 | 17 | import 'package:flutter/widgets.dart'; 18 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 19 | import 'notification_action_sync.dart'; 20 | 21 | /// Observes app lifecycle to trigger native pending action sync when the app 22 | /// returns to foreground (resumed). Ensures no persisted native action is lost. 23 | class LifecycleSyncObserver with WidgetsBindingObserver { 24 | final ProviderContainer container; 25 | LifecycleSyncObserver(this.container); 26 | 27 | void start() { 28 | WidgetsBinding.instance.addObserver(this); 29 | } 30 | 31 | void dispose() { 32 | WidgetsBinding.instance.removeObserver(this); 33 | } 34 | 35 | @override 36 | void didChangeAppLifecycleState(AppLifecycleState state) { 37 | if (state == AppLifecycleState.resumed) { 38 | NotificationActionSync.instance.syncPending(container); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/todoapp/todoflutter/ExactAlarmPermissionHelper.kt: -------------------------------------------------------------------------------- 1 | package com.todoapp.todoflutter 2 | 3 | import android.app.AlarmManager 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.net.Uri 7 | import android.os.Build 8 | import android.provider.Settings 9 | 10 | /** 11 | * Requests the exact alarm permission (Android 12+/API 31+) one time on first launch. 12 | * This launches the system settings panel similar to notification permission prompting. 13 | * The result is not directly delivered; the app should attempt to schedule alarms regardless 14 | * and rely on [AlarmManager.canScheduleExactAlarms] checks. 15 | */ 16 | object ExactAlarmPermissionHelper { 17 | private const val PREFS = "exact_alarm_permission" 18 | private const val KEY_PROMPTED = "prompted_v1" 19 | 20 | fun maybePrompt(context: Context) { 21 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return 22 | val am = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager 23 | if (am.canScheduleExactAlarms()) return // already allowed 24 | 25 | val prefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE) 26 | if (prefs.getBoolean(KEY_PROMPTED, false)) return // already prompted once 27 | 28 | prefs.edit().putBoolean(KEY_PROMPTED, true).apply() 29 | val intent = Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM).apply { 30 | data = Uri.parse("package:" + context.packageName) 31 | addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 32 | } 33 | try { 34 | context.startActivity(intent) 35 | } catch (_: Exception) { 36 | // Some OEMs may block the intent; ignore gracefully. 37 | } 38 | } 39 | // (removed legacy content) 40 | } 41 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/trudido/app/DeferredReminderWork.kt: -------------------------------------------------------------------------------- 1 | package com.trudido.app 2 | 3 | import android.content.Context 4 | import android.util.Log 5 | import androidx.work.Data 6 | import androidx.work.ExistingWorkPolicy 7 | import androidx.work.OneTimeWorkRequestBuilder 8 | import androidx.work.WorkManager 9 | import java.util.concurrent.TimeUnit 10 | 11 | /** Schedules WorkManager checkpoint for far-future reminders so we avoid holding long-lived exact alarms. */ 12 | object DeferredReminderWork { 13 | private const val UNIQUE_PREFIX = "deferred_reminder_" 14 | 15 | fun enqueue(context: Context, taskId: String, title: String, body: String, triggerAt: Long, delayMs: Long) { 16 | val wm = WorkManager.getInstance(context) 17 | val data = Data.Builder() 18 | .putString(DeferredReminderWorker.KEY_TASK_ID, taskId) 19 | .putString(DeferredReminderWorker.KEY_TITLE, title) 20 | .putString(DeferredReminderWorker.KEY_BODY, body) 21 | .putLong(DeferredReminderWorker.KEY_TRIGGER_AT, triggerAt) 22 | .build() 23 | val req = OneTimeWorkRequestBuilder() 24 | .setInitialDelay(delayMs, TimeUnit.MILLISECONDS) 25 | .setInputData(data) 26 | .addTag(uniqueTag(taskId)) 27 | .build() 28 | wm.enqueueUniqueWork(uniqueName(taskId), ExistingWorkPolicy.REPLACE, req) 29 | Log.d("DeferredReminderWork", "Enqueued taskId=$taskId delayMs=$delayMs triggerAt=$triggerAt") 30 | } 31 | 32 | fun cancel(context: Context, taskId: String) { 33 | WorkManager.getInstance(context).cancelUniqueWork(uniqueName(taskId)) 34 | } 35 | 36 | private fun uniqueName(taskId: String) = UNIQUE_PREFIX + taskId 37 | private fun uniqueTag(taskId: String) = UNIQUE_PREFIX + taskId 38 | } 39 | -------------------------------------------------------------------------------- /lib/utils/formatters.dart: -------------------------------------------------------------------------------- 1 | // Trudido - A privacy-focused todo and notes app 2 | // Copyright (C) 2025 Dominik Müller 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with this program. If not, see . 16 | 17 | // Formats an integer minutes offset into a human readable string. 18 | String formatMinutesReadable(int minutes) { 19 | String result; 20 | if (minutes == 0) { 21 | result = 'At time of due date'; 22 | } else if (minutes < 60) { 23 | result = '$minutes ${minutes == 1 ? 'minute' : 'minutes'} before'; 24 | } else if (minutes < 1440) { 25 | final hours = minutes ~/ 60; 26 | final remMinutes = minutes % 60; 27 | if (remMinutes == 0) { 28 | result = '$hours ${hours == 1 ? 'hour' : 'hours'} before'; 29 | } else { 30 | result = 31 | '$hours ${hours == 1 ? 'hour' : 'hours'} $remMinutes ${remMinutes == 1 ? 'minute' : 'minutes'} before'; 32 | } 33 | } else { 34 | final days = minutes ~/ 1440; 35 | final remHours = (minutes % 1440) ~/ 60; 36 | if (remHours == 0) { 37 | result = '$days ${days == 1 ? 'day' : 'days'} before'; 38 | } else { 39 | result = 40 | '$days ${days == 1 ? 'day' : 'days'} $remHours ${remHours == 1 ? 'hour' : 'hours'} before'; 41 | } 42 | } 43 | 44 | return result; 45 | } 46 | -------------------------------------------------------------------------------- /fdroid/com.trudido.app.yml: -------------------------------------------------------------------------------- 1 | Categories: 2 | - Note 3 | - Task 4 | License: GPL-3.0-or-later 5 | AuthorName: Dominik Müller 6 | AuthorWebSite: https://github.com/dominikmuellr 7 | SourceCode: https://github.com/dominikmuellr/trudido 8 | IssueTracker: https://github.com/dominikmuellr/trudido/issues 9 | 10 | Name: Trudido 11 | AutoName: trudido 12 | Summary: Privacy-focused todo and notes app 13 | Description: | 14 | Trudido is a privacy-focused todo and notes app with rich features. 15 | 16 | Features: 17 | * Task management with categories, priorities, and due dates 18 | * Rich text notes with markdown support 19 | * Encrypted vault for private notes (biometric unlock) 20 | * Calendar integration (DAVx5/Android Calendar sync) 21 | * Audio recordings, images, and video attachments 22 | * PDF export 23 | * Material You dynamic theming 24 | * Fully offline - no internet required 25 | * No ads, no tracking 26 | 27 | RepoType: git 28 | Repo: https://github.com/dominikmuellr/trudido.git 29 | 30 | Builds: 31 | - versionName: 1.2.2 32 | versionCode: 13 33 | commit: v1.2.2 34 | output: build/app/outputs/flutter-apk/app-arm64-v8a-release.apk 35 | srclibs: 36 | - flutter@stable 37 | rm: 38 | - ios 39 | - web 40 | prebuild: 41 | - export flutterVersion=$(sed -n -E 's/^\s*flutter:\s*"?([0-9.]+)"?/\1/p' pubspec.yaml) 42 | - "[[ $flutterVersion ]] || exit 1" 43 | - git -C $$flutter$$ checkout -f $flutterVersion 44 | - export PUB_CACHE=$(pwd)/.pub-cache 45 | - $$flutter$$/bin/flutter config --no-analytics 46 | - $$flutter$$/bin/flutter pub get 47 | scandelete: 48 | - .pub-cache 49 | build: 50 | - export PUB_CACHE=$(pwd)/.pub-cache 51 | - $$flutter$$/bin/flutter build apk --release --split-per-abi --target-platform="android-arm64" 52 | 53 | AutoUpdateMode: Version 54 | UpdateCheckMode: Tags 55 | UpdateCheckData: pubspec.yaml|version:\s.+\+(\d+)|.|version:\s(.+)\+ 56 | CurrentVersion: 1.2.3 57 | CurrentVersionCode: 14 58 | -------------------------------------------------------------------------------- /lib/utils/responsive_size.dart: -------------------------------------------------------------------------------- 1 | // Trudido - A privacy-focused todo and notes app 2 | // Copyright (C) 2025 Dominik Müller 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with this program. If not, see . 16 | 17 | import 'package:flutter/material.dart'; 18 | 19 | /// Scales a size value based on the current text scale factor 20 | double scaledSize(BuildContext context, double baseSize) { 21 | final textScale = MediaQuery.textScaleFactorOf(context); 22 | return baseSize * textScale; 23 | } 24 | 25 | /// A wrapper around Icon that automatically scales based on text scale factor 26 | /// Use this instead of Icon() for icons that should scale with font size 27 | class ScaledIcon extends StatelessWidget { 28 | final IconData icon; 29 | final double? size; 30 | final Color? color; 31 | final String? semanticLabel; 32 | final TextDirection? textDirection; 33 | 34 | const ScaledIcon( 35 | this.icon, { 36 | super.key, 37 | this.size, 38 | this.color, 39 | this.semanticLabel, 40 | this.textDirection, 41 | }); 42 | 43 | @override 44 | Widget build(BuildContext context) { 45 | final baseSize = size ?? 24.0; 46 | final effectiveSize = scaledSize(context, baseSize); 47 | 48 | return Icon( 49 | icon, 50 | size: effectiveSize, 51 | color: color, 52 | semanticLabel: semanticLabel, 53 | textDirection: textDirection, 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/services/app_refresh_service.dart: -------------------------------------------------------------------------------- 1 | // Trudido - A privacy-focused todo and notes app 2 | // Copyright (C) 2025 Dominik Müller 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with this program. If not, see . 16 | 17 | import 'package:flutter/foundation.dart'; 18 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 19 | import '../providers/app_providers.dart'; 20 | 21 | class AppRefreshService { 22 | static const AppRefreshService instance = AppRefreshService._(); 23 | const AppRefreshService._(); 24 | 25 | /// Refreshes all app data providers after import or major data changes 26 | Future refreshAllProviders(WidgetRef ref) async { 27 | try { 28 | debugPrint('[AppRefreshService] Starting provider refresh...'); 29 | 30 | // Refresh tasks 31 | final tasksNotifier = ref.read(tasksProvider.notifier); 32 | await tasksNotifier.refresh(); 33 | debugPrint('[AppRefreshService] Tasks refreshed'); 34 | 35 | // Refresh preferences state 36 | ref.invalidate(preferencesStateProvider); 37 | debugPrint('[AppRefreshService] Preferences state invalidated'); 38 | 39 | debugPrint('[AppRefreshService] All providers refreshed successfully'); 40 | } catch (e, stackTrace) { 41 | debugPrint('[AppRefreshService] Error refreshing providers: $e'); 42 | debugPrint('[AppRefreshService] Stack trace: $stackTrace'); 43 | rethrow; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/repositories/folder_repository.dart: -------------------------------------------------------------------------------- 1 | // Trudido - A privacy-focused todo and notes app 2 | // Copyright (C) 2025 Dominik Müller 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with this program. If not, see . 16 | 17 | import '../models/folder.dart'; 18 | 19 | /// Abstract repository interface for folder operations 20 | /// This defines the contract that concrete implementations must follow 21 | abstract class FolderRepository { 22 | /// Get all folders 23 | Future> getAllFolders(); 24 | 25 | /// Get folder by ID 26 | Future getFolderById(String id); 27 | 28 | /// Create a new folder 29 | Future createFolder(Folder folder); 30 | 31 | /// Update an existing folder 32 | Future updateFolder(Folder folder); 33 | 34 | /// Delete a folder by ID 35 | Future deleteFolder(String id); 36 | 37 | /// Get folders sorted by custom order 38 | Future> getFoldersSorted(); 39 | 40 | /// Update folder sort order 41 | Future updateFolderOrder(List folderIds); 42 | 43 | /// Get default folders 44 | Future> getDefaultFolders(); 45 | 46 | /// Check if folder name already exists 47 | Future folderNameExists(String name, {String? excludeId}); 48 | 49 | /// Get folder with task count 50 | Future> getFolderTaskCounts(); 51 | 52 | /// Search folders by name 53 | Future> searchFolders(String query); 54 | } 55 | -------------------------------------------------------------------------------- /lib/providers/clock.dart: -------------------------------------------------------------------------------- 1 | // Trudido - A privacy-focused todo and notes app 2 | // Copyright (C) 2025 Dominik Müller 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with this program. If not, see . 16 | 17 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 18 | 19 | /// Clock abstraction for testable time-dependent code. 20 | /// 21 | /// Use this instead of calling DateTime.now() directly to make time-based 22 | /// logic deterministic and testable. In production, uses SystemClock; in 23 | /// tests, override clockProvider with FixedClock or a custom implementation. 24 | abstract class Clock { 25 | DateTime now(); 26 | } 27 | 28 | /// Production clock that returns the actual current time. 29 | class SystemClock implements Clock { 30 | const SystemClock(); 31 | 32 | @override 33 | DateTime now() => DateTime.now(); 34 | } 35 | 36 | /// Fixed clock for tests that always returns the same time. 37 | class FixedClock implements Clock { 38 | final DateTime _now; 39 | 40 | const FixedClock(this._now); 41 | 42 | @override 43 | DateTime now() => _now; 44 | } 45 | 46 | /// Global clock provider. Override in tests to control time. 47 | /// 48 | /// Example usage in production code: 49 | /// ```dart 50 | /// final now = ref.watch(clockProvider).now(); 51 | /// ``` 52 | /// 53 | /// Example override in tests: 54 | /// ```dart 55 | /// final container = ProviderContainer(overrides: [ 56 | /// clockProvider.overrideWithValue(FixedClock(DateTime(2025, 10, 28))), 57 | /// ]); 58 | /// ``` 59 | final clockProvider = Provider((ref) => const SystemClock()); 60 | -------------------------------------------------------------------------------- /lib/widgets/search_bar.dart: -------------------------------------------------------------------------------- 1 | // Trudido - A privacy-focused todo and notes app 2 | // Copyright (C) 2025 Dominik Müller 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with this program. If not, see . 16 | 17 | import 'package:flutter/material.dart'; 18 | 19 | class TodoSearchBar extends StatefulWidget { 20 | final String searchQuery; 21 | final Function(String) onSearchChanged; 22 | 23 | const TodoSearchBar({ 24 | super.key, 25 | required this.searchQuery, 26 | required this.onSearchChanged, 27 | }); 28 | 29 | @override 30 | State createState() => _TodoSearchBarState(); 31 | } 32 | 33 | class _TodoSearchBarState extends State { 34 | late TextEditingController _controller; 35 | 36 | @override 37 | void initState() { 38 | super.initState(); 39 | _controller = TextEditingController(text: widget.searchQuery); 40 | } 41 | 42 | @override 43 | void dispose() { 44 | _controller.dispose(); 45 | super.dispose(); 46 | } 47 | 48 | @override 49 | Widget build(BuildContext context) { 50 | return TextField( 51 | controller: _controller, 52 | decoration: InputDecoration( 53 | hintText: 'Search todos...', 54 | prefixIcon: Icon(Icons.search), 55 | suffixIcon: widget.searchQuery.isNotEmpty 56 | ? IconButton( 57 | icon: Icon(Icons.close), 58 | onPressed: () { 59 | _controller.clear(); 60 | widget.onSearchChanged(''); 61 | }, 62 | ) 63 | : null, 64 | ), 65 | onChanged: widget.onSearchChanged, 66 | ); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /android/app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.application") 3 | id("kotlin-android") 4 | // Flutter plugin must be applied last 5 | id("dev.flutter.flutter-gradle-plugin") 6 | } 7 | 8 | android { 9 | namespace = "com.trudido.app" 10 | compileSdk = flutter.compileSdkVersion 11 | ndkVersion = "27.0.12077973" 12 | 13 | compileOptions { 14 | sourceCompatibility = JavaVersion.VERSION_11 15 | targetCompatibility = JavaVersion.VERSION_11 16 | isCoreLibraryDesugaringEnabled = true 17 | } 18 | 19 | kotlinOptions { 20 | jvmTarget = JavaVersion.VERSION_11.toString() 21 | } 22 | 23 | defaultConfig { 24 | applicationId = "com.trudido.app" 25 | minSdk = 24 // Required for video_player and other media features 26 | targetSdk = flutter.targetSdkVersion 27 | versionCode = flutter.versionCode 28 | versionName = flutter.versionName 29 | } 30 | 31 | signingConfigs { 32 | create("release") { 33 | val home = System.getenv("HOME") ?: System.getenv("USERPROFILE") ?: "" 34 | storeFile = file("$home/trudido-release-key.jks") 35 | storePassword = System.getenv("KEYSTORE_PASSWORD") 36 | keyAlias = System.getenv("KEY_ALIAS") 37 | keyPassword = System.getenv("KEY_PASSWORD") 38 | } 39 | } 40 | 41 | 42 | buildTypes { 43 | getByName("release") { 44 | signingConfig = signingConfigs.getByName("release") 45 | // Enable code shrinking, obfuscation, and optimization (standard for production) 46 | isMinifyEnabled = true 47 | // Remove unused resources to reduce APK size 48 | isShrinkResources = true 49 | // Apply ProGuard rules for proper minification 50 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 51 | } 52 | } 53 | 54 | dependenciesInfo { 55 | includeInApk = false 56 | includeInBundle = false 57 | } 58 | } 59 | 60 | flutter { 61 | source = "../.." 62 | } 63 | 64 | dependencies { 65 | coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4") 66 | implementation("androidx.work:work-runtime-ktx:2.9.0") 67 | implementation("com.google.guava:guava:31.1-android") 68 | } 69 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/10.txt: -------------------------------------------------------------------------------- 1 | Version 1.1.1 - Major UI & Feature Update 2 | 3 | NEW FEATURES: 4 | • Customizable greeting language in Settings → Display & Theme 5 | • Material 3 expandable FAB menu with tab-specific actions 6 | • Vault Note creation directly from FAB menu with enhanced animations 7 | • Quick drawer access - tap the same navigation tab twice to open drawer 8 | • Cycling theme mode switcher in drawer header (Light → Dark → Auto/System) 9 | • Calendar-Switcher over FAB for better reachability 10 | 11 | 12 | UI IMPROVEMENTS: 13 | • Enhanced greeting header with bold font weights (w700) for better readability 14 | • Improved subtitle font weight (w500) for better visual hierarchy 15 | • Raised FAB and calendar button position for better spacing from navigation bar (bottom: 110→130, 174→194) 16 | • Hide task completion checkbox in multi-select mode to reduce visual clutter 17 | • Cleaner greeting display - shows just greeting without ", there" when no name is set 18 | • Better Material 3 compliance in greeting presentation 19 | • Staggered pop-up animations for FAB menu items 20 | • Improved vault folder creation flow with better user guidance 21 | 22 | NAVIGATION: 23 | • Drawer opens when tapping active navigation tab for quick folder access 24 | • Theme mode cycling: Light → Dark → System (tap icon in drawer header) 25 | • Smart vault clearing when leaving Notes tab for better security 26 | • Contextual folder lists in drawer based on current tab (Tasks/Notes) 27 | 28 | TECHNICAL IMPROVEMENTS: 29 | • New GreetingService for centralized multilingual greeting management 30 | • Greeting language preference now stored properly and syncs across app 31 | • Changed greeting provider from StateProvider to Provider for automatic updates 32 | • Greeting updates immediately when language is changed in settings 33 | • Preferences model simplified: removed randomGreetingsEnabled, renamed fixedGreetingLanguage to greetingLanguage 34 | • Updated preferences service with device locale detection via PlatformDispatcher 35 | • Enhanced animation controllers for FAB menu stagger effects 36 | 37 | FIXES: 38 | • Greeting language now updates immediately when changed in settings 39 | • Better visual hierarchy in app bar greetings 40 | • Fixed multi-select mode checkbox overlap issue 41 | • Improved Material 3 FAB menu animations 42 | • Better vault folder password setup flow 43 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/trudido/app/NotificationActionReceiver.kt: -------------------------------------------------------------------------------- 1 | package com.trudido.app 2 | 3 | import android.content.BroadcastReceiver 4 | import android.content.Context 5 | import android.content.Intent 6 | import androidx.core.app.NotificationManagerCompat 7 | import android.util.Log 8 | 9 | class NotificationActionReceiver : BroadcastReceiver() { 10 | private val SNOOZE_MINUTES = 10 11 | private val ACTION_COMPLETE = "com.trudido.app.ACTION_COMPLETE" 12 | private val ACTION_SNOOZE = "com.trudido.app.ACTION_SNOOZE" 13 | override fun onReceive(context: Context, intent: Intent) { 14 | val taskId = intent.getStringExtra("taskId") ?: return 15 | val action = intent.action 16 | Log.d("NotifActionReceiver", "onReceive action=$action taskId=$taskId processAlive=${MainActivity.methodChannel != null}") 17 | when (action) { 18 | ACTION_COMPLETE -> { 19 | if (!TaskStatusStore.isCompleted(context, taskId)) { 20 | TaskStatusStore.markCompleted(context, taskId) 21 | Log.d("NotifActionReceiver", "Marked completed + persisting pending action for $taskId") 22 | PendingActionStore.addAction(context, mapOf("type" to "taskCompleted", "taskId" to taskId)) 23 | } 24 | NotificationManagerCompat.from(context).cancel(taskId.hashCode()) 25 | MainActivity.methodChannel?.invokeMethod("notificationAction", mapOf("type" to "taskCompleted", "taskId" to taskId)) 26 | } 27 | ACTION_SNOOZE -> { 28 | NotificationManagerCompat.from(context).cancel(taskId.hashCode()) 29 | val newTime = System.currentTimeMillis() + SNOOZE_MINUTES * 60_000 30 | val requestCode = (taskId + "_snooze_" + newTime).hashCode() 31 | NotificationScheduler.scheduleExact(context, taskId, "Task Reminder", "Reminder after snooze", newTime, requestCode) 32 | Log.d("NotifActionReceiver", "Snoozed $taskId newTime=$newTime persisting action") 33 | PendingActionStore.addAction(context, mapOf("type" to "taskSnoozed", "taskId" to taskId, "newTime" to newTime)) 34 | MainActivity.methodChannel?.invokeMethod("notificationAction", mapOf("type" to "taskSnoozed", "taskId" to taskId, "newTime" to newTime)) 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build Signed APKs 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout repository 14 | uses: actions/checkout@v4 15 | 16 | - name: Install Flutter 17 | uses: subosito/flutter-action@v2 18 | with: 19 | flutter-version: "3.35.7" 20 | 21 | - name: Restore keystore 22 | run: | 23 | echo "$KEYSTORE_BASE64" | base64 --decode > $HOME/trudido-release-key.jks 24 | echo "TRUDIDO_KEYSTORE=$HOME/trudido-release-key.jks" >> $GITHUB_ENV 25 | env: 26 | KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }} 27 | 28 | - name: Get dependencies 29 | run: flutter pub get 30 | 31 | - name: Extract version 32 | id: version 33 | run: | 34 | VERSION=$(grep 'version:' pubspec.yaml | awk '{print $2}' | cut -d+ -f1) 35 | echo "VERSION=$VERSION" >> $GITHUB_ENV 36 | 37 | - name: Build universal signed APK 38 | env: 39 | KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} 40 | KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} 41 | KEY_ALIAS: ${{ secrets.KEY_ALIAS }} 42 | run: flutter build apk --release --obfuscate --split-debug-info=build/app/outputs/symbols 43 | 44 | - name: Rename universal APK with version 45 | run: | 46 | mv build/app/outputs/flutter-apk/app-release.apk build/app/outputs/flutter-apk/app-release-$VERSION.apk 47 | env: 48 | VERSION: ${{ env.VERSION }} 49 | 50 | - name: Build ARM64 signed APK 51 | env: 52 | KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} 53 | KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} 54 | KEY_ALIAS: ${{ secrets.KEY_ALIAS }} 55 | run: flutter build apk --release --target-platform=android-arm64 --obfuscate --split-debug-info=build/app/outputs/symbols-arm64 56 | 57 | - name: Rename ARM64 APK with version 58 | run: | 59 | mv build/app/outputs/flutter-apk/app-release.apk build/app/outputs/flutter-apk/app-release-arm64-$VERSION.apk 60 | env: 61 | VERSION: ${{ env.VERSION }} 62 | 63 | - name: Upload APK artifacts 64 | uses: actions/upload-artifact@v4 65 | with: 66 | name: signed-apks-$VERSION 67 | path: build/app/outputs/flutter-apk/app-release*.apk 68 | -------------------------------------------------------------------------------- /lib/repositories/folder_template_repository.dart: -------------------------------------------------------------------------------- 1 | // Trudido - A privacy-focused todo and notes app 2 | // Copyright (C) 2025 Dominik Müller 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with this program. If not, see . 16 | 17 | import '../models/folder_template.dart'; 18 | 19 | /// Abstract repository interface for folder template operations 20 | abstract class FolderTemplateRepository { 21 | /// Get all templates (built-in + custom) 22 | Future> getAllTemplates(); 23 | 24 | /// Get template by ID 25 | Future getTemplateById(String id); 26 | 27 | /// Create a new template 28 | Future createTemplate(FolderTemplate template); 29 | 30 | /// Update an existing template 31 | Future updateTemplate(FolderTemplate template); 32 | 33 | /// Delete a template by ID (only custom templates) 34 | Future deleteTemplate(String id); 35 | 36 | /// Get built-in templates only 37 | Future> getBuiltInTemplates(); 38 | 39 | /// Get user-created templates only 40 | Future> getCustomTemplates(); 41 | 42 | /// Search templates by name or keywords 43 | Future> searchTemplates(String query); 44 | 45 | /// Suggest templates based on folder name 46 | Future> suggestTemplatesForFolder(String folderName); 47 | 48 | /// Create template from existing folder 49 | Future createTemplateFromFolder( 50 | String folderId, 51 | String templateName, 52 | ); 53 | 54 | /// Track template usage 55 | Future incrementTemplateUsage(String templateId); 56 | 57 | /// Get most used templates 58 | Future> getMostUsedTemplates(int limit); 59 | 60 | /// Reset built-in template to original (if customized) 61 | Future resetBuiltInTemplate(String templateId); 62 | } 63 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/trudido/app/DeferredReminderWorker.kt: -------------------------------------------------------------------------------- 1 | package com.trudido.app 2 | 3 | import android.content.Context 4 | import android.util.Log 5 | import androidx.work.CoroutineWorker 6 | import androidx.work.WorkerParameters 7 | 8 | /** 9 | * Worker invoked for non time-critical reminders that were scheduled >24h out. 10 | * It re-evaluates how far in the future the target time still is: 11 | * - If now within 24h horizon -> hands off to AlarmManager exact/inexact path. 12 | * - If still far (>24h due to device being off or reschedule drift) -> re-enqueue itself. 13 | */ 14 | class DeferredReminderWorker( 15 | appContext: Context, 16 | params: WorkerParameters 17 | ) : CoroutineWorker(appContext, params) { 18 | 19 | override suspend fun doWork(): Result { 20 | val taskId = inputData.getString(KEY_TASK_ID) ?: return Result.failure() 21 | val title = inputData.getString(KEY_TITLE) ?: "Task Reminder" 22 | val body = inputData.getString(KEY_BODY) ?: "" 23 | val triggerAt = inputData.getLong(KEY_TRIGGER_AT, -1L) 24 | if (triggerAt <= 0) return Result.failure() 25 | val now = System.currentTimeMillis() 26 | val remaining = triggerAt - now 27 | Log.d(TAG, "Worker run taskId=$taskId remainingMs=$remaining") 28 | if (remaining <= 0) { 29 | // Time passed while deferred – show immediately 30 | NotificationScheduler.showNow(applicationContext, taskId, title, body) 31 | ScheduledNotificationsStore.remove(applicationContext, taskId) 32 | return Result.success() 33 | } 34 | val DAY_MS = 24 * 60 * 60 * 1000L 35 | if (remaining <= DAY_MS) { 36 | // Move to regular alarm scheduling path 37 | val requestCode = taskId.hashCode() 38 | NotificationScheduler.scheduleExact(applicationContext, taskId, title, body, triggerAt, requestCode) 39 | return Result.success() 40 | } 41 | // Still far out; re-enqueue self for another checkpoint just before next 24h boundary. 42 | val delay = (remaining - DAY_MS).coerceAtLeast(DAY_MS / 2) // wake up midway if extremely far 43 | DeferredReminderWork.enqueue(applicationContext, taskId, title, body, triggerAt, delay) 44 | return Result.success() 45 | } 46 | 47 | companion object { 48 | const val TAG = "DeferredReminderWorker" 49 | const val KEY_TASK_ID = "taskId" 50 | const val KEY_TITLE = "title" 51 | const val KEY_BODY = "body" 52 | const val KEY_TRIGGER_AT = "triggerAt" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/theme/solarized_colors.dart: -------------------------------------------------------------------------------- 1 | // Trudido - A privacy-focused todo and notes app 2 | // Copyright (C) 2025 Dominik Müller 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with this program. If not, see . 16 | 17 | import 'package:flutter/material.dart'; 18 | 19 | /// Solarized Color Palette 20 | /// Based on https://github.com/altercation/solarized 21 | /// 22 | /// Solarized is a sixteen color palette (eight monotones, eight accent colors) 23 | /// designed for use with terminal and gui applications. 24 | class SolarizedColors { 25 | // Base (Monotone) Colors 26 | // Light theme uses base3 as background, base00 as primary text 27 | // Dark theme uses base03 as background, base0 as primary text 28 | 29 | static const Color base03 = Color(0xFF002B36); // darkest - dark background 30 | static const Color base02 = Color(0xFF073642); // dark background highlights 31 | static const Color base01 = Color(0xFF586E75); // dark content / comments 32 | static const Color base00 = Color(0xFF657B83); // light emphasized content 33 | static const Color base0 = Color(0xFF839496); // dark emphasized content 34 | static const Color base1 = Color(0xFF93A1A1); // light content / comments 35 | static const Color base2 = Color(0xFFEEE8D5); // light background highlights 36 | static const Color base3 = Color(0xFFFDF6E3); // lightest - light background 37 | 38 | // Accent Colors 39 | static const Color yellow = Color(0xFFB58900); 40 | static const Color orange = Color(0xFFCB4B16); 41 | static const Color red = Color(0xFFDC322F); 42 | static const Color magenta = Color(0xFFD33682); 43 | static const Color violet = Color(0xFF6C71C4); 44 | static const Color blue = Color(0xFF268BD2); 45 | static const Color cyan = Color(0xFF2AA198); 46 | static const Color green = Color(0xFF859900); 47 | 48 | /// Helper to convert hex string at runtime if needed 49 | static Color fromHex(String hex) { 50 | final cleaned = hex.replaceFirst('#', ''); 51 | return Color(int.parse('0xFF$cleaned')); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lib/widgets/battery_optimization_nudge.dart: -------------------------------------------------------------------------------- 1 | // Trudido - A privacy-focused todo and notes app 2 | // Copyright (C) 2025 Dominik Müller 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with this program. If not, see . 16 | 17 | import 'package:flutter/material.dart'; 18 | import '../services/late_alarm_nudge_service.dart'; 19 | import '../services/system_settings_service.dart'; 20 | 21 | /// Periodically checks if native layer flagged repeated late alarms and shows a gentle prompt. 22 | class BatteryOptimizationNudge extends StatefulWidget { 23 | final Widget child; 24 | const BatteryOptimizationNudge({super.key, required this.child}); 25 | @override 26 | State createState() => 27 | _BatteryOptimizationNudgeState(); 28 | } 29 | 30 | class _BatteryOptimizationNudgeState extends State { 31 | @override 32 | void initState() { 33 | super.initState(); 34 | // Delay to avoid showing over critical first-run flows. 35 | WidgetsBinding.instance.addPostFrameCallback((_) => _check()); 36 | } 37 | 38 | Future _check() async { 39 | final needed = await LateAlarmNudgeService.instance.consumePromptIfNeeded(); 40 | if (!needed || !mounted) return; 41 | if (await SystemSettingsService.instance.isIgnoringBatteryOptimizations()) 42 | return; // Already optimized 43 | if (!mounted) return; 44 | // Show lightweight SnackBar with action. 45 | final messenger = ScaffoldMessenger.maybeOf(context); 46 | messenger?.showSnackBar( 47 | SnackBar( 48 | content: const Text( 49 | 'Reminders seem delayed. Allow unrestricted background?', 50 | ), 51 | action: SnackBarAction( 52 | label: 'Allow', 53 | onPressed: () => SystemSettingsService.instance 54 | .requestIgnoreBatteryOptimizations(), 55 | ), 56 | duration: const Duration(milliseconds: 4000), 57 | ), 58 | ); 59 | } 60 | 61 | @override 62 | Widget build(BuildContext context) => widget.child; 63 | } 64 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/trudido/app/LateAlarmTracker.kt: -------------------------------------------------------------------------------- 1 | package com.trudido.app 2 | 3 | import android.content.Context 4 | import android.util.Log 5 | 6 | /** Tracks late alarm firings to decide when to nudge user about battery optimization. */ 7 | object LateAlarmTracker { 8 | private const val PREFS = "late_alarm_tracker" 9 | private const val KEY_WINDOW_START = "window_start" 10 | private const val KEY_LATE_COUNT = "late_count" 11 | private const val KEY_PROMPT_NEEDED = "prompt_needed" 12 | private const val KEY_LAST_PROMPT = "last_prompt" 13 | 14 | private const val WINDOW_MS = 6 * 60 * 60 * 1000L // 6h rolling window 15 | private const val LATE_THRESHOLD_MS = 2 * 60 * 1000L // consider >2 min late 16 | private const val LATE_COUNT_THRESHOLD = 3 // within window 17 | private const val PROMPT_COOLDOWN_MS = 48 * 60 * 60 * 1000L // 48h 18 | 19 | private fun prefs(ctx: Context) = ctx.getSharedPreferences(PREFS, Context.MODE_PRIVATE) 20 | 21 | fun recordFire(ctx: Context, scheduledAt: Long, firedAt: Long = System.currentTimeMillis()) { 22 | if (scheduledAt <= 0L) return 23 | val lateness = firedAt - scheduledAt 24 | if (lateness < LATE_THRESHOLD_MS) return 25 | val p = prefs(ctx) 26 | val now = firedAt 27 | var windowStart = p.getLong(KEY_WINDOW_START, 0L) 28 | var count = p.getInt(KEY_LATE_COUNT, 0) 29 | if (windowStart == 0L || now - windowStart > WINDOW_MS) { 30 | windowStart = now 31 | count = 0 32 | } 33 | count += 1 34 | var promptNeeded = p.getBoolean(KEY_PROMPT_NEEDED, false) 35 | val lastPrompt = p.getLong(KEY_LAST_PROMPT, 0L) 36 | if (!promptNeeded && count >= LATE_COUNT_THRESHOLD && (lastPrompt == 0L || now - lastPrompt > PROMPT_COOLDOWN_MS)) { 37 | promptNeeded = true 38 | Log.i("LateAlarmTracker", "Triggering promptNeeded count=$count latenessMs=$lateness") 39 | } 40 | p.edit() 41 | .putLong(KEY_WINDOW_START, windowStart) 42 | .putInt(KEY_LATE_COUNT, count) 43 | .putBoolean(KEY_PROMPT_NEEDED, promptNeeded) 44 | .apply() 45 | } 46 | 47 | /** Returns true if a prompt should be shown now and consumes the flag. */ 48 | fun consumePromptIfNeeded(ctx: Context): Boolean { 49 | val p = prefs(ctx) 50 | val needed = p.getBoolean(KEY_PROMPT_NEEDED, false) 51 | if (!needed) return false 52 | p.edit() 53 | .putBoolean(KEY_PROMPT_NEEDED, false) 54 | .putLong(KEY_LAST_PROMPT, System.currentTimeMillis()) 55 | .apply() 56 | return true 57 | } 58 | } -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/trudido/app/ScheduledNotificationsStore.kt: -------------------------------------------------------------------------------- 1 | package com.trudido.app 2 | 3 | import android.content.Context 4 | import org.json.JSONArray 5 | import org.json.JSONObject 6 | 7 | /** Simple persistence layer (SharedPreferences JSON) for scheduled notifications so we can restore after reboot. */ 8 | object ScheduledNotificationsStore { 9 | private const val PREFS = "scheduled_notifications_store" 10 | private const val KEY = "items" 11 | 12 | private fun prefs(ctx: Context) = ctx.getSharedPreferences(PREFS, Context.MODE_PRIVATE) 13 | 14 | fun upsert(ctx: Context, taskId: String, title: String, body: String, triggerTime: Long) { 15 | val arr = loadArray(ctx) 16 | // Remove existing entry with same taskId 17 | val filtered = JSONArray() 18 | for (i in 0 until arr.length()) { 19 | val o = arr.getJSONObject(i) 20 | if (o.optString("taskId") != taskId) filtered.put(o) 21 | } 22 | val obj = JSONObject().apply { 23 | put("taskId", taskId) 24 | put("title", title) 25 | put("body", body) 26 | put("triggerTime", triggerTime) 27 | } 28 | filtered.put(obj) 29 | saveArray(ctx, filtered) 30 | } 31 | 32 | fun remove(ctx: Context, taskId: String) { 33 | val arr = loadArray(ctx) 34 | val filtered = JSONArray() 35 | for (i in 0 until arr.length()) { 36 | val o = arr.getJSONObject(i) 37 | if (o.optString("taskId") != taskId) filtered.put(o) 38 | } 39 | saveArray(ctx, filtered) 40 | } 41 | 42 | fun all(ctx: Context): List { 43 | val arr = loadArray(ctx) 44 | val out = mutableListOf() 45 | for (i in 0 until arr.length()) { 46 | val o = arr.getJSONObject(i) 47 | out.add( 48 | ScheduledItem( 49 | o.optString("taskId"), 50 | o.optString("title"), 51 | o.optString("body"), 52 | o.optLong("triggerTime") 53 | ) 54 | ) 55 | } 56 | return out 57 | } 58 | 59 | private fun loadArray(ctx: Context): JSONArray { 60 | val raw = prefs(ctx).getString(KEY, null) ?: return JSONArray() 61 | return try { JSONArray(raw) } catch (_: Exception) { JSONArray() } 62 | } 63 | 64 | private fun saveArray(ctx: Context, arr: JSONArray) { 65 | prefs(ctx).edit().putString(KEY, arr.toString()).apply() 66 | } 67 | 68 | data class ScheduledItem(val taskId: String, val title: String, val body: String, val triggerTime: Long) 69 | } -------------------------------------------------------------------------------- /lib/models/note.g.dart: -------------------------------------------------------------------------------- 1 | // Trudido - A privacy-focused todo and notes app 2 | // Copyright (C) 2025 Dominik Müller 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with this program. If not, see . 16 | 17 | // GENERATED CODE - DO NOT MODIFY BY HAND 18 | 19 | part of 'note.dart'; 20 | 21 | // ************************************************************************** 22 | // TypeAdapterGenerator 23 | // ************************************************************************** 24 | 25 | class NoteAdapter extends TypeAdapter { 26 | @override 27 | final int typeId = 6; 28 | 29 | @override 30 | Note read(BinaryReader reader) { 31 | final numOfFields = reader.readByte(); 32 | final fields = { 33 | for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), 34 | }; 35 | return Note( 36 | id: fields[0] as String?, 37 | title: fields[1] as String, 38 | content: fields[2] as String, 39 | createdAt: fields[3] as DateTime?, 40 | updatedAt: fields[4] as DateTime?, 41 | isPinned: fields[5] == null ? false : fields[5] as bool, 42 | folderId: fields[6] as String?, 43 | todoTxtContent: fields[7] as String?, 44 | ); 45 | } 46 | 47 | @override 48 | void write(BinaryWriter writer, Note obj) { 49 | writer 50 | ..writeByte(8) 51 | ..writeByte(0) 52 | ..write(obj.id) 53 | ..writeByte(1) 54 | ..write(obj.title) 55 | ..writeByte(2) 56 | ..write(obj.content) 57 | ..writeByte(3) 58 | ..write(obj.createdAt) 59 | ..writeByte(4) 60 | ..write(obj.updatedAt) 61 | ..writeByte(5) 62 | ..write(obj.isPinned) 63 | ..writeByte(6) 64 | ..write(obj.folderId) 65 | ..writeByte(7) 66 | ..write(obj.todoTxtContent); 67 | } 68 | 69 | @override 70 | int get hashCode => typeId.hashCode; 71 | 72 | @override 73 | bool operator ==(Object other) => 74 | identical(this, other) || 75 | other is NoteAdapter && 76 | runtimeType == other.runtimeType && 77 | typeId == other.typeId; 78 | } 79 | -------------------------------------------------------------------------------- /lib/widgets/alarm_settings_watcher.dart: -------------------------------------------------------------------------------- 1 | // Trudido - A privacy-focused todo and notes app 2 | // Copyright (C) 2025 Dominik Müller 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with this program. If not, see . 16 | 17 | import 'package:flutter/widgets.dart'; 18 | import '../services/system_settings_service.dart'; 19 | 20 | /// Observes app lifecycle; on resume re-checks system settings relevant to reminders. 21 | /// Notify listeners when the state changes so UI (settings screen / indicators) can refresh. 22 | class AlarmSettingsWatcher with WidgetsBindingObserver, ChangeNotifier { 23 | bool _canExact = false; // pessimistic until first refresh 24 | bool _ignoringBattery = false; // pessimistic until first refresh 25 | bool _loaded = false; 26 | bool get canExact => _canExact; 27 | bool get ignoringBattery => _ignoringBattery; 28 | bool get loaded => _loaded; 29 | 30 | final _svc = SystemSettingsService.instance; 31 | bool _started = false; 32 | 33 | void start() { 34 | if (_started) return; 35 | _started = true; 36 | WidgetsBinding.instance.addObserver(this); 37 | // Kick off after a microtask so channel registration on cold start finishes. 38 | Future.microtask(() async { 39 | try { 40 | await SystemSettingsService.instance.ensureReady(); 41 | } catch (_) {} 42 | if (_started) refresh(); 43 | }); 44 | } 45 | 46 | void disposeWatcher() { 47 | if (!_started) return; 48 | WidgetsBinding.instance.removeObserver(this); 49 | _started = false; 50 | } 51 | 52 | @override 53 | void didChangeAppLifecycleState(AppLifecycleState state) { 54 | if (state == AppLifecycleState.resumed) { 55 | refresh(); 56 | } 57 | } 58 | 59 | Future refresh() async { 60 | final can = await _svc.canScheduleExactAlarms(); 61 | final batt = await _svc.isIgnoringBatteryOptimizations(); 62 | final changed = can != _canExact || batt != _ignoringBattery || !_loaded; 63 | _canExact = can; 64 | _ignoringBattery = batt; 65 | _loaded = true; 66 | if (changed) notifyListeners(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /lib/services/text_scale_service.dart: -------------------------------------------------------------------------------- 1 | // Trudido - A privacy-focused todo and notes app 2 | // Copyright (C) 2025 Dominik Müller 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with this program. If not, see . 16 | 17 | import 'package:flutter/foundation.dart'; 18 | import 'package:flutter/services.dart'; 19 | import 'package:shared_preferences/shared_preferences.dart'; 20 | 21 | const _kTextScaleKey = 'textScale'; 22 | const _kIgnoreSystemKey = 'ignoreSystemTextScale'; 23 | const MethodChannel _platform = MethodChannel('trudido/text_scale'); 24 | 25 | final ValueNotifier textScaleNotifier = ValueNotifier(1.0); 26 | final ValueNotifier ignoreSystemNotifier = ValueNotifier(false); 27 | 28 | Future initTextScale() async { 29 | final prefs = await SharedPreferences.getInstance(); 30 | textScaleNotifier.value = prefs.getDouble(_kTextScaleKey) ?? 1.0; 31 | ignoreSystemNotifier.value = prefs.getBool(_kIgnoreSystemKey) ?? false; 32 | } 33 | 34 | Future setTextScale(double value) async { 35 | // Clamp to avoid floating-point precision issues (Android standard range) 36 | final clamped = value.clamp(0.9, 1.3); 37 | final prefs = await SharedPreferences.getInstance(); 38 | await prefs.setDouble(_kTextScaleKey, clamped); 39 | textScaleNotifier.value = clamped; 40 | // Tell native to update widget display 41 | try { 42 | await _platform.invokeMethod('updateWidgetTextSize', { 43 | 'scale': clamped, 44 | 'ignoreSystem': ignoreSystemNotifier.value, 45 | }); 46 | } catch (e) { 47 | debugPrint('Failed to update widget text size: $e'); 48 | } 49 | } 50 | 51 | Future setIgnoreSystem(bool value) async { 52 | final prefs = await SharedPreferences.getInstance(); 53 | await prefs.setBool(_kIgnoreSystemKey, value); 54 | ignoreSystemNotifier.value = value; 55 | // Notify native widget 56 | try { 57 | await _platform.invokeMethod('updateWidgetTextSize', { 58 | 'scale': textScaleNotifier.value, 59 | 'ignoreSystem': value, 60 | }); 61 | } catch (e) { 62 | debugPrint('Failed to update widget text size: $e'); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/widgets/app_error_boundary.dart: -------------------------------------------------------------------------------- 1 | // Trudido - A privacy-focused todo and notes app 2 | // Copyright (C) 2025 Dominik Müller 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with this program. If not, see . 16 | 17 | import 'package:flutter/material.dart'; 18 | import '../models/app_error.dart'; 19 | 20 | typedef ErrorViewBuilder = 21 | Widget Function(BuildContext context, Object error, StackTrace? stackTrace); 22 | 23 | /// Lightweight error boundary capturing build errors for child subtree. 24 | class AppErrorBoundary extends StatefulWidget { 25 | final Widget child; 26 | final ErrorViewBuilder? builder; 27 | const AppErrorBoundary({super.key, required this.child, this.builder}); 28 | 29 | @override 30 | State createState() => _AppErrorBoundaryState(); 31 | } 32 | 33 | class _AppErrorBoundaryState extends State { 34 | Object? _error; 35 | StackTrace? _stack; 36 | 37 | @override 38 | Widget build(BuildContext context) { 39 | if (_error != null) { 40 | final b = widget.builder; 41 | return b != null 42 | ? b(context, _error!, _stack) 43 | : _DefaultErrorView(error: _error!, stack: _stack); 44 | } 45 | try { 46 | return widget.child; 47 | } catch (e, st) { 48 | setState(() { 49 | _error = e; 50 | _stack = st; 51 | }); 52 | return _DefaultErrorView(error: e, stack: st); 53 | } 54 | } 55 | } 56 | 57 | class _DefaultErrorView extends StatelessWidget { 58 | final Object error; 59 | final StackTrace? stack; 60 | const _DefaultErrorView({required this.error, this.stack}); 61 | 62 | @override 63 | Widget build(BuildContext context) { 64 | final isAppError = error is AppError; 65 | final msg = isAppError ? (error as AppError).message : error.toString(); 66 | return Center( 67 | child: Padding( 68 | padding: const EdgeInsets.all(24), 69 | child: Text( 70 | 'Oops: $msg', 71 | style: Theme.of( 72 | context, 73 | ).textTheme.bodyLarge?.copyWith(color: Colors.red), 74 | textAlign: TextAlign.center, 75 | ), 76 | ), 77 | ); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /lib/services/haptic_feedback_service.dart: -------------------------------------------------------------------------------- 1 | // Trudido - A privacy-focused todo and notes app 2 | // Copyright (C) 2025 Dominik Müller 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with this program. If not, see . 16 | 17 | import 'package:flutter/services.dart'; 18 | 19 | /// Haptic Feedback Service 20 | /// Provides consistent haptic feedback throughout the app following Material Design guidelines 21 | class HapticFeedbackService { 22 | /// Light impact feedback for subtle interactions 23 | /// Use for: chip selections, switch toggles, minor UI changes 24 | static Future lightImpact() async { 25 | await HapticFeedback.lightImpact(); 26 | } 27 | 28 | /// Medium impact feedback for standard interactions 29 | /// Use for: button presses, card taps, list item selections 30 | static Future mediumImpact() async { 31 | await HapticFeedback.mediumImpact(); 32 | } 33 | 34 | /// Heavy impact feedback for important interactions 35 | /// Use for: FAB presses, delete confirmations, major actions 36 | static Future heavyImpact() async { 37 | await HapticFeedback.heavyImpact(); 38 | } 39 | 40 | /// Selection feedback for picker-style interactions 41 | /// Use for: scrolling through options, date pickers, dropdowns 42 | static Future selectionClick() async { 43 | await HapticFeedback.selectionClick(); 44 | } 45 | 46 | /// Vibrate for success actions 47 | /// Use for: task completion, successful saves 48 | static Future success() async { 49 | await HapticFeedback.mediumImpact(); 50 | await Future.delayed(const Duration(milliseconds: 100)); 51 | await HapticFeedback.lightImpact(); 52 | } 53 | 54 | /// Vibrate for error actions 55 | /// Use for: failed operations, validation errors 56 | static Future error() async { 57 | await HapticFeedback.heavyImpact(); 58 | await Future.delayed(const Duration(milliseconds: 50)); 59 | await HapticFeedback.mediumImpact(); 60 | } 61 | 62 | /// Vibrate for warning actions 63 | /// Use for: important confirmations, destructive actions 64 | static Future warning() async { 65 | await HapticFeedback.mediumImpact(); 66 | await Future.delayed(const Duration(milliseconds: 80)); 67 | await HapticFeedback.mediumImpact(); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /lib/models/folder.g.dart: -------------------------------------------------------------------------------- 1 | // Trudido - A privacy-focused todo and notes app 2 | // Copyright (C) 2025 Dominik Müller 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with this program. If not, see . 16 | 17 | // GENERATED CODE - DO NOT MODIFY BY HAND 18 | 19 | part of 'folder.dart'; 20 | 21 | // ************************************************************************** 22 | // TypeAdapterGenerator 23 | // ************************************************************************** 24 | 25 | class FolderAdapter extends TypeAdapter { 26 | @override 27 | final int typeId = 2; 28 | 29 | @override 30 | Folder read(BinaryReader reader) { 31 | final numOfFields = reader.readByte(); 32 | final fields = { 33 | for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), 34 | }; 35 | return Folder( 36 | id: fields[0] as String?, 37 | name: fields[1] as String, 38 | description: fields[2] as String?, 39 | color: fields[3] as int, 40 | icon: fields[4] as String?, 41 | createdAt: fields[5] as DateTime?, 42 | updatedAt: fields[6] as DateTime?, 43 | sortOrder: fields[7] as int, 44 | isDefault: fields[8] as bool, 45 | parentId: fields[9] as String?, 46 | isVault: fields[10] == null ? false : fields[10] as bool, 47 | ); 48 | } 49 | 50 | @override 51 | void write(BinaryWriter writer, Folder obj) { 52 | writer 53 | ..writeByte(11) 54 | ..writeByte(0) 55 | ..write(obj.id) 56 | ..writeByte(1) 57 | ..write(obj.name) 58 | ..writeByte(2) 59 | ..write(obj.description) 60 | ..writeByte(3) 61 | ..write(obj.color) 62 | ..writeByte(4) 63 | ..write(obj.icon) 64 | ..writeByte(5) 65 | ..write(obj.createdAt) 66 | ..writeByte(6) 67 | ..write(obj.updatedAt) 68 | ..writeByte(7) 69 | ..write(obj.sortOrder) 70 | ..writeByte(8) 71 | ..write(obj.isDefault) 72 | ..writeByte(9) 73 | ..write(obj.parentId) 74 | ..writeByte(10) 75 | ..write(obj.isVault); 76 | } 77 | 78 | @override 79 | int get hashCode => typeId.hashCode; 80 | 81 | @override 82 | bool operator ==(Object other) => 83 | identical(this, other) || 84 | other is FolderAdapter && 85 | runtimeType == other.runtimeType && 86 | typeId == other.typeId; 87 | } 88 | -------------------------------------------------------------------------------- /lib/models/note_folder.g.dart: -------------------------------------------------------------------------------- 1 | // Trudido - A privacy-focused todo and notes app 2 | // Copyright (C) 2025 Dominik Müller 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with this program. If not, see . 16 | 17 | // GENERATED CODE - DO NOT MODIFY BY HAND 18 | 19 | part of 'note_folder.dart'; 20 | 21 | // ************************************************************************** 22 | // TypeAdapterGenerator 23 | // ************************************************************************** 24 | 25 | class NoteFolderAdapter extends TypeAdapter { 26 | @override 27 | final int typeId = 7; 28 | 29 | @override 30 | NoteFolder read(BinaryReader reader) { 31 | final numOfFields = reader.readByte(); 32 | final fields = { 33 | for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), 34 | }; 35 | return NoteFolder( 36 | id: fields[0] as String?, 37 | name: fields[1] as String, 38 | description: fields[2] as String?, 39 | createdAt: fields[3] as DateTime?, 40 | updatedAt: fields[4] as DateTime?, 41 | isVault: fields[5] == null ? false : fields[5] as bool, 42 | sortOrder: fields[6] as int, 43 | hasPassword: fields[7] == null ? false : fields[7] as bool, 44 | useBiometric: fields[8] == null ? true : fields[8] as bool, 45 | noteFormat: fields[9] == null ? 'markdown' : fields[9] as String, 46 | ); 47 | } 48 | 49 | @override 50 | void write(BinaryWriter writer, NoteFolder obj) { 51 | writer 52 | ..writeByte(10) 53 | ..writeByte(0) 54 | ..write(obj.id) 55 | ..writeByte(1) 56 | ..write(obj.name) 57 | ..writeByte(2) 58 | ..write(obj.description) 59 | ..writeByte(3) 60 | ..write(obj.createdAt) 61 | ..writeByte(4) 62 | ..write(obj.updatedAt) 63 | ..writeByte(5) 64 | ..write(obj.isVault) 65 | ..writeByte(6) 66 | ..write(obj.sortOrder) 67 | ..writeByte(7) 68 | ..write(obj.hasPassword) 69 | ..writeByte(8) 70 | ..write(obj.useBiometric) 71 | ..writeByte(9) 72 | ..write(obj.noteFormat); 73 | } 74 | 75 | @override 76 | int get hashCode => typeId.hashCode; 77 | 78 | @override 79 | bool operator ==(Object other) => 80 | identical(this, other) || 81 | other is NoteFolderAdapter && 82 | runtimeType == other.runtimeType && 83 | typeId == other.typeId; 84 | } 85 | -------------------------------------------------------------------------------- /lib/widgets/reminder_components.dart: -------------------------------------------------------------------------------- 1 | // Trudido - A privacy-focused todo and notes app 2 | // Copyright (C) 2025 Dominik Müller 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with this program. If not, see . 16 | 17 | import 'package:flutter/material.dart'; 18 | import '../utils/formatters.dart'; 19 | 20 | class ReminderChip extends StatelessWidget { 21 | final int minutes; 22 | final VoidCallback onDelete; 23 | 24 | const ReminderChip({ 25 | super.key, 26 | required this.minutes, 27 | required this.onDelete, 28 | }); 29 | 30 | @override 31 | Widget build(BuildContext context) { 32 | return ListTile( 33 | leading: const Icon(Icons.notifications_active_outlined, size: 20), 34 | title: Text(formatMinutesReadable(minutes)), 35 | trailing: IconButton( 36 | icon: const Icon(Icons.delete_outline, size: 20), 37 | onPressed: onDelete, 38 | ), 39 | ); 40 | } 41 | } 42 | 43 | class AddReminderChip extends StatelessWidget { 44 | final VoidCallback onPressed; 45 | 46 | const AddReminderChip({super.key, required this.onPressed}); 47 | 48 | @override 49 | Widget build(BuildContext context) { 50 | return Center( 51 | child: ActionChip( 52 | avatar: const Icon(Icons.add), 53 | label: const Text('Add Reminder'), 54 | onPressed: onPressed, 55 | ), 56 | ); 57 | } 58 | } 59 | 60 | class RemindersSection extends StatelessWidget { 61 | final List reminderOffsets; 62 | final Function(int) onRemoveReminder; 63 | final VoidCallback onAddReminder; 64 | 65 | const RemindersSection({ 66 | super.key, 67 | required this.reminderOffsets, 68 | required this.onRemoveReminder, 69 | required this.onAddReminder, 70 | }); 71 | 72 | @override 73 | Widget build(BuildContext context) { 74 | return Column( 75 | crossAxisAlignment: CrossAxisAlignment.start, 76 | children: [ 77 | const Divider(), 78 | Padding( 79 | padding: const EdgeInsets.symmetric(vertical: 8.0), 80 | child: Text( 81 | 'Reminders', 82 | style: Theme.of(context).textTheme.titleMedium, 83 | ), 84 | ), 85 | ...reminderOffsets.map((minutes) { 86 | return ReminderChip( 87 | minutes: minutes, 88 | onDelete: () => onRemoveReminder(minutes), 89 | ); 90 | }), 91 | AddReminderChip(onPressed: onAddReminder), 92 | ], 93 | ); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /lib/providers/app_providers.dart: -------------------------------------------------------------------------------- 1 | // Trudido - A privacy-focused todo and notes app 2 | // Copyright (C) 2025 Dominik Müller 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with this program. If not, see . 16 | 17 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 18 | import '../models/preferences_state.dart'; 19 | import '../models/todo.dart'; 20 | import '../repositories/task_repository.dart'; 21 | import '../services/preferences_service.dart'; 22 | import 'clock.dart'; 23 | 24 | /// Singleton preferences service provider. 25 | final preferencesServiceProvider = Provider( 26 | (ref) => PreferencesService(), 27 | ); 28 | 29 | /// Reactive preferences state for quick rebuilds. 30 | final preferencesStateProvider = StateProvider((ref) { 31 | final svc = ref.watch(preferencesServiceProvider); 32 | return svc.snapshot; 33 | }); 34 | 35 | /// Task repository provider (lazy load). Use [tasksProvider] for list. 36 | final taskRepositoryProvider = Provider( 37 | (ref) => TaskRepository(), 38 | ); 39 | 40 | class _TasksNotifier extends StateNotifier> { 41 | final TaskRepository repo; 42 | _TasksNotifier(this.repo) : super(const []) { 43 | _load(); 44 | } 45 | Future _load() async { 46 | await repo.load(); 47 | state = repo.tasks; 48 | } 49 | 50 | Future refresh() async { 51 | await repo.load(); 52 | state = repo.tasks; 53 | } 54 | } 55 | 56 | final tasksProvider = StateNotifierProvider<_TasksNotifier, List>((ref) { 57 | final repo = ref.watch(taskRepositoryProvider); 58 | return _TasksNotifier(repo); 59 | }); 60 | 61 | /// Convenience filtered list example (incomplete tasks only). 62 | final incompleteTasksProvider = Provider>((ref) { 63 | final all = ref.watch(tasksProvider); 64 | return all.where((t) => !t.isCompleted).toList(); 65 | }); 66 | 67 | /// Tasks active today (due today OR spanning including today). 68 | final todayActiveTasksProvider = Provider>((ref) { 69 | final all = ref.watch(tasksProvider); 70 | final today = ref.watch(clockProvider).now(); 71 | return all.where((t) => t.activeOn(today)).toList(); 72 | }); 73 | 74 | /// Guard helper turning exceptions into AsyncValue. 75 | extension AsyncGuard on Ref { 76 | Future> guardAsync(Future Function() run) async { 77 | try { 78 | final value = await run(); 79 | return AsyncValue.data(value); 80 | } catch (e, st) { 81 | return AsyncValue.error(e, st); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /lib/widgets/link_embed_builder.dart: -------------------------------------------------------------------------------- 1 | // Trudido - A privacy-focused todo and notes app 2 | // Copyright (C) 2025 Dominik Müller 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with this program. If not, see . 16 | 17 | import 'dart:convert'; 18 | import 'package:flutter/material.dart'; 19 | import 'package:flutter_quill/flutter_quill.dart' as quill; 20 | import 'package:url_launcher/url_launcher.dart'; 21 | 22 | /// Custom embed builder for rendering clickable links in Quill editor 23 | class LinkEmbedBuilder extends quill.EmbedBuilder { 24 | @override 25 | String get key => 'link'; 26 | 27 | @override 28 | Widget build(BuildContext context, quill.EmbedContext embedContext) { 29 | final node = embedContext.node; 30 | 31 | // Get data - it could be a map or a JSON string 32 | Map data; 33 | if (node.value.data is Map) { 34 | data = node.value.data as Map; 35 | } else { 36 | // Parse JSON string to map if it's a string 37 | data = jsonDecode(node.value.data as String) as Map; 38 | } 39 | 40 | final url = data['url'] as String; 41 | final text = data['text'] as String? ?? url; 42 | 43 | return InkWell( 44 | onTap: () => _openLink(context, url), 45 | child: Text( 46 | text, 47 | style: TextStyle( 48 | color: Theme.of(context).colorScheme.primary, 49 | decoration: TextDecoration.underline, 50 | decorationColor: Theme.of(context).colorScheme.primary, 51 | ), 52 | ), 53 | ); 54 | } 55 | 56 | Future _openLink(BuildContext context, String url) async { 57 | try { 58 | // Add scheme if not present 59 | String urlString = url; 60 | if (!urlString.startsWith('http://') && 61 | !urlString.startsWith('https://')) { 62 | urlString = 'https://$urlString'; 63 | } 64 | 65 | // Use url_launcher to open the link in external browser 66 | final uri = Uri.parse(urlString); 67 | 68 | // Open in external browser app (not in-app webview) 69 | final launched = await launchUrl( 70 | uri, 71 | mode: LaunchMode.externalApplication, 72 | ); 73 | 74 | if (!launched && context.mounted) { 75 | ScaffoldMessenger.of(context).showSnackBar( 76 | SnackBar(content: Text('Could not open link: $urlString')), 77 | ); 78 | } 79 | } catch (e) { 80 | print('Error opening link: $e'); 81 | if (context.mounted) { 82 | ScaffoldMessenger.of( 83 | context, 84 | ).showSnackBar(const SnackBar(content: Text('Error opening link'))); 85 | } 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /lib/utils/encryption_helper.dart: -------------------------------------------------------------------------------- 1 | // Trudido - A privacy-focused todo and notes app 2 | // Copyright (C) 2025 Dominik Müller 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with this program. If not, see . 16 | 17 | import 'package:encrypt/encrypt.dart'; 18 | import 'package:flutter_secure_storage/flutter_secure_storage.dart'; 19 | 20 | /// Helper class for AES-256 encryption/decryption of vault notes 21 | class EncryptionHelper { 22 | static const _storage = FlutterSecureStorage(); 23 | static const _keyName = 'vault_encryption_key'; 24 | static const _ivName = 'vault_encryption_iv'; 25 | 26 | /// Gets or generates the encryption key 27 | static Future _getKey() async { 28 | String? keyString = await _storage.read(key: _keyName); 29 | return Key.fromBase64(keyString!); 30 | } 31 | 32 | /// Gets or generates the initialization vector 33 | static Future _getIV() async { 34 | String? ivString = await _storage.read(key: _ivName); 35 | return IV.fromBase64(ivString!); 36 | } 37 | 38 | /// Encrypts plain text using AES-256-CBC 39 | static Future encryptText(String plainText) async { 40 | if (plainText.isEmpty) return plainText; 41 | 42 | try { 43 | final key = await _getKey(); 44 | final iv = await _getIV(); 45 | final encrypter = Encrypter(AES(key, mode: AESMode.cbc)); 46 | final encrypted = encrypter.encrypt(plainText, iv: iv); 47 | return encrypted.base64; 48 | } catch (e) { 49 | // If encryption fails, log error but don't crash 50 | print('Encryption error: $e'); 51 | rethrow; 52 | } 53 | } 54 | 55 | /// Decrypts encrypted text using AES-256-CBC 56 | static Future decryptText(String encryptedText) async { 57 | if (encryptedText.isEmpty) return encryptedText; 58 | 59 | try { 60 | final key = await _getKey(); 61 | final iv = await _getIV(); 62 | final encrypter = Encrypter(AES(key, mode: AESMode.cbc)); 63 | final decrypted = encrypter.decrypt64(encryptedText, iv: iv); 64 | return decrypted; 65 | } catch (e) { 66 | // If decryption fails, log error but don't crash 67 | print('Decryption error: $e'); 68 | rethrow; 69 | } 70 | } 71 | 72 | /// Checks if encryption is available and properly set up 73 | static Future isEncryptionAvailable() async { 74 | try { 75 | await _getKey(); 76 | await _getIV(); 77 | return true; 78 | } catch (e) { 79 | return false; 80 | } 81 | } 82 | 83 | /// Resets encryption keys (use with caution - may make vault notes unreadable) 84 | static Future resetEncryptionKeys() async { 85 | await _storage.delete(key: _keyName); 86 | await _storage.delete(key: _ivName); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/trudido/app/TaskFileHandler.kt: -------------------------------------------------------------------------------- 1 | package com.trudido.app 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.net.Uri 6 | import android.widget.Toast 7 | 8 | /** 9 | * TaskFileHandler 10 | * 11 | * Provides intent builders and file I/O helpers for import/export using SAF. 12 | * All activity result handling is done in MainActivity. 13 | */ 14 | class TaskFileHandler(private val context: Context) { 15 | companion object { 16 | const val REQUEST_CODE_EXPORT = 9001 17 | const val REQUEST_CODE_IMPORT = 9002 18 | } 19 | 20 | // Temporarily store export data passed from Flutter 21 | var pendingExportData: String? = null 22 | 23 | // Sample JSON to export. Replace with your real data when integrating. 24 | val sampleJsonForExport: String = """ 25 | { 26 | "version": 1, 27 | "exportedAt": "${System.currentTimeMillis()}", 28 | "tasks": [ 29 | { 30 | "id": "1", 31 | "title": "Buy groceries", 32 | "completed": false, 33 | "priority": "medium", 34 | "dueDate": "2025-09-05", 35 | "category": "Personal" 36 | }, 37 | { 38 | "id": "2", 39 | "title": "Prepare project report", 40 | "completed": true, 41 | "priority": "high", 42 | "dueDate": "2025-09-04", 43 | "category": "Work" 44 | } 45 | ] 46 | } 47 | """.trimIndent() 48 | 49 | fun buildExportIntent(): Intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply { 50 | addCategory(Intent.CATEGORY_OPENABLE) 51 | type = "application/json" 52 | putExtra(Intent.EXTRA_TITLE, "tasks_export.json") 53 | addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION) 54 | } 55 | 56 | fun buildImportIntent(): Intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply { 57 | addCategory(Intent.CATEGORY_OPENABLE) 58 | type = "application/json" 59 | addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) 60 | } 61 | 62 | fun writeJsonToUri(uri: Uri, json: String? = null): Boolean { 63 | return try { 64 | context.contentResolver.openOutputStream(uri)?.use { output -> 65 | (json ?: sampleJsonForExport).toByteArray(Charsets.UTF_8).let { output.write(it) } 66 | output.flush() 67 | } ?: return false 68 | showToast("Export successful") 69 | true 70 | } catch (t: Throwable) { 71 | showToast("Export failed: ${t.message ?: "Unknown error"}") 72 | false 73 | } 74 | } 75 | 76 | fun readJsonFromUri(uri: Uri): String? { 77 | return try { 78 | context.contentResolver.openInputStream(uri)?.use { input -> 79 | input.bufferedReader(Charsets.UTF_8).readText() 80 | } ?: run { 81 | showToast("Import failed: Unable to open input stream") 82 | null 83 | } 84 | } catch (t: Throwable) { 85 | showToast("Import failed: ${t.message ?: "Unknown error"}") 86 | null 87 | } 88 | } 89 | 90 | fun showToast(message: String) { 91 | Toast.makeText(context.applicationContext, message, Toast.LENGTH_SHORT).show() 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /lib/repositories/task_repository.dart: -------------------------------------------------------------------------------- 1 | // Trudido - A privacy-focused todo and notes app 2 | // Copyright (C) 2025 Dominik Müller 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with this program. If not, see . 16 | 17 | import 'package:meta/meta.dart'; 18 | import '../models/app_error.dart'; 19 | import '../models/todo.dart'; 20 | import '../services/storage_service.dart'; 21 | 22 | /// Repository abstraction over StorageService for todos, enabling future 23 | /// replacement (e.g. network sync) without touching UI providers. 24 | class TaskRepository { 25 | List _cache = const []; 26 | bool _loaded = false; 27 | bool get isLoaded => _loaded; 28 | void Function(List)? _testSaveOrderHook; 29 | 30 | List get tasks => _cache; 31 | 32 | @visibleForTesting 33 | void setTestTasks(List list) { 34 | _cache = List.from(list); 35 | _loaded = true; 36 | } 37 | 38 | @visibleForTesting 39 | void setTestSaveOrderHook(void Function(List) hook) { 40 | _testSaveOrderHook = hook; 41 | } 42 | 43 | Future load() async { 44 | try { 45 | await StorageService.waitTodosReady(); 46 | _cache = await StorageService.getAllTodosAsync(); 47 | _loaded = true; 48 | } catch (e, st) { 49 | throw AppError( 50 | AppErrorType.storageRead, 51 | 'Failed to load tasks', 52 | cause: e, 53 | stackTrace: st, 54 | ); 55 | } 56 | } 57 | 58 | Future add(Todo todo) async { 59 | await StorageService.saveTodo(todo); 60 | _cache = [..._cache, todo]; 61 | return todo; 62 | } 63 | 64 | Future update(Todo todo) async { 65 | final index = _cache.indexWhere((t) => t.id == todo.id); 66 | if (index == -1) 67 | throw const AppError(AppErrorType.notFound, 'Task not found'); 68 | await StorageService.updateTodo(todo); 69 | final list = [..._cache]; 70 | list[index] = todo; 71 | _cache = list; 72 | return todo; 73 | } 74 | 75 | Future delete(String id) async { 76 | final before = _cache.length; 77 | await StorageService.deleteTodo(id); 78 | _cache = _cache.where((t) => t.id != id).toList(); 79 | if (_cache.length == before) { 80 | throw const AppError(AppErrorType.notFound, 'Task not found'); 81 | } 82 | } 83 | 84 | Future bulkDelete(Iterable ids) async { 85 | final set = ids.toSet(); 86 | for (final id in set) { 87 | await StorageService.deleteTodo(id); 88 | } 89 | _cache = _cache.where((t) => !set.contains(t.id)).toList(); 90 | } 91 | 92 | Future saveOrder(List ordered) async { 93 | // Persist entire ordered list (legacy storage clears & rewrites) 94 | await StorageService.saveTodosOrder(ordered); 95 | _cache = List.from(ordered); 96 | _testSaveOrderHook?.call(_cache); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /lib/services/category_migration_service.dart: -------------------------------------------------------------------------------- 1 | // Trudido - A privacy-focused todo and notes app 2 | // Copyright (C) 2025 Dominik Müller 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with this program. If not, see . 16 | 17 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 18 | import '../services/folder_provider.dart'; 19 | 20 | /// Service to handle migration from categories to folders 21 | class CategoryMigrationService { 22 | final Ref ref; 23 | 24 | CategoryMigrationService(this.ref); 25 | 26 | /// Creates default folders that correspond to the old category system 27 | Future createDefaultFolders() async { 28 | final foldersNotifier = ref.read(folderNotifierProvider.notifier); 29 | 30 | // Get current folders 31 | final foldersAsync = ref.read(folderNotifierProvider); 32 | final existingFolders = foldersAsync.value ?? []; 33 | 34 | // Define default folders that replace the old categories 35 | final defaultFolders = [ 36 | { 37 | 'name': 'Personal', 38 | 'description': 'Personal tasks and reminders', 39 | 'color': 0xFF2196F3, // Blue 40 | 'icon': 'person', 41 | }, 42 | { 43 | 'name': 'Work', 44 | 'description': 'Work-related tasks and projects', 45 | 'color': 0xFF4CAF50, // Green 46 | 'icon': 'work', 47 | }, 48 | { 49 | 'name': 'Shopping', 50 | 'description': 'Shopping lists and errands', 51 | 'color': 0xFFFF9800, // Orange 52 | 'icon': 'shopping_cart', 53 | }, 54 | { 55 | 'name': 'Health', 56 | 'description': 'Health and wellness tasks', 57 | 'color': 0xFFE91E63, // Pink 58 | 'icon': 'favorite', 59 | }, 60 | ]; 61 | 62 | // Create folders that don't already exist 63 | for (final folderData in defaultFolders) { 64 | final name = folderData['name'] as String; 65 | final exists = existingFolders.any( 66 | (f) => f.name.toLowerCase() == name.toLowerCase(), 67 | ); 68 | 69 | if (!exists) { 70 | await foldersNotifier.createFolder( 71 | name: name, 72 | description: folderData['description'] as String, 73 | color: folderData['color'] as int, 74 | icon: folderData['icon'] as String, 75 | ); 76 | } 77 | } 78 | } 79 | 80 | /// Migrates any data by ensuring default folders exist 81 | /// This should be called once during app startup 82 | Future migrateFromCategories() async { 83 | await createDefaultFolders(); 84 | // Note: Since we removed the category field from Todo model, 85 | // tasks will simply have null folderId and appear 86 | // in the general task list. Users can manually move them to 87 | // the appropriate folders. 88 | } 89 | } 90 | 91 | final categoryMigrationProvider = Provider((ref) { 92 | return CategoryMigrationService(ref); 93 | }); 94 | -------------------------------------------------------------------------------- /lib/services/vault_password_service.dart: -------------------------------------------------------------------------------- 1 | // Trudido - A privacy-focused todo and notes app 2 | // Copyright (C) 2025 Dominik Müller 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with this program. If not, see . 16 | 17 | import 'package:flutter_secure_storage/flutter_secure_storage.dart'; 18 | import 'package:crypto/crypto.dart'; 19 | import 'dart:convert'; 20 | 21 | /// Service for managing vault passwords/PINs stored securely per folder 22 | class VaultPasswordService { 23 | static const _storage = FlutterSecureStorage(); 24 | static const _passwordPrefix = 'vault_password_'; 25 | 26 | /// Get the storage key for a specific vault folder 27 | static String _getPasswordKey(String folderId) { 28 | return '$_passwordPrefix$folderId'; 29 | } 30 | 31 | /// Hash a password using SHA-256 for secure storage comparison 32 | static String _hashPassword(String password) { 33 | final bytes = utf8.encode(password); 34 | final digest = sha256.convert(bytes); 35 | return digest.toString(); 36 | } 37 | 38 | /// Set password for a specific vault folder 39 | static Future setVaultPassword(String folderId, String password) async { 40 | if (password.isEmpty) { 41 | throw ArgumentError('Password cannot be empty'); 42 | } 43 | 44 | final hashedPassword = _hashPassword(password); 45 | await _storage.write(key: _getPasswordKey(folderId), value: hashedPassword); 46 | } 47 | 48 | /// Verify password for a specific vault folder 49 | /// Returns true if password matches, false otherwise 50 | static Future verifyVaultPassword( 51 | String folderId, 52 | String password, 53 | ) async { 54 | final storedHash = await _storage.read(key: _getPasswordKey(folderId)); 55 | 56 | if (storedHash == null) { 57 | return false; // No password set 58 | } 59 | 60 | final inputHash = _hashPassword(password); 61 | return storedHash == inputHash; 62 | } 63 | 64 | /// Check if a vault folder has a password set 65 | static Future hasVaultPassword(String folderId) async { 66 | final password = await _storage.read(key: _getPasswordKey(folderId)); 67 | return password != null; 68 | } 69 | 70 | /// Remove password for a specific vault folder 71 | static Future removeVaultPassword(String folderId) async { 72 | await _storage.delete(key: _getPasswordKey(folderId)); 73 | } 74 | 75 | /// Update password for a vault folder (requires old password verification) 76 | static Future updateVaultPassword( 77 | String folderId, 78 | String oldPassword, 79 | String newPassword, 80 | ) async { 81 | // Verify old password first 82 | final isValid = await verifyVaultPassword(folderId, oldPassword); 83 | if (!isValid) { 84 | return false; 85 | } 86 | 87 | // Set new password 88 | await setVaultPassword(folderId, newPassword); 89 | return true; 90 | } 91 | 92 | /// Clear all vault passwords (use with caution - for testing or reset) 93 | static Future clearAllVaultPasswords() async { 94 | await _storage.deleteAll(); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/trudido/app/PermissionsHelper.kt: -------------------------------------------------------------------------------- 1 | package com.trudido.app 2 | 3 | import android.app.Activity 4 | import android.app.AlarmManager 5 | import android.app.NotificationManager 6 | import android.content.ActivityNotFoundException 7 | import android.content.Context 8 | import android.content.Intent 9 | import android.net.Uri 10 | import android.os.Build 11 | import android.os.PowerManager 12 | import android.provider.Settings 13 | 14 | /** 15 | * Centralized native permission / settings helpers for notification related reliability. 16 | * Migrated from previous package (com.todoapp.todoflutter) during package unification. 17 | */ 18 | object PermissionsHelper { 19 | fun canScheduleExactAlarms(context: Context): Boolean { 20 | return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) true else { 21 | val am = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager 22 | am.canScheduleExactAlarms() 23 | } 24 | } 25 | 26 | fun openExactAlarmSettings(activity: Activity): Boolean { 27 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return true 28 | return try { 29 | val intent = Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM).apply { 30 | data = Uri.parse("package:" + activity.packageName) 31 | } 32 | activity.startActivity(intent) 33 | true 34 | } catch (e: Exception) { false } 35 | } 36 | 37 | fun isIgnoringBatteryOptimizations(context: Context): Boolean { 38 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return true 39 | val pm = context.getSystemService(Context.POWER_SERVICE) as PowerManager 40 | return pm.isIgnoringBatteryOptimizations(context.packageName) 41 | } 42 | 43 | fun requestIgnoreBatteryOptimizations(activity: Activity): Boolean { 44 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return true 45 | return try { 46 | val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply { 47 | data = Uri.parse("package:" + activity.packageName) 48 | } 49 | activity.startActivity(intent) 50 | true 51 | } catch (e: ActivityNotFoundException) { 52 | openBatteryOptimizationSettings(activity) 53 | } catch (_: Exception) { false } 54 | } 55 | 56 | fun openBatteryOptimizationSettings(activity: Activity): Boolean { 57 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return true 58 | return try { activity.startActivity(Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS)); true } catch (_: Exception) { false } 59 | } 60 | 61 | fun areNotificationsEnabled(context: Context): Boolean { 62 | val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager 63 | return nm.areNotificationsEnabled() 64 | } 65 | 66 | fun openChannelSettings(activity: Activity, channelId: String): Boolean { 67 | return try { 68 | val intent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 69 | Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS).apply { 70 | putExtra(Settings.EXTRA_APP_PACKAGE, activity.packageName) 71 | putExtra(Settings.EXTRA_CHANNEL_ID, channelId) 72 | } 73 | } else { 74 | Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply { 75 | putExtra(Settings.EXTRA_APP_PACKAGE, activity.packageName) 76 | } 77 | } 78 | activity.startActivity(intent) 79 | true 80 | } catch (_: Exception) { false } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /lib/models/todo.g.dart: -------------------------------------------------------------------------------- 1 | // Trudido - A privacy-focused todo and notes app 2 | // Copyright (C) 2025 Dominik Müller 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with this program. If not, see . 16 | 17 | // GENERATED CODE - DO NOT MODIFY BY HAND 18 | 19 | part of 'todo.dart'; 20 | 21 | // ************************************************************************** 22 | // TypeAdapterGenerator 23 | // ************************************************************************** 24 | 25 | class TodoAdapter extends TypeAdapter { 26 | @override 27 | final int typeId = 0; 28 | 29 | @override 30 | Todo read(BinaryReader reader) { 31 | final numOfFields = reader.readByte(); 32 | final fields = { 33 | for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), 34 | }; 35 | return Todo( 36 | id: fields[0] as String?, 37 | text: fields[1] as String, 38 | isCompleted: fields[2] as bool, 39 | createdAt: fields[3] as DateTime?, 40 | dueDate: fields[4] as DateTime?, 41 | startDate: fields[12] as DateTime?, 42 | priority: fields[5] as String, 43 | tags: (fields[7] as List?)?.cast(), 44 | completedAt: fields[8] as DateTime?, 45 | notes: fields[9] as String?, 46 | folderId: fields[10] as String?, 47 | reminderOffsetsMinutes: (fields[11] as List?)?.cast(), 48 | repeatType: fields[13] == null ? 'none' : fields[13] as String, 49 | repeatInterval: fields[14] as int?, 50 | repeatDays: (fields[15] as List?)?.cast(), 51 | repeatEndDate: fields[16] as DateTime?, 52 | parentRecurringTaskId: fields[17] as String?, 53 | sourceCalendarColor: fields[18] as int?, 54 | ); 55 | } 56 | 57 | @override 58 | void write(BinaryWriter writer, Todo obj) { 59 | writer 60 | ..writeByte(18) 61 | ..writeByte(0) 62 | ..write(obj.id) 63 | ..writeByte(1) 64 | ..write(obj.text) 65 | ..writeByte(2) 66 | ..write(obj.isCompleted) 67 | ..writeByte(3) 68 | ..write(obj.createdAt) 69 | ..writeByte(4) 70 | ..write(obj.dueDate) 71 | ..writeByte(5) 72 | ..write(obj.priority) 73 | ..writeByte(7) 74 | ..write(obj.tags) 75 | ..writeByte(8) 76 | ..write(obj.completedAt) 77 | ..writeByte(9) 78 | ..write(obj.notes) 79 | ..writeByte(10) 80 | ..write(obj.folderId) 81 | ..writeByte(11) 82 | ..write(obj.reminderOffsetsMinutes) 83 | ..writeByte(12) 84 | ..write(obj.startDate) 85 | ..writeByte(13) 86 | ..write(obj.repeatType) 87 | ..writeByte(14) 88 | ..write(obj.repeatInterval) 89 | ..writeByte(15) 90 | ..write(obj.repeatDays) 91 | ..writeByte(16) 92 | ..write(obj.repeatEndDate) 93 | ..writeByte(17) 94 | ..write(obj.parentRecurringTaskId) 95 | ..writeByte(18) 96 | ..write(obj.sourceCalendarColor); 97 | } 98 | 99 | @override 100 | int get hashCode => typeId.hashCode; 101 | 102 | @override 103 | bool operator ==(Object other) => 104 | identical(this, other) || 105 | other is TodoAdapter && 106 | runtimeType == other.runtimeType && 107 | typeId == other.typeId; 108 | } 109 | -------------------------------------------------------------------------------- /.github/workflows/crowdin-sync.yml: -------------------------------------------------------------------------------- 1 | name: Crowdin Sync 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths: 7 | - "lib/l10n/app_en.arb" 8 | pull_request: 9 | branches: [main] 10 | paths: 11 | - "lib/l10n/app_en.arb" 12 | schedule: 13 | # Run twice daily to pull translations 14 | - cron: "0 8,20 * * *" 15 | workflow_dispatch: 16 | 17 | permissions: 18 | contents: write 19 | pull-requests: write 20 | 21 | jobs: 22 | sync-crowdin: 23 | runs-on: ubuntu-latest 24 | 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v4 28 | with: 29 | token: ${{ secrets.GH_PAT }} 30 | 31 | - name: Remove old Dart SDK 32 | run: sudo rm -rf /opt/hostedtoolcache/dart 33 | 34 | - name: Setup Flutter 35 | uses: subosito/flutter-action@v2 36 | with: 37 | flutter-version: "master" 38 | channel: "master" 39 | 40 | - name: Check Flutter & Dart version 41 | run: | 42 | flutter --version 43 | dart --version 44 | 45 | - name: Get dependencies 46 | run: flutter pub get 47 | 48 | - name: Validate ARB files 49 | run: flutter gen-l10n 50 | 51 | - name: Upload source files to Crowdin 52 | if: github.event_name == 'push' && github.ref == 'refs/heads/main' 53 | uses: crowdin/github-action@v2 54 | with: 55 | upload_sources: true 56 | upload_translations: false 57 | download_translations: false 58 | crowdin_branch_name: main 59 | env: 60 | CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} 61 | CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_API_TOKEN }} 62 | 63 | - name: Download translations from Crowdin 64 | if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' 65 | uses: crowdin/github-action@v2 66 | with: 67 | upload_sources: false 68 | upload_translations: false 69 | download_translations: true 70 | push_translations: true 71 | commit_message: "chore: update translations from Crowdin [skip ci]" 72 | crowdin_branch_name: main 73 | create_pull_request: true 74 | pull_request_title: "Update translations from Crowdin" 75 | pull_request_body: | 76 | This PR contains updated translations from Crowdin. 77 | 78 | - Translations are automatically downloaded from Crowdin 79 | - Please review the changes before merging 80 | - The translations will be included in the next app release 81 | env: 82 | CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} 83 | CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_API_TOKEN }} 84 | GITHUB_TOKEN: ${{ secrets.GH_PAT }} 85 | 86 | - name: Fix @@locale in arb files 87 | if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' 88 | run: | 89 | find lib/l10n -name "app_*.arb" -not -name "app_en.arb" | while read filename; do 90 | locale=$(basename "$filename" .arb | sed 's/app_//') 91 | sed -i "s/\"@@locale\": \".*\"/\"@@locale\": \"$locale\"/" "$filename" 92 | echo "Fixed @@locale in $filename to $locale" 93 | done 94 | 95 | - name: Generate localization files 96 | if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' 97 | run: flutter gen-l10n 98 | 99 | - name: Commit generated files 100 | if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' 101 | uses: stefanzweifel/git-auto-commit-action@v5 102 | with: 103 | commit_message: "chore: regenerate localization files [skip ci]" 104 | file_pattern: "lib/l10n/app_localizations*.dart" 105 | -------------------------------------------------------------------------------- /lib/services/navigation_service.dart: -------------------------------------------------------------------------------- 1 | // Trudido - A privacy-focused todo and notes app 2 | // Copyright (C) 2025 Dominik Müller 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with this program. If not, see . 16 | 17 | import 'package:flutter/material.dart'; 18 | 19 | /// Global navigation service for handling navigation from outside the widget tree 20 | /// (such as from notification callbacks) 21 | class NavigationService { 22 | static final GlobalKey navigatorKey = 23 | GlobalKey(); 24 | 25 | /// Get the current navigator state 26 | static NavigatorState? get navigator => navigatorKey.currentState; 27 | 28 | /// Get the current context 29 | static BuildContext? get context => navigatorKey.currentContext; 30 | 31 | /// Navigate to a new route 32 | static Future navigateTo(Route route) { 33 | final navigator = NavigationService.navigator; 34 | if (navigator == null) { 35 | throw Exception( 36 | 'Navigator not available. Make sure NavigationService.navigatorKey is assigned to MaterialApp.navigatorKey', 37 | ); 38 | } 39 | return navigator.push(route); 40 | } 41 | 42 | /// Navigate to a new route and replace the current one 43 | static Future navigateAndReplace( 44 | Route newRoute, 45 | ) { 46 | final navigator = NavigationService.navigator; 47 | if (navigator == null) { 48 | throw Exception( 49 | 'Navigator not available. Make sure NavigationService.navigatorKey is assigned to MaterialApp.navigatorKey', 50 | ); 51 | } 52 | return navigator.pushReplacement(newRoute); 53 | } 54 | 55 | /// Pop the current route 56 | static void pop([T? result]) { 57 | final navigator = NavigationService.navigator; 58 | if (navigator == null) { 59 | throw Exception( 60 | 'Navigator not available. Make sure NavigationService.navigatorKey is assigned to MaterialApp.navigatorKey', 61 | ); 62 | } 63 | if (navigator.canPop()) { 64 | navigator.pop(result); 65 | } 66 | } 67 | 68 | /// Navigate to a named route 69 | static Future navigateToNamed( 70 | String routeName, { 71 | Object? arguments, 72 | }) { 73 | final navigator = NavigationService.navigator; 74 | if (navigator == null) { 75 | throw Exception( 76 | 'Navigator not available. Make sure NavigationService.navigatorKey is assigned to MaterialApp.navigatorKey', 77 | ); 78 | } 79 | return navigator.pushNamed(routeName, arguments: arguments); 80 | } 81 | 82 | /// Pop until a specific route 83 | static void popUntil(RoutePredicate predicate) { 84 | final navigator = NavigationService.navigator; 85 | if (navigator == null) { 86 | throw Exception( 87 | 'Navigator not available. Make sure NavigationService.navigatorKey is assigned to MaterialApp.navigatorKey', 88 | ); 89 | } 90 | navigator.popUntil(predicate); 91 | } 92 | 93 | /// Check if we can pop the current route 94 | static bool canPop() { 95 | final navigator = NavigationService.navigator; 96 | if (navigator == null) return false; 97 | return navigator.canPop(); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Flutter / Dart / Pub 2 | # See https://github.com/flutter/flutter/blob/main/.gitignore for reference 3 | .dart_tool/ 4 | .pub/ 5 | .packages 6 | .flutter-plugins 7 | .flutter-plugins-dependencies 8 | **/doc/api/ 9 | # pubspec.lock - MUST be committed for F-Droid reproducible builds 10 | 11 | # Build outputs 12 | /build/ 13 | **/build/ 14 | 15 | # Generated plugin registrant 16 | **/GeneratedPluginRegistrant.* 17 | 18 | # iOS 19 | **/ios/Flutter/.last_build_id 20 | **/ios/ServiceDefinitions.json 21 | **/ios/Flutter/Flutter.framework 22 | ios/Flutter/Flutter.podspec 23 | Pods/ 24 | Podfile.lock 25 | Flutter/Flutter.framework 26 | Flutter/Flutter.podspec 27 | 28 | # Android 29 | /android/.gradle 30 | /android/app/debug 31 | /android/app/profile 32 | /android/app/release 33 | **/android/key.properties 34 | **/*.keystore 35 | **/*.jks 36 | local.properties 37 | key.properties 38 | 39 | # Sensitive files / service credentials (optional - keep out of repo) 40 | # google-services.json (Android) and GoogleService-Info.plist (iOS) 41 | **/google-services.json 42 | **/GoogleService-Info.plist 43 | *.env 44 | *.p12 45 | *.pem 46 | 47 | # VS Code 48 | .vscode/ 49 | 50 | # IntelliJ / Android Studio 51 | *.iml 52 | .idea/ 53 | .metadata 54 | 55 | # macOS 56 | .DS_Store 57 | **/DerivedData/ 58 | 59 | # Windows 60 | Thumbs.db 61 | ehthumbs.db 62 | 63 | # Linux 64 | *~ 65 | 66 | # Other editors 67 | *.swp 68 | *.swo 69 | *.log 70 | 71 | # Archives and binaries 72 | *.zip 73 | *.tar.gz 74 | 75 | # Symbolication and obfuscation artifacts 76 | app.*.symbols 77 | app.*.map.json 78 | 79 | # Flutter build cache and tooling 80 | /.flutter-plugins 81 | .flutter-plugins-dependencies 82 | .flutter-plugins 83 | 84 | # Misc 85 | .history 86 | .gradle 87 | .idea 88 | migrate_working_dir/ 89 | 90 | # Optional additions 91 | # Flutter / tool artifacts 92 | .metadata 93 | coverage/ 94 | .flutter_export_environment.sh 95 | 96 | # Build outputs / packaged artifacts 97 | *.apk 98 | *.aab 99 | *.apks 100 | **/outputs/** 101 | 102 | # Generated plugin registrants 103 | **/generated_plugin_registrant.* 104 | **/generated_plugins.* 105 | 106 | # Test / reports 107 | test_reports/ 108 | reports/ 109 | 110 | # Misc packaging / signing helpers 111 | *.keystore 112 | *.jks 113 | **/key.properties 114 | 115 | # Optional local Dev files 116 | .vs/ 117 | .idea/*workspace.xml 118 | .idea/*tasks.xml 119 | # dotenv environment variables file 120 | .env* 121 | 122 | # Avoid committing generated Javascript files: 123 | *.dart.js 124 | # Produced by the --dump-info flag. 125 | *.info.json 126 | # When generated by dart2js. Don't specify *.js if your 127 | # project includes source files written in JavaScript. 128 | *.js 129 | *.js_ 130 | *.js.deps 131 | *.js.map 132 | 133 | .flutter-plugins 134 | .flutter-plugins-dependencies 135 | 136 | # Windows thumbnail cache files 137 | Thumbs.db 138 | Thumbs.db:encryptable 139 | ehthumbs.db 140 | ehthumbs_vista.db 141 | 142 | # Dump file 143 | *.stackdump 144 | 145 | # Folder config file 146 | [Dd]esktop.ini 147 | 148 | # Recycle Bin used on file shares 149 | $RECYCLE.BIN/ 150 | 151 | # Windows Installer files 152 | *.cab 153 | *.msi 154 | *.msix 155 | *.msm 156 | *.msp 157 | 158 | # Windows shortcuts 159 | *.lnk 160 | 161 | # Documentation 162 | docs/ 163 | test_cache/ 164 | web/.dart_tool/ 165 | web/build/ 166 | android/.gradle/ 167 | android/local.properties 168 | ios/Pods/ 169 | ios/Flutter/Flutter.framework 170 | ios/Flutter/Flutter.podspec 171 | ios/Flutter/Generated.xcconfig 172 | ios/Flutter/App.framework 173 | ios/Flutter/engine/ 174 | ios/Flutter/ephemeral/ 175 | macos/Pods/ 176 | *.tmp 177 | *.bak 178 | *.orig 179 | *.class 180 | *.pyc 181 | *.pyo 182 | *.exe 183 | *.dll 184 | *.so 185 | *.dylib 186 | *.key 187 | *.pem 188 | *.jks 189 | *.log 190 | *.DS_Store 191 | *.swp 192 | *.pyo 193 | *.env 194 | *.env.* 195 | 196 | # Keystore 197 | *.jks 198 | trudido-clean/ 199 | -------------------------------------------------------------------------------- /lib/models/note.dart: -------------------------------------------------------------------------------- 1 | // Trudido - A privacy-focused todo and notes app 2 | // Copyright (C) 2025 Dominik Müller 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with this program. If not, see . 16 | 17 | import 'package:hive/hive.dart'; 18 | import 'package:uuid/uuid.dart'; 19 | 20 | part 'note.g.dart'; 21 | 22 | /// Represents a markdown note with metadata 23 | @HiveType(typeId: 6) 24 | class Note extends HiveObject { 25 | @HiveField(0) 26 | String id; 27 | 28 | @HiveField(1) 29 | String title; 30 | 31 | @HiveField(2) 32 | String content; 33 | 34 | @HiveField(3) 35 | DateTime createdAt; 36 | 37 | @HiveField(4) 38 | DateTime updatedAt; 39 | 40 | @HiveField(5, defaultValue: false) 41 | bool isPinned; 42 | 43 | @HiveField(6) 44 | String? folderId; // Reference to folder (including vault folders) 45 | 46 | @HiveField(7) 47 | String? todoTxtContent; // Optional todo.txt format representation 48 | 49 | Note({ 50 | String? id, 51 | required this.title, 52 | required this.content, 53 | DateTime? createdAt, 54 | DateTime? updatedAt, 55 | this.isPinned = false, 56 | this.folderId, 57 | this.todoTxtContent, 58 | }) : id = id ?? const Uuid().v4(), 59 | createdAt = createdAt ?? DateTime.now(), 60 | updatedAt = updatedAt ?? DateTime.now(); 61 | 62 | /// Creates a copy of this note with updated fields 63 | Note copyWith({ 64 | String? id, 65 | String? title, 66 | String? content, 67 | DateTime? createdAt, 68 | DateTime? updatedAt, 69 | bool? isPinned, 70 | String? folderId, 71 | String? todoTxtContent, 72 | }) { 73 | return Note( 74 | id: id ?? this.id, 75 | title: title ?? this.title, 76 | content: content ?? this.content, 77 | createdAt: createdAt ?? this.createdAt, 78 | updatedAt: updatedAt ?? this.updatedAt, 79 | isPinned: isPinned ?? this.isPinned, 80 | folderId: folderId ?? this.folderId, 81 | todoTxtContent: todoTxtContent ?? this.todoTxtContent, 82 | ); 83 | } 84 | 85 | @override 86 | bool operator ==(Object other) { 87 | if (identical(this, other)) return true; 88 | return other is Note && other.id == id; 89 | } 90 | 91 | @override 92 | int get hashCode => id.hashCode; 93 | 94 | @override 95 | String toString() { 96 | return 'Note(id: $id, title: $title, isPinned: $isPinned, folderId: $folderId, createdAt: $createdAt, updatedAt: $updatedAt)'; 97 | } 98 | 99 | /// Converts the note to a JSON map for export/import 100 | Map toJson() { 101 | return { 102 | 'id': id, 103 | 'title': title, 104 | 'content': content, 105 | 'createdAt': createdAt.toIso8601String(), 106 | 'updatedAt': updatedAt.toIso8601String(), 107 | 'isPinned': isPinned, 108 | 'folderId': folderId, 109 | 'todoTxtContent': todoTxtContent, 110 | }; 111 | } 112 | 113 | /// Creates a Note from a JSON map 114 | factory Note.fromJson(Map json) { 115 | return Note( 116 | id: json['id'] as String, 117 | title: json['title'] as String, 118 | content: json['content'] as String, 119 | createdAt: DateTime.parse(json['createdAt'] as String), 120 | updatedAt: DateTime.parse(json['updatedAt'] as String), 121 | isPinned: json['isPinned'] as bool? ?? false, 122 | folderId: json['folderId'] as String?, 123 | todoTxtContent: json['todoTxtContent'] as String?, 124 | ); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /lib/use_cases/folder_template_use_cases.dart: -------------------------------------------------------------------------------- 1 | // Trudido - A privacy-focused todo and notes app 2 | // Copyright (C) 2025 Dominik Müller 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with this program. If not, see . 16 | 17 | import 'package:flutter/foundation.dart'; 18 | import '../models/folder_template.dart'; 19 | import '../models/todo.dart'; 20 | import '../repositories/folder_template_repository.dart'; 21 | import '../services/storage_service.dart'; 22 | 23 | /// Use case for getting all templates 24 | class GetTemplatesUseCase { 25 | final FolderTemplateRepository _repository; 26 | 27 | GetTemplatesUseCase(this._repository); 28 | 29 | Future> call() async { 30 | return await _repository.getAllTemplates(); 31 | } 32 | } 33 | 34 | /// Use case for creating a new template 35 | class CreateTemplateUseCase { 36 | final FolderTemplateRepository _repository; 37 | 38 | CreateTemplateUseCase(this._repository); 39 | 40 | Future call(FolderTemplate template) async { 41 | await _repository.createTemplate(template); 42 | } 43 | } 44 | 45 | /// Use case for suggesting templates based on folder name 46 | class SuggestTemplatesUseCase { 47 | final FolderTemplateRepository _repository; 48 | 49 | SuggestTemplatesUseCase(this._repository); 50 | 51 | Future> call(String folderName) async { 52 | return await _repository.suggestTemplatesForFolder(folderName); 53 | } 54 | } 55 | 56 | /// Use case for creating template from a folder 57 | class CreateTemplateFromFolderUseCase { 58 | final FolderTemplateRepository _repository; 59 | 60 | CreateTemplateFromFolderUseCase(this._repository); 61 | 62 | Future call(String folderId, String templateName) async { 63 | return await _repository.createTemplateFromFolder(folderId, templateName); 64 | } 65 | } 66 | 67 | /// Use case for applying a template to create tasks in a folder 68 | class ApplyTemplateUseCase { 69 | final FolderTemplateRepository _repository; 70 | 71 | ApplyTemplateUseCase(this._repository); 72 | 73 | Future> call( 74 | FolderTemplate template, 75 | String folderId, { 76 | DateTime? baseDueDate, 77 | }) async { 78 | final todos = []; 79 | // Ensure storage (lazy todos box) is ready before attempting to save created todos. 80 | await StorageService.waitTodosReady(); 81 | 82 | for (final taskTemplate in template.taskTemplates) { 83 | // Calculate due date if template has offset 84 | DateTime? dueDate; 85 | if (taskTemplate.dueDateOffset != null && baseDueDate != null) { 86 | dueDate = baseDueDate.add(Duration(days: taskTemplate.dueDateOffset!)); 87 | } 88 | 89 | // Create todo from template 90 | final todo = Todo( 91 | text: taskTemplate.text, 92 | folderId: folderId, 93 | priority: taskTemplate.priority, 94 | tags: taskTemplate.tags, 95 | notes: taskTemplate.notes, 96 | dueDate: dueDate, 97 | reminderOffsetsMinutes: taskTemplate.reminderOffsets, 98 | ); 99 | 100 | // Add to storage - protect each save so one failure doesn't abort the rest 101 | try { 102 | await StorageService.saveTodo(todo); 103 | todos.add(todo); 104 | } catch (e, st) { 105 | // Log and continue applying remaining tasks 106 | debugPrint( 107 | '[ApplyTemplateUseCase] Failed to save todo from template: $e\n$st', 108 | ); 109 | } 110 | } 111 | 112 | // Increment template usage 113 | await _repository.incrementTemplateUsage(template.id); 114 | 115 | return todos; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /lib/services/permissions_channel.dart: -------------------------------------------------------------------------------- 1 | // Trudido - A privacy-focused todo and notes app 2 | // Copyright (C) 2025 Dominik Müller 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with this program. If not, see . 16 | 17 | import 'dart:io'; 18 | import 'package:flutter/foundation.dart'; 19 | import 'package:flutter/services.dart'; 20 | 21 | /// Thin wrapper around native MethodChannel 'app.perms'. 22 | /// All calls are idempotent + API guarded on native side; here we add 23 | /// Dart-level guards & error handling so UI code stays clean. 24 | class PermissionsChannel { 25 | static const MethodChannel _channel = MethodChannel('app.perms'); 26 | PermissionsChannel._(); 27 | static final instance = PermissionsChannel._(); 28 | 29 | Future canScheduleExactAlarms() async { 30 | if (!Platform.isAndroid) return true; 31 | try { 32 | return (await _channel.invokeMethod('canScheduleExactAlarms')) == true; 33 | } catch (e) { 34 | _log('canScheduleExactAlarms', e); 35 | return true; 36 | } 37 | } 38 | 39 | Future openExactAlarmSettings() async { 40 | if (!Platform.isAndroid) return true; 41 | try { 42 | return (await _channel.invokeMethod('openExactAlarmSettings')) == true; 43 | } catch (e) { 44 | _log('openExactAlarmSettings', e); 45 | return false; 46 | } 47 | } 48 | 49 | Future isIgnoringBatteryOptimizations() async { 50 | if (!Platform.isAndroid) return true; 51 | try { 52 | return (await _channel.invokeMethod('isIgnoringBatteryOptimizations')) == 53 | true; 54 | } catch (e) { 55 | _log('isIgnoringBatteryOptimizations', e); 56 | return true; 57 | } 58 | } 59 | 60 | Future requestIgnoreBatteryOptimizations() async { 61 | if (!Platform.isAndroid) return true; 62 | try { 63 | return (await _channel.invokeMethod( 64 | 'requestIgnoreBatteryOptimizations', 65 | )) == 66 | true; 67 | } catch (e) { 68 | _log('requestIgnoreBatteryOptimizations', e); 69 | return false; 70 | } 71 | } 72 | 73 | Future openBatteryOptimizationSettings() async { 74 | if (!Platform.isAndroid) return true; 75 | try { 76 | return (await _channel.invokeMethod('openBatteryOptimizationSettings')) == 77 | true; 78 | } catch (e) { 79 | _log('openBatteryOptimizationSettings', e); 80 | return false; 81 | } 82 | } 83 | 84 | Future areNotificationsEnabled() async { 85 | if (!Platform.isAndroid) return true; 86 | try { 87 | return (await _channel.invokeMethod('areNotificationsEnabled')) == true; 88 | } catch (e) { 89 | _log('areNotificationsEnabled', e); 90 | return true; 91 | } 92 | } 93 | 94 | Future requestPostNotifications() async { 95 | if (!Platform.isAndroid) return true; 96 | try { 97 | return (await _channel.invokeMethod('requestPostNotifications')) == true; 98 | } catch (e) { 99 | _log('requestPostNotifications', e); 100 | return false; 101 | } 102 | } 103 | 104 | Future openAppNotificationSettings() async { 105 | if (!Platform.isAndroid) return true; 106 | try { 107 | return (await _channel.invokeMethod('openAppNotificationSettings')) == 108 | true; 109 | } catch (e) { 110 | _log('openAppNotificationSettings', e); 111 | return false; 112 | } 113 | } 114 | 115 | Future getSdkInt() async { 116 | if (!Platform.isAndroid) return 0; 117 | try { 118 | final v = await _channel.invokeMethod('getSdkInt'); 119 | return (v is int) ? v : 0; 120 | } catch (e) { 121 | _log('getSdkInt', e); 122 | return 0; 123 | } 124 | } 125 | 126 | void _log(String m, Object e) { 127 | debugPrint('[PermissionsChannel] $m error: $e'); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /lib/services/template_provider.dart: -------------------------------------------------------------------------------- 1 | // Trudido - A privacy-focused todo and notes app 2 | // Copyright (C) 2025 Dominik Müller 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with this program. If not, see . 16 | 17 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 18 | import '../models/folder_template.dart'; 19 | import '../repositories/folder_template_repository.dart'; 20 | import '../repositories/hive_folder_template_repository.dart'; 21 | import '../use_cases/folder_template_use_cases.dart'; 22 | 23 | // Repository provider 24 | final folderTemplateRepositoryProvider = Provider(( 25 | ref, 26 | ) { 27 | return HiveFolderTemplateRepository(); 28 | }); 29 | 30 | // Use case providers 31 | final getTemplatesUseCaseProvider = Provider((ref) { 32 | return GetTemplatesUseCase(ref.read(folderTemplateRepositoryProvider)); 33 | }); 34 | 35 | final createTemplateUseCaseProvider = Provider((ref) { 36 | return CreateTemplateUseCase(ref.read(folderTemplateRepositoryProvider)); 37 | }); 38 | 39 | final suggestTemplatesUseCaseProvider = Provider(( 40 | ref, 41 | ) { 42 | return SuggestTemplatesUseCase(ref.read(folderTemplateRepositoryProvider)); 43 | }); 44 | 45 | final createFromFolderUseCaseProvider = 46 | Provider((ref) { 47 | return CreateTemplateFromFolderUseCase( 48 | ref.read(folderTemplateRepositoryProvider), 49 | ); 50 | }); 51 | 52 | final applyTemplateUseCaseProvider = Provider((ref) { 53 | return ApplyTemplateUseCase(ref.read(folderTemplateRepositoryProvider)); 54 | }); 55 | 56 | // State notifier for templates 57 | final templateNotifierProvider = 58 | StateNotifierProvider>>(( 59 | ref, 60 | ) { 61 | return TemplateNotifier( 62 | ref.read(getTemplatesUseCaseProvider), 63 | ref.read(folderTemplateRepositoryProvider), 64 | ); 65 | }); 66 | 67 | /// State notifier for managing folder templates 68 | class TemplateNotifier extends StateNotifier>> { 69 | final GetTemplatesUseCase _getTemplatesUseCase; 70 | final FolderTemplateRepository _repository; 71 | 72 | TemplateNotifier(this._getTemplatesUseCase, this._repository) 73 | : super(const AsyncValue.loading()) { 74 | loadTemplates(); 75 | } 76 | 77 | /// Load all templates 78 | Future loadTemplates() async { 79 | state = const AsyncValue.loading(); 80 | try { 81 | final templates = await _getTemplatesUseCase(); 82 | state = AsyncValue.data(templates); 83 | } catch (error, stackTrace) { 84 | state = AsyncValue.error(error, stackTrace); 85 | } 86 | } 87 | 88 | /// Create a new template 89 | Future createTemplate(FolderTemplate template) async { 90 | await _repository.createTemplate(template); 91 | await loadTemplates(); // Reload to update state 92 | } 93 | 94 | /// Update a template 95 | Future updateTemplate(FolderTemplate template) async { 96 | await _repository.updateTemplate(template); 97 | await loadTemplates(); // Reload to update state 98 | } 99 | 100 | /// Delete a template 101 | Future deleteTemplate(String templateId) async { 102 | final result = await _repository.deleteTemplate(templateId); 103 | await loadTemplates(); // Reload to update state 104 | return result; 105 | } 106 | 107 | /// Increment template usage count 108 | Future incrementUsage(String templateId) async { 109 | await _repository.incrementTemplateUsage(templateId); 110 | await loadTemplates(); // Reload to update state 111 | } 112 | 113 | /// Reset built-in template to original 114 | Future resetTemplate(String templateId) async { 115 | await _repository.resetBuiltInTemplate(templateId); 116 | await loadTemplates(); // Reload to update state 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /lib/utils/week_start_utils.dart: -------------------------------------------------------------------------------- 1 | // Trudido - A privacy-focused todo and notes app 2 | // Copyright (C) 2025 Dominik Müller 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with this program. If not, see . 16 | 17 | import 'package:flutter/material.dart'; 18 | import 'package:table_calendar/table_calendar.dart'; 19 | 20 | /// Utility class for week start day configuration. 21 | /// 22 | /// Week day indices follow the Material convention: 23 | /// 0 = Sunday, 1 = Monday, 2 = Tuesday, ..., 6 = Saturday 24 | class WeekStartUtils { 25 | /// Maps the preference index (0-6, where 0=Sunday) to TableCalendar's StartingDayOfWeek. 26 | /// 27 | /// TableCalendar enum order: monday=0, tuesday=1, ..., saturday=5, sunday=6 28 | /// Material/preference order: sunday=0, monday=1, ..., saturday=6 29 | static StartingDayOfWeek toTableCalendarDay(int index) { 30 | // Convert from Material convention (0=Sunday) to TableCalendar convention (0=Monday) 31 | switch (index) { 32 | case 0: // Sunday 33 | return StartingDayOfWeek.sunday; 34 | case 1: // Monday 35 | return StartingDayOfWeek.monday; 36 | case 2: // Tuesday 37 | return StartingDayOfWeek.tuesday; 38 | case 3: // Wednesday 39 | return StartingDayOfWeek.wednesday; 40 | case 4: // Thursday 41 | return StartingDayOfWeek.thursday; 42 | case 5: // Friday 43 | return StartingDayOfWeek.friday; 44 | case 6: // Saturday 45 | return StartingDayOfWeek.saturday; 46 | default: 47 | return StartingDayOfWeek.monday; // Default to Monday 48 | } 49 | } 50 | 51 | /// Returns the localized name for a day index (0=Sunday, 1=Monday, etc.) 52 | static String getDayName(int index) { 53 | const days = [ 54 | 'Sunday', 55 | 'Monday', 56 | 'Tuesday', 57 | 'Wednesday', 58 | 'Thursday', 59 | 'Friday', 60 | 'Saturday', 61 | ]; 62 | return days[index.clamp(0, 6)]; 63 | } 64 | 65 | /// Returns the short name for a day index 66 | static String getDayShortName(int index) { 67 | const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; 68 | return days[index.clamp(0, 6)]; 69 | } 70 | } 71 | 72 | /// A MaterialLocalizations that overrides only firstDayOfWeekIndex. 73 | /// 74 | /// Extends DefaultMaterialLocalizations so we get all the English strings 75 | /// and only override the week start. 76 | class _WeekStartMaterialLocalizations extends DefaultMaterialLocalizations { 77 | final int _firstDayOfWeekIndex; 78 | 79 | const _WeekStartMaterialLocalizations(this._firstDayOfWeekIndex); 80 | 81 | @override 82 | int get firstDayOfWeekIndex => _firstDayOfWeekIndex; 83 | } 84 | 85 | /// A LocalizationsDelegate that provides MaterialLocalizations 86 | /// with a custom firstDayOfWeekIndex. 87 | class WeekStartLocalizationsDelegate 88 | extends LocalizationsDelegate { 89 | final int firstDayOfWeekIndex; 90 | 91 | const WeekStartLocalizationsDelegate(this.firstDayOfWeekIndex); 92 | 93 | @override 94 | bool isSupported(Locale locale) => true; 95 | 96 | @override 97 | Future load(Locale locale) async { 98 | return _WeekStartMaterialLocalizations(firstDayOfWeekIndex); 99 | } 100 | 101 | @override 102 | bool shouldReload(WeekStartLocalizationsDelegate old) => 103 | old.firstDayOfWeekIndex != firstDayOfWeekIndex; 104 | } 105 | 106 | /// Helper widget to wrap child widgets with custom week start localization. 107 | /// 108 | /// Use this to wrap date pickers so they respect the user's week start preference. 109 | class WeekStartOverride extends StatelessWidget { 110 | final int firstDayOfWeekIndex; 111 | final Widget child; 112 | 113 | const WeekStartOverride({ 114 | super.key, 115 | required this.firstDayOfWeekIndex, 116 | required this.child, 117 | }); 118 | 119 | @override 120 | Widget build(BuildContext context) { 121 | return Localizations.override( 122 | context: context, 123 | delegates: [WeekStartLocalizationsDelegate(firstDayOfWeekIndex)], 124 | child: child, 125 | ); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /lib/models/note_folder.dart: -------------------------------------------------------------------------------- 1 | // Trudido - A privacy-focused todo and notes app 2 | // Copyright (C) 2025 Dominik Müller 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with this program. If not, see . 16 | 17 | import 'package:hive/hive.dart'; 18 | import 'package:uuid/uuid.dart'; 19 | 20 | part 'note_folder.g.dart'; 21 | 22 | /// Folder model specifically for organizing notes (separate from todo folders) 23 | @HiveType(typeId: 7) // Using typeId 7 for note folders 24 | class NoteFolder extends HiveObject { 25 | @HiveField(0) 26 | String id; 27 | 28 | @HiveField(1) 29 | String name; 30 | 31 | @HiveField(2) 32 | String? description; 33 | 34 | @HiveField(3) 35 | DateTime createdAt; 36 | 37 | @HiveField(4) 38 | DateTime updatedAt; 39 | 40 | @HiveField(5, defaultValue: false) 41 | bool isVault; // Encrypted vault folder flag 42 | 43 | @HiveField(6) 44 | int sortOrder; // For custom ordering 45 | 46 | @HiveField(7, defaultValue: false) 47 | bool hasPassword; // Whether vault has a password/PIN set 48 | 49 | @HiveField(8, defaultValue: true) 50 | bool useBiometric; // Whether to use biometric shortcut (if available) 51 | 52 | @HiveField(9, defaultValue: 'markdown') 53 | String noteFormat; // 'markdown' or 'todotxt' - format for all notes in this folder 54 | 55 | NoteFolder({ 56 | String? id, 57 | required this.name, 58 | this.description, 59 | DateTime? createdAt, 60 | DateTime? updatedAt, 61 | this.isVault = false, 62 | this.sortOrder = 0, 63 | this.hasPassword = false, 64 | this.useBiometric = true, 65 | this.noteFormat = 'markdown', 66 | }) : id = id ?? const Uuid().v4(), 67 | createdAt = createdAt ?? DateTime.now(), 68 | updatedAt = updatedAt ?? DateTime.now(); 69 | 70 | NoteFolder copyWith({ 71 | String? id, 72 | String? name, 73 | String? description, 74 | DateTime? createdAt, 75 | DateTime? updatedAt, 76 | bool? isVault, 77 | int? sortOrder, 78 | bool? hasPassword, 79 | bool? useBiometric, 80 | String? noteFormat, 81 | }) { 82 | return NoteFolder( 83 | id: id ?? this.id, 84 | name: name ?? this.name, 85 | description: description ?? this.description, 86 | createdAt: createdAt ?? this.createdAt, 87 | updatedAt: updatedAt ?? DateTime.now(), 88 | isVault: isVault ?? this.isVault, 89 | sortOrder: sortOrder ?? this.sortOrder, 90 | hasPassword: hasPassword ?? this.hasPassword, 91 | useBiometric: useBiometric ?? this.useBiometric, 92 | noteFormat: noteFormat ?? this.noteFormat, 93 | ); 94 | } 95 | 96 | @override 97 | String toString() { 98 | return 'NoteFolder(id: $id, name: $name, isVault: $isVault)'; 99 | } 100 | 101 | @override 102 | bool operator ==(Object other) { 103 | if (identical(this, other)) return true; 104 | return other is NoteFolder && other.id == id; 105 | } 106 | 107 | @override 108 | int get hashCode => id.hashCode; 109 | 110 | Map toJson() { 111 | return { 112 | 'id': id, 113 | 'name': name, 114 | 'description': description, 115 | 'createdAt': createdAt.toIso8601String(), 116 | 'updatedAt': updatedAt.toIso8601String(), 117 | 'isVault': isVault, 118 | 'sortOrder': sortOrder, 119 | 'hasPassword': hasPassword, 120 | 'useBiometric': useBiometric, 121 | 'noteFormat': noteFormat, 122 | }; 123 | } 124 | 125 | factory NoteFolder.fromJson(Map json) { 126 | return NoteFolder( 127 | id: json['id'] as String, 128 | name: json['name'] as String, 129 | description: json['description'] as String?, 130 | createdAt: DateTime.parse(json['createdAt'] as String), 131 | updatedAt: DateTime.parse(json['updatedAt'] as String), 132 | isVault: json['isVault'] as bool? ?? false, 133 | sortOrder: json['sortOrder'] as int? ?? 0, 134 | hasPassword: json['hasPassword'] as bool? ?? false, 135 | useBiometric: json['useBiometric'] as bool? ?? true, 136 | noteFormat: json['noteFormat'] as String? ?? 'markdown', 137 | ); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /lib/widgets/system_permission_dialogs.dart: -------------------------------------------------------------------------------- 1 | // Trudido - A privacy-focused todo and notes app 2 | // Copyright (C) 2025 Dominik Müller 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with this program. If not, see . 16 | 17 | import 'package:flutter/material.dart'; 18 | import '../services/system_settings_service.dart'; 19 | import '../services/navigation_service.dart'; 20 | 21 | Future showExactAlarmDialogIfNeeded(BuildContext context) async { 22 | final service = SystemSettingsService.instance; 23 | if (await service.canScheduleExactAlarms()) return true; 24 | final dialogContext = await _materialDialogContext(context); 25 | bool? proceed; 26 | if (dialogContext.mounted) { 27 | proceed = await showDialog( 28 | context: dialogContext, 29 | builder: (ctx) => AlertDialog( 30 | title: const Text('Enable Exact Alarms'), 31 | content: const Text( 32 | 'Exact alarms keep reminders precise even when:\n' 33 | '- Device is idle / in Doze\n' 34 | '- After overnight charging\n' 35 | '- During short snoozes (5-15 min)\n\n' 36 | 'Android requires a manual toggle. We\'ll open system settings; enable it then come back.', 37 | ), 38 | actions: [ 39 | TextButton( 40 | onPressed: () => Navigator.of(ctx).pop(false), 41 | child: const Text('Later'), 42 | ), 43 | FilledButton( 44 | onPressed: () => Navigator.of(ctx).pop(true), 45 | child: const Text('Open Settings'), 46 | ), 47 | ], 48 | ), 49 | ); 50 | } 51 | if (proceed == true) { 52 | await service.openExactAlarmSettings(); 53 | await Future.delayed(const Duration(milliseconds: 200)); 54 | } 55 | return service.canScheduleExactAlarms(); 56 | } 57 | 58 | Future showBatteryOptimizationDialogIfNeeded(BuildContext context) async { 59 | final service = SystemSettingsService.instance; 60 | if (await service.isIgnoringBatteryOptimizations()) return true; 61 | final dialogContext = await _materialDialogContext(context); 62 | bool? proceed; 63 | if (dialogContext.mounted) { 64 | proceed = await showDialog( 65 | context: dialogContext, 66 | builder: (ctx) => AlertDialog( 67 | title: const Text('Allow Unrestricted Background'), 68 | content: const Text( 69 | 'To prevent the system from delaying or cancelling reminders, allow the app to bypass battery optimization. ' 70 | 'We will open the system screen; accept the prompt (or add to the allowlist), then return here.', 71 | ), 72 | actions: [ 73 | TextButton( 74 | onPressed: () => Navigator.of(ctx).pop(false), 75 | child: const Text('Later'), 76 | ), 77 | FilledButton( 78 | onPressed: () => Navigator.of(ctx).pop(true), 79 | child: const Text('Open Settings'), 80 | ), 81 | ], 82 | ), 83 | ); 84 | } 85 | if (proceed == true) { 86 | await service.requestIgnoreBatteryOptimizations(); 87 | await Future.delayed(const Duration(milliseconds: 200)); 88 | } 89 | return service.isIgnoringBatteryOptimizations(); 90 | } 91 | 92 | Future _materialDialogContext(BuildContext fallback) async { 93 | for (var i = 0; i < 12; i++) { 94 | final ctx = NavigationService.context ?? fallback; 95 | final has = 96 | Localizations.of(ctx, MaterialLocalizations) != 97 | null; 98 | if (has) return ctx; 99 | await Future.delayed(Duration(milliseconds: 30 * (i + 1))); 100 | } 101 | return NavigationService.context ?? fallback; 102 | } 103 | 104 | Future showExactAlarmDialogIfNeededAuto() async { 105 | final ctx = NavigationService.navigatorKey.currentContext; 106 | if (ctx == null) return false; 107 | return showExactAlarmDialogIfNeeded(ctx); 108 | } 109 | 110 | Future showBatteryOptimizationDialogIfNeededAuto() async { 111 | final ctx = NavigationService.navigatorKey.currentContext; 112 | if (ctx == null) return false; 113 | return showBatteryOptimizationDialogIfNeeded(ctx); 114 | } 115 | -------------------------------------------------------------------------------- /lib/services/files_channel.dart: -------------------------------------------------------------------------------- 1 | // Trudido - A privacy-focused todo and notes app 2 | // Copyright (C) 2025 Dominik Müller 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with this program. If not, see . 16 | 17 | import 'dart:convert'; 18 | import 'dart:io' show Platform; 19 | import 'package:flutter/foundation.dart'; 20 | import 'package:flutter/services.dart'; 21 | import '../services/storage_service.dart'; 22 | 23 | /// FilesChannel bridges Flutter and native (Android) import/export using SAF. 24 | /// On Android, it uses MethodChannel('app.files') to trigger native pickers. 25 | /// On iOS/web/desktop, it no-ops for now. 26 | class FilesChannel { 27 | FilesChannel._(); 28 | static final FilesChannel instance = FilesChannel._(); 29 | 30 | static const MethodChannel _ch = MethodChannel('app.files'); 31 | 32 | bool _initialized = false; 33 | Function(String)? _onImportComplete; 34 | Function(String)? _onImportError; 35 | Function()? _onRefreshNeeded; 36 | Function(String)? _onBackupFolderSelected; 37 | 38 | void setImportCallbacks({ 39 | Function(String)? onComplete, 40 | Function(String)? onError, 41 | Function()? onRefreshNeeded, 42 | }) { 43 | _onImportComplete = onComplete; 44 | _onImportError = onError; 45 | _onRefreshNeeded = onRefreshNeeded; 46 | } 47 | 48 | void setBackupFolderCallback(Function(String)? onFolderSelected) { 49 | _onBackupFolderSelected = onFolderSelected; 50 | } 51 | 52 | Future ensureInitialized() async { 53 | if (_initialized) return; 54 | if (!Platform.isAndroid) { 55 | _initialized = true; 56 | return; 57 | } 58 | _ch.setMethodCallHandler((call) async { 59 | if (call.method == 'onImport') { 60 | final jsonStr = call.arguments as String?; 61 | debugPrint( 62 | '[FilesChannel] Received import data, length: ${jsonStr?.length ?? 0}', 63 | ); 64 | if (jsonStr == null) { 65 | debugPrint('[FilesChannel] Import data is null, aborting'); 66 | _onImportError?.call('No data received'); 67 | return; 68 | } 69 | try { 70 | debugPrint('[FilesChannel] Parsing JSON...'); 71 | final map = json.decode(jsonStr) as Map; 72 | debugPrint( 73 | '[FilesChannel] JSON parsed successfully, keys: ${map.keys.toList()}', 74 | ); 75 | debugPrint('[FilesChannel] Calling StorageService.importData...'); 76 | await StorageService.importData(map); 77 | debugPrint('[FilesChannel] Import completed successfully'); 78 | _onRefreshNeeded?.call(); 79 | _onImportComplete?.call('Import completed successfully'); 80 | } catch (e, st) { 81 | debugPrint('[FilesChannel] import handler error: $e'); 82 | debugPrint('[FilesChannel] Stack trace: $st'); 83 | _onImportError?.call('Import failed: $e'); 84 | } 85 | } else if (call.method == 'onBackupFolderSelected') { 86 | final folderUri = call.arguments as String?; 87 | debugPrint('[FilesChannel] Backup folder selected: $folderUri'); 88 | if (folderUri != null) { 89 | _onBackupFolderSelected?.call(folderUri); 90 | } 91 | } 92 | }); 93 | _initialized = true; 94 | } 95 | 96 | Future startExport() async { 97 | if (!Platform.isAndroid) return; 98 | try { 99 | // Keep call to ensureInitialized in case caller forgot 100 | await ensureInitialized(); 101 | 102 | // Get actual data to export 103 | final exportData = await StorageService.exportData(); 104 | final jsonString = json.encode(exportData); 105 | 106 | // Trigger native export flow with real data 107 | await _ch.invokeMethod('startExport', jsonString); 108 | } catch (e, st) { 109 | debugPrint('[FilesChannel] startExport error: $e\n$st'); 110 | } 111 | } 112 | 113 | Future startImport() async { 114 | if (!Platform.isAndroid) return; 115 | try { 116 | await ensureInitialized(); 117 | await _ch.invokeMethod('startImport'); 118 | } catch (e, st) { 119 | debugPrint('[FilesChannel] startImport error: $e\n$st'); 120 | } 121 | } 122 | } 123 | --------------------------------------------------------------------------------