From 97cee06ebe87e7e5649ad27ff1ca7bcf5aef88e3 Mon Sep 17 00:00:00 2001 From: Amine Date: Sun, 28 May 2017 02:29:42 +0200 Subject: [PATCH] Drawables and strings and everything else. Some unconverted java classes. --- README.md | 4 +- app/build.gradle | 51 +- app/proguard-rules.pro | 6 + app/src/main/AndroidManifest.xml | 41 +- .../bou/readerforselfoss/AddSourceActivity.kt | 122 +++++ .../bou/readerforselfoss/HomeActivity.kt | 477 ++++++++++++++++++ .../bou/readerforselfoss/IntroActivity.kt | 57 +++ .../bou/readerforselfoss/LoginActivity.kt | 258 ++++++++++ .../bou/readerforselfoss/MainActivity.kt | 26 + .../amine/bou}/readerforselfoss/MyApp.kt | 8 +- .../bou/readerforselfoss/ReaderActivity.kt | 84 +++ .../bou/readerforselfoss/SourcesActivity.kt | 57 +++ .../adapters/ItemCardAdapter.java | 310 ++++++++++++ .../adapters/ItemListAdapter.java | 363 +++++++++++++ .../adapters/SourcesListAdapter.java | 133 +++++ .../api/mercury/MercuryApi.java | 36 ++ .../api/mercury/MercuryModels.kt | 55 ++ .../api/mercury/MercuryService.java | 13 + .../api/selfoss/BooleanTypeAdapter.kt | 19 + .../api/selfoss/SelfossApi.java | 138 +++++ .../api/selfoss/SelfossModels.kt | 127 +++++ .../api/selfoss/SelfossService.java | 68 +++ .../settings/AppCompatPreferenceActivity.java | 111 ++++ .../settings/SettingsActivity.java | 211 ++++++++ .../bou/readerforselfoss/utils/AppUtils.kt | 93 ++++ .../bou/readerforselfoss/utils/Config.kt | 34 ++ .../bou/readerforselfoss/utils/LinksUtils.kt | 81 +++ .../customtabs/CustomTabActivityHelper.java | 146 ++++++ .../utils/customtabs/CustomTabsHelper.java | 126 +++++ .../customtabs/helpers/KeepAliveService.java | 15 + .../apps/readerforselfoss/MainActivity.kt | 14 - app/src/main/res/anim/slide_in_right.xml | 16 + app/src/main/res/anim/slide_out_left.xml | 16 + .../res/drawable-hdpi/ic_add_black_24dp.png | Bin 0 -> 124 bytes .../drawable-hdpi/ic_archive_black_24dp.png | Bin 0 -> 239 bytes .../res/drawable-hdpi/ic_close_white_24dp.png | Bin 0 -> 221 bytes .../drawable-hdpi/ic_done_all_white_24dp.png | Bin 0 -> 275 bytes .../drawable-hdpi/ic_favorite_black_24dp.png | Bin 0 -> 361 bytes .../drawable-hdpi/ic_fiber_new_black_24dp.png | Bin 0 -> 301 bytes .../res/drawable-hdpi/ic_info_black_24.png | Bin 0 -> 355 bytes .../ic_info_outline_white_48dp.png | Bin 0 -> 953 bytes .../ic_open_in_browser_black_24dp.png | Bin 0 -> 187 bytes .../ic_remove_circle_outline_black_24dp.png | Bin 0 -> 473 bytes .../drawable-hdpi/ic_settings_black_24dp.png | Bin 0 -> 453 bytes .../res/drawable-hdpi/ic_share_black_24dp.png | Bin 0 -> 398 bytes .../res/drawable-hdpi/ic_share_white_24dp.png | Bin 0 -> 397 bytes .../drawable-hdpi/ic_thumb_up_white_48dp.png | Bin 0 -> 434 bytes .../res/drawable-mdpi/ic_add_black_24dp.png | Bin 0 -> 86 bytes .../drawable-mdpi/ic_archive_black_24dp.png | Bin 0 -> 174 bytes .../res/drawable-mdpi/ic_close_white_24dp.png | Bin 0 -> 175 bytes .../drawable-mdpi/ic_done_all_white_24dp.png | Bin 0 -> 213 bytes .../drawable-mdpi/ic_favorite_black_24dp.png | Bin 0 -> 247 bytes .../drawable-mdpi/ic_fiber_new_black_24dp.png | Bin 0 -> 208 bytes .../res/drawable-mdpi/ic_info_black_24.png | Bin 0 -> 241 bytes .../ic_info_outline_white_48dp.png | Bin 0 -> 655 bytes .../ic_open_in_browser_black_24dp.png | Bin 0 -> 144 bytes .../ic_remove_circle_outline_black_24dp.png | Bin 0 -> 309 bytes .../drawable-mdpi/ic_settings_black_24dp.png | Bin 0 -> 322 bytes .../res/drawable-mdpi/ic_share_black_24dp.png | Bin 0 -> 262 bytes .../res/drawable-mdpi/ic_share_white_24dp.png | Bin 0 -> 268 bytes .../drawable-mdpi/ic_thumb_up_white_48dp.png | Bin 0 -> 307 bytes .../res/drawable-xhdpi/ic_add_black_24dp.png | Bin 0 -> 108 bytes .../drawable-xhdpi/ic_archive_black_24dp.png | Bin 0 -> 261 bytes .../drawable-xhdpi/ic_close_white_24dp.png | Bin 0 -> 257 bytes .../drawable-xhdpi/ic_done_all_white_24dp.png | Bin 0 -> 300 bytes .../drawable-xhdpi/ic_favorite_black_24dp.png | Bin 0 -> 437 bytes .../ic_fiber_new_black_24dp.png | Bin 0 -> 308 bytes .../res/drawable-xhdpi/ic_info_black_24.png | Bin 0 -> 464 bytes .../ic_info_outline_white_48dp.png | Bin 0 -> 1279 bytes .../ic_open_in_browser_black_24dp.png | Bin 0 -> 208 bytes .../ic_remove_circle_outline_black_24dp.png | Bin 0 -> 625 bytes .../drawable-xhdpi/ic_settings_black_24dp.png | Bin 0 -> 557 bytes .../drawable-xhdpi/ic_share_black_24dp.png | Bin 0 -> 483 bytes .../drawable-xhdpi/ic_share_white_24dp.png | Bin 0 -> 496 bytes .../drawable-xhdpi/ic_thumb_up_white_48dp.png | Bin 0 -> 542 bytes .../res/drawable-xxhdpi/ic_add_black_24dp.png | Bin 0 -> 114 bytes .../drawable-xxhdpi/ic_archive_black_24dp.png | Bin 0 -> 377 bytes .../drawable-xxhdpi/ic_close_white_24dp.png | Bin 0 -> 347 bytes .../ic_done_all_white_24dp.png | Bin 0 -> 398 bytes .../ic_favorite_black_24dp.png | Bin 0 -> 636 bytes .../ic_fiber_new_black_24dp.png | Bin 0 -> 448 bytes .../res/drawable-xxhdpi/ic_info_black_24.png | Bin 0 -> 649 bytes .../ic_info_outline_white_48dp.png | Bin 0 -> 1985 bytes .../ic_open_in_browser_black_24dp.png | Bin 0 -> 283 bytes .../ic_remove_circle_outline_black_24dp.png | Bin 0 -> 907 bytes .../ic_settings_black_24dp.png | Bin 0 -> 827 bytes .../drawable-xxhdpi/ic_share_black_24dp.png | Bin 0 -> 675 bytes .../drawable-xxhdpi/ic_share_white_24dp.png | Bin 0 -> 698 bytes .../ic_thumb_up_white_48dp.png | Bin 0 -> 768 bytes .../drawable-xxxhdpi/ic_add_black_24dp.png | Bin 0 -> 119 bytes .../ic_archive_black_24dp.png | Bin 0 -> 483 bytes .../drawable-xxxhdpi/ic_close_white_24dp.png | Bin 0 -> 436 bytes .../ic_done_all_white_24dp.png | Bin 0 -> 473 bytes .../ic_favorite_black_24dp.png | Bin 0 -> 802 bytes .../ic_fiber_new_black_24dp.png | Bin 0 -> 527 bytes .../res/drawable-xxxhdpi/ic_info_black_24.png | Bin 0 -> 874 bytes .../ic_info_outline_white_48dp.png | Bin 0 -> 2633 bytes .../ic_open_in_browser_black_24dp.png | Bin 0 -> 359 bytes .../ic_remove_circle_outline_black_24dp.png | Bin 0 -> 1222 bytes .../ic_settings_black_24dp.png | Bin 0 -> 1073 bytes .../drawable-xxxhdpi/ic_share_black_24dp.png | Bin 0 -> 888 bytes .../drawable-xxxhdpi/ic_share_white_24dp.png | Bin 0 -> 938 bytes .../ic_thumb_up_white_48dp.png | Bin 0 -> 1085 bytes .../main/res/layout-land/activity_home.xml | 74 +++ .../main/res/layout/activity_add_source.xml | 118 +++++ app/src/main/res/layout/activity_home.xml | 67 +++ app/src/main/res/layout/activity_login.xml | 137 +++++ app/src/main/res/layout/activity_main.xml | 12 +- app/src/main/res/layout/activity_reader.xml | 66 +++ app/src/main/res/layout/activity_sources.xml | 38 ++ app/src/main/res/layout/card_item.xml | 159 ++++++ app/src/main/res/layout/list_item.xml | 119 +++++ app/src/main/res/layout/source_list_item.xml | 55 ++ app/src/main/res/menu/home_menu.xml | 40 ++ app/src/main/res/menu/login_menu.xml | 9 + app/src/main/res/values-fr/strings.xml | 106 ++++ app/src/main/res/values-nl/strings.xml | 106 ++++ app/src/main/res/values/dimens.xml | 6 + app/src/main/res/values/strings.xml | 109 +++- app/src/main/res/xml/bottombar.xml | 20 + .../main/res/xml/default_remote_config.xml | 7 + app/src/main/res/xml/pref_general.xml | 64 +++ app/src/main/res/xml/pref_headers.xml | 13 + app/src/main/res/xml/pref_links.xml | 10 + build.gradle | 3 + 125 files changed, 4619 insertions(+), 35 deletions(-) create mode 100644 app/src/main/java/apps/amine/bou/readerforselfoss/AddSourceActivity.kt create mode 100644 app/src/main/java/apps/amine/bou/readerforselfoss/HomeActivity.kt create mode 100644 app/src/main/java/apps/amine/bou/readerforselfoss/IntroActivity.kt create mode 100644 app/src/main/java/apps/amine/bou/readerforselfoss/LoginActivity.kt create mode 100644 app/src/main/java/apps/amine/bou/readerforselfoss/MainActivity.kt rename app/src/main/java/{bou/amine/apps => apps/amine/bou}/readerforselfoss/MyApp.kt (50%) create mode 100644 app/src/main/java/apps/amine/bou/readerforselfoss/ReaderActivity.kt create mode 100644 app/src/main/java/apps/amine/bou/readerforselfoss/SourcesActivity.kt create mode 100644 app/src/main/java/apps/amine/bou/readerforselfoss/adapters/ItemCardAdapter.java create mode 100644 app/src/main/java/apps/amine/bou/readerforselfoss/adapters/ItemListAdapter.java create mode 100644 app/src/main/java/apps/amine/bou/readerforselfoss/adapters/SourcesListAdapter.java create mode 100644 app/src/main/java/apps/amine/bou/readerforselfoss/api/mercury/MercuryApi.java create mode 100644 app/src/main/java/apps/amine/bou/readerforselfoss/api/mercury/MercuryModels.kt create mode 100644 app/src/main/java/apps/amine/bou/readerforselfoss/api/mercury/MercuryService.java create mode 100644 app/src/main/java/apps/amine/bou/readerforselfoss/api/selfoss/BooleanTypeAdapter.kt create mode 100644 app/src/main/java/apps/amine/bou/readerforselfoss/api/selfoss/SelfossApi.java create mode 100644 app/src/main/java/apps/amine/bou/readerforselfoss/api/selfoss/SelfossModels.kt create mode 100644 app/src/main/java/apps/amine/bou/readerforselfoss/api/selfoss/SelfossService.java create mode 100644 app/src/main/java/apps/amine/bou/readerforselfoss/settings/AppCompatPreferenceActivity.java create mode 100644 app/src/main/java/apps/amine/bou/readerforselfoss/settings/SettingsActivity.java create mode 100644 app/src/main/java/apps/amine/bou/readerforselfoss/utils/AppUtils.kt create mode 100644 app/src/main/java/apps/amine/bou/readerforselfoss/utils/Config.kt create mode 100644 app/src/main/java/apps/amine/bou/readerforselfoss/utils/LinksUtils.kt create mode 100644 app/src/main/java/apps/amine/bou/readerforselfoss/utils/customtabs/CustomTabActivityHelper.java create mode 100644 app/src/main/java/apps/amine/bou/readerforselfoss/utils/customtabs/CustomTabsHelper.java create mode 100644 app/src/main/java/apps/amine/bou/readerforselfoss/utils/customtabs/helpers/KeepAliveService.java delete mode 100644 app/src/main/java/bou/amine/apps/readerforselfoss/MainActivity.kt create mode 100644 app/src/main/res/anim/slide_in_right.xml create mode 100644 app/src/main/res/anim/slide_out_left.xml create mode 100644 app/src/main/res/drawable-hdpi/ic_add_black_24dp.png create mode 100644 app/src/main/res/drawable-hdpi/ic_archive_black_24dp.png create mode 100644 app/src/main/res/drawable-hdpi/ic_close_white_24dp.png create mode 100644 app/src/main/res/drawable-hdpi/ic_done_all_white_24dp.png create mode 100644 app/src/main/res/drawable-hdpi/ic_favorite_black_24dp.png create mode 100644 app/src/main/res/drawable-hdpi/ic_fiber_new_black_24dp.png create mode 100644 app/src/main/res/drawable-hdpi/ic_info_black_24.png create mode 100644 app/src/main/res/drawable-hdpi/ic_info_outline_white_48dp.png create mode 100644 app/src/main/res/drawable-hdpi/ic_open_in_browser_black_24dp.png create mode 100644 app/src/main/res/drawable-hdpi/ic_remove_circle_outline_black_24dp.png create mode 100644 app/src/main/res/drawable-hdpi/ic_settings_black_24dp.png create mode 100644 app/src/main/res/drawable-hdpi/ic_share_black_24dp.png create mode 100644 app/src/main/res/drawable-hdpi/ic_share_white_24dp.png create mode 100644 app/src/main/res/drawable-hdpi/ic_thumb_up_white_48dp.png create mode 100644 app/src/main/res/drawable-mdpi/ic_add_black_24dp.png create mode 100644 app/src/main/res/drawable-mdpi/ic_archive_black_24dp.png create mode 100644 app/src/main/res/drawable-mdpi/ic_close_white_24dp.png create mode 100644 app/src/main/res/drawable-mdpi/ic_done_all_white_24dp.png create mode 100644 app/src/main/res/drawable-mdpi/ic_favorite_black_24dp.png create mode 100644 app/src/main/res/drawable-mdpi/ic_fiber_new_black_24dp.png create mode 100644 app/src/main/res/drawable-mdpi/ic_info_black_24.png create mode 100644 app/src/main/res/drawable-mdpi/ic_info_outline_white_48dp.png create mode 100644 app/src/main/res/drawable-mdpi/ic_open_in_browser_black_24dp.png create mode 100644 app/src/main/res/drawable-mdpi/ic_remove_circle_outline_black_24dp.png create mode 100644 app/src/main/res/drawable-mdpi/ic_settings_black_24dp.png create mode 100644 app/src/main/res/drawable-mdpi/ic_share_black_24dp.png create mode 100644 app/src/main/res/drawable-mdpi/ic_share_white_24dp.png create mode 100644 app/src/main/res/drawable-mdpi/ic_thumb_up_white_48dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_add_black_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_archive_black_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_close_white_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_done_all_white_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_favorite_black_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_fiber_new_black_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_info_black_24.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_info_outline_white_48dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_open_in_browser_black_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_remove_circle_outline_black_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_settings_black_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_share_black_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_share_white_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_thumb_up_white_48dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_add_black_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_archive_black_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_close_white_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_done_all_white_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_favorite_black_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_fiber_new_black_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_info_black_24.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_info_outline_white_48dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_open_in_browser_black_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_remove_circle_outline_black_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_settings_black_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_share_black_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_share_white_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_thumb_up_white_48dp.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_add_black_24dp.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_archive_black_24dp.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_close_white_24dp.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_done_all_white_24dp.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_favorite_black_24dp.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_fiber_new_black_24dp.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_info_black_24.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_info_outline_white_48dp.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_open_in_browser_black_24dp.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_remove_circle_outline_black_24dp.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_settings_black_24dp.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_share_black_24dp.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_share_white_24dp.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_thumb_up_white_48dp.png create mode 100644 app/src/main/res/layout-land/activity_home.xml create mode 100644 app/src/main/res/layout/activity_add_source.xml create mode 100644 app/src/main/res/layout/activity_home.xml create mode 100644 app/src/main/res/layout/activity_login.xml create mode 100644 app/src/main/res/layout/activity_reader.xml create mode 100644 app/src/main/res/layout/activity_sources.xml create mode 100644 app/src/main/res/layout/card_item.xml create mode 100644 app/src/main/res/layout/list_item.xml create mode 100644 app/src/main/res/layout/source_list_item.xml create mode 100644 app/src/main/res/menu/home_menu.xml create mode 100644 app/src/main/res/menu/login_menu.xml create mode 100644 app/src/main/res/values-fr/strings.xml create mode 100644 app/src/main/res/values-nl/strings.xml create mode 100644 app/src/main/res/values/dimens.xml create mode 100644 app/src/main/res/xml/bottombar.xml create mode 100644 app/src/main/res/xml/default_remote_config.xml create mode 100644 app/src/main/res/xml/pref_general.xml create mode 100644 app/src/main/res/xml/pref_headers.xml create mode 100644 app/src/main/res/xml/pref_links.xml diff --git a/README.md b/README.md index bf660a0..d5e5491 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,9 @@ You'll have to: - Define the following in `res/values/strings.xml` or create `res/values/secrets.xml` - mercury: A [Mercury](https://mercury.postlight.com/web-parser/) web parser api key for the internal browser - + - feedback_email: An email to receive users feedback. + - source_url: an url to the source code, used in the settings + - tracker_url: an url to the tracker, used in the settings ## Useful links diff --git a/app/build.gradle b/app/build.gradle index 5f902b7..822a678 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -22,7 +22,7 @@ android { compileSdkVersion 25 buildToolsVersion "25.0.3" defaultConfig { - applicationId "bou.amine.apps.readerforselfoss" + applicationId "apps.amine.bou.readerforselfoss" minSdkVersion 16 targetSdkVersion 25 versionCode 1500 @@ -64,21 +64,62 @@ dependencies { // Android Support compile 'com.android.support:appcompat-v7:25.3.1' + compile 'com.android.support:design:25.3.1' + compile 'com.android.support:recyclerview-v7:25.3.1' + compile 'com.android.support:support-v4:25.3.1' + compile 'com.android.support:support-vector-drawable:25.3.1' + compile 'com.android.support:customtabs:25.3.1' + compile 'com.android.support:cardview-v7:25.3.1' compile 'com.android.support.constraint:constraint-layout:1.0.2' // Firebase + crashlytics - compile 'com.google.firebase:firebase-core:10.2.4' - compile 'com.google.firebase:firebase-config:10.2.4' - compile 'com.google.firebase:firebase-invites:10.2.4' + compile 'com.google.android.gms:play-services:10.2.6' + compile 'com.google.firebase:firebase-core:10.2.6' + compile 'com.google.firebase:firebase-config:10.2.6' + compile 'com.google.firebase:firebase-invites:10.2.6' compile('com.crashlytics.sdk.android:crashlytics:2.6.8@aar') { - transitive = true; + transitive = true } //multidex compile 'com.android.support:multidex:1.0.1' + + // Intro + compile 'agency.tango.android:material-intro-screen:0.0.5' + + // About + compile('com.mikepenz:aboutlibraries:5.9.6@aar') { + transitive = true + } + + // Retrofit + http logging + okhttp + compile 'com.squareup.retrofit2:retrofit:2.1.0' + compile 'com.squareup.okhttp3:logging-interceptor:3.4.1' + compile 'com.squareup.retrofit2:converter-gson:2.1.0' + compile 'com.burgstaller:okhttp-digest:1.12' + + // Material-ish things + compile 'com.roughike:bottom-bar:2.2.0' + compile 'com.melnykov:floatingactionbutton:1.3.0' + compile 'com.github.jd-alexander:LikeButton:0.2.1' + compile 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1' + compile 'org.sufficientlysecure:html-textview:3.3' + + // glide + compile 'com.github.bumptech.glide:glide:3.7.0' + + // Asking politely users to rate the app + compile 'com.github.stkent:amplify:1.5.0' + + // For the article reader + compile 'com.klinkerapps:drag-dismiss-activity:1.4.0' + } + + + apply plugin: 'com.google.gms.google-services' \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 44927d8..623ce19 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -23,3 +23,9 @@ # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile + +#About libraries +-keep class .R +-keep class **.R$* { + ; +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f961aa7..8751646 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,6 +1,6 @@ + package="apps.amine.bou.readerforselfoss"> @@ -20,7 +20,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/apps/amine/bou/readerforselfoss/AddSourceActivity.kt b/app/src/main/java/apps/amine/bou/readerforselfoss/AddSourceActivity.kt new file mode 100644 index 0000000..76ddc1f --- /dev/null +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/AddSourceActivity.kt @@ -0,0 +1,122 @@ +package apps.amine.bou.readerforselfoss + +import android.content.Intent +import android.os.Bundle +import android.support.constraint.ConstraintLayout +import android.support.v7.app.AppCompatActivity +import android.view.View +import android.widget.* +import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi +import apps.amine.bou.readerforselfoss.api.selfoss.Spout +import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse +import apps.amine.bou.readerforselfoss.utils.Config +import apps.amine.bou.readerforselfoss.utils.isUrlValid +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response + + +class AddSourceActivity : AppCompatActivity() { + + private var mSpoutsValue: String? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_add_source) + + val mProgress = findViewById(R.id.progress) as ProgressBar + val mForm = findViewById(R.id.formContainer) as ConstraintLayout + val mNameInput = findViewById(R.id.nameInput) as EditText + val mSourceUri = findViewById(R.id.sourceUri) as EditText + val mTags = findViewById(R.id.tags) as EditText + val mSpoutsSpinner = findViewById(R.id.spoutsSpinner) as Spinner + val mSaveBtn = findViewById(R.id.saveBtn) as Button + val api = SelfossApi(this) + + + val intent = intent + if (Intent.ACTION_SEND == intent.action && "text/plain" == intent.type) { + mSourceUri.setText(intent.getStringExtra(Intent.EXTRA_TEXT)) + mNameInput.setText(intent.getStringExtra(Intent.EXTRA_TITLE)) + } + + mSaveBtn.setOnClickListener { handleSaveSource(mTags, mNameInput.text.toString(), mSourceUri.text.toString(), api) } + + + val spoutsKV = HashMap() + mSpoutsSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onItemSelected(adapterView: AdapterView<*>, view: View, i: Int, l: Long) { + val spoutName = (view as TextView).text.toString() + mSpoutsValue = spoutsKV[spoutName] + } + + override fun onNothingSelected(adapterView: AdapterView<*>) { + mSpoutsValue = null + } + } + + val config = Config(this) + + if (config.baseUrl.isEmpty() || !isUrlValid(config.baseUrl)) { + Toast.makeText(this, getString(R.string.addStringNoUrl), Toast.LENGTH_SHORT).show() + val i = Intent(this, LoginActivity::class.java) + startActivity(i) + finish() + } else { + + var items: Map + api.spouts().enqueue(object : Callback> { + override fun onResponse(call: Call>, response: Response>) { + if (response.body() != null) { + items = response.body() + + val itemsStrings = items.map { it.value.name } + for ((key, value) in items) { + spoutsKV.put(value.name, key) + } + + mProgress.visibility = View.GONE + mForm.visibility = View.VISIBLE + + val spinnerArrayAdapter = ArrayAdapter(this@AddSourceActivity, android.R.layout.simple_spinner_item, itemsStrings) + spinnerArrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + mSpoutsSpinner.adapter = spinnerArrayAdapter + + } else { + handleProblemWithSpouts() + } + } + + override fun onFailure(call: Call>, t: Throwable) { + handleProblemWithSpouts() + } + + private fun handleProblemWithSpouts() { + Toast.makeText(this@AddSourceActivity, R.string.cant_get_spouts, Toast.LENGTH_SHORT).show() + mProgress.visibility = View.GONE + } + }) + } + } + + private fun handleSaveSource(mTags: EditText, title: String, url: String, api: SelfossApi) { + + if (title.isEmpty() || url.isEmpty() || mSpoutsValue == null || mSpoutsValue!!.isEmpty()) { + Toast.makeText(this, R.string.form_not_complete, Toast.LENGTH_SHORT).show() + } else { + api.createSource(title, url, mSpoutsValue, mTags.text.toString(), "").enqueue(object : Callback { + override fun onResponse(call: Call, response: Response) { + if (response.body() != null && response.body().isSuccess) { + finish() + } else { + Toast.makeText(this@AddSourceActivity, R.string.cant_create_source, Toast.LENGTH_SHORT).show() + } + } + + override fun onFailure(call: Call, t: Throwable) { + Toast.makeText(this@AddSourceActivity, R.string.cant_create_source, Toast.LENGTH_SHORT).show() + } + }) + } + } +} diff --git a/app/src/main/java/apps/amine/bou/readerforselfoss/HomeActivity.kt b/app/src/main/java/apps/amine/bou/readerforselfoss/HomeActivity.kt new file mode 100644 index 0000000..6519a10 --- /dev/null +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/HomeActivity.kt @@ -0,0 +1,477 @@ +package apps.amine.bou.readerforselfoss + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.content.SharedPreferences +import android.net.Uri +import android.os.Bundle +import android.preference.PreferenceManager +import android.support.design.widget.CoordinatorLayout +import android.support.v4.widget.SwipeRefreshLayout +import android.support.v7.app.AppCompatActivity +import android.support.v7.widget.GridLayoutManager +import android.support.v7.widget.RecyclerView +import android.support.v7.widget.StaggeredGridLayoutManager +import android.support.v7.widget.helper.ItemTouchHelper +import android.util.Log +import android.view.Menu +import android.view.MenuItem +import android.widget.Toast +import apps.amine.bou.readerforselfoss.adapters.ItemCardAdapter +import apps.amine.bou.readerforselfoss.adapters.ItemListAdapter +import apps.amine.bou.readerforselfoss.api.selfoss.Item +import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi +import apps.amine.bou.readerforselfoss.api.selfoss.Stats +import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse +import apps.amine.bou.readerforselfoss.settings.SettingsActivity +import apps.amine.bou.readerforselfoss.utils.Config +import apps.amine.bou.readerforselfoss.utils.checkAndDisplayStoreApk +import apps.amine.bou.readerforselfoss.utils.checkApkVersion +import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper +import com.crashlytics.android.answers.Answers +import com.crashlytics.android.answers.InviteEvent +import com.github.stkent.amplify.prompt.DefaultLayoutPromptView +import com.github.stkent.amplify.tracking.Amplify +import com.google.android.gms.appinvite.AppInviteInvitation +import com.google.android.gms.common.ConnectionResult +import com.google.android.gms.common.GoogleApiAvailability +import com.google.firebase.crash.FirebaseCrash +import com.google.firebase.remoteconfig.FirebaseRemoteConfig +import com.mikepenz.aboutlibraries.Libs +import com.mikepenz.aboutlibraries.LibsBuilder +import com.roughike.bottombar.BottomBar +import com.roughike.bottombar.BottomBarTab +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response + + +class HomeActivity : AppCompatActivity() { + + + private val MENU_PREFERENCES = 12302 + private val REQUEST_INVITE = 13231 + private val REQUEST_INVITE_BYMAIL = 13232 + private var mRecyclerView: RecyclerView? = null + private var api: SelfossApi? = null + private var items: List? = null + private var mCustomTabActivityHelper: CustomTabActivityHelper? = null + + private var clickBehavior = false + private var internalBrowser = false + private var articleViewer = false + private var shouldBeCardView = false + private var displayUnreadCount = false + private var displayAllCount = false + private var editor: SharedPreferences.Editor? = null + + private val UNREAD_SHOWN = 1 + private val READ_SHOWN = 2 + private val FAV_SHOWN = 3 + private var elementsShown: Int = 0 + private var mBottomBar: BottomBar? = null + private var mCoordinatorLayout: CoordinatorLayout? = null + private var mSwipeRefreshLayout: SwipeRefreshLayout? = null + private var sharedPref: SharedPreferences? = null + private var tabNew: BottomBarTab? = null + private var tabArchive: BottomBarTab? = null + private var tabStarred: BottomBarTab? = null + private var mFirebaseRemoteConfig: FirebaseRemoteConfig? = null + private var fullHeightCards: Boolean = false + + private fun handleSharedPrefs() { + clickBehavior = this.sharedPref!!.getBoolean("tab_on_tap", false) + internalBrowser = this.sharedPref!!.getBoolean("prefer_internal_browser", true) + articleViewer = this.sharedPref!!.getBoolean("prefer_article_viewer", true) + shouldBeCardView = this.sharedPref!!.getBoolean("card_view_active", false) + displayUnreadCount = this.sharedPref!!.getBoolean("display_unread_count", true) + displayAllCount = this.sharedPref!!.getBoolean("display_other_count", false) + fullHeightCards = this.sharedPref!!.getBoolean("full_height_cards", false) + } + + override fun onResume() { + super.onResume() + + sharedPref = PreferenceManager.getDefaultSharedPreferences(this) + + val settings = getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE) + editor = settings.edit() + + if (BuildConfig.GITHUB_VERSION) { + checkApkVersion(settings, editor!!, this@HomeActivity, mFirebaseRemoteConfig!!) + } + + handleSharedPrefs() + + tabNew = mBottomBar!!.getTabWithId(R.id.tab_new) + tabArchive = mBottomBar!!.getTabWithId(R.id.tab_archive) + tabStarred = mBottomBar!!.getTabWithId(R.id.tab_fav) + + + getElementsAccordingToTab() + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_home) + + if (savedInstanceState == null) { + val promptView = findViewById(R.id.prompt_view) as DefaultLayoutPromptView + + Amplify.getSharedInstance().promptIfReady(promptView) + } + + mFirebaseRemoteConfig = FirebaseRemoteConfig.getInstance() + mFirebaseRemoteConfig!!.setDefaults(R.xml.default_remote_config) + + mCustomTabActivityHelper = CustomTabActivityHelper() + + api = SelfossApi(this) + items = ArrayList() + + mBottomBar = findViewById(R.id.bottomBar) as BottomBar + + // TODO: clean this hack + val listenerAlreadySet = booleanArrayOf(false) + mBottomBar!!.setOnTabSelectListener { tabId -> + if (listenerAlreadySet[0]) { + if (tabId == R.id.tab_new) { + getUnRead() + } else if (tabId == R.id.tab_archive) { + getRead() + } else if (tabId == R.id.tab_fav) { + getStarred() + } + getElementsAccordingToTab() + } else { + listenerAlreadySet[0] = true + } + } + + mCoordinatorLayout = findViewById(R.id.coordLayout) as CoordinatorLayout + mSwipeRefreshLayout = findViewById(R.id.swipeRefreshLayout) as SwipeRefreshLayout + mRecyclerView = findViewById(R.id.my_recycler_view) as RecyclerView + + reloadLayoutManager() + + mSwipeRefreshLayout!!.setColorSchemeResources( + R.color.refresh_progress_1, + R.color.refresh_progress_2, + R.color.refresh_progress_3) + mSwipeRefreshLayout!!.setOnRefreshListener { getElementsAccordingToTab() } + + val simpleItemTouchCallback = object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) { + + override fun getSwipeDirs(recyclerView: RecyclerView?, viewHolder: RecyclerView.ViewHolder?): Int { + if (elementsShown != UNREAD_SHOWN) { + return 0 + } else { + return super.getSwipeDirs(recyclerView, viewHolder) + } + } + + override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean { + return false + } + + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, swipeDir: Int) { + try { + val i = items!![viewHolder.adapterPosition] + val position = items!!.indexOf(i) + + if (shouldBeCardView) { + (mRecyclerView!!.adapter as ItemCardAdapter).removeItemAtIndex(position) + } else { + (mRecyclerView!!.adapter as ItemListAdapter).removeItemAtIndex(position) + } + tabNew!!.setBadgeCount(items!!.size - 1) + + } catch (e: IndexOutOfBoundsException) { + FirebaseCrash.logcat(Log.ERROR, "SWIPE ERROR", "Swipe index out of bound") + FirebaseCrash.report(e) + } + + } + } + + val itemTouchHelper = ItemTouchHelper(simpleItemTouchCallback) + itemTouchHelper.attachToRecyclerView(mRecyclerView) + + + checkAndDisplayStoreApk(this@HomeActivity) + + } + + private fun reloadLayoutManager() { + val mLayoutManager: RecyclerView.LayoutManager + if (shouldBeCardView) { + mLayoutManager = StaggeredGridLayoutManager(calculateNoOfColumns(), StaggeredGridLayoutManager.VERTICAL) + mLayoutManager.gapStrategy = StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS + } else { + mLayoutManager = GridLayoutManager(this, calculateNoOfColumns()) + } + + mRecyclerView!!.layoutManager = mLayoutManager + mRecyclerView!!.setHasFixedSize(true) + + mBottomBar!!.setOnTabReselectListener { + if (shouldBeCardView) { + if ((mLayoutManager as StaggeredGridLayoutManager).findFirstCompletelyVisibleItemPositions(null)[0] == 0) { + getElementsAccordingToTab() + } else { + mLayoutManager.scrollToPositionWithOffset(0, 0) + } + } else { + if ((mLayoutManager as GridLayoutManager).findFirstCompletelyVisibleItemPosition() == 0) { + getElementsAccordingToTab() + } else { + mLayoutManager.scrollToPositionWithOffset(0, 0) + } + } + } + } + + private fun getElementsAccordingToTab() { + items = ArrayList() + + when (elementsShown) { + UNREAD_SHOWN -> getUnRead() + READ_SHOWN -> getRead() + FAV_SHOWN -> getStarred() + else -> getUnRead() + } + } + + private fun getUnRead() { + elementsShown = UNREAD_SHOWN + api!!.unreadItems.enqueue(object : Callback> { + override fun onResponse(call: Call>, response: Response>) { + if (response.body() != null && response.body().isNotEmpty()) { + items = response.body() + } else { + items = ArrayList() + } + handleListResult() + mSwipeRefreshLayout!!.isRefreshing = false + } + + override fun onFailure(call: Call>, t: Throwable) { + mSwipeRefreshLayout!!.isRefreshing = false + Toast.makeText(this@HomeActivity, R.string.cant_get_new_elements, Toast.LENGTH_SHORT).show() + } + }) + } + + private fun getRead() { + elementsShown = READ_SHOWN + api!!.readItems.enqueue(object : Callback> { + override fun onResponse(call: Call>, response: Response>) { + if (response.body() != null && response.body().isNotEmpty()) { + items = response.body() + } else { + items = ArrayList() + } + handleListResult() + mSwipeRefreshLayout!!.isRefreshing = false + } + + override fun onFailure(call: Call>, t: Throwable) { + Toast.makeText(this@HomeActivity, R.string.cant_get_read, Toast.LENGTH_SHORT).show() + mSwipeRefreshLayout!!.isRefreshing = false + } + }) + } + + private fun getStarred() { + elementsShown = FAV_SHOWN + api!!.starredItems.enqueue(object : Callback> { + override fun onResponse(call: Call>, response: Response>) { + if (response.body() != null && response.body().isNotEmpty()) { + items = response.body() + } else { + items = ArrayList() + } + handleListResult() + mSwipeRefreshLayout!!.isRefreshing = false + } + + override fun onFailure(call: Call>, t: Throwable) { + Toast.makeText(this@HomeActivity, R.string.cant_get_favs, Toast.LENGTH_SHORT).show() + mSwipeRefreshLayout!!.isRefreshing = false + } + }) + } + + private fun handleListResult() { + reloadLayoutManager() + + val mAdapter: RecyclerView.Adapter<*> + if (shouldBeCardView) { + mAdapter = ItemCardAdapter(this, items, api, mCustomTabActivityHelper, internalBrowser, articleViewer, fullHeightCards) + } else { + mAdapter = ItemListAdapter(this, items, api, mCustomTabActivityHelper, clickBehavior, internalBrowser, articleViewer) + } + mRecyclerView!!.adapter = mAdapter + mAdapter.notifyDataSetChanged() + + if (items!!.isEmpty()) Toast.makeText(this@HomeActivity, R.string.nothing_here, Toast.LENGTH_SHORT).show() + + reloadBadges() + } + + override fun onStart() { + super.onStart() + mCustomTabActivityHelper!!.bindCustomTabsService(this) + } + + override fun onStop() { + super.onStop() + mCustomTabActivityHelper!!.unbindCustomTabsService(this) + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + val inflater = menuInflater + inflater.inflate(R.menu.home_menu, menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.refresh -> { + api!!.update().enqueue(object : Callback { + override fun onResponse(call: Call, response: Response) { + Toast.makeText(this@HomeActivity, + R.string.refresh_success_response, Toast.LENGTH_LONG) + .show() + } + + override fun onFailure(call: Call, t: Throwable) { + Toast.makeText(this@HomeActivity, R.string.refresh_failer_message, Toast.LENGTH_SHORT).show() + } + }) + Toast.makeText(this, R.string.refresh_in_progress, Toast.LENGTH_SHORT).show() + return true + } + R.id.readAll -> { + if (elementsShown == UNREAD_SHOWN) { + mSwipeRefreshLayout!!.isRefreshing = false + val ids = items!!.map { it.id } + + api!!.readAll(ids).enqueue(object : Callback { + override fun onResponse(call: Call, response: Response) { + if (response.body() != null && response.body().isSuccess) { + Toast.makeText(this@HomeActivity, R.string.all_posts_read, Toast.LENGTH_SHORT).show() + } else { + Toast.makeText(this@HomeActivity, R.string.all_posts_not_read, Toast.LENGTH_SHORT).show() + } + mSwipeRefreshLayout!!.isRefreshing = false + } + + override fun onFailure(call: Call, t: Throwable) { + Toast.makeText(this@HomeActivity, R.string.all_posts_not_read, Toast.LENGTH_SHORT).show() + mSwipeRefreshLayout!!.isRefreshing = false + } + }) + items = ArrayList() + handleListResult() + } + return true + } + R.id.action_disconnect -> { + editor!!.remove("url") + editor!!.remove("login") + editor!!.remove("password") + editor!!.apply() + val intent = Intent(this, LoginActivity::class.java) + startActivity(intent) + finish() + return true + } + R.id.action_sources -> { + val intent2 = Intent(this, SourcesActivity::class.java) + startActivity(intent2) + return true + } + R.id.action_settings -> { + val intent3 = Intent(this, SettingsActivity::class.java) + startActivityForResult(intent3, MENU_PREFERENCES) + return true + } + R.id.about -> { + LibsBuilder() + .withActivityStyle(Libs.ActivityStyle.LIGHT_DARK_TOOLBAR) + .withAboutIconShown(true) + .withAboutVersionShown(true) + .start(this) + return true + } + R.id.action_share_the_app -> { + if (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS) { + val share = AppInviteInvitation.IntentBuilder(getString(R.string.invitation_title)) + .setMessage(getString(R.string.invitation_message)) + .setDeepLink(Uri.parse("https://ymbh5.app.goo.gl/qbvQ")) + .setCallToActionText(getString(R.string.invitation_cta)) + .build() + startActivityForResult(share, REQUEST_INVITE) + } else { + val sendIntent = Intent() + sendIntent.action = Intent.ACTION_SEND + sendIntent.putExtra(Intent.EXTRA_TEXT, getString(R.string.invitation_message) + " https://ymbh5.app.goo.gl/qbvQ") + sendIntent.type = "text/plain" + startActivityForResult(sendIntent, REQUEST_INVITE_BYMAIL) + } + return super.onOptionsItemSelected(item) + } + else -> return super.onOptionsItemSelected(item) + } + } + + fun reloadBadges() { + if (displayUnreadCount || displayAllCount) { + api!!.stats.enqueue(object : Callback { + override fun onResponse(call: Call, response: Response) { + if (response.body() != null) { + tabNew!!.setBadgeCount(response.body().unread) + if (displayAllCount) { + tabArchive!!.setBadgeCount(response.body().total) + tabStarred!!.setBadgeCount(response.body().starred) + } else { + tabArchive!!.removeBadge() + tabStarred!!.removeBadge() + } + } + } + + override fun onFailure(call: Call, t: Throwable) { + + } + }) + } else { + tabNew!!.removeBadge() + tabArchive!!.removeBadge() + tabStarred!!.removeBadge() + } + } + + override fun onActivityResult(req: Int, result: Int, data: Intent?) { + when (req) { + MENU_PREFERENCES -> recreate() + REQUEST_INVITE -> if (result == Activity.RESULT_OK) { + Answers.getInstance().logInvite(InviteEvent()) + } + REQUEST_INVITE_BYMAIL -> { + Answers.getInstance().logInvite(InviteEvent()) + super.onActivityResult(req, result, data) + } + else -> super.onActivityResult(req, result, data) + } + + } + + fun calculateNoOfColumns(): Int { + val displayMetrics = resources.displayMetrics + val dpWidth = displayMetrics.widthPixels / displayMetrics.density + return (dpWidth / 300).toInt() + } +} diff --git a/app/src/main/java/apps/amine/bou/readerforselfoss/IntroActivity.kt b/app/src/main/java/apps/amine/bou/readerforselfoss/IntroActivity.kt new file mode 100644 index 0000000..61720a6 --- /dev/null +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/IntroActivity.kt @@ -0,0 +1,57 @@ +package apps.amine.bou.readerforselfoss + +import agency.tango.materialintroscreen.MaterialIntroActivity +import agency.tango.materialintroscreen.MessageButtonBehaviour +import agency.tango.materialintroscreen.SlideFragmentBuilder +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.preference.PreferenceManager +import android.view.View + + +class IntroActivity : MaterialIntroActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + addSlide(SlideFragmentBuilder() + .backgroundColor(R.color.colorPrimary) + .buttonsColor(R.color.colorAccent) + .image(R.mipmap.ic_launcher) + .title(getString(R.string.intro_hello_title)) + .description(getString(R.string.intro_hello_message)) + .build()) + + addSlide(SlideFragmentBuilder() + .backgroundColor(R.color.colorAccent) + .buttonsColor(R.color.colorPrimary) + .image(R.drawable.ic_info_outline_white_48dp) + .title(getString(R.string.intro_needs_selfoss_title)) + .description(getString(R.string.intro_needs_selfoss_message)) + .build(), + MessageButtonBehaviour(View.OnClickListener { + val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://selfoss.aditu.de")) + startActivity(browserIntent) + }, getString(R.string.intro_needs_selfoss_link))) + + addSlide(SlideFragmentBuilder() + .backgroundColor(R.color.colorPrimaryDark) + .buttonsColor(R.color.colorAccentDark) + .image(R.drawable.ic_thumb_up_white_48dp) + .title(getString(R.string.intro_all_set_title)) + .description(getString(R.string.intro_all_set_message)) + .build()) + } + + override fun onFinish() { + super.onFinish() + val getPrefs = PreferenceManager.getDefaultSharedPreferences(baseContext) + val e = getPrefs.edit() + e.putBoolean("firstStart", false) + e.apply() + val intent = Intent(this, LoginActivity::class.java) + startActivity(intent) + finish() + } +} diff --git a/app/src/main/java/apps/amine/bou/readerforselfoss/LoginActivity.kt b/app/src/main/java/apps/amine/bou/readerforselfoss/LoginActivity.kt new file mode 100644 index 0000000..c06d0fb --- /dev/null +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/LoginActivity.kt @@ -0,0 +1,258 @@ +package apps.amine.bou.readerforselfoss + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.content.Context +import android.content.Intent +import android.content.SharedPreferences +import android.os.Bundle +import android.support.design.widget.TextInputLayout +import android.support.v7.app.AlertDialog +import android.support.v7.app.AppCompatActivity +import android.text.TextUtils +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.view.inputmethod.EditorInfo +import android.widget.Button +import android.widget.EditText +import android.widget.Switch +import android.widget.TextView +import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi +import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse +import apps.amine.bou.readerforselfoss.utils.Config +import apps.amine.bou.readerforselfoss.utils.checkAndDisplayStoreApk +import apps.amine.bou.readerforselfoss.utils.isUrlValid +import com.mikepenz.aboutlibraries.Libs +import com.mikepenz.aboutlibraries.LibsBuilder +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response + + +class LoginActivity : AppCompatActivity() { + + private var settings: SharedPreferences? = null + private var mProgressView: View? = null + private var mUrlView: EditText? = null + private var mLoginView: TextView? = null + private var mHTTPLoginView: TextView? = null + private var mPasswordView: EditText? = null + private var mHTTPPasswordView: EditText? = null + private var inValidCount: Int = 0 + private var isWithLogin = false + private var isWithHTTPLogin = false + private var mLoginFormView: View? = null + + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_login) + + settings = getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE) + if (settings!!.getString("url", "").isNotEmpty()) { + goToMain() + } else { + checkAndDisplayStoreApk(this@LoginActivity) + } + + isWithLogin = false + isWithHTTPLogin = false + inValidCount = 0 + + mUrlView = findViewById(R.id.url) as EditText + mLoginView = findViewById(R.id.login) as TextView + mHTTPLoginView = findViewById(R.id.httpLogin) as TextView + mPasswordView = findViewById(R.id.password) as EditText + mHTTPPasswordView = findViewById(R.id.httpPassword) as EditText + mLoginFormView = findViewById(R.id.login_form) + mProgressView = findViewById(R.id.login_progress) + + val mSwitch = findViewById(R.id.withLogin) as Switch + val mHTTPSwitch = findViewById(R.id.withHttpLogin) as Switch + val mLoginLayout = findViewById(R.id.loginLayout) as TextInputLayout + val mHTTPLoginLayout = findViewById(R.id.httpLoginInput) as TextInputLayout + val mPasswordLayout = findViewById(R.id.passwordLayout) as TextInputLayout + val mHTTPPasswordLayout = findViewById(R.id.httpPasswordInput) as TextInputLayout + val mEmailSignInButton = findViewById(R.id.email_sign_in_button) as Button + + mPasswordView!!.setOnEditorActionListener(TextView.OnEditorActionListener { _, id, _ -> + if (id == R.id.login || id == EditorInfo.IME_NULL) { + attemptLogin() + return@OnEditorActionListener true + } + false + }) + + mEmailSignInButton.setOnClickListener { attemptLogin() } + + mSwitch.setOnCheckedChangeListener { _, b -> + isWithLogin = !isWithLogin + val visi: Int + if (b) { + visi = View.VISIBLE + + } else { + visi = View.GONE + } + mLoginLayout.visibility = visi + mPasswordLayout.visibility = visi + } + + mHTTPSwitch.setOnCheckedChangeListener { _, b -> + isWithHTTPLogin = !isWithHTTPLogin + val visi: Int + if (b) { + visi = View.VISIBLE + + } else { + visi = View.GONE + } + mHTTPLoginLayout.visibility = visi + mHTTPPasswordLayout.visibility = visi + } + } + + private fun goToMain() { + val intent = Intent(this, HomeActivity::class.java) + startActivity(intent) + finish() + } + + private fun attemptLogin() { + + // Reset errors. + mUrlView!!.error = null + mLoginView!!.error = null + mHTTPLoginView!!.error = null + mPasswordView!!.error = null + mHTTPPasswordView!!.error = null + + // Store values at the time of the login attempt. + val url = mUrlView!!.text.toString() + val login = mLoginView!!.text.toString() + val httpLogin = mHTTPLoginView!!.text.toString() + val password = mPasswordView!!.text.toString() + val httpPassword = mHTTPPasswordView!!.text.toString() + + var cancel = false + var focusView: View? = null + + if (!isUrlValid(url)) { + mUrlView!!.error = getString(R.string.login_url_problem) + focusView = mUrlView + cancel = true + inValidCount++ + if (inValidCount == 3) { + val alertDialog = AlertDialog.Builder(this).create() + alertDialog.setTitle(getString(R.string.warning_wrong_url)) + alertDialog.setMessage(getString(R.string.text_wrong_url)) + alertDialog.setButton(AlertDialog.BUTTON_NEUTRAL, "OK", + { dialog, _ -> dialog.dismiss() }) + alertDialog.show() + inValidCount = 0 + } + } + + if (isWithLogin || isWithHTTPLogin) { + if (TextUtils.isEmpty(password)) { + mPasswordView!!.error = getString(R.string.error_invalid_password) + focusView = mPasswordView + cancel = true + } + + if (TextUtils.isEmpty(login)) { + mLoginView!!.error = getString(R.string.error_field_required) + focusView = mLoginView + cancel = true + } + } + + if (cancel) { + focusView!!.requestFocus() + } else { + showProgress(true) + + val editor = settings!!.edit() + editor.putString("url", url) + editor.putString("login", login) + editor.putString("httpUserName", httpLogin) + editor.putString("password", password) + editor.putString("httpPassword", httpPassword) + editor.apply() + + val api = SelfossApi(this@LoginActivity) + api.login().enqueue(object : Callback { + private fun preferenceError() { + editor.remove("url") + editor.remove("login") + editor.remove("httpUserName") + editor.remove("password") + editor.remove("httpPassword") + editor.apply() + mUrlView!!.error = getString(R.string.wrong_infos) + mLoginView!!.error = getString(R.string.wrong_infos) + mPasswordView!!.error = getString(R.string.wrong_infos) + mHTTPLoginView!!.error = getString(R.string.wrong_infos) + mHTTPPasswordView!!.error = getString(R.string.wrong_infos) + showProgress(false) + } + + override fun onResponse(call: Call, response: Response) { + if (response.body() != null && response.body().isSuccess) { + goToMain() + } else { + preferenceError() + } + } + + override fun onFailure(call: Call, t: Throwable) { + preferenceError() + } + }) + } + } + + /** + * Shows the progress UI and hides the login form. + */ + private fun showProgress(show: Boolean) { + val shortAnimTime = resources.getInteger(android.R.integer.config_shortAnimTime) + + mLoginFormView!!.visibility = if (show) View.GONE else View.VISIBLE + mLoginFormView!!.animate().setDuration(shortAnimTime.toLong()).alpha( + if (show) 0F else 1F).setListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + mLoginFormView!!.visibility = if (show) View.GONE else View.VISIBLE + } + }) + + mProgressView!!.visibility = if (show) View.VISIBLE else View.GONE + mProgressView!!.animate().setDuration(shortAnimTime.toLong()).alpha( + if (show) 1F else 0F).setListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + mProgressView!!.visibility = if (show) View.VISIBLE else View.GONE + } + }) + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + val inflater = menuInflater + inflater.inflate(R.menu.login_menu, menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.about -> { + LibsBuilder() + .withActivityStyle(Libs.ActivityStyle.LIGHT_DARK_TOOLBAR) + .withAboutIconShown(true) + .withAboutVersionShown(true) + .start(this) + return true + } + else -> return super.onOptionsItemSelected(item) + } + } +} diff --git a/app/src/main/java/apps/amine/bou/readerforselfoss/MainActivity.kt b/app/src/main/java/apps/amine/bou/readerforselfoss/MainActivity.kt new file mode 100644 index 0000000..c8b3692 --- /dev/null +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/MainActivity.kt @@ -0,0 +1,26 @@ +package apps.amine.bou.readerforselfoss + +import android.content.Intent +import android.os.Bundle +import android.preference.PreferenceManager +import android.support.v7.app.AppCompatActivity + + +class MainActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + + if (PreferenceManager.getDefaultSharedPreferences(baseContext).getBoolean("firstStart", true)) { + val i = Intent(this@MainActivity, IntroActivity::class.java) + startActivity(i) + } else { + val intent = Intent(this, LoginActivity::class.java) + startActivity(intent) + } + + finish() + + } +} diff --git a/app/src/main/java/bou/amine/apps/readerforselfoss/MyApp.kt b/app/src/main/java/apps/amine/bou/readerforselfoss/MyApp.kt similarity index 50% rename from app/src/main/java/bou/amine/apps/readerforselfoss/MyApp.kt rename to app/src/main/java/apps/amine/bou/readerforselfoss/MyApp.kt index 014911f..9e2f304 100644 --- a/app/src/main/java/bou/amine/apps/readerforselfoss/MyApp.kt +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/MyApp.kt @@ -1,7 +1,8 @@ -package bou.amine.apps.readerforselfoss +package apps.amine.bou.readerforselfoss import android.support.multidex.MultiDexApplication import com.crashlytics.android.Crashlytics +import com.github.stkent.amplify.tracking.Amplify import io.fabric.sdk.android.Fabric @@ -10,5 +11,10 @@ class MyApp : MultiDexApplication() { super.onCreate() if (!BuildConfig.DEBUG) Fabric.with(this, Crashlytics()) + + Amplify.initSharedInstance(this) + .setFeedbackEmailAddress(getString(R.string.feedback_email)) + .setAlwaysShow(BuildConfig.DEBUG) + .applyAllDefaultRules() } } \ No newline at end of file diff --git a/app/src/main/java/apps/amine/bou/readerforselfoss/ReaderActivity.kt b/app/src/main/java/apps/amine/bou/readerforselfoss/ReaderActivity.kt new file mode 100644 index 0000000..0243284 --- /dev/null +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/ReaderActivity.kt @@ -0,0 +1,84 @@ +package apps.amine.bou.readerforselfoss + +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import apps.amine.bou.readerforselfoss.api.mercury.MercuryApi +import apps.amine.bou.readerforselfoss.api.mercury.ParsedContent +import apps.amine.bou.readerforselfoss.utils.buildCustomTabsIntent +import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper +import com.bumptech.glide.Glide +import org.sufficientlysecure.htmltextview.HtmlHttpImageGetter +import org.sufficientlysecure.htmltextview.HtmlTextView +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response +import xyz.klinker.android.drag_dismiss.activity.DragDismissActivity + + +class ReaderActivity : DragDismissActivity() { + private var mCustomTabActivityHelper: CustomTabActivityHelper? = null + + override fun onStart() { + super.onStart() + mCustomTabActivityHelper!!.bindCustomTabsService(this) + } + + override fun onStop() { + super.onStop() + mCustomTabActivityHelper!!.unbindCustomTabsService(this) + } + + override fun onCreateContent(inflater: LayoutInflater, parent: ViewGroup, savedInstanceState: Bundle?): View { + val v = inflater.inflate(R.layout.activity_reader, parent, false) + showProgressBar() + + val image = v.findViewById(R.id.imageView) as ImageView + val source = v.findViewById(R.id.source) as TextView + val title = v.findViewById(R.id.title) as TextView + val content = v.findViewById(R.id.content) as HtmlTextView + val url = intent.getStringExtra("url") + val parser = MercuryApi(getString(R.string.mercury)) + + val customTabsIntent = buildCustomTabsIntent(this@ReaderActivity) + mCustomTabActivityHelper = CustomTabActivityHelper() + mCustomTabActivityHelper!!.bindCustomTabsService(this) + + + parser.parseUrl(url).enqueue(object : Callback { + override fun onResponse(call: Call, response: Response) { + if (response.body() != null) { + source.text = response.body().domain + title.text = response.body().title + if (response.body().content != null && !response.body().content.isEmpty()) + content.setHtml(response.body().content, HtmlHttpImageGetter(content, null, true)) + if (response.body().lead_image_url != null && !response.body().lead_image_url.isEmpty()) + Glide.with(applicationContext).load(response.body().lead_image_url).asBitmap().fitCenter().into(image) + hideProgressBar() + } else { + errorAfterMercuryCall() + } + } + + override fun onFailure(call: Call, t: Throwable) { + errorAfterMercuryCall() + } + + private fun errorAfterMercuryCall() { + CustomTabActivityHelper.openCustomTab(this@ReaderActivity, customTabsIntent, Uri.parse(url) + ) { _, uri -> + val intent = Intent(Intent.ACTION_VIEW, uri) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + startActivity(intent) + } + finish() + } + }) + return v + } +} diff --git a/app/src/main/java/apps/amine/bou/readerforselfoss/SourcesActivity.kt b/app/src/main/java/apps/amine/bou/readerforselfoss/SourcesActivity.kt new file mode 100644 index 0000000..fbffbf3 --- /dev/null +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/SourcesActivity.kt @@ -0,0 +1,57 @@ +package apps.amine.bou.readerforselfoss + +import android.content.Intent +import android.os.Bundle +import android.support.v7.app.AppCompatActivity +import android.support.v7.widget.LinearLayoutManager +import android.support.v7.widget.RecyclerView +import android.widget.Toast +import apps.amine.bou.readerforselfoss.adapters.SourcesListAdapter +import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi +import apps.amine.bou.readerforselfoss.api.selfoss.Sources +import com.melnykov.fab.FloatingActionButton +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response + + +class SourcesActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_sources) + } + + override fun onResume() { + super.onResume() + val mFab = findViewById(R.id.fab) as FloatingActionButton + val mRecyclerView = findViewById(R.id.activity_sources) as RecyclerView + val mLayoutManager = LinearLayoutManager(this) + val api = SelfossApi(this) + var items: List = ArrayList() + + mFab.attachToRecyclerView(mRecyclerView) + mRecyclerView.setHasFixedSize(true) + mRecyclerView.layoutManager = mLayoutManager + + api.sources.enqueue(object : Callback> { + override fun onResponse(call: Call>, response: Response>) { + if (response.body() != null && response.body().isNotEmpty()) { + items = response.body() + } + val mAdapter = SourcesListAdapter(this@SourcesActivity, items, api) + mRecyclerView.adapter = mAdapter + mAdapter.notifyDataSetChanged() + if (items.isEmpty()) Toast.makeText(this@SourcesActivity, R.string.nothing_here, Toast.LENGTH_SHORT).show() + } + + override fun onFailure(call: Call>, t: Throwable) { + Toast.makeText(this@SourcesActivity, R.string.cant_get_sources, Toast.LENGTH_SHORT).show() + } + }) + + mFab.setOnClickListener { + startActivity(Intent(this@SourcesActivity, AddSourceActivity::class.java)) + } + } +} diff --git a/app/src/main/java/apps/amine/bou/readerforselfoss/adapters/ItemCardAdapter.java b/app/src/main/java/apps/amine/bou/readerforselfoss/adapters/ItemCardAdapter.java new file mode 100644 index 0000000..8a987d1 --- /dev/null +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/adapters/ItemCardAdapter.java @@ -0,0 +1,310 @@ +package apps.amine.bou.readerforselfoss.adapters; + + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.net.Uri; +import android.support.constraint.ConstraintLayout; +import android.support.customtabs.CustomTabsIntent; +import android.support.design.widget.Snackbar; +import android.support.v4.graphics.drawable.RoundedBitmapDrawable; +import android.support.v4.graphics.drawable.RoundedBitmapDrawableFactory; +import android.support.v7.widget.RecyclerView; +import android.text.Html; +import android.text.format.DateUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.ImageView.ScaleType; +import android.widget.TextView; +import android.widget.Toast; + +import apps.amine.bou.readerforselfoss.R; +import apps.amine.bou.readerforselfoss.api.selfoss.Item; +import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi; +import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse; +import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper; +import com.amulyakhare.textdrawable.TextDrawable; +import com.amulyakhare.textdrawable.TextDrawable.IBuilder; +import com.amulyakhare.textdrawable.util.ColorGenerator; +import com.bumptech.glide.Glide; +import com.bumptech.glide.request.target.BitmapImageViewTarget; +import com.like.LikeButton; +import com.like.OnLikeListener; +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +import static apps.amine.bou.readerforselfoss.utils.LinksUtilsKt.buildCustomTabsIntent; +import static apps.amine.bou.readerforselfoss.utils.LinksUtilsKt.openItemUrl; + + +public class ItemCardAdapter extends RecyclerView.Adapter { + private final List items; + private final SelfossApi api; + private final CustomTabActivityHelper helper; + private final Context c; + private final boolean internalBrowser; + private final boolean articleViewer; + private final Activity app; + private final ColorGenerator generator; + private final boolean fullHeightCards; + + public ItemCardAdapter(Activity a, List myObject, SelfossApi selfossApi, + CustomTabActivityHelper mCustomTabActivityHelper, boolean internalBrowser, + boolean articleViewer, boolean fullHeightCards) { + app = a; + items = myObject; + api = selfossApi; + helper = mCustomTabActivityHelper; + c = app.getApplicationContext(); + this.internalBrowser = internalBrowser; + this.articleViewer = articleViewer; + generator = ColorGenerator.MATERIAL; + this.fullHeightCards = fullHeightCards; + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + ConstraintLayout v = (ConstraintLayout) LayoutInflater.from(c).inflate(R.layout.card_item, parent, false); + return new ViewHolder(v); + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + Item itm = items.get(position); + + + holder.saveBtn.setLiked((itm.getStarred())); + holder.title.setText(Html.fromHtml(itm.getTitle())); + + String sourceAndDate = itm.getSourcetitle(); + long d; + try { + d = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(itm.getDatetime()).getTime(); + sourceAndDate += " " + + DateUtils.getRelativeTimeSpanString( + d, + new Date().getTime(), + DateUtils.MINUTE_IN_MILLIS, + DateUtils.FORMAT_ABBREV_RELATIVE + ); + } catch (ParseException e) { + e.printStackTrace(); + } + holder.sourceTitleAndDate.setText(sourceAndDate); + + if (itm.getThumbnail(c).isEmpty()) { + Glide.clear(holder.itemImage); + holder.itemImage.setImageDrawable(null); + } else { + if (fullHeightCards) { + Glide.with(c).load(itm.getThumbnail(c)).asBitmap().fitCenter().into(holder.itemImage); + } else { + Glide.with(c).load(itm.getThumbnail(c)).asBitmap().centerCrop().into(holder.itemImage); + } + } + + final ViewHolder fHolder = holder; + if (itm.getIcon(c).isEmpty()) { + int color = generator.getColor(itm.getSourcetitle()); + StringBuilder textDrawable = new StringBuilder(); + for(String s : itm.getSourcetitle().split(" ")) + { + textDrawable.append(s.charAt(0)); + } + + IBuilder builder = TextDrawable.builder().round(); + + TextDrawable drawable = builder.build(textDrawable.toString(), color); + holder.sourceImage.setImageDrawable(drawable); + } else { + + Glide.with(c).load(itm.getIcon(c)).asBitmap().centerCrop().into(new BitmapImageViewTarget(holder.sourceImage) { + @Override + protected void setResource(Bitmap resource) { + RoundedBitmapDrawable circularBitmapDrawable = + RoundedBitmapDrawableFactory.create(c.getResources(), resource); + circularBitmapDrawable.setCircular(true); + fHolder.sourceImage.setImageDrawable(circularBitmapDrawable); + } + }); + } + + holder.saveBtn.setLiked(itm.getStarred()); + } + + @Override + public int getItemCount() { + return items.size(); + } + + private void doUnmark(final Item i, final int position) { + Snackbar s = Snackbar + .make(app.findViewById(R.id.coordLayout), R.string.marked_as_read, Snackbar.LENGTH_LONG) + .setAction(R.string.undo_string, new View.OnClickListener() { + @Override + public void onClick(View view) { + items.add(position, i); + notifyItemInserted(position); + + api.unmarkItem(i.getId()).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) {} + + @Override + public void onFailure(Call call, Throwable t) { + items.remove(i); + notifyItemRemoved(position); + doUnmark(i, position); + } + }); + } + }); + + View view = s.getView(); + TextView tv = (TextView) view.findViewById(android.support.design.R.id.snackbar_text); + tv.setTextColor(Color.WHITE); + s.show(); + } + + public void removeItemAtIndex(final int position) { + + final Item i = items.get(position); + + items.remove(i); + notifyItemRemoved(position); + + api.markItem(i.getId()).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + + doUnmark(i, position); + } + + @Override + public void onFailure(Call call, Throwable t) { + Toast.makeText(app, app.getString(R.string.cant_mark_read), Toast.LENGTH_SHORT).show(); + items.add(i); + notifyItemInserted(position); + } + }); + + } + + public class ViewHolder extends RecyclerView.ViewHolder { + LikeButton saveBtn; + ImageButton browserBtn; + ImageButton shareBtn; + ImageView itemImage; + ImageView sourceImage; + TextView title; + TextView sourceTitleAndDate; + public ConstraintLayout mView; + + public ViewHolder(ConstraintLayout itemView) { + super(itemView); + mView = itemView; + handleClickListeners(); + handleCustomTabActions(); + } + + private void handleClickListeners() { + sourceImage = (ImageView) mView.findViewById( R.id.sourceImage); + itemImage = (ImageView) mView.findViewById( R.id.itemImage); + title = (TextView) mView.findViewById( R.id.title); + sourceTitleAndDate = (TextView) mView.findViewById( R.id.sourceTitleAndDate); + saveBtn = (LikeButton) mView.findViewById( R.id.favButton); + shareBtn = (ImageButton) mView.findViewById( R.id.shareBtn); + browserBtn = (ImageButton) mView.findViewById( R.id.browserBtn); + + if (!fullHeightCards) { + itemImage.setMaxHeight((int) c.getResources().getDimension(R.dimen.card_image_max_height)); + itemImage.setScaleType(ScaleType.CENTER_CROP); + } + + saveBtn.setOnLikeListener(new OnLikeListener() { + @Override + public void liked(LikeButton likeButton) { + Item i = items.get(getAdapterPosition()); + api.starrItem(i.getId()).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) {} + + @Override + public void onFailure(Call call, Throwable t) { + saveBtn.setLiked(false); + Toast.makeText(c, R.string.cant_mark_favortie, Toast.LENGTH_SHORT).show(); + } + }); + } + + @Override + public void unLiked(LikeButton likeButton) { + Item i = items.get(getAdapterPosition()); + api.unstarrItem(i.getId()).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) {} + + @Override + public void onFailure(Call call, Throwable t) { + saveBtn.setLiked(true); + Toast.makeText(c, R.string.cant_unmark_favortie, Toast.LENGTH_SHORT).show(); + } + }); + } + }); + + shareBtn.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + Item i = items.get(getAdapterPosition()); + Intent sendIntent = new Intent(); + sendIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + sendIntent.setAction(Intent.ACTION_SEND); + sendIntent.putExtra(Intent.EXTRA_TEXT, i.getLinkDecoded()); + sendIntent.setType("text/plain"); + c.startActivity(Intent.createChooser(sendIntent, c.getString(R.string.share)).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); + } + }); + + browserBtn.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + Item i = items.get(getAdapterPosition()); + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.setData(Uri.parse(i.getLinkDecoded())); + c.startActivity(intent); + } + }); + } + + private void handleCustomTabActions() { + final CustomTabsIntent customTabsIntent = buildCustomTabsIntent(c); + helper.bindCustomTabsService(app); + + mView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + openItemUrl(items.get(getAdapterPosition()), + customTabsIntent, + internalBrowser, + articleViewer, + app, + c); + } + }); + } + } +} diff --git a/app/src/main/java/apps/amine/bou/readerforselfoss/adapters/ItemListAdapter.java b/app/src/main/java/apps/amine/bou/readerforselfoss/adapters/ItemListAdapter.java new file mode 100644 index 0000000..b972b52 --- /dev/null +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/adapters/ItemListAdapter.java @@ -0,0 +1,363 @@ +package apps.amine.bou.readerforselfoss.adapters; + + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.net.Uri; +import android.support.constraint.ConstraintLayout; +import android.support.customtabs.CustomTabsIntent; +import android.support.design.widget.Snackbar; +import android.support.v4.graphics.drawable.RoundedBitmapDrawable; +import android.support.v4.graphics.drawable.RoundedBitmapDrawableFactory; +import android.support.v7.widget.RecyclerView; +import android.text.Html; +import android.text.format.DateUtils; +import android.util.TypedValue; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.RelativeLayout; +import android.widget.TextView; +import android.widget.Toast; + +import apps.amine.bou.readerforselfoss.R; +import apps.amine.bou.readerforselfoss.api.selfoss.Item; +import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi; +import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse; +import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper; +import com.amulyakhare.textdrawable.TextDrawable; +import com.amulyakhare.textdrawable.util.ColorGenerator; +import com.bumptech.glide.Glide; +import com.bumptech.glide.request.target.BitmapImageViewTarget; +import com.like.LikeButton; +import com.like.OnLikeListener; +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +import static apps.amine.bou.readerforselfoss.utils.LinksUtilsKt.buildCustomTabsIntent; +import static apps.amine.bou.readerforselfoss.utils.LinksUtilsKt.openItemUrl; + + +public class ItemListAdapter extends RecyclerView.Adapter { + private final boolean clickBehavior; + private final boolean articleViewer; + private final boolean internalBrowser; + private final ColorGenerator generator; + private SelfossApi api; + private Context c; + private List items; + private List bars; + private Activity app; + private CustomTabActivityHelper helper; + + public ItemListAdapter(Activity a, List myObject, SelfossApi selfossApi, + CustomTabActivityHelper mCustomTabActivityHelper, boolean clickBehavior, + boolean internalBrowser, boolean articleViewer) { + app = a; + items = myObject; + api = selfossApi; + helper = mCustomTabActivityHelper; + c = app.getApplicationContext(); + this.clickBehavior = clickBehavior; + this.internalBrowser = internalBrowser; + this.articleViewer = articleViewer; + bars = new ArrayList<>(Collections.nCopies(items.size() + 1, false)); + generator = ColorGenerator.MATERIAL; + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + ConstraintLayout v = (ConstraintLayout) LayoutInflater.from(c).inflate(R.layout.list_item, parent, false); + return new ViewHolder(v); + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + Item itm = items.get(position); + + + holder.saveBtn.setLiked((itm.getStarred())); + holder.title.setText(Html.fromHtml(itm.getTitle())); + + String sourceAndDate = itm.getSourcetitle(); + long d; + try { + d = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(itm.getDatetime()).getTime(); + sourceAndDate += " " + + DateUtils.getRelativeTimeSpanString( + d, + new Date().getTime(), + DateUtils.MINUTE_IN_MILLIS, + DateUtils.FORMAT_ABBREV_RELATIVE + ); + } catch (ParseException e) { + e.printStackTrace(); + } + holder.sourceTitleAndDate.setText(sourceAndDate); + + if (itm.getThumbnail(c).isEmpty()) { + int sizeInInt = 46; + int sizeInDp = (int) TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, sizeInInt, c.getResources() + .getDisplayMetrics()); + + int marginInInt = 16; + int marginInDp = (int) TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, marginInInt, c.getResources() + .getDisplayMetrics()); + + ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) holder.sourceImage.getLayoutParams(); + params.height = sizeInDp; + params.width = sizeInDp; + params.setMargins(marginInDp, 0, 0, 0); + holder.sourceImage.setLayoutParams(params); + + if (itm.getIcon(c).isEmpty()) { + int color = generator.getColor(itm.getSourcetitle()); + StringBuilder textDrawable = new StringBuilder(); + for(String s : itm.getSourcetitle().split(" ")) + { + textDrawable.append(s.charAt(0)); + } + + TextDrawable.IBuilder builder = TextDrawable.builder().round(); + + TextDrawable drawable = builder.build(textDrawable.toString(), color); + holder.sourceImage.setImageDrawable(drawable); + } else { + + final ViewHolder fHolder = holder; + Glide.with(c).load(itm.getIcon(c)).asBitmap().centerCrop().into(new BitmapImageViewTarget(holder.sourceImage) { + @Override + protected void setResource(Bitmap resource) { + RoundedBitmapDrawable circularBitmapDrawable = + RoundedBitmapDrawableFactory.create(c.getResources(), resource); + circularBitmapDrawable.setCircular(true); + fHolder.sourceImage.setImageDrawable(circularBitmapDrawable); + } + }); + } + } else { + Glide.with(c).load(itm.getThumbnail(c)).asBitmap().centerCrop().into(holder.sourceImage); + } + + if (bars.get(position)) { + holder.actionBar.setVisibility(View.VISIBLE); + } else { + holder.actionBar.setVisibility(View.GONE); + } + + holder.saveBtn.setLiked(itm.getStarred()); + } + + @Override + public int getItemCount() { + return items.size(); + } + + + private void doUnmark(final Item i, final int position) { + Snackbar s = Snackbar + .make(app.findViewById(R.id.coordLayout), R.string.marked_as_read, Snackbar.LENGTH_LONG) + .setAction(R.string.undo_string, new View.OnClickListener() { + @Override + public void onClick(View view) { + items.add(position, i); + notifyItemInserted(position); + + api.unmarkItem(i.getId()).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) {} + + @Override + public void onFailure(Call call, Throwable t) { + items.remove(i); + notifyItemRemoved(position); + doUnmark(i, position); + } + }); + } + }); + + View view = s.getView(); + TextView tv = (TextView) view.findViewById(android.support.design.R.id.snackbar_text); + tv.setTextColor(Color.WHITE); + s.show(); + } + + public void removeItemAtIndex(final int position) { + + final Item i = items.get(position); + + items.remove(i); + notifyItemRemoved(position); + + api.markItem(i.getId()).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + doUnmark(i, position); + + } + + @Override + public void onFailure(Call call, Throwable t) { + Toast.makeText(app, app.getString(R.string.cant_mark_read), Toast.LENGTH_SHORT).show(); + items.add(i); + notifyItemInserted(position); + } + }); + + } + + public class ViewHolder extends RecyclerView.ViewHolder { + LikeButton saveBtn; + ImageButton browserBtn; + ImageButton shareBtn; + public RelativeLayout actionBar; + ImageView sourceImage; + TextView title; + TextView sourceTitleAndDate; + public ConstraintLayout mView; + + public ViewHolder(ConstraintLayout itemView) { + super(itemView); + mView = itemView; + handleClickListeners(); + handleCustomTabActions(); + } + + private void handleClickListeners() { + actionBar = (RelativeLayout) mView.findViewById(R.id.actionBar); + sourceImage = (ImageView) mView.findViewById( R.id.itemImage); + title = (TextView) mView.findViewById( R.id.title); + sourceTitleAndDate = (TextView) mView.findViewById( R.id.sourceTitleAndDate); + saveBtn = (LikeButton) mView.findViewById( R.id.favButton); + shareBtn = (ImageButton) mView.findViewById( R.id.shareBtn); + browserBtn = (ImageButton) mView.findViewById( R.id.browserBtn); + + + saveBtn.setOnLikeListener(new OnLikeListener() { + @Override + public void liked(LikeButton likeButton) { + Item i = items.get(getAdapterPosition()); + api.starrItem(i.getId()).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) {} + + @Override + public void onFailure(Call call, Throwable t) { + saveBtn.setLiked(false); + Toast.makeText(c, R.string.cant_mark_favortie, Toast.LENGTH_SHORT).show(); + } + }); + } + + @Override + public void unLiked(LikeButton likeButton) { + Item i = items.get(getAdapterPosition()); + api.unstarrItem(i.getId()).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) {} + + @Override + public void onFailure(Call call, Throwable t) { + saveBtn.setLiked(true); + Toast.makeText(c, R.string.cant_unmark_favortie, Toast.LENGTH_SHORT).show(); + } + }); + } + }); + + shareBtn.setOnClickListener(new View.OnClickListener() { + public void onClick(View view) { + Item i = items.get(getAdapterPosition()); + Intent sendIntent = new Intent(); + sendIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + sendIntent.setAction(Intent.ACTION_SEND); + sendIntent.putExtra(Intent.EXTRA_TEXT, i.getLinkDecoded()); + sendIntent.setType("text/plain"); + c.startActivity(Intent.createChooser(sendIntent, c.getString(R.string.share)).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); + } + }); + + browserBtn.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + Item i = items.get(getAdapterPosition()); + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.setData(Uri.parse(i.getLinkDecoded())); + c.startActivity(intent); + } + }); + } + + + private void handleCustomTabActions() { + final CustomTabsIntent customTabsIntent = buildCustomTabsIntent(c); + helper.bindCustomTabsService(app); + + + if (!clickBehavior) { + mView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + openItemUrl(items.get(getAdapterPosition()), + customTabsIntent, + internalBrowser, + articleViewer, + app, + c); + } + }); + mView.setOnLongClickListener(new View.OnLongClickListener() { + @Override + public boolean onLongClick(View view) { + actionBarShowHide(); + return true; + } + }); + } else { + mView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + actionBarShowHide(); + } + }); + mView.setOnLongClickListener(new View.OnLongClickListener() { + @Override + public boolean onLongClick(View view) { + openItemUrl(items.get(getAdapterPosition()), + customTabsIntent, + internalBrowser, + articleViewer, + app, + c); + return true; + } + }); + } + } + + private void actionBarShowHide() { + bars.set(getAdapterPosition(), true); + if (actionBar.getVisibility() == View.GONE) + actionBar.setVisibility(View.VISIBLE); + else + actionBar.setVisibility(View.GONE); + } + } +} diff --git a/app/src/main/java/apps/amine/bou/readerforselfoss/adapters/SourcesListAdapter.java b/app/src/main/java/apps/amine/bou/readerforselfoss/adapters/SourcesListAdapter.java new file mode 100644 index 0000000..b65d98d --- /dev/null +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/adapters/SourcesListAdapter.java @@ -0,0 +1,133 @@ +package apps.amine.bou.readerforselfoss.adapters; + +import android.app.Activity; +import android.content.Context; +import android.graphics.Bitmap; +import android.support.constraint.ConstraintLayout; +import android.support.v4.graphics.drawable.RoundedBitmapDrawable; +import android.support.v4.graphics.drawable.RoundedBitmapDrawableFactory; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.*; +import apps.amine.bou.readerforselfoss.R; +import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi; +import apps.amine.bou.readerforselfoss.api.selfoss.Sources; +import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse; +import com.amulyakhare.textdrawable.TextDrawable; +import com.amulyakhare.textdrawable.util.ColorGenerator; +import com.bumptech.glide.Glide; +import com.bumptech.glide.request.target.BitmapImageViewTarget; +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +import java.util.List; + +public class SourcesListAdapter extends RecyclerView.Adapter { + private final List items; + private final Activity app; + private final SelfossApi api; + private final Context c; + private final ColorGenerator generator; + + public SourcesListAdapter(Activity activity, List items, SelfossApi api) { + this.app = activity; + this.items = items; + this.api = api; + this.c = app.getBaseContext(); + + generator = ColorGenerator.MATERIAL; + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + ConstraintLayout v = (ConstraintLayout) LayoutInflater.from(c).inflate(R.layout.source_list_item, parent, false); + return new ViewHolder(v); + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + Sources itm = items.get(position); + + final ViewHolder fHolder = holder; + if (itm.getIcon(c).isEmpty()) { + int color = generator.getColor(itm.getTitle()); + StringBuilder textDrawable = new StringBuilder(); + for(String s : itm.getTitle().split(" ")) + { + textDrawable.append(s.charAt(0)); + } + + TextDrawable.IBuilder builder = TextDrawable.builder().round(); + + TextDrawable drawable = builder.build(textDrawable.toString(), color); + holder.sourceImage.setImageDrawable(drawable); + } else { + Glide.with(c).load(itm.getIcon(c)).asBitmap().centerCrop().into(new BitmapImageViewTarget(holder.sourceImage) { + @Override + protected void setResource(Bitmap resource) { + RoundedBitmapDrawable circularBitmapDrawable = + RoundedBitmapDrawableFactory.create(c.getResources(), resource); + circularBitmapDrawable.setCircular(true); + fHolder.sourceImage.setImageDrawable(circularBitmapDrawable); + } + }); + } + + holder.sourceTitle.setText(itm.getTitle()); + } + + @Override + public int getItemCount() { + return items.size(); + } + + public class ViewHolder extends RecyclerView.ViewHolder { + ConstraintLayout mView; + ImageView sourceImage; + TextView sourceTitle; + Button deleteBtn; + + public ViewHolder(ConstraintLayout itemView) { + super(itemView); + mView = itemView; + + handleClickListeners(); + } + + private void handleClickListeners() { + sourceImage = (ImageView) mView.findViewById(R.id.itemImage); + sourceTitle = (TextView) mView.findViewById(R.id.sourceTitle); + deleteBtn = (Button) mView.findViewById(R.id.deleteBtn); + + deleteBtn.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + Sources i = items.get(getAdapterPosition()); + api.deleteSource(i.getId()).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.body() != null && response.body().isSuccess()) { + items.remove(getAdapterPosition()); + notifyItemRemoved(getAdapterPosition()); + notifyItemRangeChanged(getAdapterPosition(), getItemCount()); + } + else { + Toast.makeText(app, "Petit soucis lors de la suppression de la source.", Toast.LENGTH_SHORT).show(); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + Toast.makeText(app, "Petit soucis lors de la suppression de la source.", Toast.LENGTH_SHORT).show(); + } + }); + } + }); + + + } + } +} diff --git a/app/src/main/java/apps/amine/bou/readerforselfoss/api/mercury/MercuryApi.java b/app/src/main/java/apps/amine/bou/readerforselfoss/api/mercury/MercuryApi.java new file mode 100644 index 0000000..d3d4332 --- /dev/null +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/api/mercury/MercuryApi.java @@ -0,0 +1,36 @@ +package apps.amine.bou.readerforselfoss.api.mercury; + + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import okhttp3.OkHttpClient; +import okhttp3.logging.HttpLoggingInterceptor; +import retrofit2.Call; +import retrofit2.Retrofit; +import retrofit2.converter.gson.GsonConverterFactory; + + +public class MercuryApi { + private final MercuryService service; + private final String key; + + public MercuryApi(String key) { + + HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor(); + interceptor.setLevel(HttpLoggingInterceptor.Level.BODY); + OkHttpClient client = new OkHttpClient.Builder().addInterceptor(interceptor).build(); + + Gson gson = new GsonBuilder() + .setLenient() + .create(); + + this.key = key; + Retrofit retrofit = new Retrofit.Builder().baseUrl("https://mercury.postlight.com").client(client) + .addConverterFactory(GsonConverterFactory.create(gson)).build(); + service = retrofit.create(MercuryService.class); + } + + public Call parseUrl(String url) { + return service.parseUrl(url, this.key); + } +} diff --git a/app/src/main/java/apps/amine/bou/readerforselfoss/api/mercury/MercuryModels.kt b/app/src/main/java/apps/amine/bou/readerforselfoss/api/mercury/MercuryModels.kt new file mode 100644 index 0000000..1b05115 --- /dev/null +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/api/mercury/MercuryModels.kt @@ -0,0 +1,55 @@ +package apps.amine.bou.readerforselfoss.api.mercury + +import android.os.Parcel +import android.os.Parcelable + + +class ParsedContent(val title: String, + val content: String, + val date_published: String, + val lead_image_url: String, + val dek: String, + val url: String, + val domain: String, + val excerpt: String, + val total_pages: Int, + val rendered_pages: Int, + val next_page_url: String) : Parcelable { + + companion object { + @JvmField val CREATOR: Parcelable.Creator = object : Parcelable.Creator { + override fun createFromParcel(source: Parcel): ParsedContent = ParsedContent(source) + override fun newArray(size: Int): Array = arrayOfNulls(size) + } + } + + constructor(source: Parcel) : this( + title = source.readString(), + content = source.readString(), + date_published = source.readString(), + lead_image_url = source.readString(), + dek = source.readString(), + url = source.readString(), + domain = source.readString(), + excerpt = source.readString(), + total_pages = source.readInt(), + rendered_pages = source.readInt(), + next_page_url = source.readString() + ) + + override fun describeContents() = 0 + + override fun writeToParcel(dest: Parcel, flags: Int) { + dest.writeString(title) + dest.writeString(content) + dest.writeString(date_published) + dest.writeString(lead_image_url) + dest.writeString(dek) + dest.writeString(url) + dest.writeString(domain) + dest.writeString(excerpt) + dest.writeInt(total_pages) + dest.writeInt(rendered_pages) + dest.writeString(next_page_url) + } +} diff --git a/app/src/main/java/apps/amine/bou/readerforselfoss/api/mercury/MercuryService.java b/app/src/main/java/apps/amine/bou/readerforselfoss/api/mercury/MercuryService.java new file mode 100644 index 0000000..85fc610 --- /dev/null +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/api/mercury/MercuryService.java @@ -0,0 +1,13 @@ +package apps.amine.bou.readerforselfoss.api.mercury; + + +import retrofit2.Call; +import retrofit2.http.GET; +import retrofit2.http.Header; +import retrofit2.http.Query; + + +public interface MercuryService { + @GET("parser") + Call parseUrl(@Query("url") String url, @Header("x-api-key") String key); +} diff --git a/app/src/main/java/apps/amine/bou/readerforselfoss/api/selfoss/BooleanTypeAdapter.kt b/app/src/main/java/apps/amine/bou/readerforselfoss/api/selfoss/BooleanTypeAdapter.kt new file mode 100644 index 0000000..69acad7 --- /dev/null +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/api/selfoss/BooleanTypeAdapter.kt @@ -0,0 +1,19 @@ +package apps.amine.bou.readerforselfoss.api.selfoss + +import com.google.gson.JsonParseException +import com.google.gson.JsonDeserializationContext +import com.google.gson.JsonElement +import com.google.gson.JsonDeserializer +import java.lang.reflect.Type + + +internal class BooleanTypeAdapter : JsonDeserializer { + + @Throws(JsonParseException::class) + override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Boolean? = + try { + json.asInt == 1 + } catch (e: Exception) { + json.asBoolean + } +} diff --git a/app/src/main/java/apps/amine/bou/readerforselfoss/api/selfoss/SelfossApi.java b/app/src/main/java/apps/amine/bou/readerforselfoss/api/selfoss/SelfossApi.java new file mode 100644 index 0000000..b966f2e --- /dev/null +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/api/selfoss/SelfossApi.java @@ -0,0 +1,138 @@ +package apps.amine.bou.readerforselfoss.api.selfoss; + + +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import android.content.Context; + +import apps.amine.bou.readerforselfoss.utils.Config; +import com.burgstaller.okhttp.AuthenticationCacheInterceptor; +import com.burgstaller.okhttp.CachingAuthenticatorDecorator; +import com.burgstaller.okhttp.DispatchingAuthenticator; +import com.burgstaller.okhttp.basic.BasicAuthenticator; +import com.burgstaller.okhttp.digest.CachingAuthenticator; +import com.burgstaller.okhttp.digest.Credentials; +import com.burgstaller.okhttp.digest.DigestAuthenticator; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import okhttp3.OkHttpClient; +import okhttp3.logging.HttpLoggingInterceptor; +import retrofit2.Call; +import retrofit2.Retrofit; +import retrofit2.converter.gson.GsonConverterFactory; + + +public class SelfossApi { + + private final SelfossService service; + private final Config config; + private final String userName; + private final String password; + + public SelfossApi(Context c) { + this.config = new Config(c); + + HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor(); + interceptor.setLevel(HttpLoggingInterceptor.Level.BODY); + + OkHttpClient.Builder httpBuilder = new OkHttpClient.Builder(); + final Map authCache = new ConcurrentHashMap<>(); + + String httpUserName = config.getHttpUserLogin(); + String httpPassword = config.getHttpUserPassword(); + + Credentials credentials = new Credentials(httpUserName, httpPassword); + final BasicAuthenticator basicAuthenticator = new BasicAuthenticator(credentials); + final DigestAuthenticator digestAuthenticator = new DigestAuthenticator(credentials); + + // note that all auth schemes should be registered as lowercase! + DispatchingAuthenticator authenticator = new DispatchingAuthenticator.Builder() + .with("digest", digestAuthenticator) + .with("basic", basicAuthenticator) + .build(); + + OkHttpClient client = httpBuilder + .authenticator(new CachingAuthenticatorDecorator(authenticator, authCache)) + .addInterceptor(new AuthenticationCacheInterceptor(authCache)) + .addInterceptor(interceptor) + .build(); + + + GsonBuilder builder = new GsonBuilder(); + builder.registerTypeAdapter(boolean.class, new BooleanTypeAdapter()); + + Gson gson = builder + .setLenient() + .create(); + + userName = config.getUserLogin(); + password = config.getUserPassword(); + Retrofit retrofit = new Retrofit.Builder().baseUrl(config.getBaseUrl()).client(client) + .addConverterFactory(GsonConverterFactory.create(gson)).build(); + service = retrofit.create(SelfossService.class); + } + + public Call login() { + return service.loginToSelfoss(config.getUserLogin(), config.getUserPassword()); + } + + public Call> getReadItems() { + return getItems("read"); + } + + public Call> getUnreadItems() { + return getItems("unread"); + } + + public Call> getStarredItems() { + return getItems("starred"); + } + + private Call> getItems(String type) { + return service.getItems(type, userName, password); + } + + public Call markItem(String itemId) { + return service.markAsRead(itemId, userName, password); + } + + public Call unmarkItem(String itemId) { + return service.unmarkAsRead(itemId, userName, password); + } + + public Call readAll(List ids) { + return service.markAllAsRead(ids, userName, password); + } + + public Call starrItem(String itemId) { + return service.starr(itemId, userName, password); + } + + + public Call unstarrItem(String itemId) { + return service.unstarr(itemId, userName, password); + } + + public Call getStats() { + return service.stats(userName, password); + } + + public Call> getTags() { + return service.tags(userName, password); + } + + public Call update() { + return service.update(userName, password); + } + + public Call> getSources() { return service.sources(userName, password); } + + public Call deleteSource(String id) { return service.deleteSource(id, userName, password);} + + public Call> spouts() { return service.spouts(userName, password); } + + public Call createSource(String title, String url, String spout, String tags, String filter) {return service.createSource(title, url, spout, tags, filter, userName, password);} + +} diff --git a/app/src/main/java/apps/amine/bou/readerforselfoss/api/selfoss/SelfossModels.kt b/app/src/main/java/apps/amine/bou/readerforselfoss/api/selfoss/SelfossModels.kt new file mode 100644 index 0000000..f87e67e --- /dev/null +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/api/selfoss/SelfossModels.kt @@ -0,0 +1,127 @@ +package apps.amine.bou.readerforselfoss.api.selfoss + +import android.content.Context +import android.net.Uri +import android.os.Parcel +import android.os.Parcelable +import apps.amine.bou.readerforselfoss.utils.Config +import apps.amine.bou.readerforselfoss.utils.isEmptyOrNullOrNullString + + +private fun constructUrl(config: Config?, path: String, file: String): String { + val baseUriBuilder = Uri.parse(config!!.baseUrl).buildUpon() + baseUriBuilder.appendPath(path).appendPath(file) + + return if (isEmptyOrNullOrNullString(file)) "" + else baseUriBuilder.toString() +} + + +data class Tag(val tag: String, val color: String, val unread: Int) + +class SuccessResponse(val success: Boolean) { + val isSuccess: Boolean + get() = success +} + +class Stats(val total: Int, val unread: Int, val starred: Int) + +data class Spout(val name: String, val description: String) + +data class Sources(val id: String, + val title: String, + val tags: String, + val spout: String, + val error: String, + val icon: String) { + var config: Config? = null + + fun getIcon(app: Context): String { + if (config == null) { + config = Config(app) + } + return constructUrl(config,"favicons", icon) + } + +} + +data class Item(val id: String, + val datetime: String, + val title: String, + val unread: Boolean, + val starred: Boolean, + val thumbnail: String, + val icon: String, + val link: String, + val sourcetitle: String) : Parcelable { + + var config: Config? = null + + companion object { + @JvmField val CREATOR: Parcelable.Creator = object : Parcelable.Creator { + override fun createFromParcel(source: Parcel): Item = Item(source) + override fun newArray(size: Int): Array = arrayOfNulls(size) + } + } + + constructor(source: Parcel) : this( + id = source.readString(), + datetime = source.readString(), + title = source.readString(), + unread = source.readByte() != 0, + starred = source.readByte() != 0, + thumbnail = source.readString(), + icon = source.readString(), + link = source.readString(), + sourcetitle = source.readString() + ) + + override fun describeContents() = 0 + + override fun writeToParcel(dest: Parcel, flags: Int) { + dest.writeString(id) + dest.writeString(datetime) + dest.writeString(title) + dest.writeByte((if (unread) 1 else 0)) + dest.writeByte((if (starred) 1 else 0)) + dest.writeString(thumbnail) + dest.writeString(icon) + dest.writeString(link) + dest.writeString(sourcetitle) + } + + fun getIcon(app: Context): String { + if (config == null) { + config = Config(app) + } + return constructUrl(config, "favicons", icon) + } + + fun getThumbnail(app: Context): String { + if (config == null) { + config = Config(app) + } + return constructUrl(config, "thumbnails", thumbnail) + } + + // TODO: maybe find a better way to handle these kind of urls + fun getLinkDecoded(): String { + var stringUrl: String + if (link.startsWith("http://news.google.com/news/") || link.startsWith("https://news.google.com/news/")) { + if (link.contains("&url=")) { + stringUrl = link.substringAfter("&url=") + } else { + stringUrl = this.link.replace("&", "&") + } + } else { + stringUrl = this.link.replace("&", "&") + } + + // handle :443 => https + if (stringUrl.contains(":443")) { + stringUrl = stringUrl.replace(":443", "").replace("http://", "https://") + } + return stringUrl + } + +} \ No newline at end of file diff --git a/app/src/main/java/apps/amine/bou/readerforselfoss/api/selfoss/SelfossService.java b/app/src/main/java/apps/amine/bou/readerforselfoss/api/selfoss/SelfossService.java new file mode 100644 index 0000000..973fe60 --- /dev/null +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/api/selfoss/SelfossService.java @@ -0,0 +1,68 @@ +package apps.amine.bou.readerforselfoss.api.selfoss; + + +import java.util.List; +import java.util.Map; + +import retrofit2.Call; +import retrofit2.http.DELETE; +import retrofit2.http.Field; +import retrofit2.http.FormUrlEncoded; +import retrofit2.http.GET; +import retrofit2.http.POST; +import retrofit2.http.Path; +import retrofit2.http.Query; + + +interface SelfossService { + @GET("login") + Call loginToSelfoss(@Query("username") String username, @Query("password") String password); + + @GET("items") + Call> getItems(@Query("type") String type, @Query("username") String username, @Query("password") String password); + + @POST("mark/{id}") + Call markAsRead(@Path("id") String id, @Query("username") String username, @Query("password") String password); + + + @POST("unmark/{id}") + Call unmarkAsRead(@Path("id") String id, @Query("username") String username, @Query("password") String password); + + @FormUrlEncoded + @POST("mark") + Call markAllAsRead(@Field("ids[]") List ids, @Query("username") String username, @Query("password") String password); + + + @POST("starr/{id}") + Call starr(@Path("id") String id, @Query("username") String username, @Query("password") String password); + + + @POST("unstarr/{id}") + Call unstarr(@Path("id") String id, @Query("username") String username, @Query("password") String password); + + + @GET("stats") + Call stats(@Query("username") String username, @Query("password") String password); + + + @GET("tags") + Call> tags(@Query("username") String username, @Query("password") String password); + + + @GET("update") + Call update(@Query("username") String username, @Query("password") String password); + + @GET("sources/spouts") + Call> spouts(@Query("username") String username, @Query("password") String password); + + @GET("sources/list") + Call> sources(@Query("username") String username, @Query("password") String password); + + + @DELETE("source/{id}") + Call deleteSource(@Path("id") String id, @Query("username") String username, @Query("password") String password); + + @FormUrlEncoded + @POST("source") + Call createSource(@Field("title") String title, @Field("url") String url, @Field("spout") String spout, @Field("tags") String tags, @Field("filter") String filter, @Query("username") String username, @Query("password") String password); +} diff --git a/app/src/main/java/apps/amine/bou/readerforselfoss/settings/AppCompatPreferenceActivity.java b/app/src/main/java/apps/amine/bou/readerforselfoss/settings/AppCompatPreferenceActivity.java new file mode 100644 index 0000000..ef49cfd --- /dev/null +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/settings/AppCompatPreferenceActivity.java @@ -0,0 +1,111 @@ +package apps.amine.bou.readerforselfoss.settings; + +import android.content.res.Configuration; +import android.os.Bundle; +import android.preference.PreferenceActivity; +import android.support.annotation.LayoutRes; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v7.app.ActionBar; +import android.support.v7.app.AppCompatDelegate; +import android.support.v7.widget.Toolbar; +import android.view.MenuInflater; +import android.view.View; +import android.view.ViewGroup; + +/** + * A {@link PreferenceActivity} which implements and proxies the necessary calls + * to be used with AppCompat. + */ +public abstract class AppCompatPreferenceActivity extends PreferenceActivity { + + private AppCompatDelegate mDelegate; + + @Override + protected void onCreate(Bundle savedInstanceState) { + getDelegate().installViewFactory(); + getDelegate().onCreate(savedInstanceState); + super.onCreate(savedInstanceState); + } + + @Override + protected void onPostCreate(Bundle savedInstanceState) { + super.onPostCreate(savedInstanceState); + getDelegate().onPostCreate(savedInstanceState); + } + + ActionBar getSupportActionBar() { + return getDelegate().getSupportActionBar(); + } + + public void setSupportActionBar(@Nullable Toolbar toolbar) { + getDelegate().setSupportActionBar(toolbar); + } + + @NonNull + @Override + public MenuInflater getMenuInflater() { + return getDelegate().getMenuInflater(); + } + + @Override + public void setContentView(@LayoutRes int layoutResID) { + getDelegate().setContentView(layoutResID); + } + + @Override + public void setContentView(View view) { + getDelegate().setContentView(view); + } + + @Override + public void setContentView(View view, ViewGroup.LayoutParams params) { + getDelegate().setContentView(view, params); + } + + @Override + public void addContentView(View view, ViewGroup.LayoutParams params) { + getDelegate().addContentView(view, params); + } + + @Override + protected void onPostResume() { + super.onPostResume(); + getDelegate().onPostResume(); + } + + @Override + protected void onTitleChanged(CharSequence title, int color) { + super.onTitleChanged(title, color); + getDelegate().setTitle(title); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + getDelegate().onConfigurationChanged(newConfig); + } + + @Override + protected void onStop() { + super.onStop(); + getDelegate().onStop(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + getDelegate().onDestroy(); + } + + public void invalidateOptionsMenu() { + getDelegate().invalidateOptionsMenu(); + } + + private AppCompatDelegate getDelegate() { + if (mDelegate == null) { + mDelegate = AppCompatDelegate.create(this, null); + } + return mDelegate; + } +} diff --git a/app/src/main/java/apps/amine/bou/readerforselfoss/settings/SettingsActivity.java b/app/src/main/java/apps/amine/bou/readerforselfoss/settings/SettingsActivity.java new file mode 100644 index 0000000..36d0fb5 --- /dev/null +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/settings/SettingsActivity.java @@ -0,0 +1,211 @@ +package apps.amine.bou.readerforselfoss.settings; + + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.Intent; +import android.content.res.Configuration; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.preference.Preference; +import android.preference.Preference.OnPreferenceChangeListener; +import android.preference.PreferenceActivity; +import android.preference.SwitchPreference; +import android.support.v7.app.ActionBar; +import android.preference.PreferenceFragment; +import android.preference.PreferenceManager; +import android.view.MenuItem; + +import java.util.List; + +import apps.amine.bou.readerforselfoss.R; + + +/** + * A {@link PreferenceActivity} that presents a set of application settings. On + * handset devices, settings are presented as a single list. On tablets, + * settings are split by category, with category headers shown to the left of + * the list of settings. + *

