├── .gitignore ├── .idea ├── codeStyles │ └── Project.xml ├── compiler.xml ├── encodings.xml ├── inspectionProfiles │ └── Project_Default.xml ├── jarRepositories.xml ├── kotlinc.xml ├── misc.xml ├── modules.xml └── vcs.xml ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── objectbox-models │ ├── default.json │ └── default.json.bak ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── ic_launcher-web.png │ ├── java │ └── com │ │ └── louiskirsch │ │ └── quickdynalist │ │ ├── AdvancedItemActivity.kt │ │ ├── BaseItemListFragment.kt │ │ ├── DetailsActivity.kt │ │ ├── DialogSetupFragment.kt │ │ ├── Dynalist.kt │ │ ├── DynalistApp.kt │ │ ├── DynalistLinkSpan.kt │ │ ├── DynalistTagSpan.kt │ │ ├── EditFilterActivity.kt │ │ ├── Events.kt │ │ ├── FilteredItemListFragment.kt │ │ ├── InboxConfigurationFragment.kt │ │ ├── InsertBarFragment.kt │ │ ├── ItemListFragment.kt │ │ ├── Location.kt │ │ ├── LoginFragment.kt │ │ ├── MainActivity.kt │ │ ├── NavigationActivity.kt │ │ ├── OnLinkTouchListener.kt │ │ ├── ProcessTextActivity.kt │ │ ├── QuickDialogTileService.kt │ │ ├── SearchActivity.kt │ │ ├── SettingsActivity.kt │ │ ├── ShortcutActivity.kt │ │ ├── SyncShortcutActivity.kt │ │ ├── SyncStatusFragment.kt │ │ ├── TagManagerFragment.kt │ │ ├── ViewModels.kt │ │ ├── WizardActivity.kt │ │ ├── adapters │ │ ├── EmojiAdapter.kt │ │ ├── FilterAdapter.kt │ │ ├── FilterItemListAdapter.kt │ │ ├── ItemListAdapter.kt │ │ ├── ItemTouchCallback.kt │ │ ├── SectionedAdapter.kt │ │ └── SwipeBackgroundDrawer.kt │ │ ├── jobs │ │ ├── AddItemJob.kt │ │ ├── BulkEditItemJob.kt │ │ ├── CloneItemJob.kt │ │ ├── DeleteItemJob.kt │ │ ├── EditItemJob.kt │ │ ├── Exceptions.kt │ │ ├── ItemJob.kt │ │ ├── JobService.kt │ │ ├── MoveItemJob.kt │ │ ├── SyncJob.kt │ │ └── VerifyTokenJob.kt │ │ ├── network │ │ ├── AddItemTreeService.kt │ │ └── DynalistService.kt │ │ ├── objectbox │ │ ├── DocumentTreeNode.kt │ │ ├── DynalistDocument.kt │ │ ├── DynalistFolder.kt │ │ ├── DynalistItem.kt │ │ ├── DynalistItemFilter.kt │ │ ├── DynalistItemMetaData.kt │ │ ├── DynalistTag.kt │ │ ├── Exceptions.kt │ │ ├── SubscriberOBLiveData.kt │ │ └── TransformedOBLiveData.kt │ │ ├── text │ │ ├── EnhancedMovementMethod.kt │ │ ├── IndentedBulletSpan.kt │ │ ├── ThemedSpan.kt │ │ └── TintedImageSpan.kt │ │ ├── utils │ │ ├── DynalistEditActionHelper.kt │ │ ├── EmojiFactory.kt │ │ ├── Extensions.kt │ │ ├── ImageCache.kt │ │ └── SpeechRecognitionHelper.kt │ │ ├── views │ │ ├── BottomBarScrollingViewBehavior.kt │ │ ├── MoveUpwardBehavior.kt │ │ ├── NestedCoordinatorLayout.kt │ │ ├── ScrollFABBehavior.kt │ │ └── SquareImageView.kt │ │ └── widget │ │ ├── ListAppWidget.kt │ │ ├── ListAppWidgetConfigurationReceiver.kt │ │ ├── ListAppWidgetConfigureActivity.kt │ │ └── ListAppWidgetService.kt │ └── res │ ├── drawable-night │ ├── dashed_border.xml │ └── dashed_border_activated.xml │ ├── drawable-nodpi │ └── appwidget_preview.png │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ ├── activatable_selectable_background.xml │ ├── circular_progress_bar.xml │ ├── dashed_border.xml │ ├── dashed_border_activated.xml │ ├── dropoff_background.xml │ ├── ic_action_advanced_item.xml │ ├── ic_action_change_color.xml │ ├── ic_action_clear.xml │ ├── ic_action_create_filter.xml │ ├── ic_action_create_shortcut.xml │ ├── ic_action_date.xml │ ├── ic_action_details.xml │ ├── ic_action_discard.xml │ ├── ic_action_duplicate.xml │ ├── ic_action_edit.xml │ ├── ic_action_edit_filter.xml │ ├── ic_action_goto_parent.xml │ ├── ic_action_help.xml │ ├── ic_action_item_list.xml │ ├── ic_action_jump_to_bottom.xml │ ├── ic_action_link.xml │ ├── ic_action_manage_tags.xml │ ├── ic_action_more.xml │ ├── ic_action_move.xml │ ├── ic_action_move_inbox.xml │ ├── ic_action_open_large.xml │ ├── ic_action_open_settings.xml │ ├── ic_action_rate_quickdynalist.xml │ ├── ic_action_record_speech.xml │ ├── ic_action_search.xml │ ├── ic_action_search_white.xml │ ├── ic_action_send_bug_report.xml │ ├── ic_action_send_item.xml │ ├── ic_action_share.xml │ ├── ic_action_share_quickdynalist.xml │ ├── ic_action_show_image.xml │ ├── ic_action_show_text.xml │ ├── ic_action_sync.xml │ ├── ic_backward_link.xml │ ├── ic_chip_add.xml │ ├── ic_folder.xml │ ├── ic_folder_open.xml │ ├── ic_folder_stateful.xml │ ├── ic_forward_link.xml │ ├── ic_launcher_background.xml │ ├── ic_shortcut_quick_dialog_foreground.xml │ ├── ic_shortcut_record_speech_foreground.xml │ ├── ic_shortcut_search_item_foreground.xml │ ├── ic_shortcut_sync_foreground.xml │ ├── ic_shortcut_type_dialog.xml │ ├── ic_shortcut_type_list.xml │ ├── ic_swipe_delete.xml │ ├── ic_swipe_edit.xml │ ├── ic_tile_quick_dialog.xml │ ├── item_color_picker.xml │ └── side_nav_bar.xml │ ├── layout-w900dp │ └── activity_navigation.xml │ ├── layout │ ├── activity_advanced_item.xml │ ├── activity_auth.xml │ ├── activity_details.xml │ ├── activity_edit_filter.xml │ ├── activity_main.xml │ ├── activity_navigation.xml │ ├── activity_search.xml │ ├── activity_settings.xml │ ├── activity_shortcut.xml │ ├── activity_wizard.xml │ ├── app_bar_navigation.xml │ ├── dropdown_menu_popup_item.xml │ ├── emoji_list_item.xml │ ├── fragment_dialog_setup.xml │ ├── fragment_inbox_configuration.xml │ ├── fragment_insert_bar.xml │ ├── fragment_item_list.xml │ ├── fragment_login.xml │ ├── fragment_sync_status.xml │ ├── fragment_tag_manager.xml │ ├── item_list_dropoff.xml │ ├── item_list_item.xml │ ├── list_app_widget.xml │ ├── list_app_widget_configure.xml │ ├── list_app_widget_item.xml │ ├── list_separator.xml │ ├── menu_color_picker.xml │ ├── nav_header_navigation.xml │ └── search_list_item.xml │ ├── menu │ ├── activity_details.xml │ ├── activity_edit_filter.xml │ ├── activity_list_app_widget_configure.xml │ ├── activity_navigation_drawer.xml │ ├── activity_shortcut.xml │ ├── advanced_item_activity_menu.xml │ ├── filter_item_list_menu.xml │ ├── item_list_activity_menu.xml │ ├── item_list_popup_image_extension.xml │ ├── item_list_popup_menu.xml │ ├── item_text_insert_context_menu.xml │ ├── item_text_selection_context_menu.xml │ ├── navigation.xml │ └── quick_dialog_menu.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ ├── ic_launcher_round.xml │ ├── ic_shortcut_quick_dialog.xml │ ├── ic_shortcut_quick_dialog_round.xml │ ├── ic_shortcut_record_speech.xml │ ├── ic_shortcut_record_speech_round.xml │ ├── ic_shortcut_search_item.xml │ ├── ic_shortcut_search_item_round.xml │ ├── ic_shortcut_sync.xml │ └── ic_shortcut_sync_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ ├── ic_launcher_round.png │ ├── ic_shortcut_quick_dialog.png │ ├── ic_shortcut_quick_dialog_round.png │ ├── ic_shortcut_record_speech.png │ ├── ic_shortcut_record_speech_round.png │ ├── ic_shortcut_search_item.png │ ├── ic_shortcut_search_item_round.png │ ├── ic_shortcut_sync.png │ └── ic_shortcut_sync_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ ├── ic_launcher_round.png │ ├── ic_shortcut_quick_dialog.png │ ├── ic_shortcut_quick_dialog_round.png │ ├── ic_shortcut_record_speech.png │ ├── ic_shortcut_record_speech_round.png │ ├── ic_shortcut_search_item.png │ ├── ic_shortcut_search_item_round.png │ ├── ic_shortcut_sync.png │ └── ic_shortcut_sync_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ ├── ic_launcher_round.png │ ├── ic_shortcut_quick_dialog.png │ ├── ic_shortcut_quick_dialog_round.png │ ├── ic_shortcut_record_speech.png │ ├── ic_shortcut_record_speech_round.png │ ├── ic_shortcut_search_item.png │ ├── ic_shortcut_search_item_round.png │ ├── ic_shortcut_sync.png │ └── ic_shortcut_sync_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ ├── ic_launcher_round.png │ ├── ic_shortcut_quick_dialog.png │ ├── ic_shortcut_quick_dialog_round.png │ ├── ic_shortcut_record_speech.png │ ├── ic_shortcut_record_speech_round.png │ ├── ic_shortcut_search_item.png │ ├── ic_shortcut_search_item_round.png │ ├── ic_shortcut_sync.png │ └── ic_shortcut_sync_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ ├── ic_launcher_round.png │ ├── ic_shortcut_quick_dialog.png │ ├── ic_shortcut_quick_dialog_round.png │ ├── ic_shortcut_record_speech.png │ ├── ic_shortcut_record_speech_round.png │ ├── ic_shortcut_search_item.png │ ├── ic_shortcut_search_item_round.png │ ├── ic_shortcut_sync.png │ └── ic_shortcut_sync_round.png │ ├── raw │ └── dialog_tutorial.webm │ ├── transition │ ├── change_view_bounds.xml │ └── window_explode.xml │ ├── values-night │ ├── colors.xml │ ├── styles.xml │ └── widget.xml │ ├── values-v28 │ └── preferences.xml │ ├── values-w900dp │ ├── dimens.xml │ └── styles.xml │ ├── values │ ├── actions.xml │ ├── colors.xml │ ├── dimens.xml │ ├── ic_launcher_background.xml │ ├── ic_shortcut_background.xml │ ├── ic_shortcut_quick_dialog_background.xml │ ├── ids.xml │ ├── preferences.xml │ ├── request_codes.xml │ ├── strings.xml │ ├── styles.xml │ └── widget.xml │ └── xml │ ├── chip.xml │ ├── file_paths.xml │ ├── list_app_widget_info.xml │ ├── preferences.xml │ └── shortcuts.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── hashkode ├── .gitignore ├── build.gradle └── src │ └── main │ └── java │ └── nl │ └── pvdberg │ └── hashkode │ ├── Difference.kt │ ├── Equals.kt │ ├── HashKode.kt │ ├── HashKodeContext.kt │ └── Hashing.kt └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/android 3 | 4 | ### Android ### 5 | # Built application files 6 | *.apk 7 | *.ap_ 8 | 9 | # Files for the ART/Dalvik VM 10 | *.dex 11 | 12 | # Java class files 13 | *.class 14 | 15 | # Generated files 16 | bin/ 17 | gen/ 18 | out/ 19 | 20 | # Gradle files 21 | .gradle/ 22 | build/ 23 | 24 | # Local configuration file (sdk path, etc) 25 | local.properties 26 | 27 | # Proguard folder generated by Eclipse 28 | proguard/ 29 | 30 | # Log Files 31 | *.log 32 | 33 | # Android Studio Navigation editor temp files 34 | .navigation/ 35 | 36 | # Android Studio captures folder 37 | captures/ 38 | 39 | # IntelliJ 40 | *.iml 41 | .idea/workspace.xml 42 | .idea/tasks.xml 43 | .idea/gradle.xml 44 | .idea/assetWizardSettings.xml 45 | .idea/dictionaries 46 | .idea/libraries 47 | .idea/caches 48 | 49 | # Keystore files 50 | # Uncomment the following line if you do not want to check your keystore files in. 51 | #*.jks 52 | 53 | # External native build folder generated in Android Studio 2.2 and later 54 | .externalNativeBuild 55 | 56 | # Google Services (e.g. APIs or Firebase) 57 | google-services.json 58 | 59 | # Freeline 60 | freeline.py 61 | freeline/ 62 | freeline_project_description.json 63 | 64 | # fastlane 65 | fastlane/report.xml 66 | fastlane/Preview.html 67 | fastlane/screenshots 68 | fastlane/test_output 69 | fastlane/readme.md 70 | 71 | ### Android Patch ### 72 | gen-external-apklibs 73 | 74 | 75 | # End of https://www.gitignore.io/api/android 76 | 77 | app/release 78 | .DS_Store 79 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 29 | 30 | 34 | 35 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 11 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # QuickDynalist 2 | 3 | Quick Dynalist is an independent open-source client for [Dynalist](https://dynalist.io/) (an enhanced [Workflowy](https://workflowy.com/)). 4 | Its focus is on providing rapid access to any part of your Dynalist database via shortcuts, widgets, and other deep integrations into Android. 5 | New items should be added with as little overhead as possible to make Dynalist a true extension to your brain. 6 | We also provide an extremly responsive native experience optimized for the Android platform. 7 | 8 | The quickest access to your Dynalist Beautiful lists with infinite depth and possibilities Widgets & Shortcuts put you into control over all your notes 9 | 10 | Get it on Google Play 11 | 12 | ## Features 13 | 14 | Dynalist is the best place for all of your todos, ideas, and notes. 15 | Quick Dynalist transforms your mobile phone into your extended memory: See your lists, add & edit new items amazingly fast, search & filter for anything. 16 | 17 | This app requires a Dynalist account: http://dynalist.io 18 | 19 | - A much faster experience compared to the original Dynalist App 20 | - Quickly add items anywhere using shortcuts and widgets 21 | - Quickly share text, URLs, and more to your inbox 22 | - Even works offline 23 | - Use the Google Assistant "Add a note" or "Note to myself" 24 | - Browse & edit the contents of your different inboxes and documents 25 | - Create permanent filters that show you exactly what you need across your Dynalist 26 | - Create shortcuts to jump directly to a specific location 27 | - Create widgets to see exactly what you need right from your homescreen 28 | - Share your favorite list with other apps and people. 29 | Add new items to dynalist quickly (native android app) 30 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | 4 | apply plugin: 'kotlin-android-extensions' 5 | apply plugin: 'kotlin-kapt' 6 | apply plugin: 'io.objectbox' 7 | 8 | android { 9 | compileSdk 33 10 | defaultConfig { 11 | applicationId "com.louiskirsch.quickdynalist" 12 | minSdkVersion 21 13 | targetSdkVersion 33 14 | versionCode 55 15 | versionName "2.10.1" 16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 17 | } 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 22 | } 23 | } 24 | compileOptions { 25 | sourceCompatibility JavaVersion.VERSION_17 26 | targetCompatibility JavaVersion.VERSION_17 27 | } 28 | namespace 'com.louiskirsch.quickdynalist' 29 | } 30 | 31 | dependencies { 32 | def lifecycle_version = "2.0.0" 33 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" 34 | implementation 'org.jetbrains.anko:anko-commons:0.10.7' 35 | implementation fileTree(include: ['*.jar'], dir: 'libs') 36 | implementation 'androidx.constraintlayout:constraintlayout:2.0.0-alpha3' 37 | implementation 'androidx.legacy:legacy-support-v4:1.0.0' 38 | implementation 'androidx.appcompat:appcompat:1.1.0-alpha03' 39 | testImplementation 'junit:junit:4.12' 40 | androidTestImplementation 'androidx.test:runner:1.1.1' 41 | // androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' 42 | // implementation 'org.jetbrains:annotations-java5:16.0.3' 43 | implementation 'com.birbit:android-priority-jobqueue:2.0.1' 44 | implementation 'org.greenrobot:eventbus:3.1.1' 45 | implementation 'com.squareup.retrofit2:retrofit:2.5.0' 46 | implementation 'com.squareup.retrofit2:converter-gson:2.5.0' 47 | implementation 'com.google.android.material:material:1.1.0-alpha02' 48 | implementation "io.objectbox:objectbox-kotlin:$objectboxVersion" 49 | implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version" 50 | implementation "androidx.preference:preference:1.0.0" 51 | implementation 'com.squareup.picasso:picasso:2.71828' 52 | implementation 'com.github.timediv:jlatexmath-android:e661bda' 53 | implementation 'ru.noties:jlatexmath-android-font-cyrillic:0.1.0' 54 | implementation 'ru.noties:jlatexmath-android-font-greek:0.1.0' 55 | implementation project(':hashkode') 56 | } 57 | 58 | configurations { 59 | cleanedAnnotations 60 | compile.exclude group: 'org.jetbrains', module: 'annotations' 61 | } 62 | 63 | repositories { 64 | mavenCentral() 65 | jcenter() 66 | } 67 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/louiskirsch/QuickDynalist/451cd22f69f2c467aeec22c012299d97af462cae/app/src/main/ic_launcher-web.png -------------------------------------------------------------------------------- /app/src/main/java/com/louiskirsch/quickdynalist/DynalistLinkSpan.kt: -------------------------------------------------------------------------------- 1 | package com.louiskirsch.quickdynalist 2 | 3 | import android.text.style.ClickableSpan 4 | import android.view.View 5 | import com.louiskirsch.quickdynalist.objectbox.DynalistItem 6 | import io.objectbox.kotlin.boxFor 7 | import org.greenrobot.eventbus.EventBus 8 | 9 | class DynalistLinkSpan(val item: DynalistItem): ClickableSpan() { 10 | 11 | override fun onClick(widget: View) { 12 | EventBus.getDefault().post(DynalistLocateEvent(item)) 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/louiskirsch/quickdynalist/DynalistTagSpan.kt: -------------------------------------------------------------------------------- 1 | package com.louiskirsch.quickdynalist 2 | 3 | import android.text.style.ClickableSpan 4 | import android.view.View 5 | import com.louiskirsch.quickdynalist.objectbox.DynalistItem 6 | import com.louiskirsch.quickdynalist.objectbox.DynalistItemFilter 7 | import com.louiskirsch.quickdynalist.objectbox.DynalistTag 8 | import io.objectbox.kotlin.boxFor 9 | import org.greenrobot.eventbus.EventBus 10 | 11 | class DynalistTagSpan(private val tag: DynalistTag): ClickableSpan() { 12 | 13 | override fun onClick(widget: View) { 14 | val filter = DynalistItemFilter().apply { 15 | name = widget.context.getString(R.string.filter_name_tag, tag.fullName) 16 | tags.add(tag) 17 | searchDepth = DynalistItemFilter.MAX_SEARCH_DEPTH 18 | } 19 | EventBus.getDefault().post(DynalistFilterEvent(filter)) 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/louiskirsch/quickdynalist/Events.kt: -------------------------------------------------------------------------------- 1 | package com.louiskirsch.quickdynalist 2 | 3 | import android.net.Uri 4 | import com.louiskirsch.quickdynalist.objectbox.DynalistItem 5 | import com.louiskirsch.quickdynalist.objectbox.DynalistItemFilter 6 | 7 | class AuthenticatedEvent(val success: Boolean) 8 | class ItemEvent(val success: Boolean, val retrying: Boolean = false) 9 | class InboxEvent(val configured: Boolean) 10 | class RateLimitDelay(val delay: Long, val jobTag: String) 11 | class DynalistLocateEvent(val item: DynalistItem) 12 | class DynalistFilterEvent(val filter: DynalistItemFilter) 13 | class ForbiddenImageEvent(val uri: Uri) 14 | class ItemAddedEvent(val item: DynalistItem) 15 | 16 | enum class SyncStatus { RUNNING, NOT_RUNNING, SUCCESS, NO_SUCCESS } 17 | class SyncEvent(val status: SyncStatus, val isManual: Boolean) 18 | class SyncProgressEvent(val progress: Float) 19 | 20 | class NightModeChangedEvent 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/louiskirsch/quickdynalist/LoginFragment.kt: -------------------------------------------------------------------------------- 1 | package com.louiskirsch.quickdynalist 2 | 3 | 4 | import android.os.Bundle 5 | import android.text.Editable 6 | import android.text.TextWatcher 7 | import androidx.fragment.app.Fragment 8 | import android.view.LayoutInflater 9 | import android.view.View 10 | import android.view.ViewGroup 11 | import com.louiskirsch.quickdynalist.jobs.SyncJob 12 | import com.louiskirsch.quickdynalist.jobs.VerifyTokenJob 13 | import kotlinx.android.synthetic.main.fragment_login.* 14 | import org.greenrobot.eventbus.EventBus 15 | import org.greenrobot.eventbus.Subscribe 16 | import org.greenrobot.eventbus.ThreadMode 17 | import org.jetbrains.anko.browse 18 | import org.jetbrains.anko.toast 19 | 20 | class LoginFragment : Fragment() { 21 | 22 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, 23 | savedInstanceState: Bundle?): View? { 24 | return inflater.inflate(R.layout.fragment_login, container, false) 25 | 26 | } 27 | 28 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 29 | super.onViewCreated(view, savedInstanceState) 30 | auth_open_browser.setOnClickListener { 31 | context!!.browse("https://dynalist.io/developer") 32 | } 33 | auth_submit_token.isEnabled = false 34 | auth_submit_token.setOnClickListener { 35 | val token = auth_token.text.toString() 36 | val jobManager = DynalistApp.instance.jobManager 37 | val job = VerifyTokenJob(token) 38 | jobManager.addJobInBackground(job) 39 | auth_submit_token.isEnabled = false 40 | } 41 | auth_token.addTextChangedListener(object : TextWatcher{ 42 | override fun afterTextChanged(s: Editable?) {} 43 | override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} 44 | override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { 45 | auth_submit_token.isEnabled = s?.isNotBlank() ?: false 46 | } 47 | 48 | }) 49 | val dynalist = Dynalist(context!!) 50 | sync_mobile_data.isChecked = dynalist.syncMobileData 51 | sync_mobile_data.setOnCheckedChangeListener { _, isChecked -> 52 | dynalist.syncMobileData = isChecked 53 | } 54 | } 55 | 56 | override fun onStart() { 57 | super.onStart() 58 | EventBus.getDefault().register(this) 59 | } 60 | 61 | override fun onStop() { 62 | super.onStop() 63 | EventBus.getDefault().unregister(this) 64 | } 65 | 66 | @Subscribe(threadMode = ThreadMode.MAIN) 67 | fun onAuthenticationEvent(event: AuthenticatedEvent) { 68 | if (!event.success) { 69 | auth_token_layout.error = getString(R.string.token_invalid) 70 | } else { 71 | val job = SyncJob(requireUnmeteredNetwork = false, isManual = true) 72 | DynalistApp.instance.jobManager.addJobInBackground(job) 73 | activity!!.supportFragmentManager.beginTransaction().apply { 74 | setCustomAnimations(android.R.anim.fade_in, android.R.anim.fade_out) 75 | replace(R.id.fragment_container, SyncStatusFragment()) 76 | commit() 77 | } 78 | } 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /app/src/main/java/com/louiskirsch/quickdynalist/OnLinkTouchListener.kt: -------------------------------------------------------------------------------- 1 | package com.louiskirsch.quickdynalist 2 | 3 | import android.annotation.SuppressLint 4 | import android.view.MotionEvent 5 | import android.view.View 6 | import android.text.style.ClickableSpan 7 | import android.widget.TextView 8 | import android.text.Spannable 9 | 10 | 11 | /** 12 | * Allows TextViews with clickable links while still not handling all touch events 13 | */ 14 | class OnLinkTouchListener: View.OnTouchListener { 15 | 16 | @SuppressLint("ClickableViewAccessibility") 17 | override fun onTouch(v: View?, event: MotionEvent?): Boolean { 18 | var ret = false 19 | val text = (v as TextView).text 20 | val stext = Spannable.Factory.getInstance().newSpannable(text) 21 | val action = event!!.action 22 | 23 | if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) { 24 | var x = event.x 25 | var y = event.y 26 | 27 | x -= v.totalPaddingLeft 28 | y -= v.totalPaddingTop 29 | 30 | x += v.scrollX 31 | y += v.scrollY 32 | 33 | val layout = v.layout 34 | val line = layout.getLineForVertical(y.toInt()) 35 | val off = layout.getOffsetForHorizontal(line, x) 36 | 37 | val link = stext.getSpans(off, off, ClickableSpan::class.java) 38 | 39 | if (link.isNotEmpty()) { 40 | if (action == MotionEvent.ACTION_UP) { 41 | link[0].onClick(v) 42 | } 43 | ret = true 44 | } 45 | } 46 | return ret 47 | } 48 | } -------------------------------------------------------------------------------- /app/src/main/java/com/louiskirsch/quickdynalist/QuickDialogTileService.kt: -------------------------------------------------------------------------------- 1 | package com.louiskirsch.quickdynalist 2 | 3 | import android.content.Intent 4 | import android.os.Build 5 | import android.service.quicksettings.TileService 6 | import androidx.annotation.RequiresApi 7 | 8 | @RequiresApi(api = Build.VERSION_CODES.N) 9 | class QuickDialogTileService : TileService() { 10 | 11 | private fun showQuickDialog() { 12 | startActivityAndCollapse(Intent(this, MainActivity::class.java).apply { 13 | addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 14 | addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) 15 | }) 16 | } 17 | 18 | override fun onClick() { 19 | if (isLocked) { 20 | unlockAndRun { showQuickDialog() } 21 | } else { 22 | showQuickDialog() 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/louiskirsch/quickdynalist/SearchActivity.kt: -------------------------------------------------------------------------------- 1 | package com.louiskirsch.quickdynalist 2 | 3 | import android.app.Activity 4 | import android.app.ActivityOptions 5 | import android.app.PendingIntent 6 | import android.content.Intent 7 | import androidx.appcompat.app.AppCompatActivity 8 | import android.os.Bundle 9 | import android.os.Parcelable 10 | import android.text.Editable 11 | import android.text.TextWatcher 12 | import androidx.lifecycle.Observer 13 | import androidx.lifecycle.ViewModelProviders 14 | import androidx.recyclerview.widget.LinearLayoutManager 15 | import com.louiskirsch.quickdynalist.adapters.FilterItemListAdapter 16 | import com.louiskirsch.quickdynalist.objectbox.DynalistItem 17 | import com.louiskirsch.quickdynalist.utils.fixedFinishAfterTransition 18 | import kotlinx.android.synthetic.main.activity_search.* 19 | 20 | class SearchActivity : AppCompatActivity() { 21 | 22 | companion object { 23 | const val ACTION_SEARCH_DISPLAY_ITEM = "com.louiskirsch.quickdynalist.SEARCH_DISPLAY_ITEM" 24 | } 25 | 26 | override fun onCreate(savedInstanceState: Bundle?) { 27 | super.onCreate(savedInstanceState) 28 | setContentView(R.layout.activity_search) 29 | window.allowEnterTransitionOverlap = true 30 | 31 | val adapter = FilterItemListAdapter(this) 32 | searchResults.layoutManager = LinearLayoutManager(this) 33 | searchResults.adapter = adapter 34 | 35 | adapter.onClickListener = { finishWithSelectedItem(it) } 36 | 37 | val model = ViewModelProviders.of(this).get(DynalistItemViewModel::class.java) 38 | model.searchTerm.value = "" 39 | model.searchItemsLiveData.observe(this, Observer { 40 | adapter.updateItems(it) 41 | }) 42 | 43 | searchBar.addTextChangedListener(object: TextWatcher { 44 | override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} 45 | override fun afterTextChanged(s: Editable?) {} 46 | override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { 47 | val searchTerm = s.toString().trim() 48 | model.searchTerm.value = searchTerm 49 | adapter.searchTerm = searchTerm 50 | } 51 | }) 52 | } 53 | 54 | private fun finishWithSelectedItem(item: DynalistItem) { 55 | val result = Intent().apply { 56 | putExtra(DynalistApp.EXTRA_DISPLAY_ITEM, item as Parcelable) 57 | if (intent.hasExtra("payload")) { 58 | putExtra("payload", intent.getBundleExtra("payload")) 59 | } 60 | } 61 | if (intent.action == ACTION_SEARCH_DISPLAY_ITEM) { 62 | val transition = ActivityOptions.makeSceneTransitionAnimation( 63 | this, toolbar, "toolbar") 64 | val intent = Intent(this, NavigationActivity::class.java) 65 | intent.fillIn(result, 0) 66 | startActivity(intent, transition.toBundle()) 67 | } else { 68 | setResult(Activity.RESULT_OK, result) 69 | fixedFinishAfterTransition() 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /app/src/main/java/com/louiskirsch/quickdynalist/SyncShortcutActivity.kt: -------------------------------------------------------------------------------- 1 | package com.louiskirsch.quickdynalist 2 | 3 | import androidx.appcompat.app.AppCompatActivity 4 | import android.os.Bundle 5 | import com.louiskirsch.quickdynalist.jobs.SyncJob 6 | import org.jetbrains.anko.toast 7 | 8 | class SyncShortcutActivity : AppCompatActivity() { 9 | 10 | override fun onCreate(savedInstanceState: Bundle?) { 11 | super.onCreate(savedInstanceState) 12 | val dynalist = Dynalist(this) 13 | if (dynalist.isAuthenticated) { 14 | SyncJob.forceSync() 15 | toast(R.string.sync_started) 16 | finish() 17 | } else { 18 | dynalist.authenticate() 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/louiskirsch/quickdynalist/WizardActivity.kt: -------------------------------------------------------------------------------- 1 | package com.louiskirsch.quickdynalist 2 | 3 | import androidx.appcompat.app.AppCompatActivity 4 | import android.os.Bundle 5 | import org.jetbrains.anko.toast 6 | 7 | class WizardActivity : AppCompatActivity() { 8 | 9 | companion object { 10 | const val EXTRA_CONFIG_INBOX_ONLY = "EXTRA_CONFIG_INBOX_ONLY" 11 | const val EXTRA_DIALOG_SETUP_ONLY = "EXTRA_DIALOG_SETUP_ONLY" 12 | } 13 | 14 | override fun onCreate(savedInstanceState: Bundle?) { 15 | super.onCreate(savedInstanceState) 16 | setContentView(R.layout.activity_wizard) 17 | 18 | if (savedInstanceState == null) { 19 | val fragment = when { 20 | intent.getBooleanExtra(EXTRA_CONFIG_INBOX_ONLY, false) -> 21 | InboxConfigurationFragment.newInstance(finishAfter = true) 22 | intent.getBooleanExtra(EXTRA_DIALOG_SETUP_ONLY, false) -> 23 | DialogSetupFragment() 24 | else -> LoginFragment() 25 | } 26 | supportFragmentManager.beginTransaction() 27 | .add(R.id.fragment_container, fragment).commit() 28 | } 29 | } 30 | 31 | override fun onBackPressed() { 32 | toast(R.string.wizard_not_completed) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/com/louiskirsch/quickdynalist/adapters/EmojiAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.louiskirsch.quickdynalist.adapters 2 | 3 | import android.view.LayoutInflater 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import android.widget.TextView 7 | import androidx.recyclerview.widget.RecyclerView 8 | import com.louiskirsch.quickdynalist.R 9 | import com.louiskirsch.quickdynalist.utils.EmojiFactory 10 | 11 | class EmojiViewHolder(val textView: TextView): RecyclerView.ViewHolder(textView) 12 | 13 | class EmojiAdapter: RecyclerView.Adapter() { 14 | 15 | var selectedPosition: Int = 0 16 | set(value) { 17 | assert(value in 0..(itemCount - 1)) 18 | val oldValue = field 19 | field = value 20 | notifyItemChanged(oldValue) 21 | notifyItemChanged(value) 22 | } 23 | 24 | var selectedValue: String 25 | get() = EmojiFactory.getEmojiAt(selectedPosition) 26 | set(value) = EmojiFactory.emojis.indexOf(value).let { 27 | if (it >= 0) selectedPosition = it 28 | } 29 | 30 | init { 31 | setHasStableIds(true) 32 | } 33 | 34 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EmojiViewHolder { 35 | val inflater = LayoutInflater.from(parent.context) 36 | return EmojiViewHolder(inflater.inflate(R.layout.emoji_list_item, 37 | parent, false) as TextView) 38 | } 39 | 40 | override fun getItemCount(): Int = EmojiFactory.size 41 | override fun getItemId(position: Int): Long = position.toLong() 42 | 43 | override fun onBindViewHolder(holder: EmojiViewHolder, position: Int) { 44 | holder.textView.run { 45 | isActivated = selectedPosition == position 46 | text = EmojiFactory.getEmojiAt(position) 47 | setOnClickListener { selectedPosition = position } 48 | forceLayout() 49 | } 50 | } 51 | 52 | } -------------------------------------------------------------------------------- /app/src/main/java/com/louiskirsch/quickdynalist/adapters/FilterAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.louiskirsch.quickdynalist.adapters 2 | 3 | import android.content.Context 4 | import android.widget.ArrayAdapter 5 | import android.widget.Filter 6 | 7 | class FilterAdapter(context: Context, resource: Int, objects: MutableList) 8 | : ArrayAdapter(context, resource, objects) { 9 | 10 | private val filterableItems = ArrayList(objects) 11 | private val excludedItems = HashSet() 12 | 13 | fun excludeItem(item: T) { 14 | excludedItems.add(item) 15 | } 16 | 17 | fun includeItem(item: T) { 18 | excludedItems.remove(item) 19 | } 20 | 21 | fun updateFilterableItems(items: List) { 22 | filterableItems.clear() 23 | filterableItems.addAll(items) 24 | clear() 25 | addAll(items) 26 | notifyDataSetChanged() 27 | } 28 | 29 | private val filter = object: Filter() { 30 | override fun performFiltering(constraint: CharSequence?): FilterResults { 31 | val results = FilterResults() 32 | if (constraint != null && constraint.isNotEmpty()) { 33 | val items: List = filterableItems.filter { 34 | it.toString().contains(constraint, true) && 35 | it !in excludedItems 36 | } 37 | results.values = items 38 | results.count = items.size 39 | } else { 40 | results.values = filterableItems 41 | results.count = filterableItems.size 42 | } 43 | return results 44 | } 45 | 46 | override fun publishResults(constraint: CharSequence?, results: FilterResults) { 47 | val values = results.values as List 48 | clear() 49 | addAll(values) 50 | 51 | if (results.count > 0) { 52 | notifyDataSetChanged() 53 | } else { 54 | notifyDataSetInvalidated() 55 | } 56 | } 57 | 58 | } 59 | 60 | override fun getFilter(): Filter = filter 61 | } -------------------------------------------------------------------------------- /app/src/main/java/com/louiskirsch/quickdynalist/jobs/AddItemJob.kt: -------------------------------------------------------------------------------- 1 | package com.louiskirsch.quickdynalist.jobs 2 | 3 | import com.louiskirsch.quickdynalist.* 4 | import com.louiskirsch.quickdynalist.network.* 5 | import com.louiskirsch.quickdynalist.objectbox.DynalistItem 6 | import com.louiskirsch.quickdynalist.objectbox.DynalistItem_ 7 | import com.louiskirsch.quickdynalist.widget.ListAppWidget 8 | import io.objectbox.kotlin.query 9 | import io.objectbox.query.QueryBuilder 10 | import org.greenrobot.eventbus.EventBus 11 | 12 | 13 | class AddItemJob(text: String, note: String, val parent: DynalistItem): ItemJob() { 14 | 15 | private val newItem = DynalistItem(parent.serverFileId, parent.serverItemId, 16 | null, text, note) 17 | 18 | override fun addToDatabase() { 19 | val dynalist = Dynalist(applicationContext) 20 | DynalistApp.instance.boxStore.runInTx { 21 | box.get(parent.clientId)?.also { parent -> 22 | newItem.syncJob = id 23 | newItem.position = if (dynalist.addToTopOfList) { 24 | (minPosition(parent.clientId) ?: 1) - 1 25 | } else { 26 | (maxPosition(parent.clientId) ?: -1) + 1 27 | } 28 | newItem.parent.target = parent 29 | newItem.isChecklist = parent.isChecklist 30 | newItem.checkbox = parent.isChecklist 31 | newItem.notifyModified() 32 | box.put(newItem) 33 | } 34 | } 35 | ListAppWidget.notifyItemChanged(applicationContext, newItem) 36 | EventBus.getDefault().post(ItemAddedEvent(newItem)) 37 | } 38 | 39 | private fun insertAPIRequest(): DynalistResponse { 40 | val dynalist = Dynalist(applicationContext) 41 | val token = dynalist.token 42 | requireItemId(parent) 43 | val request = InsertItemRequest(parent.serverFileId!!, parent.serverItemId!!, 44 | newItem.name, newItem.note, parent.isChecklist, token!!, 45 | index = dynalist.insertPosition) 46 | return dynalistService.addToDocument(request).execute().body()!! 47 | } 48 | 49 | @Throws(Throwable::class) 50 | override fun onRun() { 51 | val response = insertAPIRequest() 52 | requireSuccess(response) 53 | val newItemId = when (response) { 54 | is InboxItemResponse -> response.node_id 55 | is InsertedItemsResponse -> response.new_node_ids!![0] 56 | else -> throw BackendException("Invalid response - no itemId included") 57 | } 58 | DynalistApp.instance.boxStore.runInTx { 59 | val updatedItem = box.query { 60 | equal(DynalistItem_.syncJob, id, QueryBuilder.StringOrder.CASE_INSENSITIVE) 61 | }.findFirst()?.apply { 62 | syncJob = null 63 | serverItemId = newItemId 64 | } 65 | updatedItem?.let { box.put(it) } 66 | } 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /app/src/main/java/com/louiskirsch/quickdynalist/jobs/BulkEditItemJob.kt: -------------------------------------------------------------------------------- 1 | package com.louiskirsch.quickdynalist.jobs 2 | 3 | import com.louiskirsch.quickdynalist.* 4 | import com.louiskirsch.quickdynalist.network.* 5 | import com.louiskirsch.quickdynalist.objectbox.DynalistItem 6 | import com.louiskirsch.quickdynalist.objectbox.DynalistItem_ 7 | import com.louiskirsch.quickdynalist.widget.ListAppWidget 8 | import io.objectbox.kotlin.query 9 | import org.jetbrains.anko.collections.forEachWithIndex 10 | import retrofit2.Response 11 | import java.lang.IllegalArgumentException 12 | 13 | 14 | class BulkEditItemJob(val items: List): ItemJob() { 15 | 16 | init { 17 | require(items.isNotEmpty()) { "Items to edit must not be empty" } 18 | require(items.all { it.serverFileId == items[0].serverFileId }) { "Items mut be in the same file" } 19 | } 20 | 21 | private val serverFileId = items[0].serverFileId!! 22 | 23 | override fun addToDatabase() { 24 | items.forEach { item -> 25 | item.syncJob = id 26 | box.attach(item) 27 | item.notifyModified() 28 | } 29 | box.put(items) 30 | val parents = items.mapNotNull { it.parent.target }.distinct() 31 | if (parents.size == 1) 32 | ListAppWidget.notifyItemChanged(applicationContext, parents[0]) 33 | else 34 | items.forEach { ListAppWidget.notifyItemChanged(applicationContext, it) } 35 | } 36 | 37 | @Throws(Throwable::class) 38 | override fun onRun() { 39 | items.forEach { requireItemId(it) } 40 | val token = Dynalist(applicationContext).token 41 | val edits = items.map { item -> 42 | EditItemRequest.EditSpec(item.serverItemId!!, item.name, 43 | item.note, item.isChecked, item.checkbox, item.heading, item.color) 44 | }.toTypedArray() 45 | val request = BulkEditItemRequest(serverFileId, token!!, edits) 46 | val response = dynalistService.editItems(request).execute() 47 | val body = response.body()!! 48 | requireSuccess(body) 49 | markItemsCompleted() 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /app/src/main/java/com/louiskirsch/quickdynalist/jobs/CloneItemJob.kt: -------------------------------------------------------------------------------- 1 | package com.louiskirsch.quickdynalist.jobs 2 | 3 | import com.louiskirsch.quickdynalist.* 4 | import com.louiskirsch.quickdynalist.network.* 5 | import com.louiskirsch.quickdynalist.objectbox.DynalistItem 6 | import com.louiskirsch.quickdynalist.objectbox.DynalistItem_ 7 | import com.louiskirsch.quickdynalist.widget.ListAppWidget 8 | import io.objectbox.kotlin.query 9 | import io.objectbox.query.Query 10 | import io.objectbox.query.QueryBuilder 11 | import java.util.* 12 | 13 | 14 | class CloneItemJob(val item: DynalistItem): ItemJob() { 15 | 16 | override fun addToDatabase() { 17 | val now = Date() 18 | val dynalist = Dynalist(applicationContext) 19 | DynalistApp.instance.boxStore.runInTx { 20 | box.get(item.clientId)?.let { item -> 21 | item.position = if (dynalist.addToTopOfList) { 22 | (minPosition(item.parent.targetId) ?: 1) - 1 23 | } else { 24 | (maxPosition(item.parent.targetId) ?: -1) + 1 25 | } 26 | if (item.date != null) 27 | item.date = Date() 28 | box.put(cloneRecursively(item, now).apply { syncJob = "$id-root" }) 29 | } 30 | } 31 | ListAppWidget.notifyItemChanged(applicationContext, item) 32 | } 33 | 34 | private fun cloneRecursively(item: DynalistItem, time: Date): DynalistItem { 35 | val newChildren = item.children.map { 36 | cloneRecursively(it, time).apply { it.parent.targetId = 0 } 37 | } 38 | item.apply { 39 | clientId = 0 40 | serverItemId = null 41 | syncJob = id 42 | isChecked = false 43 | children.clear() 44 | children.addAll(newChildren) 45 | notifyModified(time) 46 | } 47 | return item 48 | } 49 | 50 | private fun waitForItem(query: Query, timeout: Long = 10000): DynalistItem? { 51 | val start = System.currentTimeMillis() 52 | while (System.currentTimeMillis() < start + timeout) { 53 | val item = query.findFirst() 54 | if (item != null) return item 55 | Thread.sleep(500) 56 | } 57 | return null 58 | } 59 | 60 | @Throws(Throwable::class) 61 | override fun onRun() { 62 | // need to wait for db operations to complete 63 | val item = waitForItem(box.query { 64 | equal(DynalistItem_.syncJob, "$id-root", QueryBuilder.StringOrder.CASE_INSENSITIVE) 65 | }) ?: throw InvalidJobException("Item to clone has vanished") 66 | 67 | val treeService = AddItemTreeService( this) 68 | val updatedItems = treeService.insert(item) 69 | box.put(updatedItems.apply { forEach { it.syncJob = null } }) 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /app/src/main/java/com/louiskirsch/quickdynalist/jobs/DeleteItemJob.kt: -------------------------------------------------------------------------------- 1 | package com.louiskirsch.quickdynalist.jobs 2 | 3 | import com.louiskirsch.quickdynalist.* 4 | import com.louiskirsch.quickdynalist.network.* 5 | import com.louiskirsch.quickdynalist.objectbox.DynalistItem 6 | import com.louiskirsch.quickdynalist.objectbox.DynalistItem_ 7 | import com.louiskirsch.quickdynalist.widget.ListAppWidget 8 | import io.objectbox.kotlin.query 9 | import io.objectbox.query.QueryBuilder 10 | import org.jetbrains.anko.collections.forEachWithIndex 11 | import retrofit2.Response 12 | 13 | 14 | class DeleteItemJob(val item: DynalistItem): ItemJob() { 15 | 16 | override fun addToDatabase() { 17 | val parent = item.parent.target 18 | DynalistApp.instance.boxStore.runInTx { 19 | box.get(item.clientId)?.let { item -> 20 | val items = getChildrenRecursively(item) + listOf(item) 21 | items.forEach { 22 | it.hidden = true 23 | it.syncJob = id 24 | } 25 | box.put(items) 26 | } 27 | } 28 | ListAppWidget.notifyItemChanged(applicationContext, parent) 29 | } 30 | 31 | private fun getChildrenRecursively(item: DynalistItem): List { 32 | val children = item.children 33 | return children.flatMap { getChildrenRecursively(it) } + children 34 | } 35 | 36 | @Throws(Throwable::class) 37 | override fun onRun() { 38 | requireItemId(item) 39 | val token = Dynalist(applicationContext).token 40 | val request = DeleteItemRequest(item.serverFileId!!, item.serverItemId!!, token!!) 41 | val response = dynalistService.deleteItem(request).execute() 42 | val body = response.body()!! 43 | requireSuccess(body) 44 | DynalistApp.instance.boxStore.runInTx { 45 | val items = box.query { 46 | equal(DynalistItem_.syncJob, id, QueryBuilder.StringOrder.CASE_INSENSITIVE) 47 | }.find() 48 | box.remove(items) 49 | } 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /app/src/main/java/com/louiskirsch/quickdynalist/jobs/EditItemJob.kt: -------------------------------------------------------------------------------- 1 | package com.louiskirsch.quickdynalist.jobs 2 | 3 | import com.louiskirsch.quickdynalist.* 4 | import com.louiskirsch.quickdynalist.network.* 5 | import com.louiskirsch.quickdynalist.objectbox.DynalistItem 6 | import com.louiskirsch.quickdynalist.objectbox.DynalistItem_ 7 | import com.louiskirsch.quickdynalist.widget.ListAppWidget 8 | import io.objectbox.kotlin.query 9 | import org.jetbrains.anko.collections.forEachWithIndex 10 | import retrofit2.Response 11 | 12 | 13 | class EditItemJob(val item: DynalistItem): ItemJob() { 14 | 15 | override fun addToDatabase() { 16 | item.syncJob = id 17 | box.attach(item) 18 | item.notifyModified() 19 | box.put(item) 20 | ListAppWidget.notifyItemChanged(applicationContext, item) 21 | } 22 | 23 | @Throws(Throwable::class) 24 | override fun onRun() { 25 | requireItemId(item) 26 | val token = Dynalist(applicationContext).token 27 | val request = EditItemRequest(item.serverFileId!!, item.serverItemId!!, item.name, 28 | item.note, item.isChecked, item.checkbox, item.heading, item.color, token!!) 29 | val response = dynalistService.editItem(request).execute() 30 | val body = response.body()!! 31 | requireSuccess(body) 32 | markItemsCompleted() 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/com/louiskirsch/quickdynalist/jobs/Exceptions.kt: -------------------------------------------------------------------------------- 1 | package com.louiskirsch.quickdynalist.jobs 2 | 3 | class BackendException(message: String): Exception(message) 4 | class NoInboxException: Exception() 5 | class ItemIdUnavailableException: Exception() 6 | class InvalidJobException(message: String): Exception(message) 7 | class NotAuthenticatedException: Exception() 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/louiskirsch/quickdynalist/jobs/JobService.kt: -------------------------------------------------------------------------------- 1 | package com.louiskirsch.quickdynalist.jobs 2 | 3 | import com.birbit.android.jobqueue.JobManager 4 | import com.birbit.android.jobqueue.scheduling.FrameworkJobSchedulerService 5 | import com.louiskirsch.quickdynalist.DynalistApp 6 | 7 | class JobService: FrameworkJobSchedulerService() { 8 | 9 | override fun getJobManager(): JobManager { 10 | return DynalistApp.instance.jobManager 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/louiskirsch/quickdynalist/jobs/VerifyTokenJob.kt: -------------------------------------------------------------------------------- 1 | package com.louiskirsch.quickdynalist.jobs 2 | 3 | import com.birbit.android.jobqueue.RetryConstraint 4 | import org.greenrobot.eventbus.EventBus 5 | import com.birbit.android.jobqueue.CancelReason 6 | import com.birbit.android.jobqueue.Job 7 | import com.birbit.android.jobqueue.Params 8 | import com.louiskirsch.quickdynalist.AuthenticatedEvent 9 | import com.louiskirsch.quickdynalist.Dynalist 10 | import com.louiskirsch.quickdynalist.DynalistApp 11 | import com.louiskirsch.quickdynalist.ItemEvent 12 | import com.louiskirsch.quickdynalist.network.AuthenticatedRequest 13 | import org.jetbrains.annotations.Nullable 14 | 15 | 16 | class VerifyTokenJob(private val token: String) 17 | : Job(Params(2).requireNetwork().singleInstanceBy("verify_token")) { 18 | 19 | override fun onAdded() {} 20 | 21 | @Throws(Throwable::class) 22 | override fun onRun() { 23 | val service = DynalistApp.instance.dynalistService 24 | val dynalist = Dynalist(applicationContext) 25 | 26 | val call = service.listFiles(AuthenticatedRequest(token)) 27 | val response = call.execute() 28 | val success = response.isSuccessful && response.body()!!.isOK 29 | if (!success) 30 | throw BackendException(response.body()?.errorDesc ?: "") 31 | dynalist.token = token 32 | EventBus.getDefault().post(AuthenticatedEvent(success)) 33 | } 34 | 35 | override fun onCancel(@CancelReason cancelReason: Int, throwable: Throwable?) { 36 | EventBus.getDefault().post(AuthenticatedEvent(false)) 37 | } 38 | 39 | override fun getRetryLimit() = 0 40 | 41 | override fun shouldReRunOnThrowable(throwable: Throwable, runCount: Int, 42 | maxRunCount: Int): RetryConstraint { 43 | return RetryConstraint.CANCEL 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/java/com/louiskirsch/quickdynalist/network/AddItemTreeService.kt: -------------------------------------------------------------------------------- 1 | package com.louiskirsch.quickdynalist.network 2 | 3 | import com.louiskirsch.quickdynalist.Dynalist 4 | import com.louiskirsch.quickdynalist.DynalistApp 5 | import com.louiskirsch.quickdynalist.RateLimitDelay 6 | import com.louiskirsch.quickdynalist.jobs.ItemJob 7 | import com.louiskirsch.quickdynalist.objectbox.DynalistItem 8 | import com.louiskirsch.quickdynalist.utils.execRespectRateLimit 9 | import org.greenrobot.eventbus.EventBus 10 | 11 | class AddItemTreeService(private val itemJob: ItemJob) { 12 | 13 | private val dynalist = Dynalist(itemJob.applicationContext) 14 | private val token = dynalist.token!! 15 | 16 | private val dynalistService 17 | get() = DynalistApp.instance.dynalistService 18 | 19 | private val delayCallback = { _: Any, delay: Long -> 20 | EventBus.getDefault().post(RateLimitDelay(delay, ItemJob.TAG)) 21 | } 22 | 23 | private fun insertRecursively(item: DynalistItem): List { 24 | if (item.children.isEmpty()) 25 | return emptyList() 26 | // TODO API has weird bug that items are inserted in reverse order 27 | val children = item.children.sortedBy { it.position }.reversed() 28 | val changes = children.mapIndexed { i: Int, it -> 29 | // TODO we could define the index here, but the API is bugged 30 | InsertItemRequest.InsertSpec(item.serverItemId!!, it.name, it.note, it.checkbox, 31 | it.color) 32 | }.toTypedArray() 33 | val request = BulkInsertItemRequest(item.serverFileId!!, token, changes) 34 | val response = dynalistService.addToDocument(request) 35 | .execRespectRateLimit(delayCallback).body()!! 36 | itemJob.requireSuccess(response) 37 | response.new_node_ids!!.zip(children).forEach { (newItemId, child) -> 38 | child.serverItemId = newItemId 39 | } 40 | return children + children.flatMap { insertRecursively(it) } 41 | } 42 | 43 | fun insert(item: DynalistItem): List { 44 | val request = InsertItemRequest(item.serverFileId!!, item.serverParentId!!, item.name, 45 | item.note, item.checkbox, token, item.color, dynalist.insertPosition) 46 | val response = dynalistService.addToDocument(request) 47 | .execRespectRateLimit(delayCallback).body()!! 48 | itemJob.requireSuccess(response) 49 | item.serverItemId = response.new_node_ids!![0] 50 | val children = insertRecursively(item) 51 | return children + item 52 | } 53 | } -------------------------------------------------------------------------------- /app/src/main/java/com/louiskirsch/quickdynalist/objectbox/DocumentTreeNode.kt: -------------------------------------------------------------------------------- 1 | package com.louiskirsch.quickdynalist.objectbox 2 | 3 | import io.objectbox.relation.ToOne 4 | 5 | interface DocumentTreeNode { 6 | val parent: ToOne 7 | var position: Int 8 | } -------------------------------------------------------------------------------- /app/src/main/java/com/louiskirsch/quickdynalist/objectbox/DynalistDocument.kt: -------------------------------------------------------------------------------- 1 | package com.louiskirsch.quickdynalist.objectbox 2 | 3 | import com.louiskirsch.quickdynalist.DynalistApp 4 | import io.objectbox.Box 5 | import io.objectbox.annotation.Entity 6 | import io.objectbox.annotation.Id 7 | import io.objectbox.annotation.Transient 8 | import io.objectbox.kotlin.boxFor 9 | import io.objectbox.relation.ToOne 10 | 11 | @Entity 12 | class DynalistDocument(var serverFileId: String?): DocumentTreeNode { 13 | 14 | constructor() : this(null) 15 | 16 | @Id 17 | var id: Long = 0 18 | var version: Long = 0 19 | override var position: Int = 0 20 | lateinit var folder: ToOne 21 | 22 | val rootItem: DynalistItem? get() = DynalistItem.byServerId(serverFileId!!, "root") 23 | override val parent: ToOne get() = folder 24 | 25 | companion object { 26 | val box: Box get() = DynalistApp.instance.boxStore.boxFor() 27 | } 28 | } -------------------------------------------------------------------------------- /app/src/main/java/com/louiskirsch/quickdynalist/objectbox/DynalistFolder.kt: -------------------------------------------------------------------------------- 1 | package com.louiskirsch.quickdynalist.objectbox 2 | 3 | import com.louiskirsch.quickdynalist.DynalistApp 4 | import io.objectbox.Box 5 | import io.objectbox.annotation.Backlink 6 | import io.objectbox.annotation.Entity 7 | import io.objectbox.annotation.Id 8 | import io.objectbox.kotlin.boxFor 9 | import io.objectbox.relation.ToMany 10 | import io.objectbox.relation.ToOne 11 | 12 | @Entity 13 | class DynalistFolder(var serverFolderId: String?): DocumentTreeNode { 14 | 15 | constructor() : this(null) 16 | 17 | @Id 18 | var id: Long = 0 19 | var title: String? = null 20 | override var position: Int = 0 21 | override lateinit var parent: ToOne 22 | @Backlink(to = "parent") 23 | lateinit var children: ToMany 24 | @Backlink(to = "folder") 25 | lateinit var documents: ToMany 26 | 27 | override fun hashCode(): Int = (id % Int.MAX_VALUE).toInt() 28 | 29 | companion object { 30 | const val LOCATION_TYPE = "folder" 31 | val box: Box get() = DynalistApp.instance.boxStore.boxFor() 32 | } 33 | } -------------------------------------------------------------------------------- /app/src/main/java/com/louiskirsch/quickdynalist/objectbox/DynalistItemMetaData.kt: -------------------------------------------------------------------------------- 1 | package com.louiskirsch.quickdynalist.objectbox 2 | 3 | import com.louiskirsch.quickdynalist.DynalistApp 4 | import io.objectbox.annotation.Entity 5 | import io.objectbox.annotation.Id 6 | import io.objectbox.kotlin.boxFor 7 | import io.objectbox.relation.ToMany 8 | import io.objectbox.relation.ToOne 9 | import java.io.Serializable 10 | import java.util.* 11 | 12 | @Entity 13 | class DynalistItemMetaData(): Serializable { 14 | 15 | @Id var id: Long = 0 16 | 17 | var date: Date? = null 18 | var image: String? = null 19 | var symbol: String? = null 20 | lateinit var tags: ToMany 21 | lateinit var linkedItem: ToOne 22 | 23 | constructor(fromItem: DynalistItem) : this() { 24 | update(fromItem) 25 | } 26 | 27 | fun update(fromItem: DynalistItem) { 28 | date = fromItem.date 29 | image = fromItem.image 30 | symbol = fromItem.symbol 31 | tags.clear() 32 | tags.addAll(fromItem.tags.map { DynalistTag.find(it) }) 33 | linkedItem.target = fromItem.linkedItem 34 | } 35 | 36 | companion object { 37 | val box get() = DynalistApp.instance.boxStore.boxFor() 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/com/louiskirsch/quickdynalist/objectbox/Exceptions.kt: -------------------------------------------------------------------------------- 1 | package com.louiskirsch.quickdynalist.objectbox 2 | 3 | 4 | class InvalidTagException: Exception() 5 | -------------------------------------------------------------------------------- /app/src/main/java/com/louiskirsch/quickdynalist/objectbox/SubscriberOBLiveData.kt: -------------------------------------------------------------------------------- 1 | package com.louiskirsch.quickdynalist.objectbox 2 | 3 | import android.util.Log 4 | import androidx.lifecycle.LiveData 5 | import io.objectbox.BoxStore 6 | import io.objectbox.query.Query 7 | import io.objectbox.reactive.DataObserver 8 | import io.objectbox.reactive.DataSubscription 9 | import io.objectbox.reactive.DataTransformer 10 | 11 | class SubscriberOBLiveData(private val boxStore: BoxStore, 12 | private val subscribeClass: Class, 13 | private val query: Query, 14 | private val transformer: (Query) -> List) : 15 | LiveData>() { 16 | private var subscription: DataSubscription? = null 17 | private val listener = DataObserver> { data -> postValue(data) } 18 | 19 | override fun onActive() { 20 | // called when the LiveData object has an active observer 21 | if (subscription == null) { 22 | subscription = boxStore.subscribe(subscribeClass).transform { 23 | transformer(query) 24 | }.onError { 25 | Log.e(javaClass.name, "Transform failed", it) 26 | }.observer(listener) 27 | } 28 | } 29 | 30 | override fun onInactive() { 31 | // called when the LiveData object doesn't have any active observers 32 | if (!hasObservers()) { 33 | subscription!!.cancel() 34 | subscription = null 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/com/louiskirsch/quickdynalist/objectbox/TransformedOBLiveData.kt: -------------------------------------------------------------------------------- 1 | package com.louiskirsch.quickdynalist.objectbox 2 | 3 | import android.util.Log 4 | import androidx.lifecycle.LiveData 5 | import io.objectbox.query.Query 6 | import io.objectbox.reactive.DataObserver 7 | import io.objectbox.reactive.DataSubscription 8 | import io.objectbox.reactive.DataTransformer 9 | 10 | class TransformedOBLiveData(private val query: Query, 11 | private val transformer: (List) -> List) : 12 | LiveData>() { 13 | private var subscription: DataSubscription? = null 14 | private val listener = DataObserver> { data -> postValue(data) } 15 | 16 | override fun onActive() { 17 | // called when the LiveData object has an active observer 18 | if (subscription == null) { 19 | subscription = query.subscribe().transform(transformer).onError { 20 | Log.e(javaClass.name, "Transform failed", it) 21 | }.observer(listener) 22 | } 23 | } 24 | 25 | override fun onInactive() { 26 | // called when the LiveData object doesn't have any active observers 27 | if (!hasObservers()) { 28 | subscription!!.cancel() 29 | subscription = null 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/com/louiskirsch/quickdynalist/text/EnhancedMovementMethod.kt: -------------------------------------------------------------------------------- 1 | package com.louiskirsch.quickdynalist.text 2 | 3 | import android.text.Selection 4 | import android.text.Spannable 5 | import android.text.method.ArrowKeyMovementMethod 6 | import android.text.method.MovementMethod 7 | import android.text.style.ClickableSpan 8 | import android.view.MotionEvent 9 | import android.widget.TextView 10 | 11 | /** 12 | * ArrowKeyMovementMethod does support selection of text but not the clicking of links. 13 | * LinkMovementMethod does support clicking of links but not the selection of text. 14 | * This class adds the link clicking to the ArrowKeyMovementMethod. 15 | * We basically take the LinkMovementMethod onTouchEvent code and remove the line 16 | * Selection.removeSelection(buffer); 17 | * which deselects all text when no link was found. 18 | */ 19 | class EnhancedMovementMethod : ArrowKeyMovementMethod() { 20 | 21 | override fun onTouchEvent(widget: TextView, buffer: Spannable, event: MotionEvent): Boolean { 22 | val action = event.action 23 | 24 | if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) { 25 | var x = event.x.toInt() 26 | var y = event.y.toInt() 27 | 28 | x -= widget.totalPaddingLeft 29 | y -= widget.totalPaddingTop 30 | 31 | x += widget.scrollX 32 | y += widget.scrollY 33 | 34 | val layout = widget.layout 35 | val line = layout.getLineForVertical(y) 36 | val off = layout.getOffsetForHorizontal(line, x.toFloat()) 37 | 38 | val link = buffer.getSpans(off, off, ClickableSpan::class.java) 39 | 40 | if (link.isNotEmpty()) { 41 | if (action == MotionEvent.ACTION_UP) { 42 | link[0].onClick(widget) 43 | } else if (action == MotionEvent.ACTION_DOWN) { 44 | Selection.setSelection(buffer, buffer.getSpanStart(link[0]), buffer.getSpanEnd(link[0])) 45 | } 46 | 47 | return true 48 | } 49 | } 50 | 51 | return super.onTouchEvent(widget, buffer, event) 52 | } 53 | 54 | companion object { 55 | val instance: MovementMethod by lazy { EnhancedMovementMethod() } 56 | } 57 | } -------------------------------------------------------------------------------- /app/src/main/java/com/louiskirsch/quickdynalist/text/IndentedBulletSpan.kt: -------------------------------------------------------------------------------- 1 | package com.louiskirsch.quickdynalist.text 2 | 3 | import android.graphics.Canvas 4 | import android.graphics.Paint 5 | import android.os.Parcel 6 | import android.text.Layout 7 | import android.text.style.BulletSpan 8 | 9 | class IndentedBulletSpan: BulletSpan { 10 | private val indentWidth: Int 11 | 12 | constructor(gapWidth: Int, indentWidth: Int) : super(gapWidth) { 13 | this.indentWidth = indentWidth 14 | } 15 | constructor(src: Parcel) : super(src) { 16 | this.indentWidth = src.readInt() 17 | } 18 | 19 | override fun writeToParcel(dest: Parcel, flags: Int) { 20 | super.writeToParcel(dest, flags) 21 | dest.writeInt(indentWidth) 22 | } 23 | 24 | override fun getLeadingMargin(first: Boolean): Int { 25 | return super.getLeadingMargin(first) + indentWidth 26 | } 27 | 28 | override fun drawLeadingMargin(canvas: Canvas, paint: Paint, x: Int, dir: Int, top: Int, 29 | baseline: Int, bottom: Int, text: CharSequence, start: Int, 30 | end: Int, first: Boolean, layout: Layout?) { 31 | val newX = x + indentWidth * dir 32 | super.drawLeadingMargin(canvas, paint, newX, dir, top, baseline, bottom, text, start, end, 33 | first, layout) 34 | } 35 | 36 | } -------------------------------------------------------------------------------- /app/src/main/java/com/louiskirsch/quickdynalist/text/ThemedSpan.kt: -------------------------------------------------------------------------------- 1 | package com.louiskirsch.quickdynalist.text 2 | 3 | import android.content.Context 4 | import android.content.res.Resources 5 | import android.text.Spannable 6 | 7 | class ThemedSpan(private val spanCreator: (Context) -> Any) { 8 | 9 | fun apply(context: Context, spannable: Spannable) { 10 | val start = spannable.getSpanStart(this) 11 | val end = spannable.getSpanEnd(this) 12 | val flags = spannable.getSpanFlags(this) 13 | val newSpan = spanCreator(context) 14 | spannable.removeSpan(this) 15 | spannable.setSpan(newSpan, start, end, flags) 16 | } 17 | 18 | companion object { 19 | fun applyAll(context: Context, spannable: Spannable) { 20 | spannable.getSpans(0, spannable.length, ThemedSpan::class.java).forEach { 21 | it.apply(context, spannable) 22 | } 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /app/src/main/java/com/louiskirsch/quickdynalist/text/TintedImageSpan.kt: -------------------------------------------------------------------------------- 1 | package com.louiskirsch.quickdynalist.text 2 | 3 | import android.graphics.drawable.Drawable 4 | import android.text.style.ImageSpan 5 | 6 | class TintedImageSpan(drawable: Drawable, verticalAlignment: Int, val lineHeight: Float = 1.0f) 7 | : ImageSpan(drawable, verticalAlignment) -------------------------------------------------------------------------------- /app/src/main/java/com/louiskirsch/quickdynalist/utils/SpeechRecognitionHelper.kt: -------------------------------------------------------------------------------- 1 | package com.louiskirsch.quickdynalist.utils 2 | 3 | import android.app.Activity 4 | import android.content.ActivityNotFoundException 5 | import android.content.Context 6 | import android.content.Intent 7 | import android.speech.RecognizerIntent 8 | import androidx.fragment.app.Fragment 9 | import com.louiskirsch.quickdynalist.R 10 | import org.jetbrains.anko.toast 11 | 12 | class SpeechRecognitionHelper { 13 | 14 | fun startSpeechRecognition(activity: Activity) { 15 | startSpeechRecognition(activity, activity::startActivityForResult) 16 | } 17 | 18 | fun startSpeechRecognition(fragment: Fragment) { 19 | startSpeechRecognition(fragment.context!!, fragment::startActivityForResult) 20 | } 21 | 22 | private fun startSpeechRecognition(context: Context, starter: (Intent, Int) -> Unit) { 23 | val intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply { 24 | putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM) 25 | } 26 | val speechRecognitionRequestCode = 27 | context.resources.getInteger(R.integer.request_code_speech_recognition) 28 | try { 29 | starter(intent, speechRecognitionRequestCode) 30 | } catch (e: ActivityNotFoundException) { 31 | context.toast(R.string.error_speech_recognition_not_available) 32 | } 33 | } 34 | 35 | fun dispatchResult(context:Context, requestCode: Int, resultCode: Int, data: Intent?, 36 | cancelCallback: (() -> Unit)? = null, callback: (String) -> Unit) { 37 | val speechRecognitionRequestCode = 38 | context.resources.getInteger(R.integer.request_code_speech_recognition) 39 | if (requestCode == speechRecognitionRequestCode) { 40 | if (resultCode == Activity.RESULT_OK) { 41 | val spokenText = data!!.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS) 42 | .let { it!![0] } 43 | callback(spokenText) 44 | } else { 45 | cancelCallback?.invoke() 46 | } 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /app/src/main/java/com/louiskirsch/quickdynalist/views/BottomBarScrollingViewBehavior.kt: -------------------------------------------------------------------------------- 1 | package com.louiskirsch.quickdynalist.views 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import android.view.View 6 | import androidx.coordinatorlayout.widget.CoordinatorLayout 7 | import com.google.android.material.appbar.AppBarLayout 8 | 9 | class BottomBarScrollingViewBehavior: AppBarLayout.ScrollingViewBehavior { 10 | 11 | constructor() : super() 12 | constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) 13 | 14 | override fun layoutDependsOn(parent: CoordinatorLayout, child: View, dependency: View): Boolean { 15 | val superDependence = super.layoutDependsOn(parent, child, dependency) 16 | return superDependence || dependency.tag == "bottomBar" 17 | } 18 | 19 | override fun onLayoutChild(parent: CoordinatorLayout, child: View, layoutDirection: Int): Boolean { 20 | parent.getDependencies(child).first { it.tag == "bottomBar" }?.let { dependency -> 21 | val paddingBottom = if (dependency.visibility == View.VISIBLE) dependency.height else 0 22 | child.setPadding(child.paddingLeft, child.paddingTop, child.paddingRight, paddingBottom) 23 | } 24 | return super.onLayoutChild(parent, child, layoutDirection) 25 | } 26 | } -------------------------------------------------------------------------------- /app/src/main/java/com/louiskirsch/quickdynalist/views/MoveUpwardBehavior.kt: -------------------------------------------------------------------------------- 1 | package com.louiskirsch.quickdynalist.views 2 | 3 | import android.content.Context 4 | import androidx.core.view.ViewCompat 5 | import androidx.coordinatorlayout.widget.CoordinatorLayout 6 | import android.util.AttributeSet 7 | import android.view.View 8 | import com.google.android.material.snackbar.Snackbar 9 | import androidx.annotation.Keep 10 | 11 | 12 | @Keep 13 | class MoveUpwardBehavior(context: Context?, attrs: AttributeSet?) 14 | : CoordinatorLayout.Behavior(context, attrs) { 15 | 16 | override fun layoutDependsOn(parent: CoordinatorLayout, child: View, dependency: View): Boolean { 17 | return dependency is Snackbar.SnackbarLayout 18 | } 19 | 20 | override fun onDependentViewChanged(parent: CoordinatorLayout, child: View, dependency: View): Boolean { 21 | child.translationY = Math.min(0f, dependency.translationY - dependency.height) 22 | return true 23 | } 24 | 25 | override fun onDependentViewRemoved(parent: CoordinatorLayout, child: View, dependency: View) { 26 | ViewCompat.animate(child).translationY(0f).start() 27 | } 28 | } -------------------------------------------------------------------------------- /app/src/main/java/com/louiskirsch/quickdynalist/views/ScrollFABBehavior.kt: -------------------------------------------------------------------------------- 1 | package com.louiskirsch.quickdynalist.views 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import android.view.View 6 | import androidx.annotation.Keep 7 | import androidx.coordinatorlayout.widget.CoordinatorLayout 8 | import androidx.core.view.ViewCompat 9 | import androidx.recyclerview.widget.LinearLayoutManager 10 | import androidx.recyclerview.widget.RecyclerView 11 | import com.google.android.material.floatingactionbutton.FloatingActionButton 12 | 13 | @Keep 14 | class ScrollFABBehavior(context: Context?, attrs: AttributeSet?) 15 | : FloatingActionButton.Behavior(context, attrs) { 16 | 17 | companion object { 18 | val hideListener = object: FloatingActionButton.OnVisibilityChangedListener() { 19 | override fun onHidden(fab: FloatingActionButton) { 20 | fab.visibility = View.INVISIBLE 21 | } 22 | } 23 | } 24 | 25 | override fun onStartNestedScroll(coordinatorLayout: CoordinatorLayout, 26 | child: FloatingActionButton, directTargetChild: View, 27 | target: View, axes: Int, type: Int): Boolean { 28 | return axes == ViewCompat.SCROLL_AXIS_VERTICAL || 29 | super.onStartNestedScroll(coordinatorLayout, child, directTargetChild,target, axes, 30 | type) 31 | } 32 | 33 | override fun onNestedScroll(coordinatorLayout: CoordinatorLayout, child: FloatingActionButton, 34 | target: View, dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int, 35 | dyUnconsumed: Int, type: Int) { 36 | super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, 37 | dyUnconsumed, type) 38 | if (dyConsumed < 0 && child.visibility == View.VISIBLE) { 39 | child.hide(hideListener) 40 | } else if (dyConsumed > 0 && child.visibility != View.VISIBLE) { 41 | child.show() 42 | } 43 | } 44 | 45 | override fun onStopNestedScroll(coordinatorLayout: CoordinatorLayout, 46 | child: FloatingActionButton, target: View, type: Int) { 47 | super.onStopNestedScroll(coordinatorLayout, child, target, type) 48 | (target as? RecyclerView)?.let { recycler -> 49 | (recycler.layoutManager as? LinearLayoutManager)?.let { 50 | val lastItem = it.itemCount - 1 51 | val lastVisible = it.findLastVisibleItemPosition() 52 | if (lastVisible >= lastItem) { 53 | child.hide(hideListener) 54 | } 55 | } 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /app/src/main/java/com/louiskirsch/quickdynalist/views/SquareImageView.kt: -------------------------------------------------------------------------------- 1 | package com.louiskirsch.quickdynalist.views 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import android.widget.ImageView 6 | import androidx.appcompat.widget.AppCompatImageView 7 | 8 | class SquareImageView @JvmOverloads constructor( 9 | context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 10 | ) : AppCompatImageView(context, attrs, defStyleAttr) { 11 | 12 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { 13 | super.onMeasure(widthMeasureSpec, heightMeasureSpec) 14 | 15 | val height = measuredHeight 16 | setMeasuredDimension(height, height) 17 | } 18 | } -------------------------------------------------------------------------------- /app/src/main/java/com/louiskirsch/quickdynalist/widget/ListAppWidgetConfigurationReceiver.kt: -------------------------------------------------------------------------------- 1 | package com.louiskirsch.quickdynalist.widget 2 | 3 | import android.appwidget.AppWidgetManager 4 | import android.content.BroadcastReceiver 5 | import android.content.Context 6 | import android.content.Intent 7 | import com.louiskirsch.quickdynalist.DynalistApp 8 | import com.louiskirsch.quickdynalist.FilterLocation 9 | import com.louiskirsch.quickdynalist.ItemLocation 10 | import com.louiskirsch.quickdynalist.objectbox.DynalistItem 11 | import com.louiskirsch.quickdynalist.objectbox.DynalistItemFilter 12 | import io.objectbox.kotlin.boxFor 13 | 14 | class ListAppWidgetConfigurationReceiver : BroadcastReceiver() { 15 | 16 | override fun onReceive(context: Context, intent: Intent) { 17 | val widgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, 0) 18 | val location = if (intent.hasExtra(DynalistApp.EXTRA_DISPLAY_ITEM_ID)) { 19 | val locationId = intent.getLongExtra(DynalistApp.EXTRA_DISPLAY_ITEM_ID, 1) 20 | val item = DynalistApp.instance.boxStore.boxFor().get(locationId) 21 | ItemLocation(item) 22 | } else { 23 | val locationId = intent.getLongExtra(DynalistApp.EXTRA_DISPLAY_FILTER_ID, 1) 24 | val filter = DynalistApp.instance.boxStore.boxFor().get(locationId) 25 | FilterLocation(filter, context) 26 | } 27 | ListAppWidgetConfigureActivity.saveWidgetInfo(context, widgetId, location) 28 | val appWidgetManager = AppWidgetManager.getInstance(context) 29 | ListAppWidget.updateAppWidget(context, appWidgetManager, widgetId) 30 | } 31 | 32 | 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-night/dashed_border.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-night/dashed_border_activated.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/appwidget_preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/louiskirsch/QuickDynalist/451cd22f69f2c467aeec22c012299d97af462cae/app/src/main/res/drawable-nodpi/appwidget_preview.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/activatable_selectable_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/circular_progress_bar.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 10 | 11 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/dashed_border.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/dashed_border_activated.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/dropoff_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_action_advanced_item.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_action_change_color.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_action_clear.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_action_create_filter.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_action_create_shortcut.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_action_date.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_action_details.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_action_discard.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_action_duplicate.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_action_edit.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_action_edit_filter.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_action_goto_parent.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_action_help.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_action_item_list.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_action_jump_to_bottom.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_action_link.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_action_manage_tags.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_action_more.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_action_move.xml: -------------------------------------------------------------------------------- 1 | 4 | 7 | 10 | 13 | 16 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_action_move_inbox.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_action_open_large.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_action_open_settings.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_action_rate_quickdynalist.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_action_record_speech.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_action_search.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_action_search_white.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_action_send_bug_report.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_action_send_item.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_action_share.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_action_share_quickdynalist.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_action_show_image.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_action_show_text.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_action_sync.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_backward_link.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_chip_add.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_folder.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_folder_open.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_folder_stateful.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_forward_link.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_shortcut_quick_dialog_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_shortcut_record_speech_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_shortcut_search_item_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_shortcut_sync_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_shortcut_type_dialog.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_shortcut_type_list.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_swipe_delete.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_swipe_edit.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_tile_quick_dialog.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/item_color_picker.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/side_nav_bar.xml: -------------------------------------------------------------------------------- 1 | 3 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/layout-w900dp/activity_navigation.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 17 | 18 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_auth.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_details.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 12 | 13 | 22 | 23 | 32 | 33 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_navigation.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 15 | 16 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_search.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 13 | 14 | 23 | 24 | 33 | 34 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_wizard.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/layout/dropdown_menu_popup_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/layout/emoji_list_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_dialog_setup.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 20 | 21 | 28 | 29 | 39 | 40 | 48 | 49 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_inbox_configuration.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 19 | 20 | 27 | 28 | 39 | 40 | 41 | 42 | 52 | 53 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_insert_bar.xml: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 24 | 25 | 34 | 35 | 47 | 48 | 49 | 50 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_item_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 13 | 14 | 18 | 19 | 27 | 28 | 36 | 37 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_sync_status.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 20 | 21 | 32 | 33 | 44 | 45 | 57 | 58 | 68 | 69 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_tag_manager.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 18 | 19 | 26 | 27 | 34 | 35 | 44 | 45 | 50 | 51 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_list_dropoff.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/layout/list_app_widget.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 26 | 27 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/layout/list_app_widget_configure.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/layout/list_app_widget_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 15 | 16 | 25 | 26 | 34 | 35 | 45 | 46 | 54 | 55 | -------------------------------------------------------------------------------- /app/src/main/res/layout/list_separator.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/layout/menu_color_picker.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 |