+ * See + * Android Design: Settings for design guidelines and the Settings + * API Guide for more information on developing a Settings UI. + */ +public class SettingsActivity extends AppCompatPreferenceActivity { + /** + * A preference value change listener that updates the preference's summary + * to reflect its new value. + */ + private static Preference.OnPreferenceChangeListener sBindPreferenceSummaryToValueListener = new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object value) { + String stringValue = value.toString(); + preference.setSummary(stringValue); + return true; + } + }; + + /** + * Helper method to determine if the device has an extra-large screen. For + * example, 10" tablets are extra-large. + */ + private static boolean isXLargeTablet(Context context) { + return (context.getResources().getConfiguration().screenLayout + & Configuration.SCREENLAYOUT_SIZE_MASK) >= Configuration.SCREENLAYOUT_SIZE_XLARGE; + } + + /** + * Binds a preference's summary to its value. More specifically, when the + * preference's value is changed, its summary (line of text below the + * preference title) is updated to reflect the value. The summary is also + * immediately updated upon calling this method. The exact display format is + * dependent on the type of preference. + * + * @see #sBindPreferenceSummaryToValueListener + */ + private static void bindPreferenceSummaryToValue(Preference preference) { + // Set the listener to watch for value changes. + preference.setOnPreferenceChangeListener(sBindPreferenceSummaryToValueListener); + + // Trigger the listener immediately with the preference's + // current value. + sBindPreferenceSummaryToValueListener.onPreferenceChange(preference, + PreferenceManager + .getDefaultSharedPreferences(preference.getContext()) + .getString(preference.getKey(), "")); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setupActionBar(); + } + + /** + * Set up the {@link android.app.ActionBar}, if the API is available. + */ + private void setupActionBar() { + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + // Show the Up button in the action bar. + actionBar.setDisplayHomeAsUpEnabled(true); + } + } + + /** + * {@inheritDoc} + */ + @Override + public boolean onIsMultiPane() { + return isXLargeTablet(this); + } + + /** + * {@inheritDoc} + */ + @Override + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + public void onBuildHeaders(List

target) { + loadHeadersFromResource(R.xml.pref_headers, target); + } + + /** + * This method stops fragment injection in malicious applications. + * Make sure to deny any unknown fragments here. + */ + protected boolean isValidFragment(String fragmentName) { + return PreferenceFragment.class.getName().equals(fragmentName) + || GeneralPreferenceFragment.class.getName().equals(fragmentName) + || LinksPreferenceFragment.class.getName().equals(fragmentName); + } + + /** + * This fragment shows general preferences only. It is used when the + * activity is showing a two-pane settings UI. + */ + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + public static class GeneralPreferenceFragment extends PreferenceFragment { + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + addPreferencesFromResource(R.xml.pref_general); + setHasOptionsMenu(true); + + SwitchPreference cardViewActive = (SwitchPreference) findPreference("card_view_active"); + final SwitchPreference tabOnTap = (SwitchPreference) findPreference("tab_on_tap"); + tabOnTap.setEnabled(!cardViewActive.isChecked()); + cardViewActive.setOnPreferenceChangeListener(new OnPreferenceChangeListener() { + public boolean onPreferenceChange(Preference preference, Object newValue){ + boolean isEnabled = (Boolean) newValue; + tabOnTap.setEnabled(!isEnabled); + return true; + } + }); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + int id = item.getItemId(); + if (id == android.R.id.home) { + getActivity().finish(); + return true; + } + return super.onOptionsItemSelected(item); + } + } + + /** + * This fragment shows general preferences only. It is used when the + * activity is showing a two-pane settings UI. + */ + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + public static class LinksPreferenceFragment extends PreferenceFragment { + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + addPreferencesFromResource(R.xml.pref_links); + setHasOptionsMenu(true); + + Preference tracker = findPreference( "trackerLink" ); + tracker.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.tracker_url))); + startActivity(browserIntent); + return true; + } + }); + + findPreference("sourceLink").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.source_url))); + startActivity(browserIntent); + return false; + } + }); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + int id = item.getItemId(); + if (id == android.R.id.home) { + getActivity().finish(); + return true; + } + return super.onOptionsItemSelected(item); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + int id = item.getItemId(); + if (id == android.R.id.home) { + finish(); + return true; + } + return super.onOptionsItemSelected(item); + } +} diff --git a/app/src/main/java/apps/amine/bou/readerforselfoss/utils/AppUtils.kt b/app/src/main/java/apps/amine/bou/readerforselfoss/utils/AppUtils.kt new file mode 100644 index 0000000..bf123fc --- /dev/null +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/utils/AppUtils.kt @@ -0,0 +1,93 @@ +package apps.amine.bou.readerforselfoss.utils + +import android.content.Context +import android.content.DialogInterface +import android.content.Intent +import android.content.SharedPreferences +import android.net.Uri +import android.support.v7.app.AlertDialog +import android.text.TextUtils +import android.util.Log +import android.util.Patterns +import apps.amine.bou.readerforselfoss.BuildConfig +import apps.amine.bou.readerforselfoss.R +import com.google.firebase.crash.FirebaseCrash +import com.google.firebase.remoteconfig.FirebaseRemoteConfig +import okhttp3.HttpUrl + +private fun isStoreVersion(context: Context): Boolean { + var result = false + try { + val installer = context.packageManager + .getInstallerPackageName(context.packageName) + result = !TextUtils.isEmpty(installer) + } catch (e: Throwable) { + } + + return result +} + +fun checkAndDisplayStoreApk(context: Context) = + if (!isStoreVersion(context) && !BuildConfig.GITHUB_VERSION) { + val alertDialog = AlertDialog.Builder(context).create() + alertDialog.setTitle(context.getString(R.string.warning_version)) + alertDialog.setMessage(context.getString(R.string.text_version)) + alertDialog.setButton(AlertDialog.BUTTON_NEUTRAL, "OK", + { dialog, _ -> dialog.dismiss() }) + alertDialog.show() + } else Unit + + +fun isUrlValid(url: String): Boolean { + val baseUrl = HttpUrl.parse(url) + var existsAndEndsWithSlash = false + if (baseUrl != null) { + val pathSegments = baseUrl.pathSegments() + existsAndEndsWithSlash = "" == pathSegments[pathSegments.size - 1] + } + + return Patterns.WEB_URL.matcher(url).matches() && existsAndEndsWithSlash +} + +fun isEmptyOrNullOrNullString(str: String?): Boolean = + str == null || str == "null" || str.isEmpty() + +fun checkApkVersion(settings: SharedPreferences, editor: SharedPreferences.Editor, context: Context, mFirebaseRemoteConfig: FirebaseRemoteConfig) { + mFirebaseRemoteConfig.fetch(43200) + .addOnCompleteListener { task -> + if (task.isSuccessful) { + mFirebaseRemoteConfig.activateFetched() + } else { + FirebaseCrash.logcat(Log.DEBUG, "CONFIG FETCH", "remote config task unsuccessful") + FirebaseCrash.report(Exception(task.exception)) + } + + isThereAnUpdate(settings, editor, context, mFirebaseRemoteConfig) + } +} + +private fun isThereAnUpdate(settings: SharedPreferences, editor: SharedPreferences.Editor, context: Context, mFirebaseRemoteConfig: FirebaseRemoteConfig) { + val APK_LINK = "github_apk" + + val apkLink = mFirebaseRemoteConfig.getString(APK_LINK) + val storedLink = settings.getString(APK_LINK, "") + if (apkLink != storedLink && !apkLink.isEmpty()) { + val alertDialog = AlertDialog.Builder(context).create() + alertDialog.setTitle(context.getString(R.string.new_apk_available_title)) + alertDialog.setMessage(context.getString(R.string.new_apk_available_message)) + alertDialog.setButton(AlertDialog.BUTTON_POSITIVE, context.getString(R.string.new_apk_available_get)) { _, _ -> + editor.putString(APK_LINK, apkLink) + editor.apply() + val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(apkLink)) + context.startActivity(browserIntent) + } + alertDialog.setButton(AlertDialog.BUTTON_NEUTRAL, context.getString(R.string.new_apk_available_no), + { dialog, _ -> + editor.putString(APK_LINK, apkLink) + editor.apply() + dialog.dismiss() + }) + alertDialog.show() + } + +} \ No newline at end of file diff --git a/app/src/main/java/apps/amine/bou/readerforselfoss/utils/Config.kt b/app/src/main/java/apps/amine/bou/readerforselfoss/utils/Config.kt new file mode 100644 index 0000000..7e9f037 --- /dev/null +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/utils/Config.kt @@ -0,0 +1,34 @@ +package apps.amine.bou.readerforselfoss.utils + +import android.content.Context +import android.content.SharedPreferences + + +class Config(c: Context) { + + private val settings: SharedPreferences + + init { + this.settings = c.getSharedPreferences(settingsName, Context.MODE_PRIVATE) + } + + val baseUrl: String + get() = settings.getString("url", "") + + val userLogin: String + get() = settings.getString("login", "") + + val userPassword: String + get() = settings.getString("password", "") + + val httpUserLogin: String + get() = settings.getString("httpUserName", "") + + val httpUserPassword: String + get() = settings.getString("httpPassword", "") + + companion object { + val settingsName = "paramsselfoss" + + } +} diff --git a/app/src/main/java/apps/amine/bou/readerforselfoss/utils/LinksUtils.kt b/app/src/main/java/apps/amine/bou/readerforselfoss/utils/LinksUtils.kt new file mode 100644 index 0000000..466e825 --- /dev/null +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/utils/LinksUtils.kt @@ -0,0 +1,81 @@ +package apps.amine.bou.readerforselfoss.utils + +import android.app.Activity +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.graphics.BitmapFactory +import android.net.Uri +import android.support.customtabs.CustomTabsIntent +import apps.amine.bou.readerforselfoss.R +import apps.amine.bou.readerforselfoss.ReaderActivity +import apps.amine.bou.readerforselfoss.api.selfoss.Item +import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper +import xyz.klinker.android.drag_dismiss.DragDismissIntentBuilder + +fun buildCustomTabsIntent(c: Context): CustomTabsIntent { + + fun createPendingShareIntent(c: Context): PendingIntent { + val actionIntent = Intent(Intent.ACTION_SEND) + actionIntent.type = "text/plain" + return PendingIntent.getActivity( + c, 0, actionIntent, 0) + } + + val intentBuilder = CustomTabsIntent.Builder() + + // TODO: change to primary when it's possible to customize custom tabs title color + //intentBuilder.setToolbarColor(c.getResources().getColor(R.color.colorPrimary)); + intentBuilder.setToolbarColor(c.resources.getColor(R.color.colorAccentDark)) + intentBuilder.setShowTitle(true) + + + intentBuilder.setStartAnimations(c, + R.anim.slide_in_right, + R.anim.slide_out_left) + intentBuilder.setExitAnimations(c, + android.R.anim.slide_in_left, + android.R.anim.slide_out_right) + + val closeicon = BitmapFactory.decodeResource(c.resources, R.drawable.ic_close_white_24dp) + intentBuilder.setCloseButtonIcon(closeicon) + + val shareLabel = c.getString(R.string.label_share) + val icon = BitmapFactory.decodeResource(c.resources, + R.drawable.ic_share_white_24dp) + intentBuilder.setActionButton(icon, shareLabel, createPendingShareIntent(c)) + + return intentBuilder.build() +} + +fun openItemUrl(i: Item, + customTabsIntent: CustomTabsIntent, + internalBrowser: Boolean, + articleViewer: Boolean, + app: Activity, + c: Context) { + if (!internalBrowser) { + val intent = Intent(Intent.ACTION_VIEW) + intent.data = Uri.parse(i.getLinkDecoded()) + app.startActivity(intent) + } else { + if (articleViewer) { + val intent = Intent(c, ReaderActivity::class.java) + + DragDismissIntentBuilder(c) + .setFullscreenOnTablets(true) // defaults to false, tablets will have padding on each side + .setDragElasticity(DragDismissIntentBuilder.DragElasticity.NORMAL) // Larger elasticities will make it easier to dismiss. + .build(intent) + + intent.putExtra("url", i.getLinkDecoded()) + app.startActivity(intent) + } else { + CustomTabActivityHelper.openCustomTab(app, customTabsIntent, Uri.parse(i.getLinkDecoded()) + ) { _, uri -> + val intent = Intent(Intent.ACTION_VIEW, uri) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + c.startActivity(intent) + } + } + } +} diff --git a/app/src/main/java/apps/amine/bou/readerforselfoss/utils/customtabs/CustomTabActivityHelper.java b/app/src/main/java/apps/amine/bou/readerforselfoss/utils/customtabs/CustomTabActivityHelper.java new file mode 100644 index 0000000..8c287df --- /dev/null +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/utils/customtabs/CustomTabActivityHelper.java @@ -0,0 +1,146 @@ +package apps.amine.bou.readerforselfoss.utils.customtabs; + +import android.app.Activity; +import android.content.ComponentName; +import android.net.Uri; +import android.os.Bundle; +import android.support.customtabs.CustomTabsClient; +import android.support.customtabs.CustomTabsIntent; +import android.support.customtabs.CustomTabsServiceConnection; +import android.support.customtabs.CustomTabsSession; + +import java.util.List; + +@SuppressWarnings("ALL") +public class CustomTabActivityHelper { + private CustomTabsSession mCustomTabsSession; + private CustomTabsClient mClient; + private CustomTabsServiceConnection mConnection; + private ConnectionCallback mConnectionCallback; + + /** + * Opens the URL on a Custom Tab if possible. Otherwise fallsback to opening it on a WebView + * + * @param activity The host activity + * @param customTabsIntent a CustomTabsIntent to be used if Custom Tabs is available + * @param uri the Uri to be opened + * @param fallback a CustomTabFallback to be used if Custom Tabs is not available + */ + public static void openCustomTab(Activity activity, + CustomTabsIntent customTabsIntent, + Uri uri, + CustomTabFallback fallback) { + String packageName = CustomTabsHelper.getPackageNameToUse(activity); + + //If we cant find a package name, it means there's no browser that supports + //Chrome Custom Tabs installed. So, we fallback to the webview + if (packageName == null) { + if (fallback != null) { + fallback.openUri(activity, uri); + } + } else { + customTabsIntent.intent.setPackage(packageName); + customTabsIntent.launchUrl(activity, uri); + } + } + + /** + * Unbinds the Activity from the Custom Tabs Service + * @param activity the activity that is connected to the service + */ + public void unbindCustomTabsService(Activity activity) { + try { + if (mConnection == null) return; + activity.unbindService(mConnection); + mClient = null; + mCustomTabsSession = null; + } catch (RuntimeException e) {} + } + + /** + * Creates or retrieves an exiting CustomTabsSession + * + * @return a CustomTabsSession + */ + public CustomTabsSession getSession() { + if (mClient == null) { + mCustomTabsSession = null; + } else if (mCustomTabsSession == null) { + mCustomTabsSession = mClient.newSession(null); + } + return mCustomTabsSession; + } + + /** + * Register a Callback to be called when connected or disconnected from the Custom Tabs Service + * @param connectionCallback + */ + public void setConnectionCallback(ConnectionCallback connectionCallback) { + this.mConnectionCallback = connectionCallback; + } + + /** + * Binds the Activity to the Custom Tabs Service + * @param activity the activity to be binded to the service + */ + public void bindCustomTabsService(Activity activity) { + if (mClient != null) return; + + String packageName = CustomTabsHelper.getPackageNameToUse(activity); + if (packageName == null) return; + mConnection = new CustomTabsServiceConnection() { + @Override + public void onCustomTabsServiceConnected(ComponentName name, CustomTabsClient client) { + mClient = client; + mClient.warmup(0L); + if (mConnectionCallback != null) mConnectionCallback.onCustomTabsConnected(); + //Initialize a session as soon as possible. + getSession(); + } + + @Override + public void onServiceDisconnected(ComponentName name) { + mClient = null; + if (mConnectionCallback != null) mConnectionCallback.onCustomTabsDisconnected(); + } + }; + CustomTabsClient.bindCustomTabsService(activity, packageName, mConnection); + } + + public boolean mayLaunchUrl(Uri uri, Bundle extras, List otherLikelyBundles) { + if (mClient == null) return false; + + CustomTabsSession session = getSession(); + return session != null && session.mayLaunchUrl(uri, extras, otherLikelyBundles); + + } + + /** + * A Callback for when the service is connected or disconnected. Use those callbacks to + * handle UI changes when the service is connected or disconnected + */ + public interface ConnectionCallback { + /** + * Called when the service is connected + */ + void onCustomTabsConnected(); + + /** + * Called when the service is disconnected + */ + void onCustomTabsDisconnected(); + } + + /** + * To be used as a fallback to open the Uri when Custom Tabs is not available + */ + public interface CustomTabFallback { + /** + * + * @param activity The Activity that wants to open the Uri + * @param uri The uri to be opened by the fallback + */ + void openUri(Activity activity, Uri uri); + } + +} diff --git a/app/src/main/java/apps/amine/bou/readerforselfoss/utils/customtabs/CustomTabsHelper.java b/app/src/main/java/apps/amine/bou/readerforselfoss/utils/customtabs/CustomTabsHelper.java new file mode 100644 index 0000000..19ac086 --- /dev/null +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/utils/customtabs/CustomTabsHelper.java @@ -0,0 +1,126 @@ +package apps.amine.bou.readerforselfoss.utils.customtabs; + +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.net.Uri; +import android.support.customtabs.CustomTabsService; +import android.text.TextUtils; +import android.util.Log; +import apps.amine.bou.readerforselfoss.utils.customtabs.helpers.KeepAliveService; + +import java.util.ArrayList; +import java.util.List; + +@SuppressWarnings("ALL") +class CustomTabsHelper { + private static final String TAG = "CustomTabsHelper"; + private static final String STABLE_PACKAGE = "com.android.chrome"; + private static final String BETA_PACKAGE = "com.chrome.beta"; + private static final String DEV_PACKAGE = "com.chrome.dev"; + private static final String LOCAL_PACKAGE = "com.google.android.apps.chrome"; + private static final String EXTRA_CUSTOM_TABS_KEEP_ALIVE = + "android.support.customtabs.extra.KEEP_ALIVE"; + + private static String sPackageNameToUse; + + private CustomTabsHelper() {} + + public static void addKeepAliveExtra(Context context, Intent intent) { + Intent keepAliveIntent = new Intent().setClassName( + context.getPackageName(), KeepAliveService.class.getCanonicalName()); + intent.putExtra(EXTRA_CUSTOM_TABS_KEEP_ALIVE, keepAliveIntent); + } + + /** + * Goes through all apps that handle VIEW intents and have a warmup service. Picks + * the one chosen by the user if there is one, otherwise makes a best effort to return a + * valid package name. + * + * This is not threadsafe. + * + * @param context {@link Context} to use for accessing {@link PackageManager}. + * @return The package name recommended to use for connecting to custom tabs related components. + */ + public static String getPackageNameToUse(Context context) { + if (sPackageNameToUse != null) return sPackageNameToUse; + + PackageManager pm = context.getPackageManager(); + // Get default VIEW intent handler. + Intent activityIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://www.example.com")); + ResolveInfo defaultViewHandlerInfo = pm.resolveActivity(activityIntent, 0); + String defaultViewHandlerPackageName = null; + if (defaultViewHandlerInfo != null) { + defaultViewHandlerPackageName = defaultViewHandlerInfo.activityInfo.packageName; + } + + // Get all apps that can handle VIEW intents. + List resolvedActivityList = pm.queryIntentActivities(activityIntent, 0); + List packagesSupportingCustomTabs = new ArrayList<>(); + for (ResolveInfo info : resolvedActivityList) { + Intent serviceIntent = new Intent(); + serviceIntent.setAction(CustomTabsService.ACTION_CUSTOM_TABS_CONNECTION); + serviceIntent.setPackage(info.activityInfo.packageName); + if (pm.resolveService(serviceIntent, 0) != null) { + packagesSupportingCustomTabs.add(info.activityInfo.packageName); + } + } + + // Now packagesSupportingCustomTabs contains all apps that can handle both VIEW intents + // and service calls. + if (packagesSupportingCustomTabs.isEmpty()) { + sPackageNameToUse = null; + } else if (packagesSupportingCustomTabs.size() == 1) { + sPackageNameToUse = packagesSupportingCustomTabs.get(0); + } else if (!TextUtils.isEmpty(defaultViewHandlerPackageName) + && !hasSpecializedHandlerIntents(context, activityIntent) + && packagesSupportingCustomTabs.contains(defaultViewHandlerPackageName)) { + sPackageNameToUse = defaultViewHandlerPackageName; + } else if (packagesSupportingCustomTabs.contains(STABLE_PACKAGE)) { + sPackageNameToUse = STABLE_PACKAGE; + } else if (packagesSupportingCustomTabs.contains(BETA_PACKAGE)) { + sPackageNameToUse = BETA_PACKAGE; + } else if (packagesSupportingCustomTabs.contains(DEV_PACKAGE)) { + sPackageNameToUse = DEV_PACKAGE; + } else if (packagesSupportingCustomTabs.contains(LOCAL_PACKAGE)) { + sPackageNameToUse = LOCAL_PACKAGE; + } + return sPackageNameToUse; + } + + /** + * Used to check whether there is a specialized handler for a given intent. + * @param intent The intent to check with. + * @return Whether there is a specialized handler for the given intent. + */ + private static boolean hasSpecializedHandlerIntents(Context context, Intent intent) { + try { + PackageManager pm = context.getPackageManager(); + List handlers = pm.queryIntentActivities( + intent, + PackageManager.GET_RESOLVED_FILTER); + if (handlers == null || handlers.size() == 0) { + return false; + } + for (ResolveInfo resolveInfo : handlers) { + IntentFilter filter = resolveInfo.filter; + if (filter == null) continue; + if (filter.countDataAuthorities() == 0 || filter.countDataPaths() == 0) continue; + if (resolveInfo.activityInfo == null) continue; + return true; + } + } catch (RuntimeException e) { + Log.e(TAG, "Runtime exception while getting specialized handlers"); + } + return false; + } + + /** + * @return All possible chrome package names that provide custom tabs feature. + */ + public static String[] getPackages() { + return new String[]{"", STABLE_PACKAGE, BETA_PACKAGE, DEV_PACKAGE, LOCAL_PACKAGE}; + } +} diff --git a/app/src/main/java/apps/amine/bou/readerforselfoss/utils/customtabs/helpers/KeepAliveService.java b/app/src/main/java/apps/amine/bou/readerforselfoss/utils/customtabs/helpers/KeepAliveService.java new file mode 100644 index 0000000..9758d72 --- /dev/null +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/utils/customtabs/helpers/KeepAliveService.java @@ -0,0 +1,15 @@ +package apps.amine.bou.readerforselfoss.utils.customtabs.helpers; + +import android.app.Service; +import android.content.Intent; +import android.os.Binder; +import android.os.IBinder; + +public class KeepAliveService extends Service { + private static final Binder sBinder = new Binder(); + + @Override + public IBinder onBind(Intent intent) { + return sBinder; + } +} diff --git a/app/src/main/java/bou/amine/apps/readerforselfoss/MainActivity.kt b/app/src/main/java/bou/amine/apps/readerforselfoss/MainActivity.kt deleted file mode 100644 index 04628ee..0000000 --- a/app/src/main/java/bou/amine/apps/readerforselfoss/MainActivity.kt +++ /dev/null @@ -1,14 +0,0 @@ -package bou.amine.apps.readerforselfoss - -import android.support.v7.app.AppCompatActivity -import android.os.Bundle - - - -class MainActivity : AppCompatActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) - } -} diff --git a/app/src/main/res/anim/slide_in_right.xml b/app/src/main/res/anim/slide_in_right.xml new file mode 100644 index 0000000..3189c25 --- /dev/null +++ b/app/src/main/res/anim/slide_in_right.xml @@ -0,0 +1,16 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/slide_out_left.xml b/app/src/main/res/anim/slide_out_left.xml new file mode 100644 index 0000000..0ec7682 --- /dev/null +++ b/app/src/main/res/anim/slide_out_left.xml @@ -0,0 +1,16 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-hdpi/ic_add_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_add_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..c04b523c482b77824608a836515e865579f8b00c GIT binary patch literal 124 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k0wldT1B8K;v!{z=NCji^0d}{88U+a%3{LE) zc(W3I@VD?TX6u!5iTdQ+%zl7P_JBaSLWcds4UNy(0^e=DSmGkkBjwV6s9vOpiQ(Iw WlC8ByowtC-F?hQAxvX|k0wldT1B8LpHcuDFkP61Pmp5`A3Sek?_&>6H zSI+|u%|(-6Fmz31&zy7kn$pzf8y0Q7zFPCw7D(vvJ5Km>z5cG@mEDrki#D1HTg@s` zp1IrTnX|H<$~TXo2QG^g7cjaDl`q;li@j573+u}7^Uh2xThfy|F-c5>{YCYK>UxH) zA|;_0swM;-ikJ`p$2)-R8&C{nzxDU4KGCGLBvdR!iFMY!Gcz095og+*{ z>6z1@lfD*AYSPav78{Ak@VRh%KH&5X7pR&Ys0V@HlSONsykxQLQI%)8g`XNk4_QZ!0L+XG=(a zByT>Hs#E6+VT6@v8!XB&Q+&LL!e9?9rOX5$05`z^^kvKgfD2}dk*p|W&efRlk22jV zjQtsAsF_m%1_K8>(KBO1Qz@nBaV^TUH~{yEV$KtALdq?)DLQx%r&#IXnhMvQR~xK1 zYOSYi1FB$&^C-bdWl%fXq65#MZo+$$L5Z(lKpknMDDl^2;RTN&MoAe>nW>&Z38v0} ZLBujZ*v#oRq5~|G=K)=-`BMmD668COt z4584+xyYQ+i&8g*R;EA~dN}02GkF}^WV6i{Htoa0;xGFGbQ>|DZJ|&d00000NkvXX Hu0mjfe4U!8 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/ic_fiber_new_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_fiber_new_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..c701cda1b5f59b0420c1a49cbcecef86db19a3fd GIT binary patch literal 301 zcmV+|0n+}7P)I!o{vJ+$+Y=Sngh!l>q?3k!&fVQ+PF81q$zh6Av4shV zQ43pC;lmaZrkKxJ`r?4aZYh@ zY!#*ve)(}N6FLO5z99@~cHiA018^5PRKl)LNJX5+6dD0~xbzBD(`@%Vw$K%Qz_3@S zl6t#mv4wPwD22oiO-GoAkSDtvg?3VZ%MF$QF?xjfR3KT221QFbW6X{OGrrm%}>Z>4Vdw^#AVG;rqXGhfEoMpQR|ee>bwm2t8zFC{w9|+=Sz&4& zO{l#!rq+)4rgBuIkCfixGsQ{JthZeg!Gt+gyjBIXx@xCi=>;m9DLQ9;FkP+IR0XBZ zM!&UDL76w`qJpIN)p8Z|U^kl0u5kYmNP2^ETeG`B4YRl2ptTB`d4s}ci4cs_n@Lq!DdhkF002ovPDHLkV1ley BmnZ-L literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/ic_info_outline_white_48dp.png b/app/src/main/res/drawable-hdpi/ic_info_outline_white_48dp.png new file mode 100644 index 0000000000000000000000000000000000000000..c41a5fcffa8c4dc3684252fbfd61623837e19d50 GIT binary patch literal 953 zcmV;q14jIbP)+=u1!Si^=c~o6pU56D8MW!jS z$u@O7JnC$-Nr`E0Z~~QViq1F4UDojk)8rj4KiUtv${KCrYEfp?UeGn(lf*h#%mwiZO?K>;SqC6p{w@`EooS#EOw+c}UBbluHuSO9kD3{Dexd zGgQ&NCewM)0=iXlsD$fh4c%;~LATKTP7f-@j@01>+c-g20r3E3BuF*`h>_@^ zHN+KB7e-Jm@Z>j$y%;(Bn$f*7_b_YN7RCYJVeH7P%ol+c!LiAkL*xA)X9%M!t}A%e=*t&auG}M zUL7k|gPtKKgM)4$K3fdZU#`Cd2c5t}oVOJe{qaNg4l!>nXc19Cg$22YJsc}RIU0!D zQ6KVKhxdR-j0Rnxtv}Fl zg03K-O24t7lYB&c=W=+E>@fn$hQ5wD$_oT6qv8ggqJe-vjQr`djDR|Y=pflR=t-%+ z(kr36+3A;_^XSU-C(b#^3v@H6PJ=SMLRaN%qMQqSME8wcL$0HkK4xj7 z`v)iS)q*bxcDYYB<~wVOFW}3`r@4H|@fyE97P!cfkP~%@MH=|M#7TQSm0^y9)p&}_ z7{lc0-Cv$z#&CH?jf4HhF*6r<<1|RC&P|h7oP+#Da^G2|VEz(#l(!^NVZ`1w`3S4D zh|A+Sm+k)}L4iBGp-Gr;RJbh%++L9EIM5D%liv*A40h bb%Xu~a~|hNXxfgr00000NkvXXu0mjfV)nVh literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/ic_open_in_browser_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_open_in_browser_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..5dc9b662012697aaaa4ff6874b4096afe6801366 GIT binary patch literal 187 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k0wldT1B8K8lc$SgNCo5DDF->73atCj&VBV&X!Opy-!V%~i22WQ%mvv4FO#p!MM>_xj literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/ic_remove_circle_outline_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_remove_circle_outline_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..21d5ee88779cff7e30e74050f8bd72cd62de918f GIT binary patch literal 473 zcmV;~0Ve*5P)bjwVNDV)z z<5Hbr`KB^`ntEE?>DWs)c%rje$Ay3w@gMUAU+-ogR}S>9x)|*0!rQpw@hmml7^D(B zr3vR!lVC8B;E*Paq-KnPY>Ykvo5U7vA~2Ae83vyc^bv^3Es7CXlV2apZ?xQ3TiUR% zC3r>?E~MrPgB|^YG&o58plo5_Gna6LD~ICG=wPConzDfl-*^@O5&zNST~fEy)8awA zZv>POl8Y{B)wD^}xkm-58tw=zFZH0V#I}ihHN9|vSCT*ERUNbdAc8oEppaIOE(LXPbaD_uiXgfuI0y=Ui?$Rg z3N9|rijvq&CiQlchlhJevpDqnjrr%mb8?c{t6qzumxlIL6n!zIh8Gl(U}e3LlM0Ef;V2BeJSjZb=;SQ|z+u z4yaibL-bf>PG6*TZCbM}(t2i;JWOXDi<+RJrJZgbD4Fy&xy-!MTym*5Ce+-mj<=}R zs*bfH=Sq2RG}Riq#p042E*7&*ne}3EUJe(E*_zBou{bA(tHo?vL#bs|A+IvyeHm7t#M^wEtHOK?m*3tk?OFDx*kh6Lst!v$eu1NdssLB&DM32-Q z^h4~jZ>A+~7n2>4r=NU@Dq}tg9ji`Suql$(%vHn+eg}v!7dsOm%)+nCb00000NkvXXu0mjfkE75f literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/ic_share_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_share_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..20ba48063e8e8dd520cbff33fc74267f73290ed1 GIT binary patch literal 398 zcmV;90df9`P)ZFkHm}@zHRp0w0bW3B~m|KP-%}=Y-wz_ zDsv+1vMM)4)(sUp?ux8SiZxz|tk1fN9Pr475L!RBytVTR>~P(h5H{SmS7YR>FP5B8 z;GETT{!7OcX!H%zSx1c0$Op4J{#e&gq=xRKr}5owI~5rUd5=vh)QGG%POH!tS$$Oo zBI}AOCq>qZGpdY)yr-rV>sk`2C+4jSVcoo4iZ!mf<~W%0EX{s2rvvrQdYi_U(|on< z2HJFKAKGuyqIXs-I;_|}57K93O)FAkJr#PEMb@eb6*_K-teL;{7nxP%l*pP^Wt;ce sC7MvFXDm{m9HrhN_q^V0!9o9q-!{wmnqcRJ!2kdN07*qoM6N<$f+kh3$p8QV literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/ic_share_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_share_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..b09a6926de5aa48dee59265aadac32da236f9e1c GIT binary patch literal 397 zcmV;80doF{P)>pxai;{B2*!N!D>LzNpR^R2)fik{R2|aH0bP5 z*MdarD7a}-Ez;H;8^R0acFB7?>Nyer70h^N-MY7?BcJR3C~UbFc^uKRQ`h0hp&@lM za=0&bFUnK#Sn5WUrS5~&57j8c2`}{x^lU0@xf-K$z+Gzw`kpzeuI-nfmQ2OyT(N5C zy>l@-b#t1gVw*GChQ69riBYL)DYQJYY2deq4n`STc6((!%2Aj4=wg&-PHHZ4XiD9R z9L`I1T#6jpc6)6i%2KtYuxQ?fferHxMOo@@yJIr8nefKYHw~583zvN|v}`i=!hTIX z1AQ+XH*Uda9ScrJS&n(GZ(vPNp{o|YU7>4m6XvM`Ee>32ap2g3b5DHxTAWW7+;UdNa-glnfeS4T9J=ImnB1Ngr_Zb#&T=9S z)U`Nps>OkW{5kX7lDysgK__HipC%Ow=|I=yRhwcO(184EG57n=VoLIxj4G>v8fu`P z8fd5n!h<~MuYo?)Ky@`xMGN%Kq88{Z{Apne8nUAWs*Qep*@8-1pbj%ypr~2P5cC#0 zcymxS`bA*{8a;SpP%ZAwWd$l}fuauH6coQ>n}DJZ-VjtBHzc?R4cXQL)y7XPNkDxk z2R{X<>Ez(2097&s9Zg!tl7I@dGveHY_Of)KAfQOg(W00P6a*9~)1a63bxMDu#y>XUgYdUW;(q!n%8 zd??N)?$q}^ry$tgKuDFdn90+n9E^Q|w69Bly}_DKh3PBabJ)5sDBp5QzUUzPEOVRS RUZ4XQJYD@<);T3K0RUBCI4}SJ literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_close_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_close_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..af7f8288da6854204dcc4e6678b9053cd72032c4 GIT binary patch literal 175 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM0wlfaz7_+iGEW!B5R21KC-3Avpdi4K$<+1X zay^SH!@6{thMdKZi+L1hS^BMZUAdKCT}(HRw{NeWg~AlBHtr3jj>2B5kK^|J))MUC zlz7ydwCsb8xn}W?^r?}5wtsjdmvX{3=JbT!J9sKPrYmk={m|7^>A|Z*7j^D$`>}ry Zqi^Z9GQN4ct^!@a;OXk;vd$@?2>?I&LQeny literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_done_all_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_done_all_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..4724d66cb7c14c9f13177511f304fe94e3c5ffb5 GIT binary patch literal 213 zcmV;`04o29P)4`@?Vp%^63A6{x?9Cum1nxzd3&8Q9$MY|JOs6|0kf~eemY)InMN|928pK9T?c3f24I`9x; zrGu#H7tRPfrGqWPQ0c&e%%L{llk7Fi*Ar*|t5@re=4^Gkp+s zR*12UuuS|RwD6&Q;e>rsQW85P^O!Q(6;?AUaim!?9R25TTWpF7$W+8_TSB8-l>w2$g$H?mh=$QWqF_avZkVPK2~GVU2r zNx+kVH7U~a2cVLGiGd*rcref;0rmFb<|v@H4+m;w>{1K2O22NX4g$Gh;r2I3UU zC}4AjSPu(ixIwI-i~@=>;)?%*fd#|^BPhd-@r0NU2I`1i6z1DU)PjK~qBKyTf@lN- zCy2s8fg++640I7Wl<`GYIEcGoz(-^U3JfE>zYypm@_+0U8C+z9*+p(5%4UK1zv(Js zZ=k^ch-xrUL_9KT9C*VMq7V#ZxJHzW0~N$&*uzfo7172v3S>D)tf9gJQ}~FgQJ{|K z@irKcxri5LQU~VfAvRG_fjnJAhY3`QO>%*_Wjs2dSVB8ak(i`~wix%~<)U?%MJ3oA z7ie3kxPZkW+6y&o6w%94r-ydHP_ma_Nr*Gbj78j570v p!{d$?4XPC6k!Bt+*lWOGzX87wDYF(rDzN|n002ovPDHLkV1ir08p{9x literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_open_in_browser_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_open_in_browser_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..bbe5c14aad1571a9f9804c0981426154eb934849 GIT binary patch literal 144 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM0wlfaz7_+ia8DP~UZ+?s3UA!1n`YD888c!;fXKSqxK9%}X{Et@vK%sF>mHwC% z|6<4_Rb~KCr|qwXu@=rM09I-HFetRb0)V~H$axdC0Cd8WoRM$>;4V}k1`cZB_8)~# zc$PC3jsWb0wVV%O1He*f6`8jlggF4UOZKtvLZ&-~5Hum8lgzrj&X3nB_ovDnJ%}fHzV<>iUk{yCv=|P(yFM9CIKrv6;vdj!^e)OQ=oM~3L z3Hov zYMc*TMS6^4Y*s#v8YK}AT(ig&XB4{J=afm7xZ$2C{CQWp)PoCx)G7YcXPY7T1DnU) UWG_v5V*mgE07*qoM6N<$f<$+V!Tz!E9V%weI_yd=Fl&Tqx@ zU4J%T|887QWvJ8?(v!Z|6;kD3AYj^i4*F^|tb3C3I)1h6LNc`Bz(8Nem>Ns1e8+Vqcw#ZP2I2i_O(=!b4$Wz9T^!pHYaS+ zndHTmJ$G*HYN%0I^&sW#2$=MogKHr-Ik*(k&cUXT!nUi=wG{ry2jCQLp-zXh2LJ#7 M07*qoM6N<$f$QBEc|F(1{8;NsTa>qG@_iujyHwlsAj>Z++)l8Ht%+R#_uubi^6>6 zO!7X-WO8PV`Or~hL_(SW1tuhwd4Mr7_xaKx4^Yz3=O0bdOe0dEI3YM`SA zDq3KAdL$@-h89>7*8)H1r>1LQPg)CfgXg|$psEFCmtQWoz?T+SoxPv}pmpuzehb)I z=l{sq0R7M}=mWrGZTb9jR)8^h^zQ!M9p}>}Z=yvL>>t)ei17R0wxR$4002ovPDHLk FV1mZqicbIl literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_add_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_add_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..3191d5283e981b9eecdcaa54feda419515ef656f GIT binary patch literal 108 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}a}tV^0^ykP61P7Z(aL7zi*O{Br+` z`;vw;dk$=HXVkVR^_;bx1E`8&^BsGK4=F%~hW8~_)0*>!J6N0?fLsPoS3j3^P6A_+%)J+&7! z)HCf1KM>Dw?2Znf3x{)}f`bGXGvlEE2@ZiaA%^J&Qy6x%EMz&L%A?Alm#D#5!0DXL zcxXeF#cC#HuNw`wlesQD-1bmZ_YOC2gZ~#E?!L644$rj JF6*2UngHe;VB!D( literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_close_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_close_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..b7c7ffd0e795ba76ed3a062566c9016448795f7a GIT binary patch literal 257 zcmV+c0sj7pP) zOA5m<3jYBNi%A&<@ZO?$P9};P4Y5CjG5$M&YXI45J{s}~# zf|&?x1_gn4B7+hS@X!l}&!voFhmZP^sujifL@~PKMMM~{6xH}^g$q7WOzwCQ5vHTU z6`v~H@rlA8e;CUh_(b84zg=+ih`wG<)HiJjzSlQx5#CnjMR;A)R^jtaTa9;7rSy)7O%~`cm?ZjXImW?6TYRT<;U^@VKiSj`soFk00000NkvXX Hu0mjfhD&W| literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_done_all_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_done_all_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..6ace20eecf0075cb2fe020d91f3290dbc95e6b19 GIT binary patch literal 300 zcmV+{0n`48P) zI}QO+6o>KKDG-mPXgm|ku>uK&P$R~MCQ&JWRm-@Ey4#B$VF)TMF8In=6kA!p{S2$fuJRfXKsI0np;|`+OxM z`DUi`4bK0_?=z9FDe{z9QIELK7zCfI9Fpff;Pg%~2M7_?`5d1A&{+~pgD*>>XPGy~ z7$(7kf}qPSObE*6)!Gu`eZ>9cybz7gM1s5xkuV>ZP~<~IGmc-^-&5}MW!w(@K$8#wL)k76+^aZN-?*^klj*ZP7KitrS|(I z2JC}<$~kIT4A56c^k`B;^x29DeW+O#ee}aAeLCx>=&^>H0n{{l9OtwFo%BtVw5)0f z$9)mS`Rte>RV;~u79EnoL2pEwR}RQzuX)j(IeTO@=BfCvC&pwqErif<&n~&Ct>uPY fbj4-eMn?Vt*j46-QG$eH00000NkvXXu0mjfm!-!( literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_fiber_new_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_fiber_new_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..3788145ccd72d30a0a685f7bf332424c944a938a GIT binary patch literal 308 zcmV-40n7f0P)5iT0ioy|Hh79~O(HBwcKA zPdu=X?9h#nPHu=R^;RO*h%0s*FW0KT170G;RqG{MDS!b01H5DxIRJ5aZp>v?dg}!s zRx~{EOCUnzcK{rSfXlT6I0E44cR(9w0uawN9DpGKILb8)7^ewnSNRC|1(16@U%Uq`NNM{y5Ml63V2f7a_?@o- zg;p?Nz~g`4c0WbcEP)7lKCi;6yoP!D|$s}EpXuay$8eF7nuBJ?>zUZA3vYTB#GB^?n# z*xl@AcS;f50ZEAa{r1o7%q(zd`_PWiL=OQX#7N0vdJ51*gT6SoYYZ?)rmz_vQL`66 z$B4$8X)J+{s*U^|zO~|8i_tD7eu6h+@vZsrOy$q;QCNO$6Y6O+dOP_I!eAD!*7BQ$ z(r#_$Xt()}`Qr>&qM`{P>MXs@Rs&mGaL>4rognmPM>rlij9q#!1(0&1?Gor^wWdHvF0000XA;DPbB};p_0dfAo z$vAy9#+6S9#ELYJ@(Lqa#%C_802gIqArYp#f(wN zJ5D-2x}I^0EQMtFA?SeRyu{vHx_o|^)~`kO7MeJbsRD=Aw#N4d<{5Bs2vT z(7+gW9I~nSN2#E6lJC{kK0$L^4urow?@d3Y+FGmL|;O)mE-^F4Bc9HL2 zQm7ENoloT3SxmqprfnYoS5!#bXp=BSqyK>0=yIe`A#Mj2x&gld?d0n~g}ejgYx5J3 z2Egy^UyUJ3s{D( z);XXRX&hy6FOb$b2OLD|GaAs3)ae{>38~9yz)_?g=YYFNn~esfkZw5#3?r>T`I=7o z+A5@n&H)oh4MqdzBfWAC_=prY8jwJmaSq5K#f%2TkiLBX0pHn2D2X)lKlmdaMq2s3 zeU08h+F~>yg>=(7pcm8C~mULv(P2h^i$HWskl&kK!W z1ZkhKfKETJl!_jtL1O{;k-FUlY(=tYHWIKD-3E67NoJ5v83{OpG)>%HfT9m6OI73n zbC^cDf^r+M9^H}115Tj(#eF~-qezp~L>f@f6jFv+?gJDZ=*~qN(2H(2%1b~cZ;&k3 zL>90X-2_Q*0g5hkkEx6#pqf#1X_VK11mozgMH0}DZj8A107V=5_M<}HVe)PBd(>b6 zoyB%ki2Dr--3^rAfQ4kyePSajq;2Lix=9xJ4^Xs&O~Q_l{`EhTudUbvPZ!B&aUg`x zzD2%ssA2=k8N$vrDp3KgnttpIGQ0QyMI9OJJZ23lfURW|J0sKtb?mZ<=h(66p%zud z>bZoyXEX+R@O5c{1jurnYLt)7;RI9Id&-iaj~+Mi1PVr+=0}fjTW6S}kP#ZgIQ(7D z5MMJynq@Bjr=gSke9a&=VI99OrxzzL=;tUYRxzIhF=8Z{&ni+J<2o;Ka+cZQUVzxi z6k(a9Es85BjSLWm8!U+K5>OlC1Zk9QQD4JKaDdkYVS+SqGZ*11*vk`&kzqGU(^nB& zxXOFHn&t{WGwWNq44PmQXZZ&=hUsR5_|TXQ5UXPyo%C>vhrD8jFMMH!S3Kk<=SkB- pEh@+7Fi*Ar*{oulcSzY#_k$(8~9N z;cvHpmNyRHxL01Lcx0NwCyfsYyQVe$+@>`B^jtH8jSr9UiM9RuDSAI=t)MVp#`__~^c-?mdK II;Vst0HhXD!vFvP literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_remove_circle_outline_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_remove_circle_outline_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..dc99afcdd959feb62d0380262504fbe1cd4039ed GIT binary patch literal 625 zcmV-%0*?KOP)IA@gyO!6G=Fsw)PH0c_9)nL}&(1Bq2A@l9&t7)~Q7Two}ChGATmuq8KuI zH5J89jA@}t^D#M{EEdRdbG}GH@V95#YyXc~?sdYW@FdPE4cc@WFk!-gE^Rg`5+|}( z?C=A}u&#K=0-@RLGr{p#W18d$VV$2iKG%Q}fh72JFW@%^lqrxUNs=4|DjafqucnEG zPZ_?V2l<~3(uTS+r)VX|Wbh$@#`A|aSRU0ihUg=(@bpp3Pg%r|C3->~s3 z&xxLUjTtt-^sl!GBCp@#hAc+=+WSRNWumA`N(+~+Y!!&2C@CFW z2C|h!;rLt%TR!RIA~QfT(gRXD3;+B9+M?WaS~UHCvR>25w65uP`i!$VU`do25G&bglXfG3M%Egw?%xKT+OjI{m> zGx(4}AA!Re6=n@%RYfkQ#HaDeLK>yheMFTaHYrlr6sgkEsg>q1Lm(w?aD1+7)(9cX z9%Ik&5xdM0+Do>%G=^WW#XOP4S)opg4t>Uq>C>S_jRG;AJOY0KJ`xgIh9Ai;00000 LNkvXXu0mjfAypYu literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_settings_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_settings_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..e84e188a1de15564930f961aefb5e591b2fa34ec GIT binary patch literal 557 zcmV+|0@D47P)Nkla9V}f0{{|82kXi@9MRd@?$weK4A^ju7 zLRBcZi6~YV4Y?x0YCBk^ee93}@A5qN9`;=V`Fy9md5-r894+Q7Vwa zKGHbsTJ3CbB>k$}G!3du63TIxO+;yvI}8$@RV#hZwGsJa1 zG(UeLOQIW*%YP$A_S8JsifkZzY=a`Er-+B1U^vMR;-MW*pvdqV@zg66vq)lJDKSEJ zw`0sxb4<_B;v8FuS(#q`)kg&}+hPL6xOrb?GQ?%fExvFHL!Kq|$LU4WPhFXQ&M?@z zxR1mtVOeOIy~lo>;tz)rETgf>fK;}TNLegBvB^KiQ=u3Ot|-Y( z2Uu+mzU3;1m}L#YE?|a1uJMlWb*&qR^Q=+kG@*msHr|$gaEk##XL&}z6#u1a9Hidf vu2=2VHre2#z3N8+Q5wRd6(rV6I@-ivWBUX&LwAlh00000NkvXXu0mjfs0IG% literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_share_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_share_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..81c80b700732c7f8d95022ed343a47a529bb6310 GIT binary patch literal 483 zcmV<90UZ8`P)U7)bX8PvpMCyj1<@L zk_wJ!3Plg>lVDHjd_C9D?zol5B&k{u#QWlcjS^iJqOG}YYxl0>XNS9cyuR_zENF`V zL^YY57oxS@wViYo^MZIwM)XOxQ>%0x?{xgp-Z&vypCOZ$d^P#|I@U}HMUNbiU60c~ z6f0KT&@a1w*R2#wK02dEcKbaPil)^-UBkSVhPrf)d;Q1fdtc~iLRGR}!xn{j#$~b1 zn6?mYP$o6+gh*quX$g^LyS-pWHVq-tsBG#&qyZUC2&L}Hs%pG6z9PFpV`j}5HQ+C_ Z?icFHxy#pVb)^6R002ovPDHLkV1k48;SK-* literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_share_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_share_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..22a8783e70f01488cb1396f18645fef00c7bb299 GIT binary patch literal 496 zcmVF8i@ z;dr8?qTnEcf#YSTw1b150_0@p@+*a+28_exGc&aM`@C!DnA6=W1TD^dHLWnpGa); zm^Y+_9MMXOw6r+?oO$m2@AWLMU(M?|&jf+ZW=n9iama{r{smBAR5>4@z?gDAz$@d*`2cSWE9V0| z(4>-oN%_rb=fjqEKJ07f!@71p%xmXEzjl6s2IYJ>_c#BFC|lFchgt1>=+Vx9(awhh z-}2j?@?k|gA11Z)p<6ruNjo3*wDVy}J0B)|$Zx4S|C?4H0J~JX8z1tANOqsxk|kX` zz!vpbi5#ig0Pd*Q&T2y7i~22FE+}NKkB|9@l-347Mjen>2Ncx-C3OG>Fn|FJV1R%9 zL3XVUSXKWz-Kz~aQvX3SsSUUGtGjoN_l8{=LB-s#i^Y)83i z=QChX8xZY$smFjBZNPKr8)%h)&x8QWQSZFF0&Ww@|DY=_KsFJ;PV}=6u7EV9*z>;~ z{$bz>2m*4k12$YvpSS~3oW+`dLUXKuARt99rYBN~O{e_o0Kcz=40(z*OD~cq!$4rO g*=#nO&1MVT4=SGK_{4lm>;M1&07*qoM6N<$g1~$Cc>n+a literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_add_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_add_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..a84106b01fd4402e5d15b08c496a75b76e811999 GIT binary patch literal 114 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xf3?%cF6p=fS?83{ F1OO~S9V-9; literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_archive_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_archive_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..d6d60f679154ef1cdfefd7755969e85e048702e5 GIT binary patch literal 377 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY0wn)GsXoKN!06!V;uuoF`1YKkmvf*%>%;vM z#J6k|@tLf;M3Cj6?Tx6NjSCVUo84*bZL$^+k>2Pja#8e%R>FJb1s`vnVLIoX=V$tV zW%`d7VbfA0%T6%sF1-}>&N893FET*ueY^6socWfiyuZ^<2z{M-v|0Gol>%{j`2==`r(5?|`OfW`5mQx_M39UY%i+ zQoQ>3@S)1Zj%&{EZrsdWW$QQd!n)Q=Mrkh1K|nu$<7d6?e1!j>O#?7U89ZJ6T-G@y GGywoX37y6O literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_close_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_close_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..6b717e0dda8649aa3b5f1d6851ba0dd20cc4ea66 GIT binary patch literal 347 zcmV-h0i^zkP)vMkH8EXyh{JTRaptiwcq@D(P;=ehLbl?EMKm*m7(@7_siS}}k zNFtnck{HL4mc!ypcyUoqJV~4rM^fS3C#iAnkyJU3biCmDy`VZLOv=LXld^HVqhdV05a26EH^Do>3VzkAd1j7p1$9mlKEvS8<$lOjC~rtLv7=+%qJ9CEuV( zSEHe6nx<)*rU}Pk{Y6Ypiv{D#MVl4ZDLH_jM4J@=Hz_$@(l-a-HYLYN+TsB8Q*vOm zgChx2PEAhdcR54lDCd802Fm#nXRVyS!8x)xSu_qEUO1~9dvU}=qQ|CKOfAw=Y|bSgrDIE*M32wXV#(VVG#2;9V!lm^$_XYl#W;baVj;qDlIHu6 z#5f+3ad~;LNU~q_0Fv|rqZUc#yXAh7l|mDfJ4x=-!Xii}w|loclVX4WdSGFS;bTdqQmIr_Sz733hypW| zDKgFoJ?y0pJ6Od&F7uQLij@%0hvy^6f_+mLVK|49BVmQqd zTDDm_30SOwF6Pj(edi>Gh;j^|P`za<007%~heFj)6A?5shC=s?g8&?67KQFLc|ywZ z21RO~BOGNOMQW6FgkTszktrsS zf;_Yvt8V0>i&zaI4?V%EfIRd8s~O~>X{;8IhZeA!MjrZs)dccT0jm+@p+T&!AP@Co zbqIOr09I=$BFD|q2zIxT<8HC67C%p`o#7R7*h>r*>_-l3C4~FPQT>Fni80)FW)IRp%mN;wDtkac0_wSos^|{& zB&3d;C}KCrlAuh)?beGSVTKMAvUUtkoTZE^ROSp0wNXO$DA9&vt^7omrfJ24?Uuh{ zU)g~tn|Y7^>myt6D9%{l&x2F_0ul zlBBe2KKCf13+40rDB~gXv1tp{r~p^XRxFxF6)ND>G9T0KphDOKOv?`iRDx-3p@4cY zZ3q?0OdE~}NI(J-kboXD*)8pC8BF##ei}W)V>3|t^vkgWjp!gC7B$dHTHJ#erf*`y z?m=*hVH?m9ZQl$)KbY)?J5angHXxX4)NBRfYxh!?>DHA{lp9UNo{g*4J?WlNbz;rpc_0N+c_pX z!($$E)jddY*#dMz+mHCMHn{|~u>+jH%J_(VpWT2I`OJ0>qKQRVw}cjVAjM1PAQagg zWG2SVPi^3wYUW|l)__UXY{91a qJfMW00G`Ul++!Y+BuSE#W{m=WP}<*PD{=Gy0000&CwZ)3gpx9W z9Lfyfcw`69$YR_)W*&F=^rZA=7e%DaVHQvkru1eRbNVq;xRJvA<`pJ&ayId<#BsBY z4Lz7a94ljf^8hhb&KN9h95){@s=`U(RV&BMI=id%ucM#yLrTbv9m@#E%}1HBbYzU< z=6>*r?ikMXV9VgUaZsD>iNbMhoa5$&_?{K)#=Q~jn}Qo}g$(p0R$w?}AP2MlpgU4f z!M_^(Bn5T-HjA zB^1ySA2cF0@zp?{+R==Kwp>jNU-2f*Eg-GC0^TUcmF=9B-#@Uq{JAW+#Z4K+r8Z^cPE)_6eY*kJ{d!+Fs z#j!*Ve`RA9g#aa-&FY;Q7>^V(D6x**yL7i$ahe2S3}YEPIAwijgLUT}mT&B;%G$nd jEMh47FG>_q^lN+rG7@bea~%UM00000NkvXXu0mjfu>~Q} literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_info_outline_white_48dp.png b/app/src/main/res/drawable-xxhdpi/ic_info_outline_white_48dp.png new file mode 100644 index 0000000000000000000000000000000000000000..bc0eda9fc1e392bc5d314b3efd18520c734b0457 GIT binary patch literal 1985 zcmV;y2R`_TP)A9>?+T%(T@uu2h;*7q`@H4GM~wCZ#k~z?hh5!XnrPcf&T}GO^VxW@(iFN%R32 zgY{AtA%e>mLYGSrQM3q^Z9}y&@n5MF36R=pJGPXWzV`|IerM*KIkz94*XIF{z7x(h zIU|usBoc{4B9TZW68Sx0K5I$xHU~M!Fr%cHCQFuSQj9XpmwduoBw53JB&Z2~uY{-B zMK7b+ag|Ws_$)}8A&j8h72-LuJ^7jkx(CqLqnqW2W+WOb7Q6F@m zb)?D994A>%0x57syvQl$$j=ln1Q}=(cgV+Wx_A_EagXsKx5>vWEkOlpBTH@>KA;-$ zU^R4;A-4>hg9x-KuRLdX9Px0=_#&@FEq(`D#~pIH!OMsb+ssXJndNyu12vE)mm^dl zKCO~oa+#vuuRt|i$J#VW#OJj$gSBf^`w^&=A*}sGJrckg7{S^g_jn)ZW32U4i3GGN z2C(*#*MYWTt(Q_Hz?E?lYi(WzdW0#g9HIybc*PvX$^^Au1&Z@+jy!QB1WV*dbb+`x zf&Ph=UW$;Ate6v6`G*&Q9wv=mKcz^BR>lB&lPq#S&@uFWq7n(&9^_~A4!a%bS@fn0 zzV6YHg0_Mg^j5nWD8~2bC6Q1D5?(`ZkeGXcHlcR}32h+ZIC>l03KZu@bZ$@)>Oc#) zh0ZWBcLKGb_c9XdKtdaO>)i>|kIoq+^nrx0&^hl$peA%OJZ?mwr^uqySa_iQ=zM?} z5lGmF&fY=;mGc{FGgKQBsFqpOrntYbKpp6GA;ts}_M)@Bus~mXEeoza07qc&ncPztqjqXSi- zHfBH2G-{`u5~yX|4HN~6{);(3NIwuAq@&ZktqVoX+wAB<@yp-U zt;Xl}1AT&8r_q7lbnj;ME!6fH9q4`c9(N>B`^M-%{q8+5TZ3APn6ZK4Oro~RexUj2 zEHyUJlWsn=7p|g~G&WENYFFHSarmjRfqLA1akvY$F%m`wDq`H-SI^DptTr;xGw3ur z3sj;#=D%xXpf1$LDRLG_e-ye-k#T{Fxrtg2;xte@IxiX*s0E#E&IA3CENZ8W3-lRk z87iFz(!V~O<1wQGJ%P>{q>wz0IKyv=zN|?gWbS13EXU z40WI?#?bkWm^*=l7W9sVI8cmJ=>5gb4+~sIuRXMZcA$63-A_}jMQ?_NPzGvZ7QI#O z1`>ME8=(pb*%mU2-XX;8K#Q0}Z-BB;<++c`=%x6R`+JwcT zu<~ybp~#b<2P=KVya^=KGLa)uao~AMa^xAO#>>wHwqflAWdR?1PGjw_UVmt^8*2kp z1$gGVjI}Pr>p=H1h_#<-hbBLdTgjO=-_7%%LjZ;q_x!qv{;&&in zb6$zE{EI((5~q&+dH(Fc-;f{z2`$W$k6F4|;%b~H*q8TDR%}3m3M4$w6#1FsG#g#G z(k`Zz&&byV&mlnu66(1|{%-IdYaHIQ74ZyR+$4WjS&jr9NT_BIdw%6G?JOgf=S}jH zbkM^%_FUp2Bn*Lsd+ElB2`&jlpuiw(yWaM z!eU%=OB!e(p_U7TK_4|p7y}7$b}&hhQoKqG2|kd1i}(*gIiz{k99$sHRx^m-{-=47 z6UsnBj13Iqm+v%hNkbAyA89@3@me42h#>|9(yWoar18QO-83NHPVi&*vz@bKa7%_W zY~v4z_Y?eE6-kbeq7dWsu#HN@uLbeR(3Ny@lo9N>!cjVDqzLhMLU1dmnJw(*0B0Fu zgfY@&$dG1?5r+7j1MFrCtC)ubH6fbcfucZBplDH`C{PqA3KRv30!4wMK+*mW%P81z T_=9Ik00000NkvXXu0mjfJ3gG| literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_open_in_browser_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_open_in_browser_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..4324fbf57375b56c3ce9c05b4093712d8a5ad86f GIT binary patch literal 283 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY0wn)GsXhaw9(lSrhEy=Vz2WG^>?qRmuzu=E z$rj6`j%O_ItNb_^ANx*G+&!_KeS^U62?6!F@?W;+U22{dz1vq$AVva4bT46OO%{%Dkkw%9O$6L7k4qf(9loAn`88-nZ((c{^Ku zvkOgEPO@1kY<+&0Y1by_oM&b-B~LkR)#5BR^D(Z~k-0H3$V<~UC4Sq6`R(t;mpKD{ O%i!ti=d#Wzp$P!(G;U!4 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_remove_circle_outline_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_remove_circle_outline_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..63ae14bdf7fc175c4c534fff6d606f669a6d6311 GIT binary patch literal 907 zcmV;619bd}P)!g@i zf&wd9ktOyGESX1T4dc*R&PdqMRCwPOb zo=4fGJILxWIil*}39_2n`$3o0FLV;e0p^g^kA%lDJ~`tgz!|AVkMkJU*MIw>Px zzY)?!XO(j#rN>$CLBg=E&^pIS(n;&B@RATNAYZpg+9dKdz^?oU^c@~a%duhY=PruY zpR`08ZX#2I0!>}Fu27FOWUXuW8ci)*kA*b2x4;XU@TeoxDbjKVnLcxfKttB^D?J@l zkm(YECXneqX?lQ6;{;kjrcu&VLZ-U}T1BQI(lm@rD+F3craoMXk*gn>)(BKZrruNs z$|F;SKy_rwq&iR+GHtc~KrQphT7Riq;n=sp593_8t7DNgJw&E40$oO?5Bqkx{y?C^ z*17RVff2Bgh#k?!WXFkku9TXa-r8W9-rq=VOUU_AJ9Q$6j1UV{52>IX`Iz)r1zV^|pIPE6dG^sEE-}tsR#>CL*4HYmvBDf< he9t$usJ6DY{{UGVsERg6yuJVc002ovPDHLkV1kK`p56ce literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_settings_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_settings_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..3023ff8daa925ac79e863caf679d03c56c3afc93 GIT binary patch literal 827 zcmV-B1H}A^P)>`06 zl!7Kix5q+Fq6=w?Bq2zRX%{Q=l9q~JFAfaD-fMk(ql{OMJp*hZ8Le@FKzb36b`fRvPbJZ#ihbmi`-huQ`v`H+dnnz5 zIO;AGB7BE9YM!%LJ&ic*2`mcPKpgfvmoR!03D^-c43Z$hAhR4H0ecmL8#qMtu}v3c zWQvny2hqm?F??uNd0r4z>Rub{-SwiV_KcJlc$@G(Y7OnY{oYXaZ@4M{dXjJ$v z`$eh=sKvO?Cei3I?)A;6Md`CXq2t(8SWgZw6G7n$pCQ&JiDGgU(}=YRim_lq5{{Tt>$z%L(Sw8>(002ovPDHLk FV1g%Ge~AD9 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_share_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_share_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..784933ad55a011a9b829767844969f36ecd98b80 GIT binary patch literal 675 zcmV;U0$lxxP)UZPF>#3%k9#08-1VeKX7y3y)WkyiA4SZF@~9C4WBJmnPCX0 z4zY{`TA~+6`Y9urmFRP`J7pxKlKw8mSVjmhhq7VB(jcm45lbGb)(}etRQZUd3aYBj zwxF+wr95hqT&Ij!%AjPVwmX&vP;ii!NL{_7;o5nKkJMF>12j~Oi~K+Wz2{fRa*okZ zNBMvR`p!9G?B}idu^PKOT|8ym_`}>|Cp80j6!58#C&K^@)XEJ?NTBEJ!->-rkU%S( zz=aK6bWB+xg`IvdW7 zD%@pS~Mow4tCcWJy2G*_n05h-vIn4hi&{y~G$}-F#he1O2C?)6Rf~wFc~mVRmgbu6lTD*4jaWL5s!q#2`%N3lZX@Aw6Lm>) zNJwvp<3K0bkmYpXNP)E)Qv=1Dpnk1InpvI&qSnTPM&I{WK*8?NJ&ecW~5|Co@S)v zo;+)jihdt!$TJgZIOCp9j7RYtcTHy&qi7EMT1#hYjzy7Fyz4JnxBa7?nwO$TPFj`q zr?*r*?-yB%jzw_{yK2{dO_w|wdyK1E*V43R#&{IRA)lzr`oVKi2CrL}b=NCV1}DtR zdf-F7Q4|%E=4@)&v1!&s>@nzymi_)U?dd3vQ7f{R9MxmWmaIiDM3IbXXy<|7Wc}gI zD3(zTooV{g6HzP`D?0PNXQFr}b>fbfBMo!%)O?^fQn4w|-AGAGo@S(ETb^d5WJ8`i zk&;ORsJsW`0@zk4-OaZ6`@bTm>hqOLRBE*psAIi;?hzpcu;@0>~$&4^`LKYLci zIrnAV_F@#zgsOE-E$e2C#U2B`(z4&Ks^KV)cEMwO`8>gR#dEQ>t#buIhb5Q9LKD%GxreN1uxt9@X%L{wSIr@7a>|t0nC$dn$@& z&^LB;W=UV9V8l;4^I4?g4IA>+3`Qyrxu7A>*+|KZJl{o1ZpgC`DY-7se57PXp6N)* zlss=oN`^EX3~&3lD{0HN-uDj}n2TZFIq^*d5fKs9IGxiK6daEvT}Q(4NK!u< zjz^MiBI0-?=@u%EN0RO!<9H+tHZwoxGsmA5$Q#E=9kMANpzJ2ic*23c&gU9y-9$z;+zP<3c7PJ~3SL3`5tLRaD z>FzfQPnT-b@y6iUs@WO=ZEp;qVbw#KCN!#EwdH^OEYtz{7egOm!KmY**5I_I{1Q0*~RbB>=Zww&cQ-FLY0CLS~9YC&!5dgW`0dl>L0LXO^ zAlIap1LW$90H}DG)&t}khycj710dI%2!LFN0CIgPG2ojX=u!kg#mlq-AlG07K(3tt zx!y(quVXy4K^jv|eYd1iy4-o*lx&U%bX(d3eYY_kyub|BUxrQSEa_w1qpe$jYj}c1`G?6gS zu@rztljZqd;ef9)P;at8HzHC1YD;vHw}M?hDLdOA?tP}4vvK({^Pg))=U|!(mxZ6c&OM&RJknDpTixxA!-P|JC^7D;c*rS=1>Cag9 z|G&QfG{4^Z9XskzSU#UqoOeq2T;!rhx<5}OtM%N`-M2{how2don+D5UpPt2}FZpKS zY<)wry5~}{$s5M4zLo85w#hAW3lsfUzL%1}u%SKY#$Wq?*7-M*rSCpCUGS`_I_E;q z>4(AhEfb>eK6zg7PSyBY&V2tB2hZJ1UmLert(x`cyG3Tvh3zH!fs5x(c;~Ktfc0hF zRwfaf6~osShth^B zrv+9Hwu%9Y3+4!{5DD<%2;tFCWzk|65n|fPtisWFi&3b>;VlEF(}J=F7R3N-hbDm) zvI>qISJ)@0v%F$HAVC$* X3KdUc*DT=#MjnHwtDnm{r-UW|@NLZN literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_close_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_close_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..39641921925f090e33df2767a4ee5e6d5911194f GIT binary patch literal 436 zcmV;l0ZaagP)32ETvhYyB39D^lU%DgQgv&#U%8l^-CA%qY@2qA=!B5=cm zUgehujQG*l{{`@rPr!f|fEk^>KI9WteE@ilV6HEl&_rJ@p_zU*;T}RirIc{5NocNLm*7JGdV(AM zYYDFOvk5~8{Z*t_>U=!XvoehUSEh=adIga45oe)B9kGgT}7UT3Cirmr(oHPv^YQ1-p=Hlh5u;xggf zX{&yw+El-OrrKQJRl@b7x{HLmNkj95`a#LLnW{Veb2C+!`ppt#r)=g4@?c8RY{yJj~W@W|h^mU4q`i)2y~Rw@J`jIh$1%|In!~{Tb{nMqaxlgb+dq eA%qY@zJ@mriVM?qfwL0;0000J9U_ozX?{8^>TDc4h7hq#2$_^A8mtsRFsHF?>r!XXT#{vTa!@L3C%$>-0 z%9r~x=QNYtsZ@jzLI@#*5JCtcRutAaU0}M&UT*r&Ks#7}zz4gz=@XVflK}wl?BtI+qvl%BuwZ7ys?#=z9(TuAK;bE-1O%JIQ1<9sPiO!=`XAQxPbnF^rb%* zs!3n^E34m^zVw&X@2!shVQ%_W=}Uk4fUcQ7``l9vp1$VH`TdwLEB#O4AuNF#fWU-= z`h+FW868-hkXSz;f!m2g{Rsy81qoC7{)8m@>ofYE1kRmzj5}c?(ZxyIDb;f&(BLIJ z!SVl5lzTr_LL~iE2@&;EBt+JaosdA^n^5~(LL&WG2?_OMBqY`^JHbG|%mgESPeOm8 zggeu1tCR%WWidiR>Rq?w1dCO@#00CChLRFYA4=Q_C(c=Sf~EePgp0YevkBJvj)azX zb}}Ic{i%dj;p{je7yVxemqD|`gq-w$60U-08+0?%7eWXjgb+dqA^s8Hp@h5E;aM5< P00000NkvXXu0mjfH0Rci literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_favorite_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_favorite_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..b46fa0a602af17ee0f08d927238e6be079bcaadd GIT binary patch literal 802 zcmV+-1Ks?IP)*(_R>F@{kl5{$OU7VQgW z(<)_5G2B!{frJ#KBNI{?iIe4I&SmaAZAH*??)Q7+{QmoA_dM{vN~Kb%R4SE9g$)|9 z&!j0cs%kEpa@e>H2G!$sPulB<(=Mv1nsLgcy&iX)HmTU`keawgHDB1`W_fP$ykpmy zGjGx+16pdps57EK=j^b^wJf&Nc~PLBZPTwsRy!ez=XcM_TrIQJAEJ1^w@TCcjH!vb zJ7~z2miS!M-MsDkG@)WbG}td51hC3)qQO2<(V&V+(M11P3m$bzG|@p74d^o=nrzOn zwW^}YKGvshW1L`3%_7k@ zD&&wzv;ztlR1=9d?^d}+M8Z8L*FKSO<8n=kggYSDlt{Rfa?OZ@J1=uc(OCifdqTR2c zqiLVeeCtJmtyfs)8w;HBiyGA>FIW&wR$E>;HGwgzg^`Hlw6~b97^r)pyIq6H-t9T6_afn3}YBWdbVvg43Fr(v-e-U%IOR<<#6ReK8Ua*D$0tis)1Ze(0009IL;8!RA_EoW8 zo_}B5QHuduJ>NW$7ij}v1?{u}P%9D8rv82H@&LoQUK*grk80%sU^=g(0qy*%2f&7C zfc0MafOg=`CsPMZ~F4V*T#tlYfE>x$0PHzCl zuFQp+9H7g}=!3zw{Hg{RFPRz*u>7hA7^fMMHo(}DI-tq&C_p{G9`76eeM9-t&>#Qn z7tjp=j3-P082AI_0mkNm0IMm|x-tN7nKJ<3A(M%13XjVG7&jT& z7vKkBE#nC_o^_ey#{pa~mq8iO5%;7FD3StR|-97xhpHUQQ5eIlc-ND6qpM5MqDW#NBN-3o>eFFCpy!gHh RstN!A002ovPDHLkV1h&-;f4SJ literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_info_black_24.png b/app/src/main/res/drawable-xxxhdpi/ic_info_black_24.png new file mode 100644 index 0000000000000000000000000000000000000000..2dc96347878b73964e92d52154a38b035cd49bb6 GIT binary patch literal 874 zcmV-w1C{)VP)t%Y#?j@C5b4_;9(3RgCZ;j7m&sPLR6x+*-v!|7fjKcUBe=J?ROj1eWb9@ z9lhBljH1I#_z*tfmr(eIPcVU??)YzTASHamWen*H{{-t&!#BKwE*3h38(@!T2O}w= za>xd-kYEQ`A_=g-zRPw&65yP*m=2Pez;3#q4Io1nV2TZ(NETq1ZSTP%>`px;RGoX= z0mexKjJX5Mk_Je-18kB8SaS!+OZq7J6Pe?jJ3xgrz>zyZoisqr9pH*Iz~z4c(DFm5 z<-f!&Uq&}b1FX6OWJv=|>ucuJ0HpM_(`o=1b|#{v0XkXMWCgMSyKHOXG+BU2Hh=-L z0DY_{12vKWRRmb~<`zi;%(L%}_L2m6#dDFbL=K>cJ3J=@qvQbI@tx3g;Di)Fh5w%E z1Sx^Z)<=07*qoM6N<$g2lCv Ak^lez literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_info_outline_white_48dp.png b/app/src/main/res/drawable-xxxhdpi/ic_info_outline_white_48dp.png new file mode 100644 index 0000000000000000000000000000000000000000..939ee3a9660ec99f411c4e417aca9b47a6bdaa37 GIT binary patch literal 2633 zcmV-P3byr$P)G0000UQNklG~8%QKT6(ftKw%{eb z%*=W2fAINyX6DSfea_?cAIvj3GoLeO+F+oB8gAoZ+IWk0j&g<`E-^rc3$!+u`gwtg_6E3t zcSz&66#rtneF3g#H$n0N)9eXQ%?^Uc;XNv&A7CsSNfT5lUZgy_0g|kskD%-2Aresx zP|ab&qLZp<1-Oh>h6s}(ni&z508=?i7#(496ap;eSHkKSmV`S%30rWB!x?t+5-Vxo zMryc{Qc5VIh!RSp<0%tAuExeQ^VgM_yMR$#*6 zE+phzgoBA?UIrLTpYgRZ{A|4CmH^33pQjD`Izyva=b-Ec~EE&}HAeQZ8)Y6o>7oZWHCDsLa5S@kY1=x(*5M|Z{ z7)uJZm)#3+0=1723%4J&uiXkT1_!mrtPHRUwG5^11h^HQ3M&KDqBGx}04=EffLOX7 z)K)d6lott<$?Kt8_fNiL4vo^pE zZywHVLG7Hi0WP4{=-;qLXqk94FLFWPM0xUzP!My-u$e`A4U4TzeOHt}x0O2rdX~tU>;A%3c zbs%mAXhx^mssOF%G`Sm~2AyxM`h!hp(W!JdfN&h0JFN-OfX*?*?Eo$49JD4t7dlPu z2bhZ9Oe+G^qc_?8073^kA6gM$A3FPyLIk)Uy*ZH&Fb};&g$YnXKRO2@AK)N5KQXc} z0fZOOTM+30jp(%@g$gi{G&*M}j%?k zP)7>AYYQ7d*ooeuNd6nE&(YhC6gt2(9Q2wZ7oZhAhuXpi5Z*&C#SM`PFpD<M;h; zRFXpP6lIYKFotgQhMD3`0O3VUd_W=+0g|+1q7CsXKsmjbSc^npf5t=)rQQV)9>Rn} zBNFo6kC~;2mjMzS#Ka)=NQgI&AxwNq!rK5s6+@Wl=Q<>$o6bc{3@{b(I)KoOnI5W; z5U!eD%rqf>1Q@{)%$y5hf@=PYnNEuQ2_Q`77tHj8>_Rl1Ud&u#65>|?VF{-CnH!?F z-iw&}BjRTO;cu84WKl>ycn)LgRmATA#e9h=hgK3ufS2U|FxSQ9{s#~y(u=vBlpz7# z7}_z{!&OKS0todCV(v6EkN|EL-IyC-CK8kYLL(05Qmi3~1ZYLHlE$3FLL^85geJ0c zgc>9utK)OB^CS|q074tt8Rl7v5uZ1T=SY#A|3`utKzN00b+ZuhagCfMTN{v|21xP` zviBKteL8L(Bzv!u2yOr&!7JoqFS8L3H975P z4Inh(kelyV%Y*{{w62x2M8Xz8n8=sp??<+?k~vvEb5KS-Pq2d?^4G-#B#Z%sVqV8R zXW7FhR&h5sQO7vS8ATCAjG~3nSyvd!Jl2n`vl3qsYU#l;NPb4Pg3}8m@Qn3_%*@bUClcB z@Jk{(~2e(ZpmV7!!iEDpu1;nnFm?K@*io zkR}9iWh~@nz9xekG8|(g3n)c`HzCL?Wj?EUn-iqTR*K_nrHT3a7d{T4-$*LCndLms z8|%;ox zmOVW8KHPOY8d)(-!LntMdZAhHO;)F*r0)XDt@iw1%l+%N`MbPxme1!DpWA6_8_xFV zr1Xvr%aeojWjZ3wFKu2^|4({i!ttm>jUJn9Mb~zkNWaurr!JK1UU)sE!T2ceN(S?; z?iAw#49q+;%GqqJnRyI=?0?J-D^eRcn6`3VIKDQD<;CT#QOqw+?~P*W_>q>ys_^LC z6()xx{J~`n9r4S~FbMMdL^3ML%`swf5%Ianpv1M@he1#@SgfJrlqR#ok(Nw7hkw-& zgTbcr7%Y%85Kz^wJ21b#V%HL;IU0HMxi&1^(Pzk;kmBlED|KMns%bL83~`YTx(n0i vh#G84J-EcGY|c(w*@DMM+C8Dydh3%9aTJ175;LjaTqh(Kk_}k(P=! z#8)5U?L|UU`YNPtjFEUHUUD(TTHRms{5w0$%~+_m?6q&riZ@~pH0!tCd#m? ziX2ao5*ntBG*+ZI&U++)-qJymh_&(@39u2eL}m&1kPzx+F%j6yBob2Nv=XWWKY7>; z-E^>xRWy*Ge7Xb;tY$k$>E;>Y(iJKQ$pU^y9Lw{N{bX>nl!NpkIQJ`cgkTxY2j+Xu zkin0ooMjwwXo#-}*h*d@1$##aX^K(BamJ8>y`YHzEpwRu7cMZLQq*vXyu%VQ1f;=X z^ZdnHg0POq4jZYC03;kP-JeMjlr%RH%=)RIWIq<%I&B2)P{FNql@jemjQ*j8Fzn(J zVzh@6EMd}|KHn3TE#?xKppl~8GdB+{glV_Ak?5sJt%y+@VLOZ%Whp|6=jO>n6n;Te zBP8+dn0fA#A_`RuA*w_8RK+`E_pc=?-xycI8!GW7ho~+PrA|b303XVYn}>JICrY)( zz3LOn@T?h8br7{vh-w3#^%(d2X`)tb{10y6QKj*k>I^oVQ?vL$5>Hx;hs6vwEJsvZ z@#Kmc_7Ge8)l4qpiT)bdk1hHz{{s&eAgTvLSff}+6oAi->7gqyn$SMkxfGcc``qU#_jjACHRu`~Ped;x?KB^%J zs|!dPzpT0XqK1@PT|l||)aTj1fLXSMQhE#h4=!STjIN$-$LM`EWc!TzBD;gDE;ZyR zHl0+nxjfr$=5DFkcA4q{8Yexxk!wO${#{8rrS z%^hl3-qI#&LR6daB*l~(corMZt65Bvz@u(8bet-pHqZEd?mC`rLR80z+G#}f9iEjj ztOk!!LzL>7Fn+h6gAZ+p>Jm}vLR9Qux$?s2-Ch3e^lFsvN#0 z89`Jxt-OsBo|2#lS;XiNVLOHx?W9P(h$_!6!t?`qM0J~@EoK~1ePRn?*~Y(!YK%pc zpcOIt#O{#(`sVb7EG4>%809$}!rsp#VSM{k(2rntlQco8rU${SkGTY(jv)lI$E+g= z>lsEc8>EJSWO#vKmZy_iN>R@>q+ri!BtT6LOAy96MK#5k$7v>zf<0q70n6|Naa_bX zzQ&Ix&O1(C7^IO9)T_%`*H4ZWxLHZNde+tTQA0>7=t5k2Ne?GzVGRpOQa)XZg{+~4 zlicDJ;?hOt5~@8+AR#qImIyS`i-gc^77>{&BS?S^vy+G=Im{a*fJVuYz=}!^@B}HL zA@-5NsxmfkgAa($Cb`bH%wdK|vXzVciFjy$3v4FAj8w-)+Uevjk9f@#pQoGRHIKN% kWpZqwmcYbfu~;njA3$|XlQ9%OFaQ7m07*qoM6N<$f?rxWod5s; literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_settings_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_settings_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..476d5c9780b6904004c7da0d86c1f02e4c37f892 GIT binary patch literal 1073 zcmV-11kU@3P)?ENDOXxEq##k{R3pG`V_=<^3NWfR4)j)KkE?kf% zU}CHb1trze6>4h;sTD;rtpyq(711_6TOXmFPeL**CLQm0&Kbrt$^HG8P44Zq_ne-d zIYT6GB9TZWvWW>Kq@TD8Z%rbKCJFFT3Q=|)UTQ^@t;I_ZBg!7bOS=$dFXN?m5M}%E z(wB&`ukq3!h_Yz{ypu!>x)$#=AqKVLoh^t#k5E8sIL>$MVhOq3&gY0hM`8YgceIr|YY&iqQ@`QK~6 zQos?!gFYj`%4WoqwqT*0?-5V>feMW6L_F#x3@zbr#H0S91|$0r&w3vNHzOXl4y8E9 z5Dz;}9F@lr$I={PD~(h#??P2Hv5k*OBaS_R%D0F^Gwfz5bE%<=S;V1HRE7}izM+=? zvXWuMx}&JnaTc-a1LhN^j9$d5v#dlZNx#*%UxG%$>Lq+a5(6c?YW(obN2{C>#Hd#& z!B{)f219oRCRP~?=PB;R(sE89s&->)k9y|05eEzCQO)~HF|~}eY5{Q-jsl(y{dFJ0 zLg+*>%Tom8#bC&-wqj#@$W=cjZ_b3=su3H_Ay*wEUqWZ2%FD7H{2O^AAiF$gQgUe1!CaxCt9AAy+*GP?%|S8#ZFe%*GOO> z#bLy_Ugb>^^dZI#kVL7TbBI;FloO_cKI?z#>rnXuv2KJ_AwQf^#JVG>3?mNBvWI0k zCeFQ%8(UwY@+jh1ntry^Op?+IC23&?1EdS}PEAbz5c&FESu5gU4Th($JgXZci}?%j zAcuGCUqC#n17l^3BOc^%YUO^!lkUMvz+uFL`muJC2Zudosl~w?NY31kB51DEg_A{d zo(4V7yWC2^CMFTbCfGp0ZS3d#oLeiEIC_%o8Q~cgkx|9lh(m9&kc<=^oYC)hXq9s6 z($m<(9mL4iY&qBmXe3*lJNfX^nst-}xU1(FC+XyRa$C!w;mW?1+!pg9Cm5oZ0(uZJ z=)Phh(s literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_share_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_share_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..5a8544ce5d6c8bf471c8fbd54cf4f88254ea9ccb GIT binary patch literal 888 zcmV-;1Bd*HP)9LMqRypZ5w`JO>))98$f6)oE2rdCPO(4t&OEy|HVqeUTEVJbno>6L`orcEYB zO@<!#gj`DvJ3WY+U zOXmgB9rTlB8$sA6%WE_c81+o!A$fUBQ49J#&EJTj15zm0gJ7>4#oCY}dJ?5-IY5eN zpBhw}M2c)2g=+DTB0D3FI(>*kub|EX;?NxGY$6V=qfQQS=oocQ5r>XYXB~0qXVl3e z4tVzmNT;dsiN1Pi)h5PviagRT!#CK8B$bZVC8K2!qANxqb ze#NtY#_=pZYi128*dhH~rkNeY&<>B`tGoCTF*HdH08qt9fnCb+E*1Ewj3MJ(e#-+~ zR7EGBvc?{}{Kz!VQ|a$qwzFkC7SC}5s@=!e##izkZb7x%_y94q$UP_!vCL%;d&aNH zC-7Aruae<6&hXeGLoabIdX!b`T>2=%Bwm5@oF)bU+{qV~xs2nhR?GF%E-l>5TjY#$ z*@VwtMyz>kYo5!e5PQ1kGR{6?PWxQK$A~#ab7_#^3~}m^0WPCPFJegBTpG+H4$TMs z?(%xj?=Ew}e*kqhf_{YNg8mYBBTjun9ja7wia2$|>y)C%hlq0X|_d z%}}r7A-edCpE%%<>&)^3Q7%gIgYj>?Z%_*QmGGkRxNVXm0{T@jVtnnFXe1CCSu$?c zC>7{OgcO^`%dnRyK|kxfF7pWa614L+0CA`db%qd! z22p1macB&6(m{WK;h-O(4#c4p>Li?IGzvlf-`xdNN+Lzpgi=#T5q*SO<*Xp~_>M9Z zt7h5$>vKEGU1h>@c*+QbhsfaN4d(()1WJrH1{phVkQ7lup-?CkD#AaVcIHkIiEiWo O0000OV%)fi;J>`G%jw)8gjVE${JR2aaY!G0~hnM zhG|@UElYTe3EXtc3bxQs9yfX7vVdw{;WqBh$^xF`2P*M9I}|qJ3{e@6PKArjoMZ`q zzv59!fmanSI=PO&6?&+plSP6ovR7f^QNATeik(_R0|VqKmg6Jp6&~t1O&))jcuMQg zz!ARWI(NwM6X$u29SRTC9OM@M7CFo&S;iB5kG}$E*e=U>kWUFR#U6!^Dg|IO$GL;Q zdG;$z?BZ=E`Hcb|x0vJv&02@&xrVwOhY7LL@4M9>gD@+_--REpnJm3KuU?nF43nuCUR}5|x-@k1XJG+|9FJHqb%=H!GZ? zR#tEVHz}HB3FEjJmo=nuF(+%t;UXt%$l)REk1CR+@t5HZYGfZVddcE%mgiK277p?u!wk{S zAzEYsZHy7*0=xAuo@I(+(gP_7Qy#3#WAFU@$HR+-BVK~+CCm< zEWQ+Y*(W@)N{5G8@j{s+TcYAcHDRTK@IXPui<^8M`6enRp31ne$<-yq#p8k+k8-hz zWS2oku)4Q)dVv4q7~QsQQ%x%8zqh{r^$+{^{Li1JZPhgffn?$KNVl)AMc?l8ox{8@ zVJE}RpNxGQV;b)KVK}}s4n#d(BcNbze}FR&Ooguz0r51$I48`OPiUIT^yC@)kDhFn z6KDB9G%=j~`B2H63nUZLuygMJ@O5k6vwEE4-w@XDlz5i^niTxf;hoZWQAdmr(Kw^4?f2 zk2yj>r@RqXu(W?80wi(-6fFN&wt<{7KLDgXka5!I!_7=8&)csu{rP+M&yRC{ z=ZQa9AALS@U77d{{R3}wQ<_%a7xZeldE0r>UZykmbpJS3zb>%lqUjUA9a zY{NLo)|Yd_Ya4H%IZ{A#B(DMOQU=<28z`Xb%%oD?CZup(#hcY*TR+gPYnfE6eSn(l z7$?n5^Jqxft-)C2TQnDFcCatgi;LULOt>HLynU6tDw4rHGo0UqH=)PutE{sx^9{$@ zRnxMh4kRth@DA3=mUAeauDj zCPCMAEB68ow;Si``Yt>b`Wx0i-{KUbTvE%d-`dZ9*)XzvU=cVV?7-m9z=(sR&->sn ZXFjj~9xm1VRA8~d;OXk;vd$@?2>`ZW%U1vZ literal 0 HcmV?d00001 diff --git a/app/src/main/res/layout-land/activity_home.xml b/app/src/main/res/layout-land/activity_home.xml new file mode 100644 index 0000000..c52ed6c --- /dev/null +++ b/app/src/main/res/layout-land/activity_home.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_add_source.xml b/app/src/main/res/layout/activity_add_source.xml new file mode 100644 index 0000000..1c8749a --- /dev/null +++ b/app/src/main/res/layout/activity_add_source.xml @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + +