├── .gitignore
├── LICENSE
├── README.md
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
├── schemas
│ └── com.czbix.v2ex.db.V2Db
│ │ └── 1.json
└── src
│ ├── androidTest
│ └── kotlin
│ │ └── com
│ │ └── czbix
│ │ └── v2ex
│ │ └── ui
│ │ └── util
│ │ └── HtmlTest.kt
│ ├── debug
│ ├── AndroidManifest.xml
│ ├── kotlin
│ │ └── com
│ │ │ └── czbix
│ │ │ └── v2ex
│ │ │ ├── DebugAppCtx.kt
│ │ │ └── DebugHelpersImpl.kt
│ └── res
│ │ └── values
│ │ ├── config.xml
│ │ └── strings.xml
│ ├── main
│ ├── AndroidManifest.xml
│ ├── ic_launcher-web.png
│ ├── java
│ │ └── com
│ │ │ └── czbix
│ │ │ └── v2ex
│ │ │ ├── BackupAgent.java
│ │ │ ├── common
│ │ │ ├── DeviceStatus.java
│ │ │ ├── PrefStore.java
│ │ │ └── exception
│ │ │ │ ├── ExIllegalStateException.java
│ │ │ │ ├── FatalException.java
│ │ │ │ ├── NotImplementedException.java
│ │ │ │ ├── RemoteException.java
│ │ │ │ ├── RequestException.java
│ │ │ │ └── UnauthorizedException.java
│ │ │ ├── dao
│ │ │ ├── ConfigDao.java
│ │ │ ├── DaoBase.java
│ │ │ ├── NodeDao.java
│ │ │ ├── SqlOperation.java
│ │ │ └── V2exDb.java
│ │ │ ├── eventbus
│ │ │ ├── LoginEvent.java
│ │ │ └── executor
│ │ │ │ └── HandlerExecutor.java
│ │ │ ├── helper
│ │ │ ├── CustomTabsHelper.java
│ │ │ └── MultiList.java
│ │ │ ├── model
│ │ │ ├── Favable.java
│ │ │ ├── Ignorable.java
│ │ │ ├── LoginResult.java
│ │ │ ├── Notification.java
│ │ │ ├── Page.java
│ │ │ ├── Tab.java
│ │ │ ├── Thankable.java
│ │ │ └── db
│ │ │ │ ├── Draft.java
│ │ │ │ ├── TopicDraft.java
│ │ │ │ └── ViewHistory.java
│ │ │ ├── network
│ │ │ ├── Etag.java
│ │ │ └── HttpStatus.java
│ │ │ ├── parser
│ │ │ └── AsyncImageGetter.java
│ │ │ ├── ui
│ │ │ ├── LoadingActivity.java
│ │ │ ├── TopicEditActivity.java
│ │ │ ├── adapter
│ │ │ │ ├── NotificationAdapter.java
│ │ │ │ └── StableArrayAdapter.java
│ │ │ ├── fragment
│ │ │ │ ├── BaseTabFragment.java
│ │ │ │ ├── CategoryTabFragment.java
│ │ │ │ ├── FavNodeFragment.java
│ │ │ │ ├── FavoriteTabFragment.java
│ │ │ │ ├── NodeListFragment.java
│ │ │ │ └── NotificationListFragment.java
│ │ │ ├── helper
│ │ │ │ ├── ForceTouchDetector.java
│ │ │ │ └── ReplyFormHelper.java
│ │ │ ├── loader
│ │ │ │ ├── AsyncTaskLoader.java
│ │ │ │ └── NotificationLoader.java
│ │ │ ├── model
│ │ │ │ └── EpoxyDataBindingConfig.java
│ │ │ ├── preference
│ │ │ │ └── TabListPreference.java
│ │ │ ├── util
│ │ │ │ └── Html.java
│ │ │ └── widget
│ │ │ │ ├── CommentView.java
│ │ │ │ ├── DividerItemDecoration.java
│ │ │ │ ├── DragSortListView.java
│ │ │ │ ├── ExArrayAdapter.java
│ │ │ │ ├── ExSwipeRefreshLayout.java
│ │ │ │ ├── HtmlMovementMethod.java
│ │ │ │ └── SearchListView.java
│ │ │ └── util
│ │ │ ├── CrashlyticsUtils.java
│ │ │ ├── ExecutorUtils.java
│ │ │ ├── LogUtils.java
│ │ │ ├── TrackerUtils.kt
│ │ │ └── UserUtils.java
│ ├── kotlin
│ │ └── com
│ │ │ └── czbix
│ │ │ └── v2ex
│ │ │ ├── AppCtx.kt
│ │ │ ├── DebugHelpers.kt
│ │ │ ├── ViewerProvider.kt
│ │ │ ├── common
│ │ │ ├── NotificationStatus.kt
│ │ │ ├── UpdateInfo.kt
│ │ │ ├── UserState.kt
│ │ │ └── exception
│ │ │ │ ├── ConnectionException.kt
│ │ │ │ ├── RestrictedException.kt
│ │ │ │ ├── TwoFactorAuthException.kt
│ │ │ │ └── UrlRedirectException.kt
│ │ │ ├── db
│ │ │ ├── Comment.kt
│ │ │ ├── CommentAndMember.kt
│ │ │ ├── CommentDao.kt
│ │ │ ├── Member.kt
│ │ │ ├── TopicRecord.kt
│ │ │ ├── TopicRecordDao.kt
│ │ │ └── V2Db.kt
│ │ │ ├── event
│ │ │ ├── AppUpdateEvent.kt
│ │ │ ├── BaseEvent.kt
│ │ │ └── DeviceRegisterEvent.java
│ │ │ ├── helper
│ │ │ ├── JsoupObjects.kt
│ │ │ └── RxBus.kt
│ │ │ ├── inject
│ │ │ ├── ActivityModule.kt
│ │ │ ├── AppComponent.kt
│ │ │ ├── AppInjector.kt
│ │ │ ├── AppModule.kt
│ │ │ ├── DbModule.kt
│ │ │ ├── Injectable.kt
│ │ │ ├── MainFragmentModule.kt
│ │ │ ├── NightModeModule.kt
│ │ │ ├── Scoped.kt
│ │ │ ├── TopicFragmentModule.kt
│ │ │ ├── ViewModelKey.kt
│ │ │ └── ViewModelModule.kt
│ │ │ ├── model
│ │ │ ├── AbsentLiveData.kt
│ │ │ ├── Avatar.kt
│ │ │ ├── ContentBlock.kt
│ │ │ ├── EmptyLiveData.kt
│ │ │ ├── NetworkBoundResource.kt
│ │ │ ├── Node.kt
│ │ │ ├── Postscript.kt
│ │ │ ├── PreferenceStorage.kt
│ │ │ ├── Resource.kt
│ │ │ ├── ServerConfig.kt
│ │ │ ├── Topic.kt
│ │ │ ├── TopicResponse.kt
│ │ │ ├── ViewModelFactory.kt
│ │ │ └── loader
│ │ │ │ └── GooglePhotoUrlLoader.kt
│ │ │ ├── network
│ │ │ ├── CzRequestHelper.kt
│ │ │ ├── GlideConfig.kt
│ │ │ ├── NetworkState.kt
│ │ │ ├── RequestHelper.kt
│ │ │ ├── V2exService.kt
│ │ │ └── repository
│ │ │ │ ├── BaseRepository.kt
│ │ │ │ └── TopicRepository.kt
│ │ │ ├── parser
│ │ │ ├── MyselfParser.kt
│ │ │ ├── NotificationParser.kt
│ │ │ ├── Parser.kt
│ │ │ ├── TopicListParser.kt
│ │ │ └── TopicParser.kt
│ │ │ ├── presenter
│ │ │ └── TopicSearchPresenter.kt
│ │ │ ├── res
│ │ │ └── GoogleImg.kt
│ │ │ ├── ui
│ │ │ ├── AutoClearedValue.kt
│ │ │ ├── BaseActivity.kt
│ │ │ ├── BindingAdapters.kt
│ │ │ ├── DebugActivity.kt
│ │ │ ├── ExHolder.kt
│ │ │ ├── LoginActivity.kt
│ │ │ ├── MainActivity.kt
│ │ │ ├── SettingsActivity.kt
│ │ │ ├── TopicActivity.kt
│ │ │ ├── adapter
│ │ │ │ ├── CommentController.kt
│ │ │ │ ├── NodeController.kt
│ │ │ │ └── TopicController.kt
│ │ │ ├── common
│ │ │ │ └── RetryCallback.kt
│ │ │ ├── fragment
│ │ │ │ ├── TopicFragment.kt
│ │ │ │ ├── TopicListFragment.kt
│ │ │ │ └── TwoFactorAuthDialog.kt
│ │ │ ├── loader
│ │ │ │ └── TopicListLoader.kt
│ │ │ ├── model
│ │ │ │ ├── NightModeViewModel.kt
│ │ │ │ └── TopicViewModel.kt
│ │ │ ├── settings
│ │ │ │ ├── PrefsFragment.kt
│ │ │ │ ├── SettingsModule.kt
│ │ │ │ └── SettingsViewModel.kt
│ │ │ └── widget
│ │ │ │ ├── AvatarView.kt
│ │ │ │ ├── SearchBoxLayout.kt
│ │ │ │ └── TopicView.kt
│ │ │ └── util
│ │ │ ├── CoroutineUtils.kt
│ │ │ ├── CrashlyticsTree.kt
│ │ │ ├── ExceptionUtils.kt
│ │ │ ├── GsonUtils.kt
│ │ │ ├── IoUtils.kt
│ │ │ ├── LiveDataUtils.kt
│ │ │ ├── MiscUtils.kt
│ │ │ ├── RxUtils.kt
│ │ │ └── ViewUtils.kt
│ └── res
│ │ ├── color
│ │ └── btn_reply_submit.xml
│ │ ├── drawable-anydpi
│ │ ├── ic_arrow_back_black_24dp.xml
│ │ ├── ic_close_black_24dp.xml
│ │ ├── ic_dashboard_black_24dp.xml
│ │ ├── ic_explore_black_24dp.xml
│ │ ├── ic_favorite_black_24dp.xml
│ │ ├── ic_feedback_black_24dp.xml
│ │ ├── ic_info_outline_black_24dp.xml
│ │ ├── ic_notifications_black_24dp.xml
│ │ ├── ic_notifications_none_black_24dp.xml
│ │ ├── ic_send_black_24dp.xml
│ │ └── ic_settings_black_24dp.xml
│ │ ├── drawable-hdpi-v23
│ │ └── window_background_statusbar_toolbar_tab.9.png
│ │ ├── drawable-hdpi
│ │ ├── ic_card_giftcard_white_24dp.png
│ │ ├── ic_favorite_border_white_24dp.png
│ │ ├── ic_favorite_white_24dp.png
│ │ ├── ic_keyboard_arrow_down_white_24dp.png
│ │ ├── ic_notifications_white_24dp.png
│ │ ├── ic_refresh_white_24dp.png
│ │ ├── ic_search_white_24dp.png
│ │ ├── ic_sync_disabled_white_24dp.png
│ │ ├── ic_sync_problem_white_24dp.png
│ │ ├── ic_sync_white_24dp.png
│ │ └── window_background_statusbar_toolbar_tab.9.png
│ │ ├── drawable-mdpi-v23
│ │ └── window_background_statusbar_toolbar_tab.9.png
│ │ ├── drawable-mdpi
│ │ ├── avatar_default.png
│ │ ├── ic_card_giftcard_white_24dp.png
│ │ ├── ic_favorite_border_white_24dp.png
│ │ ├── ic_favorite_white_24dp.png
│ │ ├── ic_keyboard_arrow_down_white_24dp.png
│ │ ├── ic_notifications_white_24dp.png
│ │ ├── ic_refresh_white_24dp.png
│ │ ├── ic_search_white_24dp.png
│ │ ├── ic_sync_disabled_white_24dp.png
│ │ ├── ic_sync_problem_white_24dp.png
│ │ ├── ic_sync_white_24dp.png
│ │ └── window_background_statusbar_toolbar_tab.9.png
│ │ ├── drawable-sw600dp-hdpi-v23
│ │ └── window_background_statusbar_toolbar_tab.9.png
│ │ ├── drawable-sw600dp-hdpi
│ │ └── window_background_statusbar_toolbar_tab.9.png
│ │ ├── drawable-sw600dp-mdpi-v23
│ │ └── window_background_statusbar_toolbar_tab.9.png
│ │ ├── drawable-sw600dp-mdpi
│ │ └── window_background_statusbar_toolbar_tab.9.png
│ │ ├── drawable-sw600dp-xhdpi-v23
│ │ └── window_background_statusbar_toolbar_tab.9.png
│ │ ├── drawable-sw600dp-xhdpi
│ │ └── window_background_statusbar_toolbar_tab.9.png
│ │ ├── drawable-sw600dp-xxhdpi-v23
│ │ └── window_background_statusbar_toolbar_tab.9.png
│ │ ├── drawable-sw600dp-xxhdpi
│ │ └── window_background_statusbar_toolbar_tab.9.png
│ │ ├── drawable-sw600dp-xxxhdpi-v23
│ │ └── window_background_statusbar_toolbar_tab.9.png
│ │ ├── drawable-sw600dp-xxxhdpi
│ │ └── window_background_statusbar_toolbar_tab.9.png
│ │ ├── drawable-xhdpi-v23
│ │ └── window_background_statusbar_toolbar_tab.9.png
│ │ ├── drawable-xhdpi
│ │ ├── ic_card_giftcard_white_24dp.png
│ │ ├── ic_favorite_border_white_24dp.png
│ │ ├── ic_favorite_white_24dp.png
│ │ ├── ic_keyboard_arrow_down_white_24dp.png
│ │ ├── ic_notifications_white_24dp.png
│ │ ├── ic_refresh_white_24dp.png
│ │ ├── ic_search_white_24dp.png
│ │ ├── ic_sync_disabled_white_24dp.png
│ │ ├── ic_sync_problem_white_24dp.png
│ │ ├── ic_sync_white_24dp.png
│ │ └── window_background_statusbar_toolbar_tab.9.png
│ │ ├── drawable-xxhdpi-v23
│ │ └── window_background_statusbar_toolbar_tab.9.png
│ │ ├── drawable-xxhdpi
│ │ ├── ic_card_giftcard_white_24dp.png
│ │ ├── ic_favorite_border_white_24dp.png
│ │ ├── ic_favorite_white_24dp.png
│ │ ├── ic_keyboard_arrow_down_white_24dp.png
│ │ ├── ic_notifications_white_24dp.png
│ │ ├── ic_refresh_white_24dp.png
│ │ ├── ic_search_white_24dp.png
│ │ ├── ic_sync_disabled_white_24dp.png
│ │ ├── ic_sync_problem_white_24dp.png
│ │ ├── ic_sync_white_24dp.png
│ │ └── window_background_statusbar_toolbar_tab.9.png
│ │ ├── drawable-xxxhdpi-v23
│ │ └── window_background_statusbar_toolbar_tab.9.png
│ │ ├── drawable-xxxhdpi
│ │ ├── ic_card_giftcard_white_24dp.png
│ │ ├── ic_favorite_border_white_24dp.png
│ │ ├── ic_favorite_white_24dp.png
│ │ ├── ic_keyboard_arrow_down_white_24dp.png
│ │ ├── ic_notifications_white_24dp.png
│ │ ├── ic_refresh_white_24dp.png
│ │ ├── ic_search_white_24dp.png
│ │ ├── ic_sync_disabled_white_24dp.png
│ │ ├── ic_sync_problem_white_24dp.png
│ │ ├── ic_sync_white_24dp.png
│ │ └── window_background_statusbar_toolbar_tab.9.png
│ │ ├── drawable
│ │ ├── bg_author_flag.xml
│ │ ├── btn_jump_back_background.xml
│ │ ├── count_background.xml
│ │ ├── ic_account_circle.xml
│ │ ├── ic_brightness_medium_outline.xml
│ │ ├── ic_cancel_outline_black_24dp.xml
│ │ ├── ic_comment_black_24dp.xml
│ │ ├── ic_comment_outline.xml
│ │ ├── ic_content_copy_outline_black_24dp.xml
│ │ ├── ic_get_app_black_24dp.xml
│ │ ├── ic_image_outline.xml
│ │ ├── ic_info_outline_black_24dp.xml
│ │ ├── ic_launcher_foreground.xml
│ │ ├── ic_person_add_outline.xml
│ │ ├── ic_share_black_24dp.xml
│ │ ├── ic_star_black_24dp.xml
│ │ ├── ic_star_border_black_24dp.xml
│ │ ├── ic_undo_black_24dp.xml
│ │ ├── ic_update_black_24dp.xml
│ │ ├── img_topic_image_loading.xml
│ │ └── radius_box.xml
│ │ ├── layout
│ │ ├── activity_loading.xml
│ │ ├── activity_login.xml
│ │ ├── activity_main.xml
│ │ ├── activity_topic.xml
│ │ ├── activity_topic_edit.xml
│ │ ├── app_bar_layout.xml
│ │ ├── container_fragment.xml
│ │ ├── fragment_node_list.xml
│ │ ├── fragment_notification_list.xml
│ │ ├── fragment_topic.xml
│ │ ├── fragment_topic_list.xml
│ │ ├── layout_comment.xml
│ │ ├── layout_topic.xml
│ │ ├── model_comment_placeholder.xml
│ │ ├── model_comments_footer.xml
│ │ ├── model_postscript.xml
│ │ ├── nav_header.xml
│ │ ├── nav_view.xml
│ │ ├── recycle_view.xml
│ │ ├── tab_layout.xml
│ │ ├── toolbar_action.xml
│ │ ├── view_alert_dialog.xml
│ │ ├── view_comment.xml
│ │ ├── view_content_image.xml
│ │ ├── view_content_text.xml
│ │ ├── view_edittext.xml
│ │ ├── view_float_topic.xml
│ │ ├── view_node.xml
│ │ ├── view_notification.xml
│ │ ├── view_reply_form.xml
│ │ ├── view_search_box.xml
│ │ ├── view_select_node.xml
│ │ └── view_topic.xml
│ │ ├── menu
│ │ ├── menu_comment.xml
│ │ ├── menu_drawer.xml
│ │ ├── menu_main.xml
│ │ ├── menu_nodes.xml
│ │ ├── menu_topic.xml
│ │ ├── menu_topic_edit.xml
│ │ └── menu_topic_list.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ │ ├── mipmap-hdpi
│ │ └── ic_launcher.png
│ │ ├── mipmap-mdpi
│ │ └── ic_launcher.png
│ │ ├── mipmap-xhdpi
│ │ └── ic_launcher.png
│ │ ├── mipmap-xxhdpi
│ │ └── ic_launcher.png
│ │ ├── mipmap-xxxhdpi
│ │ └── ic_launcher.png
│ │ ├── raw
│ │ └── all_nodes.json
│ │ ├── values-night
│ │ └── colors.xml
│ │ ├── values-w820dp
│ │ └── dimens.xml
│ │ ├── values-zh
│ │ ├── arrays.xml
│ │ └── strings.xml
│ │ ├── values
│ │ ├── arrays.xml
│ │ ├── colors.xml
│ │ ├── config.xml
│ │ ├── dimens.xml
│ │ ├── ic_launcher_background.xml
│ │ ├── ids.xml
│ │ ├── intent_filter.xml
│ │ ├── strings.xml
│ │ ├── strings_activity_login.xml
│ │ ├── strings_activity_settings.xml
│ │ └── styles.xml
│ │ └── xml
│ │ └── pref_general.xml
│ └── test
│ └── kotlin
│ └── com
│ └── czbix
│ └── v2ex
│ └── util
│ └── MiscUtilsTest.kt
├── build.gradle
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── settings.gradle
/.gitignore:
--------------------------------------------------------------------------------
1 | .gradle
2 | /local.properties
3 | /.idea/workspace.xml
4 | /.idea/libraries
5 | .DS_Store
6 | /build
7 | /captures
8 | /.navigation/
9 |
10 | fabric.properties
11 | google-services.json
12 |
13 | # IDEA/Android Studio project files, because
14 | # the project can be imported from settings.gradle
15 | .idea
16 | *.iml
17 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # V2EX+
2 | V2EX client for Android
3 |
4 | ## Get it
5 | [](https://play.google.com/store/apps/details?id=com.czbix.v2ex&referrer=utm_source%3Dgithub)
6 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/app/src/androidTest/kotlin/com/czbix/v2ex/ui/util/HtmlTest.kt:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.ui.util
2 |
3 | import android.text.style.URLSpan
4 | import androidx.core.text.getSpans
5 | import io.kotlintest.inspectors.forAll
6 | import io.kotlintest.matchers.types.shouldNotBeSameInstanceAs
7 | import io.kotlintest.shouldBe
8 | import org.junit.Test
9 |
10 | class HtmlTest {
11 | @Test
12 | fun fromHtml_test1() {
13 | val html = """
14 | start
end
15 | """.trimIndent()
16 |
17 | val spanned = Html.fromHtml(html)
18 | val spans = spanned.getSpans()
19 |
20 | spans.forAll {
21 | it shouldNotBeSameInstanceAs URLSpan::class
22 | }
23 | }
24 |
25 | @Test
26 | fun fromHtml_test2() {
27 | val html = """
28 | starthttps://i.loli.net/2019/08/16/NuzGcJbt4LTVSYB.jpgend
29 | """.trimIndent()
30 |
31 | val spanned = Html.fromHtml(html)
32 |
33 | spanned.length shouldBe 9
34 | }
35 |
36 | @Test
37 | fun fromHtml_withCfEncoded() {
38 | val html = """
39 | 支持 [email protected],[email protected] 超级慢动作视频
40 | """.trimIndent()
41 |
42 | val spanned = Html.fromHtml(html)
43 |
44 | spanned.toString() shouldBe "支持 720p@7680fps,1080p@960fps 超级慢动作视频"
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/app/src/debug/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
9 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/app/src/debug/kotlin/com/czbix/v2ex/DebugAppCtx.kt:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex
2 |
3 | import com.facebook.stetho.Stetho
4 |
5 | @Suppress("unused")
6 | class DebugAppCtx : AppCtx() {
7 | override fun init() {
8 | Stetho.initializeWithDefaults(this)
9 |
10 | super.init()
11 | }
12 |
13 | override val debugHelpers = DebugHelpersImpl()
14 | }
--------------------------------------------------------------------------------
/app/src/debug/kotlin/com/czbix/v2ex/DebugHelpersImpl.kt:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex
2 |
3 | import com.facebook.stetho.okhttp3.StethoInterceptor
4 | import okhttp3.OkHttpClient
5 |
6 | class DebugHelpersImpl : DebugHelpers() {
7 | override fun addStethoInterceptor(builder: OkHttpClient.Builder): OkHttpClient.Builder {
8 | return builder.addNetworkInterceptor(StethoInterceptor())
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/app/src/debug/res/values/config.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | AEdPqrEAAAAIDaR8KoBcaS0tYIoivJfM0RmqKr_lg1j-cAUtOg
4 |
5 |
--------------------------------------------------------------------------------
/app/src/debug/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | V2EXD
4 |
--------------------------------------------------------------------------------
/app/src/main/ic_launcher-web.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/ic_launcher-web.png
--------------------------------------------------------------------------------
/app/src/main/java/com/czbix/v2ex/BackupAgent.java:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex;
2 |
3 | import android.app.backup.BackupAgentHelper;
4 | import android.app.backup.BackupHelper;
5 | import android.app.backup.SharedPreferencesBackupHelper;
6 |
7 | public class BackupAgent extends BackupAgentHelper {
8 | private static final String PREFS_BACKUP_KEY = "prefs";
9 |
10 | private static final String PREFS_PREF = BuildConfig.APPLICATION_ID + "_preferences";
11 | public void onCreate() {
12 | super.onCreate();
13 |
14 | BackupHelper helper = new SharedPreferencesBackupHelper(this, PREFS_PREF);
15 | addHelper(PREFS_BACKUP_KEY, helper);
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/app/src/main/java/com/czbix/v2ex/common/DeviceStatus.java:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.common;
2 |
3 | import android.content.BroadcastReceiver;
4 | import android.content.Context;
5 | import android.content.Intent;
6 | import android.content.IntentFilter;
7 | import android.net.ConnectivityManager;
8 | import android.net.NetworkInfo;
9 | import androidx.core.net.ConnectivityManagerCompat;
10 |
11 | import com.czbix.v2ex.AppCtx;
12 |
13 | public class DeviceStatus {
14 | private static final DeviceStatus instance;
15 |
16 | private final ConnectivityManager mConnectivityManager;
17 |
18 | static {
19 | instance = new DeviceStatus(AppCtx.getInstance());
20 | }
21 |
22 | public static DeviceStatus getInstance() {
23 | return instance;
24 | }
25 |
26 | private boolean mIsNetworkMetered;
27 | private boolean mIsNetworkConnected;
28 |
29 | DeviceStatus(Context context) {
30 | mConnectivityManager = ((ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE));
31 | context.registerReceiver(new BroadcastReceiver() {
32 | @Override
33 | public void onReceive(Context context, Intent intent) {
34 | updateNetworkStatus(intent);
35 | }
36 | }, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
37 |
38 | updateNetworkStatus(null);
39 | }
40 |
41 | private void updateNetworkStatus(Intent intent) {
42 | mIsNetworkMetered = ConnectivityManagerCompat.isActiveNetworkMetered(mConnectivityManager);
43 |
44 | if (intent == null) {
45 | final NetworkInfo activeNetworkInfo = mConnectivityManager.getActiveNetworkInfo();
46 | if (activeNetworkInfo == null) {
47 | mIsNetworkConnected = false;
48 | } else {
49 | mIsNetworkConnected = activeNetworkInfo.isConnected();
50 | }
51 | } else {
52 | mIsNetworkConnected = !intent.getBooleanExtra(ConnectivityManager.EXTRA_NO_CONNECTIVITY, false);
53 | }
54 | }
55 |
56 | public boolean isNetworkMetered() {
57 | return mIsNetworkMetered;
58 | }
59 |
60 | public boolean isNetworkConnected() {
61 | return mIsNetworkConnected;
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/app/src/main/java/com/czbix/v2ex/common/exception/ExIllegalStateException.java:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.common.exception;
2 |
3 | public class ExIllegalStateException extends java.lang.IllegalStateException {
4 | public boolean shouldLogged;
5 |
6 | public ExIllegalStateException() {
7 | super();
8 | }
9 |
10 | public ExIllegalStateException(Throwable cause) {
11 | super(cause);
12 | }
13 |
14 | public ExIllegalStateException(String detailMessage) {
15 | super(detailMessage);
16 | }
17 |
18 | public ExIllegalStateException(String message, Throwable cause) {
19 | super(message, cause);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/app/src/main/java/com/czbix/v2ex/common/exception/FatalException.java:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.common.exception;
2 |
3 | public class FatalException extends RuntimeException {
4 | public FatalException(String detailMessage) {
5 | super(detailMessage);
6 | }
7 |
8 | public FatalException(String detailMessage, Throwable throwable) {
9 | super(detailMessage, throwable);
10 | }
11 |
12 | public FatalException(Throwable throwable) {
13 | super(throwable);
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/app/src/main/java/com/czbix/v2ex/common/exception/NotImplementedException.java:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.common.exception;
2 |
3 | public class NotImplementedException extends FatalException {
4 | public NotImplementedException() {
5 | super("This method not implemented yet!");
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/app/src/main/java/com/czbix/v2ex/common/exception/RemoteException.java:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.common.exception;
2 |
3 |
4 | import okhttp3.Response;
5 |
6 | public class RemoteException extends Exception {
7 | private int mCode;
8 |
9 | public RemoteException(Response response) {
10 | this(response, null);
11 | }
12 |
13 | public RemoteException(Response response, Throwable tr) {
14 | super("remote failed with code: " + response.code(), tr);
15 | mCode = response.code();
16 | }
17 |
18 | public int getCode() {
19 | return mCode;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/app/src/main/java/com/czbix/v2ex/common/exception/RequestException.java:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.common.exception;
2 |
3 | import com.google.common.base.Strings;
4 | import com.google.common.net.HttpHeaders;
5 |
6 | import okhttp3.Response;
7 |
8 | public class RequestException extends RuntimeException {
9 | private final Response mResponse;
10 | private String errorHtml;
11 | private boolean mShouldLogged;
12 |
13 | public RequestException(Response response) {
14 | this(null, response);
15 | }
16 |
17 | public RequestException(Response response, Throwable tr) {
18 | this(null, response, tr);
19 | }
20 |
21 | public RequestException(String message, Response response) {
22 | this(message, response, null);
23 | }
24 |
25 | public RequestException(String message, Response response, Throwable tr) {
26 | super(message, tr);
27 |
28 | mShouldLogged = true;
29 | mResponse = response;
30 | }
31 |
32 | /**
33 | * error info in html
34 | */
35 | public String getErrorHtml() {
36 | return errorHtml;
37 | }
38 |
39 | public void setErrorHtml(String errorHtml) {
40 | this.errorHtml = errorHtml;
41 | }
42 |
43 | public Response getResponse() {
44 | return mResponse;
45 | }
46 |
47 | public int getCode() {
48 | return mResponse.code();
49 | }
50 |
51 | public boolean isShouldLogged() {
52 | return mShouldLogged;
53 | }
54 |
55 | public void setShouldLogged(boolean shouldLogged) {
56 | mShouldLogged = shouldLogged;
57 | }
58 |
59 | @Override
60 | public String getMessage() {
61 | final String message = Strings.nullToEmpty(super.getMessage());
62 | final StringBuilder sb = new StringBuilder(message);
63 |
64 | sb.append(", url: ").append(mResponse.request().url());
65 | sb.append(", code: ").append(mResponse.code());
66 |
67 | if (mResponse.isRedirect()) {
68 | sb.append(", location: ");
69 | sb.append(mResponse.header(HttpHeaders.LOCATION));
70 | }
71 |
72 | return sb.toString();
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/app/src/main/java/com/czbix/v2ex/common/exception/UnauthorizedException.java:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.common.exception;
2 |
3 |
4 | import okhttp3.Response;
5 |
6 | public class UnauthorizedException extends RequestException {
7 | public UnauthorizedException(Response response) {
8 | super(response);
9 | }
10 |
11 | public UnauthorizedException(Response response, Throwable tr) {
12 | super(response, tr);
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/app/src/main/java/com/czbix/v2ex/dao/DaoBase.java:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.dao;
2 |
3 | import android.database.sqlite.SQLiteDatabase;
4 |
5 | public abstract class DaoBase {
6 | protected static T execute(SqlOperation operation) {
7 | return execute(operation, false);
8 | }
9 |
10 | protected static synchronized T execute(SqlOperation operation, boolean isWrite) {
11 | SQLiteDatabase db = null;
12 | try {
13 | final V2exDb instance = V2exDb.getInstance();
14 | db = isWrite ? instance.getWritableDatabase() : instance.getReadableDatabase();
15 | return operation.execute(db);
16 | } finally {
17 | if (db != null) {
18 | db.close();
19 | }
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/app/src/main/java/com/czbix/v2ex/dao/SqlOperation.java:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.dao;
2 |
3 | import android.database.sqlite.SQLiteDatabase;
4 |
5 | public abstract class SqlOperation {
6 | public abstract T execute(SQLiteDatabase db);
7 | }
8 |
--------------------------------------------------------------------------------
/app/src/main/java/com/czbix/v2ex/dao/V2exDb.java:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.dao;
2 |
3 | import android.content.Context;
4 | import android.database.sqlite.SQLiteDatabase;
5 | import android.database.sqlite.SQLiteOpenHelper;
6 |
7 | import com.czbix.v2ex.AppCtx;
8 | import com.czbix.v2ex.util.LogUtils;
9 | import com.google.common.base.Preconditions;
10 |
11 | public class V2exDb extends SQLiteOpenHelper {
12 | private static final String TAG = V2exDb.class.getSimpleName();
13 | private static final String DB_NAME = "v2ex.db";
14 | private static final int CURRENT_VERSION = 4;
15 |
16 | private static final V2exDb INSTANCE;
17 |
18 | static {
19 | INSTANCE = new V2exDb(AppCtx.getInstance());
20 | }
21 |
22 | public static V2exDb getInstance() {
23 | return INSTANCE;
24 | }
25 |
26 | public V2exDb(Context context) {
27 | super(context, DB_NAME, null, CURRENT_VERSION);
28 | }
29 |
30 | @Override
31 | public void onCreate(SQLiteDatabase db) {
32 | ConfigDao.createTable(db);
33 | NodeDao.createTable(db);
34 | }
35 |
36 | @Override
37 | public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
38 | if (oldVersion == 1) {
39 | LogUtils.i(TAG, "upgrade database from %d to %d", oldVersion, newVersion);
40 | oldVersion = 2;
41 | }
42 | if (oldVersion == 2) {
43 | LogUtils.i(TAG, "upgrade database from %d to %d", oldVersion, newVersion);
44 | oldVersion = 3;
45 | }
46 | if (oldVersion == 3) {
47 | LogUtils.i(TAG, "upgrade database from %d to %d", oldVersion, newVersion);
48 | db.execSQL("DROP TABLE topic;");
49 | db.execSQL("DROP TABLE draft;");
50 | oldVersion = 4;
51 | }
52 |
53 | Preconditions.checkState(oldVersion == newVersion, "old version not match new version");
54 | }
55 |
56 | /**
57 | * it may use a lot of time
58 | */
59 | public void init() {
60 | getWritableDatabase();
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/app/src/main/java/com/czbix/v2ex/eventbus/LoginEvent.java:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.eventbus;
2 |
3 | import com.czbix.v2ex.event.BaseEvent;
4 |
5 | public class LoginEvent extends BaseEvent {
6 | public final String mUsername;
7 |
8 | public LoginEvent() {
9 | mUsername = null;
10 | }
11 |
12 | public LoginEvent(String username) {
13 | mUsername = username;
14 | }
15 |
16 | public boolean isLogOut() {
17 | return mUsername == null;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/app/src/main/java/com/czbix/v2ex/eventbus/executor/HandlerExecutor.java:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.eventbus.executor;
2 |
3 | import androidx.annotation.NonNull;
4 |
5 | import com.czbix.v2ex.util.ExecutorUtils;
6 |
7 | import java.util.concurrent.Executor;
8 |
9 | public class HandlerExecutor implements Executor {
10 | @Override
11 | public void execute(@NonNull Runnable command) {
12 | ExecutorUtils.runInUiThread(command);
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/app/src/main/java/com/czbix/v2ex/helper/MultiList.java:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.helper;
2 |
3 | import com.google.common.base.Preconditions;
4 | import com.google.common.collect.Lists;
5 |
6 | import java.util.AbstractList;
7 | import java.util.Collections;
8 | import java.util.List;
9 |
10 | public class MultiList extends AbstractList {
11 | private final List> mList;
12 |
13 | public MultiList() {
14 | mList = Lists.newArrayList();
15 | }
16 |
17 | @SafeVarargs
18 | public MultiList(List... data) {
19 | mList = Lists.newArrayListWithCapacity(data.length);
20 | Collections.addAll(mList, data);
21 | }
22 |
23 | public void addList(List data) {
24 | mList.add(data);
25 | }
26 |
27 | public void setList(int index, List data) {
28 | Preconditions.checkElementIndex(index, mList.size());
29 |
30 | mList.set(index, data);
31 | }
32 |
33 | public int listSize() {
34 | return mList.size();
35 | }
36 |
37 | @Override
38 | public void clear() {
39 | mList.clear();
40 | }
41 |
42 | @Override
43 | public E get(int location) {
44 | int index = location;
45 |
46 | for (List list : mList) {
47 | if (list.size() <= location) {
48 | location -= list.size();
49 | continue;
50 | }
51 |
52 | return list.get(location);
53 | }
54 |
55 | throw new IndexOutOfBoundsException("Invalid index " + index + ", size is " + size());
56 | }
57 |
58 | @Override
59 | public int size() {
60 | int size = 0;
61 | for (List list : mList) {
62 | size += list.size();
63 | }
64 |
65 | return size;
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/app/src/main/java/com/czbix/v2ex/model/Favable.java:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.model;
2 |
3 | public interface Favable {
4 | String getFavUrl(String token);
5 | String getUnFavUrl(String token);
6 | }
7 |
--------------------------------------------------------------------------------
/app/src/main/java/com/czbix/v2ex/model/Ignorable.java:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.model;
2 |
3 | public interface Ignorable {
4 | String getIgnoreUrl();
5 | }
6 |
--------------------------------------------------------------------------------
/app/src/main/java/com/czbix/v2ex/model/LoginResult.java:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.model;
2 |
3 | public class LoginResult {
4 | public final Avatar mAvatar;
5 | public final String mUsername;
6 |
7 | public LoginResult(String username, Avatar avatar) {
8 | mAvatar = avatar;
9 | mUsername = username;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/app/src/main/java/com/czbix/v2ex/model/Notification.java:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.model;
2 |
3 | import androidx.annotation.IntDef;
4 |
5 | import com.czbix.v2ex.db.Member;
6 | import com.google.common.base.Objects;
7 |
8 | import java.lang.annotation.Retention;
9 | import java.lang.annotation.RetentionPolicy;
10 |
11 | public class Notification {
12 | @Retention(RetentionPolicy.SOURCE)
13 | @IntDef({TYPE_UNKNOWN, TYPE_THANK_TOPIC, TYPE_REPLY_TOPIC, TYPE_FAV_TOPIC, TYPE_THANK_COMMENT, TYPE_REPLY_COMMENT})
14 | public @interface NotificationType {}
15 |
16 | public static final int TYPE_UNKNOWN = 0;
17 | public static final int TYPE_THANK_TOPIC = 1;
18 | public static final int TYPE_REPLY_TOPIC = 2;
19 | public static final int TYPE_FAV_TOPIC = 3;
20 | public static final int TYPE_THANK_COMMENT = 4;
21 | public static final int TYPE_REPLY_COMMENT = 5;
22 |
23 | public final Member mMember;
24 | @NotificationType
25 | public final int mType;
26 | public final Topic mTopic;
27 | public final String mContent;
28 | public final String mTime;
29 |
30 | public Notification(Member member, Topic topic, @NotificationType int type, String content, String time) {
31 | mContent = content;
32 | mMember = member;
33 | mType = type;
34 | mTopic = topic;
35 | mTime = time;
36 | }
37 |
38 | @Override
39 | public boolean equals(Object o) {
40 | if (this == o) return true;
41 | if (!(o instanceof Notification)) return false;
42 | Notification that = (Notification) o;
43 | return Objects.equal(mType, that.mType) &&
44 | Objects.equal(mMember, that.mMember) &&
45 | Objects.equal(mTopic, that.mTopic) &&
46 | Objects.equal(mContent, that.mContent) &&
47 | Objects.equal(mTime, that.mTime);
48 | }
49 |
50 | @Override
51 | public int hashCode() {
52 | return Objects.hashCode(mMember, mType, mTopic, mContent, mTime);
53 | }
54 |
55 | public static class Builder {
56 | private Member mMember;
57 | private Topic mTopic;
58 | private int mType;
59 | private String mContent;
60 | private String mTime;
61 |
62 | public Builder setMember(Member member) {
63 | mMember = member;
64 | return this;
65 | }
66 |
67 | public Builder setTopic(Topic topic) {
68 | mTopic = topic;
69 | return this;
70 | }
71 |
72 | public Builder setType(@NotificationType int type) {
73 | mType = type;
74 | return this;
75 | }
76 |
77 | public Builder setContent(String content) {
78 | mContent = content;
79 | return this;
80 | }
81 |
82 | public Builder setTime(String time) {
83 | mTime = time;
84 | return this;
85 | }
86 |
87 | public Notification createNotification() {
88 | return new Notification(mMember, mTopic, mType, mContent, mTime);
89 | }
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/app/src/main/java/com/czbix/v2ex/model/Page.java:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.model;
2 |
3 | import android.os.Parcel;
4 | import android.os.Parcelable;
5 |
6 | import com.czbix.v2ex.AppCtx;
7 | import com.czbix.v2ex.R;
8 | import com.czbix.v2ex.network.RequestHelper;
9 |
10 | public abstract class Page implements Parcelable {
11 | public static final Page PAGE_FAV_TOPIC = new SimplePage(
12 | AppCtx.getInstance().getString(R.string.title_fragment_favorite), "/my/topics");
13 |
14 | public abstract String getTitle();
15 |
16 | public abstract String getUrl();
17 |
18 | @Override
19 | public int describeContents() {
20 | return 0;
21 | }
22 |
23 | @Override
24 | public String toString() {
25 | return getTitle();
26 | }
27 |
28 | @Override
29 | public boolean equals(Object o) {
30 | if (this == o) return true;
31 | if (!(o instanceof Page)) return false;
32 | Page page = (Page) o;
33 | return getUrl().equals(page.getUrl());
34 | }
35 |
36 | @Override
37 | public int hashCode() {
38 | return getUrl().hashCode();
39 | }
40 |
41 | public static class SimplePage extends Page {
42 | private final String mTitle;
43 | private final String mUrl;
44 |
45 | public SimplePage(String title, String url) {
46 | mTitle = title;
47 | mUrl = url;
48 | }
49 |
50 | @Override
51 | public String getTitle() {
52 | return mTitle;
53 | }
54 |
55 | @Override
56 | public String getUrl() {
57 | return RequestHelper.BASE_URL + mUrl;
58 | }
59 |
60 | @Override
61 | public void writeToParcel(Parcel dest, int flags) {
62 | dest.writeString(this.mTitle);
63 | dest.writeString(this.mUrl);
64 | }
65 |
66 | protected SimplePage(Parcel in) {
67 | this.mTitle = in.readString();
68 | this.mUrl = in.readString();
69 | }
70 |
71 | public static final Creator CREATOR = new Creator() {
72 | public SimplePage createFromParcel(Parcel source) {
73 | return new SimplePage(source);
74 | }
75 |
76 | public SimplePage[] newArray(int size) {
77 | return new SimplePage[size];
78 | }
79 | };
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/app/src/main/java/com/czbix/v2ex/model/Thankable.java:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.model;
2 |
3 | public interface Thankable {
4 | String getThankUrl();
5 | }
6 |
--------------------------------------------------------------------------------
/app/src/main/java/com/czbix/v2ex/model/db/Draft.java:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.model.db;
2 |
3 | import java.util.concurrent.TimeUnit;
4 |
5 | public class Draft {
6 | public final long mId;
7 | public final int mTopicId;
8 | public final String mContent;
9 | public final long mTime;
10 |
11 | public Draft(long id, int topicId, String content, long time) {
12 | mId = id;
13 | mTopicId = topicId;
14 | mContent = content;
15 | mTime = time;
16 | }
17 |
18 | public boolean isExpired() {
19 | return isExpired(mTime);
20 | }
21 |
22 | public static boolean isExpired(long draftTime) {
23 | return TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis() - draftTime) > 1;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/app/src/main/java/com/czbix/v2ex/model/db/TopicDraft.java:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.model.db;
2 |
3 | import com.czbix.v2ex.util.GsonUtilsKt;
4 |
5 | public class TopicDraft {
6 | public final String mNodeName;
7 | public final String mTitle;
8 | public final String mContent;
9 |
10 | public TopicDraft(String nodeName, String title, String content) {
11 | mNodeName = nodeName;
12 | mTitle = title;
13 | mContent = content;
14 | }
15 |
16 | public String toJson() {
17 | return GsonUtilsKt.getGSON().toJson(this);
18 | }
19 |
20 | public static TopicDraft fromJson(String str) {
21 | return GsonUtilsKt.getGSON().fromJson(str, TopicDraft.class);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/app/src/main/java/com/czbix/v2ex/model/db/ViewHistory.java:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.model.db;
2 |
3 | import com.czbix.v2ex.model.Topic;
4 |
5 | public class ViewHistory {
6 | public final Topic mTopic;
7 | /** in milliseconds */
8 | public final long mTime;
9 |
10 | public ViewHistory(Topic topic, long time) {
11 | mTime = time;
12 | mTopic = topic;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/app/src/main/java/com/czbix/v2ex/network/Etag.java:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.network;
2 |
3 | import com.google.common.base.Preconditions;
4 |
5 | public class Etag {
6 | public final String mOldEtag;
7 | private String mNewEtag;
8 |
9 | public Etag(String etag) {
10 | mOldEtag = etag;
11 | }
12 |
13 | /**
14 | * @return etag is modified
15 | */
16 | public boolean setNewEtag(String etag) {
17 | Preconditions.checkNotNull(etag);
18 |
19 | mNewEtag = etag;
20 |
21 | return isModified();
22 | }
23 |
24 | public boolean isModified() {
25 | return !mNewEtag.equals(mOldEtag);
26 | }
27 |
28 | public String getNewEtag() {
29 | return mNewEtag;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/app/src/main/java/com/czbix/v2ex/network/HttpStatus.java:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.network;
2 |
3 | @SuppressWarnings("unused")
4 | public interface HttpStatus {
5 | int SC_ACCEPTED = 202;
6 | int SC_BAD_GATEWAY = 502;
7 | int SC_BAD_REQUEST = 400;
8 | int SC_CONFLICT = 409;
9 | int SC_CONTINUE = 100;
10 | int SC_CREATED = 201;
11 | int SC_EXPECTATION_FAILED = 417;
12 | int SC_FAILED_DEPENDENCY = 424;
13 | int SC_FORBIDDEN = 403;
14 | int SC_GATEWAY_TIMEOUT = 504;
15 | int SC_GONE = 410;
16 | int SC_HTTP_VERSION_NOT_SUPPORTED = 505;
17 | int SC_INSUFFICIENT_SPACE_ON_RESOURCE = 419;
18 | int SC_INSUFFICIENT_STORAGE = 507;
19 | int SC_INTERNAL_SERVER_ERROR = 500;
20 | int SC_LENGTH_REQUIRED = 411;
21 | int SC_LOCKED = 423;
22 | int SC_METHOD_FAILURE = 420;
23 | int SC_METHOD_NOT_ALLOWED = 405;
24 | int SC_MOVED_PERMANENTLY = 301;
25 | int SC_MOVED_TEMPORARILY = 302;
26 | int SC_MULTIPLE_CHOICES = 300;
27 | int SC_MULTI_STATUS = 207;
28 | int SC_NON_AUTHORITATIVE_INFORMATION = 203;
29 | int SC_NOT_ACCEPTABLE = 406;
30 | int SC_NOT_FOUND = 404;
31 | int SC_NOT_IMPLEMENTED = 501;
32 | int SC_NOT_MODIFIED = 304;
33 | int SC_NO_CONTENT = 204;
34 | int SC_OK = 200;
35 | int SC_PARTIAL_CONTENT = 206;
36 | int SC_PAYMENT_REQUIRED = 402;
37 | int SC_PRECONDITION_FAILED = 412;
38 | int SC_PROCESSING = 102;
39 | int SC_PROXY_AUTHENTICATION_REQUIRED = 407;
40 | int SC_REQUESTED_RANGE_NOT_SATISFIABLE = 416;
41 | int SC_REQUEST_TIMEOUT = 408;
42 | int SC_REQUEST_TOO_LONG = 413;
43 | int SC_REQUEST_URI_TOO_LONG = 414;
44 | int SC_RESET_CONTENT = 205;
45 | int SC_SEE_OTHER = 303;
46 | int SC_SERVICE_UNAVAILABLE = 503;
47 | int SC_SWITCHING_PROTOCOLS = 101;
48 | int SC_TEMPORARY_REDIRECT = 307;
49 | int SC_UNAUTHORIZED = 401;
50 | int SC_UNPROCESSABLE_ENTITY = 422;
51 | int SC_UNSUPPORTED_MEDIA_TYPE = 415;
52 | int SC_USE_PROXY = 305;
53 | }
54 |
55 |
--------------------------------------------------------------------------------
/app/src/main/java/com/czbix/v2ex/ui/LoadingActivity.java:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.ui;
2 |
3 | import android.os.Bundle;
4 | import androidx.appcompat.app.AppCompatActivity;
5 | import android.widget.Toast;
6 |
7 | import com.czbix.v2ex.R;
8 |
9 | public class LoadingActivity extends AppCompatActivity {
10 | @Override
11 | protected void onCreate(Bundle savedInstanceState) {
12 | super.onCreate(savedInstanceState);
13 | setContentView(R.layout.activity_loading);
14 | }
15 |
16 | @Override
17 | public void onBackPressed() {
18 | Toast.makeText(this, R.string.toast_please_wait, Toast.LENGTH_SHORT).show();
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/app/src/main/java/com/czbix/v2ex/ui/adapter/StableArrayAdapter.java:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.ui.adapter;
2 |
3 | import android.content.Context;
4 | import android.widget.ArrayAdapter;
5 |
6 | import com.czbix.v2ex.util.MiscUtils;
7 | import com.google.common.collect.Maps;
8 |
9 | import java.util.HashMap;
10 | import java.util.List;
11 |
12 | public class StableArrayAdapter extends ArrayAdapter {
13 |
14 | final int INVALID_ID = -1;
15 |
16 | HashMap mIdMap = Maps.newHashMap();
17 |
18 | public StableArrayAdapter(Context context, int textViewResourceId, List objects) {
19 | super(context, textViewResourceId, objects);
20 | for (int i = 0; i < objects.size(); ++i) {
21 | mIdMap.put(objects.get(i), i);
22 | }
23 | }
24 |
25 | @Override
26 | public long getItemId(int position) {
27 | if (position < 0 || position >= mIdMap.size()) {
28 | return INVALID_ID;
29 | }
30 | T item = getItem(position);
31 | return mIdMap.get(item);
32 | }
33 |
34 | @Override
35 | public boolean hasStableIds() {
36 | // see http://stackoverflow.com/questions/26648991/
37 | return false;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/app/src/main/java/com/czbix/v2ex/ui/fragment/BaseTabFragment.java:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.ui.fragment;
2 |
3 | import android.os.Bundle;
4 | import com.google.android.material.tabs.TabLayout;
5 | import androidx.fragment.app.Fragment;
6 | import androidx.fragment.app.FragmentManager;
7 | import androidx.fragment.app.FragmentPagerAdapter;
8 | import androidx.core.view.ViewCompat;
9 | import androidx.viewpager.widget.ViewPager;
10 | import android.view.LayoutInflater;
11 | import android.view.View;
12 | import android.view.ViewGroup;
13 |
14 | import com.czbix.v2ex.R;
15 |
16 | public abstract class BaseTabFragment extends Fragment {
17 | protected TabLayout mTabLayout;
18 |
19 | public BaseTabFragment() {
20 | // Required empty public constructor
21 | }
22 |
23 | @Override
24 | public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
25 | final View view = inflater.inflate(R.layout.tab_layout, container, false);
26 | ViewPager viewPager = ((ViewPager) view.findViewById(R.id.view_pager));
27 | FragmentPagerAdapter adapter = getAdapter(getChildFragmentManager());
28 | viewPager.setAdapter(adapter);
29 | viewPager.setPageMargin(getResources().getDimensionPixelSize(R.dimen.activity_horizontal_margin));
30 |
31 | mTabLayout = (TabLayout) view.findViewById(R.id.tab_layout);
32 | ViewCompat.setElevation(mTabLayout, getResources().getDimension(R.dimen.appbar_elevation));
33 | mTabLayout.setupWithViewPager(viewPager);
34 |
35 | return view;
36 | }
37 |
38 | protected abstract FragmentPagerAdapter getAdapter(FragmentManager manager);
39 | }
40 |
--------------------------------------------------------------------------------
/app/src/main/java/com/czbix/v2ex/ui/fragment/FavoriteTabFragment.java:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.ui.fragment;
2 |
3 | import android.os.Bundle;
4 | import androidx.fragment.app.Fragment;
5 | import androidx.fragment.app.FragmentManager;
6 | import androidx.fragment.app.FragmentPagerAdapter;
7 |
8 | import com.czbix.v2ex.R;
9 | import com.czbix.v2ex.model.Page;
10 | import com.czbix.v2ex.ui.MainActivity;
11 |
12 | public class FavoriteTabFragment extends BaseTabFragment {
13 | public static FavoriteTabFragment newInstance() {
14 | return new FavoriteTabFragment();
15 | }
16 |
17 | @Override
18 | protected FragmentPagerAdapter getAdapter(FragmentManager manager) {
19 | return new FavoriteFragmentAdapter(manager);
20 | }
21 |
22 | @Override
23 | public void onActivityCreated(Bundle savedInstanceState) {
24 | super.onActivityCreated(savedInstanceState);
25 |
26 | final MainActivity activity = (MainActivity) getActivity();
27 | activity.setTitle(R.string.drawer_favorite);
28 | activity.setNavSelected(R.id.drawer_favorite);
29 | }
30 |
31 | private class FavoriteFragmentAdapter extends FragmentPagerAdapter {
32 | public FavoriteFragmentAdapter(FragmentManager manager) {
33 | super(manager);
34 | }
35 |
36 | public Fragment getItem(int position) {
37 | return position == 0 ? FavNodeFragment.newInstance()
38 | : TopicListFragment.Companion.newInstance(Page.PAGE_FAV_TOPIC);
39 | }
40 |
41 | @Override
42 | public int getCount() {
43 | return 2;
44 | }
45 |
46 | @Override
47 | public CharSequence getPageTitle(int position) {
48 | return getString(position == 0 ? R.string.title_fragment_nodes : R.string.title_fragment_topics);
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/app/src/main/java/com/czbix/v2ex/ui/helper/ForceTouchDetector.java:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.ui.helper;
2 |
3 | import androidx.annotation.NonNull;
4 | import android.view.MotionEvent;
5 |
6 | public class ForceTouchDetector {
7 | private boolean mInForceTouch;
8 | private final Runnable mOnStartListener;
9 | private final Runnable mOnStopListener;
10 |
11 | public ForceTouchDetector(@NonNull Runnable onStartListener,
12 | @NonNull Runnable onStopListener) {
13 | mOnStartListener = onStartListener;
14 | mOnStopListener = onStopListener;
15 | }
16 |
17 | public boolean handleEvent(MotionEvent e) {
18 | if (e.getActionIndex() != 0) {
19 | // handle first pointer only
20 | return false;
21 | }
22 |
23 | final int action = e.getActionMasked();
24 | final float pressure = e.getPressure();
25 |
26 | if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
27 | if (mInForceTouch) {
28 | onStop();
29 | e.setAction(MotionEvent.ACTION_CANCEL);
30 | return false;
31 | }
32 | }
33 |
34 | if (pressure < 0.8) {
35 | if (mInForceTouch) {
36 | if (pressure < 0.7) {
37 | onStop();
38 | e.setAction(MotionEvent.ACTION_CANCEL);
39 | return false;
40 | }
41 | } else {
42 | return false;
43 | }
44 | }
45 |
46 | if (action == MotionEvent.ACTION_MOVE) {
47 | onStart();
48 | return true;
49 | } else {
50 | if (mInForceTouch) {
51 | onStop();
52 | return true;
53 | } else {
54 | return false;
55 | }
56 | }
57 | }
58 |
59 | private void onStart() {
60 | if (mInForceTouch) {
61 | return;
62 | }
63 |
64 | mInForceTouch = true;
65 | mOnStartListener.run();
66 | }
67 |
68 | private void onStop() {
69 | if (!mInForceTouch) {
70 | return;
71 | }
72 |
73 | mInForceTouch = false;
74 | mOnStopListener.run();
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/app/src/main/java/com/czbix/v2ex/ui/loader/AsyncTaskLoader.java:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.ui.loader;
2 |
3 | import android.content.Context;
4 |
5 | import com.czbix.v2ex.util.LogUtils;
6 |
7 | import java.util.concurrent.TimeUnit;
8 |
9 | public abstract class AsyncTaskLoader extends androidx.loader.content.AsyncTaskLoader> {
10 | private static final String TAG = AsyncTaskLoader.class.getSimpleName();
11 |
12 | private static final long DEFAULT_UPDATE_THROTTLE = TimeUnit.SECONDS.toMillis(3);
13 | protected LoaderResult mResult;
14 |
15 | public AsyncTaskLoader(Context context) {
16 | super(context);
17 |
18 | setUpdateThrottle(DEFAULT_UPDATE_THROTTLE);
19 | }
20 |
21 | @Override
22 | protected void onStartLoading() {
23 | super.onStartLoading();
24 |
25 | if (mResult != null && isStarted()) {
26 | deliverResult(mResult);
27 | }
28 |
29 | if (takeContentChanged() || mResult == null) {
30 | forceLoad();
31 | }
32 | }
33 |
34 | @Override
35 | protected void onStopLoading() {
36 | super.onStopLoading();
37 |
38 | cancelLoad();
39 | }
40 |
41 | @Override
42 | protected void onReset() {
43 | super.onReset();
44 |
45 | stopLoading();
46 |
47 | mResult = null;
48 | }
49 |
50 | /**
51 | * please override {@link #loadInBackgroundWithException()}
52 | */
53 | @Override
54 | public LoaderResult loadInBackground() {
55 | LoaderResult loaderResult;
56 | try {
57 | T result = loadInBackgroundWithException();
58 | loaderResult = new LoaderResult<>(result);
59 | } catch (Exception e) {
60 | LogUtils.d(TAG, "async task loader has exception", e);
61 | loaderResult = new LoaderResult<>(e);
62 | }
63 |
64 | mResult = loaderResult;
65 | return mResult;
66 | }
67 |
68 | public abstract T loadInBackgroundWithException() throws Exception;
69 |
70 | /**
71 | * used to wrap data with exception
72 | * @param result type
73 | */
74 | public static class LoaderResult {
75 | public final Exception mException;
76 | public final T mResult;
77 |
78 | public LoaderResult(Exception exception) {
79 | mException = exception;
80 | mResult = null;
81 | }
82 |
83 | public LoaderResult(T result) {
84 | mResult = result;
85 | mException = null;
86 | }
87 |
88 | public boolean hasException() {
89 | return mException != null;
90 | }
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/app/src/main/java/com/czbix/v2ex/ui/loader/NotificationLoader.java:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.ui.loader;
2 |
3 | import android.content.Context;
4 |
5 | import com.czbix.v2ex.model.Notification;
6 | import com.czbix.v2ex.network.RequestHelper;
7 |
8 | import java.util.List;
9 |
10 | public class NotificationLoader extends AsyncTaskLoader> {
11 | public NotificationLoader(Context context) {
12 | super(context);
13 | }
14 |
15 | @Override
16 | public List loadInBackgroundWithException() throws Exception {
17 | return RequestHelper.INSTANCE.getNotifications();
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/app/src/main/java/com/czbix/v2ex/ui/model/EpoxyDataBindingConfig.java:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.ui.model;
2 |
3 | import com.airbnb.epoxy.EpoxyDataBindingPattern;
4 | import com.czbix.v2ex.R;
5 |
6 | @SuppressWarnings("unused")
7 | @EpoxyDataBindingPattern(rClass = R.class, layoutPrefix = "model_")
8 | interface EpoxyDataBindingConfig {}
9 |
--------------------------------------------------------------------------------
/app/src/main/java/com/czbix/v2ex/ui/preference/TabListPreference.java:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.ui.preference;
2 |
3 | /*
4 | public class TabListPreference extends DialogPreference {
5 | public List mTabsToShow;
6 |
7 | public TabListPreference(Context context, AttributeSet attrs) {
8 | super(context, attrs);
9 | }
10 |
11 |
12 |
13 |
14 |
15 | @Override
16 | protected void onSetInitialValue(boolean restorePersistedValue, Object defaultValue) {
17 | if (restorePersistedValue) {
18 | mTabsToShow = PrefStore.getInstance().getTabsToShow();
19 | } else {
20 | final String string = (String) defaultValue;
21 | mTabsToShow = Tab.getTabsToShow(string);
22 | }
23 | }
24 |
25 | @Override
26 | protected View onCreateDialogView() {
27 | return new DragSortListView(getContext());
28 | }
29 |
30 | @Override
31 | protected void onBindDialogView(@NonNull View view) {
32 | super.onBindDialogView(view);
33 | if (mTabsToShow == null) {
34 | onSetInitialValue(true, null);
35 | }
36 |
37 | final ArrayAdapter adapter = new StableArrayAdapter<>(getContext(),
38 | android.R.layout.simple_list_item_1, mTabsToShow);
39 | final DragSortListView listView = (DragSortListView) view;
40 | listView.setDataList(mTabsToShow);
41 | listView.setAdapter(adapter);
42 | }
43 |
44 | @Override
45 | protected void onDialogClosed(boolean positiveResult) {
46 | if (positiveResult) {
47 | persistString(Tab.getStringToSave(mTabsToShow));
48 | }
49 | mTabsToShow = null;
50 | }
51 |
52 | }
53 | */
54 |
--------------------------------------------------------------------------------
/app/src/main/java/com/czbix/v2ex/ui/widget/ExSwipeRefreshLayout.java:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.ui.widget;
2 |
3 | import android.content.Context;
4 | import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
5 | import android.util.AttributeSet;
6 |
7 | public class ExSwipeRefreshLayout extends SwipeRefreshLayout {
8 | private CanChildScrollUpCallback mCanChildScrollUpCallback;
9 | private boolean mPreMeasureRefreshing;
10 | private boolean mMeasured = false;
11 |
12 | public ExSwipeRefreshLayout(Context context) {
13 | this(context, null);
14 | }
15 |
16 | public ExSwipeRefreshLayout(Context context, AttributeSet attrs) {
17 | super(context, attrs);
18 | }
19 |
20 | public void setCanChildScrollUpCallback(CanChildScrollUpCallback canChildScrollUpCallback) {
21 | mCanChildScrollUpCallback = canChildScrollUpCallback;
22 | }
23 |
24 | public interface CanChildScrollUpCallback {
25 | boolean canSwipeRefreshChildScrollUp();
26 | }
27 |
28 | @Override
29 | public boolean canChildScrollUp() {
30 | if (mCanChildScrollUpCallback != null) {
31 | return mCanChildScrollUpCallback.canSwipeRefreshChildScrollUp();
32 | }
33 | return super.canChildScrollUp();
34 | }
35 |
36 | @Override
37 | public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
38 | super.onMeasure(widthMeasureSpec, heightMeasureSpec);
39 | if (!mMeasured) {
40 | mMeasured = true;
41 | setRefreshing(mPreMeasureRefreshing);
42 | }
43 | }
44 |
45 | @Override
46 | public void setRefreshing(boolean refreshing) {
47 | /**
48 | * avoid refreshing icon not shown
49 | * https://code.google.com/p/android/issues/detail?id=77712
50 | */
51 | if (mMeasured) {
52 | super.setRefreshing(refreshing);
53 | } else {
54 | mPreMeasureRefreshing = refreshing;
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/app/src/main/java/com/czbix/v2ex/ui/widget/SearchListView.java:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.ui.widget;
2 |
3 | import android.content.Context;
4 | import androidx.appcompat.widget.SearchView;
5 | import android.util.AttributeSet;
6 | import android.view.View;
7 | import android.widget.AdapterView;
8 | import android.widget.LinearLayout;
9 | import android.widget.ListView;
10 |
11 | import com.czbix.v2ex.R;
12 |
13 | /**
14 | * a listview with search feature
15 | */
16 | public class SearchListView extends LinearLayout implements SearchView.OnQueryTextListener {
17 | private SearchView mSearchView;
18 | private ListView mListView;
19 | private ExArrayAdapter> mAdapter;
20 | private View mLoading;
21 |
22 | public SearchListView(Context context) {
23 | this(context, null);
24 | }
25 |
26 | public SearchListView(Context context, AttributeSet attrs) {
27 | this(context, attrs, 0);
28 | }
29 |
30 | public SearchListView(Context context, AttributeSet attrs, int defStyle) {
31 | super(context, attrs, defStyle);
32 |
33 | setOrientation(VERTICAL);
34 |
35 | inflate(getContext(), R.layout.view_select_node, this);
36 |
37 | mSearchView = ((SearchView) findViewById(R.id.search));
38 | mLoading = findViewById(R.id.loading);
39 | mListView = ((ListView) findViewById(R.id.select_dialog_listview));
40 |
41 | mSearchView.setSubmitButtonEnabled(false);
42 | mSearchView.setOnQueryTextListener(this);
43 | mSearchView.setIconifiedByDefault(false);
44 | mListView.setVisibility(GONE);
45 | }
46 |
47 | public void setAdapter(ExArrayAdapter> adapter) {
48 | if (mAdapter == null) {
49 | mLoading.setVisibility(GONE);
50 | mListView.setVisibility(VISIBLE);
51 | }
52 | mAdapter = adapter;
53 | mListView.setAdapter(adapter);
54 |
55 | mAdapter.getFilter().filter(mSearchView.getQuery());
56 | }
57 |
58 | public void setOnItemClickListener(AdapterView.OnItemClickListener listener) {
59 | mListView.setOnItemClickListener(listener);
60 | }
61 |
62 | @Override
63 | public boolean onQueryTextSubmit(String query) {
64 | return false;
65 | }
66 |
67 | @Override
68 | public boolean onQueryTextChange(String newText) {
69 | if (mAdapter == null) {
70 | return true;
71 | }
72 | mAdapter.getFilter().filter(newText);
73 | return true;
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/app/src/main/java/com/czbix/v2ex/util/CrashlyticsUtils.java:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.util;
2 |
3 | import com.google.firebase.crashlytics.FirebaseCrashlytics;
4 |
5 | public class CrashlyticsUtils {
6 | private static final String KEY_IS_LOGGED_IN = "is_logged_in";
7 |
8 | public static void setUserState(boolean isLoggedIn) {
9 | FirebaseCrashlytics.getInstance().setCustomKey(KEY_IS_LOGGED_IN, isLoggedIn);
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/app/src/main/java/com/czbix/v2ex/util/TrackerUtils.kt:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.util
2 |
3 | import android.content.Context
4 | import android.os.Bundle
5 | import com.google.firebase.analytics.FirebaseAnalytics
6 | import java.security.MessageDigest
7 |
8 | object TrackerUtils {
9 | private lateinit var analytics: FirebaseAnalytics
10 |
11 | private fun hashString(data: String): String {
12 | val salted = "cz-%s-bix".format(data)
13 | return MessageDigest.getInstance("MD5")
14 | .digest(salted.toByteArray())
15 | .fold("") { str, it ->
16 | str + "%02x".format(it)
17 | }
18 | }
19 |
20 | fun init(context: Context) {
21 | analytics = FirebaseAnalytics.getInstance(context)
22 | }
23 |
24 | fun setUserId(id: String?) {
25 | val hashedId = if (id == null) null else hashString(id)
26 | analytics.setUserId(hashedId)
27 | }
28 |
29 | fun onTopicSwitchReply(isShow: Boolean) {
30 | val params = Bundle().apply {
31 | putBoolean("is_show", isShow)
32 | }
33 |
34 | analytics.logEvent(Event.SWITCH_REPLY, params)
35 | }
36 |
37 | fun onTopicReply() {
38 | analytics.logEvent(Event.REPLY_TOPIC, null)
39 | }
40 |
41 | fun onSearch() {
42 | analytics.logEvent(FirebaseAnalytics.Event.SEARCH, null)
43 | }
44 |
45 | fun onParseTopic(time: Long, commentCount: Int) {
46 | val params = Bundle().apply {
47 | putInt("cost_time", time.toInt())
48 | putInt("topic_count", commentCount)
49 | }
50 | analytics.logEvent(Event.PARSE_TOPIC, params)
51 | }
52 |
53 | private object Event {
54 | const val SWITCH_REPLY = "switch_reply"
55 | const val REPLY_TOPIC = "reply_topic"
56 | const val PARSE_TOPIC = "parse_topic"
57 | }
58 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/czbix/v2ex/util/UserUtils.java:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.util;
2 |
3 | import com.czbix.v2ex.common.UserState;
4 | import com.czbix.v2ex.common.exception.ConnectionException;
5 | import com.czbix.v2ex.common.exception.RemoteException;
6 | import com.czbix.v2ex.common.exception.RequestException;
7 | import com.czbix.v2ex.dao.ConfigDao;
8 | import com.czbix.v2ex.event.BaseEvent;
9 | import com.czbix.v2ex.helper.RxBus;
10 | import com.czbix.v2ex.model.Avatar;
11 | import com.czbix.v2ex.network.RequestHelper;
12 | import com.google.common.base.Preconditions;
13 |
14 | public class UserUtils {
15 | private static final String TAG = UserUtils.class.getSimpleName();
16 |
17 | public static Avatar getAvatar() {
18 | Preconditions.checkState(UserState.INSTANCE.isLoggedIn());
19 |
20 | final String url = ConfigDao.get(ConfigDao.KEY_AVATAR, null);
21 | Preconditions.checkNotNull(url);
22 |
23 | return new Avatar.Builder().setBaseUrl(url).build();
24 | }
25 |
26 | public static void checkDailyAward() {
27 | if (!UserState.INSTANCE.isLoggedIn()) {
28 | return;
29 | }
30 |
31 | boolean hasAward;
32 | try {
33 | hasAward = RequestHelper.INSTANCE.hasDailyAward();
34 | } catch (ConnectionException | RemoteException | RequestException e) {
35 | LogUtils.v(TAG, "check daily award failed", e);
36 | return;
37 | }
38 |
39 | if (hasAward) {
40 | RxBus.INSTANCE.post(new BaseEvent.DailyAwardEvent(true));
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/czbix/v2ex/DebugHelpers.kt:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex
2 |
3 | import okhttp3.OkHttpClient
4 |
5 | open class DebugHelpers {
6 | open fun addStethoInterceptor(builder: OkHttpClient.Builder): OkHttpClient.Builder {
7 | return builder
8 | }
9 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/czbix/v2ex/common/UpdateInfo.kt:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.common
2 |
3 | import com.czbix.v2ex.BuildConfig
4 | import com.czbix.v2ex.event.AppUpdateEvent
5 | import com.czbix.v2ex.helper.RxBus
6 | import com.czbix.v2ex.util.LogUtils
7 | import com.czbix.v2ex.util.getLogTag
8 |
9 | object UpdateInfo {
10 | private val TAG = getLogTag()
11 |
12 | var hasNewVersion: Boolean = false
13 | private set
14 | var isRecommend: Boolean = false
15 | private set
16 |
17 | fun parseVersionData(info: VersionInfo) {
18 | val currentVersion = BuildConfig.VERSION_CODE
19 |
20 | if (info.version <= currentVersion) {
21 | // no new version
22 | return
23 | }
24 |
25 | hasNewVersion = true
26 | isRecommend = info.recommend
27 |
28 | LogUtils.i(TAG, "new app version: $info")
29 | RxBus.post(AppUpdateEvent())
30 | }
31 |
32 | data class VersionInfo(
33 | val version: Int,
34 | val recommend: Boolean
35 | )
36 | }
37 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/czbix/v2ex/common/exception/ConnectionException.kt:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.common.exception
2 |
3 | import java.io.IOException
4 |
5 | class ConnectionException : IOException {
6 | constructor() {
7 | }
8 |
9 | constructor(detailMessage: String) : super(detailMessage) {
10 | }
11 |
12 | constructor(detailMessage: String, throwable: Throwable) : super(detailMessage, throwable) {
13 | }
14 |
15 | constructor(throwable: Throwable) : super(throwable) {
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/czbix/v2ex/common/exception/RestrictedException.kt:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.common.exception
2 |
3 | import okhttp3.Response
4 |
5 | class RestrictedException(response: Response, tr: Throwable? = null) : UnauthorizedException(response, tr)
6 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/czbix/v2ex/common/exception/TwoFactorAuthException.kt:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.common.exception
2 |
3 | import okhttp3.Response
4 |
5 | class TwoFactorAuthException(response: Response, tr: Throwable? = null) : UnauthorizedException(response, tr)
6 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/czbix/v2ex/common/exception/UrlRedirectException.kt:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.common.exception
2 |
3 | import okhttp3.Response
4 |
5 | class UrlRedirectException(val url: String, message: String, response: Response, tr: Throwable? = null)
6 | : RequestException(message, response, tr)
7 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/czbix/v2ex/db/Comment.kt:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.db
2 |
3 | import androidx.room.ColumnInfo
4 | import androidx.room.Entity
5 | import androidx.room.PrimaryKey
6 | import com.czbix.v2ex.model.Ignorable
7 | import com.czbix.v2ex.model.Thankable
8 | import com.czbix.v2ex.network.RequestHelper
9 |
10 | @Entity
11 | data class Comment(
12 | @PrimaryKey
13 | val id: Int,
14 | @ColumnInfo(index = true)
15 | val topicId: Int,
16 | val page: Int,
17 | val content: String,
18 | val username: String,
19 | val addAt: String,
20 | val thanks: Int,
21 | val floor: Int,
22 | val thanked: Boolean
23 | ) : Thankable, Ignorable {
24 |
25 | override fun getIgnoreUrl(): String {
26 | return String.format("%s/ignore/reply/%d", RequestHelper.BASE_URL, id)
27 | }
28 |
29 | override fun getThankUrl(): String {
30 | return String.format("%s/thank/reply/%d", RequestHelper.BASE_URL, id)
31 | }
32 |
33 | override fun equals(other: Any?): Boolean {
34 | if (this === other) return true
35 | if (javaClass != other?.javaClass) return false
36 |
37 | other as Comment
38 |
39 | if (id != other.id) return false
40 | if (topicId != other.topicId) return false
41 | if (page != other.page) return false
42 | if (content != other.content) return false
43 | if (username != other.username) return false
44 | if (addAt != other.addAt) return false
45 | if (thanks != other.thanks) return false
46 | if (floor != other.floor) return false
47 | if (thanked != other.thanked) return false
48 |
49 | return true
50 | }
51 |
52 | override fun hashCode(): Int {
53 | var result = id
54 | result = 31 * result + topicId
55 | result = 31 * result + page
56 | result = 31 * result + content.hashCode()
57 | result = 31 * result + username.hashCode()
58 | result = 31 * result + addAt.hashCode()
59 | result = 31 * result + thanks
60 | result = 31 * result + floor
61 | result = 31 * result + thanked.hashCode()
62 | return result
63 | }
64 |
65 | class Builder {
66 | var topicId: Int = 0
67 | var id: Int = 0
68 | var page: Int = 0
69 | lateinit var content: String
70 | lateinit var username: String
71 | lateinit var addAt: String
72 | var thanks: Int = 0
73 | var floor: Int = 0
74 | var thanked: Boolean = false
75 |
76 | fun build(): Comment {
77 | check(topicId > 0)
78 | check(id > 0)
79 | check(floor > 0)
80 | check(page > 0)
81 |
82 | return Comment(id, topicId, page, content, username, addAt, thanks, floor, thanked)
83 | }
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/czbix/v2ex/db/CommentAndMember.kt:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.db
2 |
3 | import androidx.room.Embedded
4 |
5 | data class CommentAndMember(
6 | @Embedded
7 | val comment: Comment,
8 |
9 | @Embedded(prefix = "member_")
10 | val member: Member
11 | )
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/czbix/v2ex/db/CommentDao.kt:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.db
2 |
3 | import androidx.paging.DataSource
4 | import androidx.room.*
5 |
6 | @Dao
7 | abstract class CommentDao {
8 | @Query("SELECT Comment.*, Member.username as member_username, Member.baseUrl as member_baseUrl FROM Comment INNER JOIN Member ON Comment.username = Member.username WHERE topicId = :id ORDER BY id ASC")
9 | abstract fun getCommentsByTopicId(id: Int): DataSource.Factory
10 |
11 | @Delete(entity = Comment::class)
12 | abstract suspend fun deleteCommentsByPage(page: CommentPage)
13 |
14 | @Insert(onConflict = OnConflictStrategy.REPLACE)
15 | abstract suspend fun insertComments(comments: List)
16 |
17 | @Insert(onConflict = OnConflictStrategy.REPLACE)
18 | abstract suspend fun insertMembers(members: List)
19 |
20 | @Transaction
21 | open suspend fun updateCommentAndMembers(list: List, page: CommentPage) {
22 | val comments = MutableList(list.size) {
23 | list[it].comment
24 | }
25 | val members = list.map {
26 | it.member
27 | }.toSet().toList()
28 |
29 | deleteCommentsByPage(page)
30 | insertMembers(members)
31 | insertComments(comments)
32 | }
33 |
34 | class CommentPage(
35 | val topicId: Int,
36 | val page: Int
37 | )
38 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/czbix/v2ex/db/TopicRecord.kt:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.db
2 |
3 | import androidx.room.Entity
4 | import androidx.room.PrimaryKey
5 |
6 | @Entity
7 | data class TopicRecord(
8 | @PrimaryKey
9 | val id: Int,
10 | val title: String,
11 | var lastReadAt: Long,
12 | var lastReadComment: Int
13 | )
14 |
15 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/czbix/v2ex/db/TopicRecordDao.kt:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.db
2 |
3 | import androidx.room.Dao
4 | import androidx.room.Insert
5 | import androidx.room.OnConflictStrategy
6 | import androidx.room.Query
7 |
8 |
9 | @Dao
10 | interface TopicRecordDao {
11 | @Query("SELECT * FROM TopicRecord WHERE id = :id")
12 | suspend fun getRecordById(id: Int): TopicRecord?
13 |
14 | @Query("SELECT lastReadComment FROM TopicRecord WHERE id = :id")
15 | fun getLastReadComment(id: Int): Int?
16 |
17 | @Insert(onConflict = OnConflictStrategy.REPLACE)
18 | fun updateRecord(record: TopicRecord)
19 | }
20 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/czbix/v2ex/db/V2Db.kt:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.db
2 |
3 | import android.content.Context
4 | import androidx.room.Database
5 | import androidx.room.Room
6 | import androidx.room.RoomDatabase
7 |
8 | @Database(
9 | entities = [
10 | TopicRecord::class,
11 | Comment::class,
12 | Member::class
13 | ],
14 | version = 1
15 | )
16 | abstract class V2Db : RoomDatabase() {
17 | companion object {
18 | fun create(context: Context): V2Db {
19 | val builder = Room.databaseBuilder(context, V2Db::class.java, "v2db.db")
20 | return builder.build()
21 | }
22 | }
23 |
24 | abstract fun topicRecords(): TopicRecordDao
25 |
26 | abstract fun comments(): CommentDao
27 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/czbix/v2ex/event/AppUpdateEvent.kt:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.event
2 |
3 | class AppUpdateEvent : BaseEvent()
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/czbix/v2ex/event/BaseEvent.kt:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.event
2 |
3 | abstract class BaseEvent {
4 | class NewUnreadEvent(val mCount: Int) : BaseEvent() {
5 | fun hasNew(): Boolean {
6 | return mCount > 0
7 | }
8 | }
9 |
10 | class DailyAwardEvent(val mHasAward: Boolean) : BaseEvent()
11 |
12 | class ContextInitFinishEvent : BaseEvent()
13 |
14 | class NodeEvent : BaseEvent()
15 | }
16 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/czbix/v2ex/event/DeviceRegisterEvent.java:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.event;
2 |
3 |
4 | public class DeviceRegisterEvent extends BaseEvent {
5 | public final boolean isRegister;
6 | public final boolean isSuccess;
7 |
8 | public DeviceRegisterEvent(boolean isRegister, boolean isSuccess) {
9 | this.isRegister = isRegister;
10 | this.isSuccess = isSuccess;
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/czbix/v2ex/helper/RxBus.kt:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.helper
2 |
3 | import com.czbix.v2ex.event.BaseEvent
4 | import io.reactivex.Observable
5 | import io.reactivex.Scheduler
6 | import io.reactivex.android.schedulers.AndroidSchedulers
7 | import io.reactivex.disposables.Disposable
8 | import io.reactivex.rxkotlin.cast
9 | import io.reactivex.subjects.PublishSubject
10 | import io.reactivex.subjects.Subject
11 |
12 | object RxBus {
13 | private val subject: Subject
14 |
15 | init {
16 | subject = PublishSubject.create().toSerialized()
17 | }
18 |
19 | fun post(event: BaseEvent) {
20 | subject.onNext(event)
21 | }
22 |
23 | @JvmName("toObservableAny")
24 | fun toObservable(): Observable {
25 | return subject
26 | }
27 |
28 | inline fun toObservable(): Observable {
29 | return toObservable().filter { it is T }.cast()
30 | }
31 |
32 | @JvmName("subscribeAny")
33 | fun subscribe(scheduler: Scheduler = AndroidSchedulers.mainThread(),
34 | action: (BaseEvent) -> Unit): Disposable {
35 | return toObservable()
36 | .observeOn(scheduler)
37 | .subscribe(action)
38 | }
39 |
40 | inline fun subscribe(scheduler: Scheduler = AndroidSchedulers.mainThread(),
41 | noinline action: (T) -> Unit): Disposable {
42 | return toObservable()
43 | .observeOn(scheduler)
44 | .subscribe(action)
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/czbix/v2ex/inject/ActivityModule.kt:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.inject
2 |
3 | import com.czbix.v2ex.ui.LoginActivity
4 | import com.czbix.v2ex.ui.MainActivity
5 | import com.czbix.v2ex.ui.SettingsActivity
6 | import com.czbix.v2ex.ui.TopicActivity
7 | import com.czbix.v2ex.ui.settings.SettingsModule
8 | import dagger.Module
9 | import dagger.android.ContributesAndroidInjector
10 |
11 |
12 | @Module
13 | abstract class ActivityModule {
14 | @ActivityScoped
15 | @ContributesAndroidInjector(modules = [MainFragmentModule::class])
16 | abstract fun mainActivity(): MainActivity
17 |
18 | @ActivityScoped
19 | @ContributesAndroidInjector(modules = [TopicFragmentModule::class])
20 | abstract fun topicActivity(): TopicActivity
21 |
22 | @ActivityScoped
23 | @ContributesAndroidInjector(modules = [SettingsModule::class])
24 | abstract fun settingsActivity(): SettingsActivity
25 |
26 | @ActivityScoped
27 | @ContributesAndroidInjector
28 | abstract fun loginActivity(): LoginActivity
29 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/czbix/v2ex/inject/AppComponent.kt:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.inject
2 |
3 | import com.czbix.v2ex.AppCtx
4 | import dagger.BindsInstance
5 | import dagger.Component
6 | import dagger.android.AndroidInjectionModule
7 | import dagger.android.AndroidInjector
8 | import javax.inject.Singleton
9 |
10 | @Singleton
11 | @Component(
12 | modules = [
13 | AndroidInjectionModule::class,
14 | AppModule::class,
15 | DbModule::class,
16 | ViewModelModule::class,
17 | ActivityModule::class,
18 | NightModeModule::class
19 | ]
20 | )
21 | interface AppComponent : AndroidInjector {
22 | @Component.Builder
23 | interface Builder {
24 | @BindsInstance
25 | fun application(application: AppCtx): Builder
26 |
27 | fun build(): AppComponent
28 | }
29 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/czbix/v2ex/inject/AppInjector.kt:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.inject
2 |
3 | import android.app.Activity
4 | import android.app.Application
5 | import android.content.Context
6 | import android.os.Bundle
7 | import androidx.fragment.app.Fragment
8 | import androidx.fragment.app.FragmentActivity
9 | import androidx.fragment.app.FragmentManager
10 | import com.czbix.v2ex.AppCtx
11 | import dagger.android.support.AndroidSupportInjection
12 |
13 | object AppInjector {
14 | fun init(app: AppCtx) {
15 | DaggerAppComponent.builder().application(app)
16 | .build().inject(app)
17 | app.registerActivityLifecycleCallbacks(object : Application.ActivityLifecycleCallbacks {
18 | override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
19 | handleActivity(activity)
20 | }
21 |
22 | override fun onActivityStarted(activity: Activity) {
23 |
24 | }
25 |
26 | override fun onActivityResumed(activity: Activity) {
27 |
28 | }
29 |
30 | override fun onActivityPaused(activity: Activity) {
31 |
32 | }
33 |
34 | override fun onActivityStopped(activity: Activity) {
35 |
36 | }
37 |
38 | override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {
39 |
40 | }
41 |
42 | override fun onActivityDestroyed(activity: Activity) {
43 |
44 | }
45 | })
46 | }
47 |
48 | private fun handleActivity(activity: Activity) {
49 | if (activity is FragmentActivity) {
50 | activity.supportFragmentManager
51 | .registerFragmentLifecycleCallbacks(
52 | object : FragmentManager.FragmentLifecycleCallbacks() {
53 | override fun onFragmentAttached(fm: FragmentManager, f: Fragment, context: Context) {
54 | if (f is Injectable) {
55 | AndroidSupportInjection.inject(f)
56 | }
57 | }
58 | }, true
59 | )
60 | }
61 | }
62 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/czbix/v2ex/inject/AppModule.kt:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.inject
2 |
3 | import android.content.ClipboardManager
4 | import android.content.Context
5 | import android.net.ConnectivityManager
6 | import android.net.wifi.WifiManager
7 | import com.czbix.v2ex.AppCtx
8 | import com.czbix.v2ex.common.PrefStore
9 | import com.czbix.v2ex.model.PreferenceStorage
10 | import com.czbix.v2ex.network.RequestHelper
11 | import com.czbix.v2ex.network.V2exService
12 | import dagger.Module
13 | import dagger.Provides
14 | import javax.inject.Singleton
15 |
16 | @Module
17 | class AppModule {
18 | @Singleton
19 | @Provides
20 | fun provideContext(app: AppCtx): Context {
21 | return app.applicationContext
22 | }
23 |
24 | @Provides
25 | fun providesWifiManager(context: Context): WifiManager {
26 | return context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
27 | }
28 |
29 | @Provides
30 | fun providesConnectivityManager(context: Context): ConnectivityManager {
31 | return context.applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE)
32 | as ConnectivityManager
33 | }
34 |
35 | @Provides
36 | fun providesClipboardManager(context: Context): ClipboardManager {
37 | return context.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE)
38 | as ClipboardManager
39 | }
40 |
41 | @Singleton
42 | @Provides
43 | fun providesPrefStore(): PrefStore {
44 | return PrefStore.getInstance()
45 | }
46 |
47 | @Singleton
48 | @Provides
49 | fun providesPreferenceStorage(context: Context): PreferenceStorage {
50 | return PreferenceStorage.SharedPreferenceStorage(context)
51 | }
52 |
53 | @Singleton
54 | @Provides
55 | fun provideV2exService(): V2exService {
56 | return V2exService(RequestHelper)
57 | }
58 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/czbix/v2ex/inject/DbModule.kt:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.inject
2 |
3 | import android.content.Context
4 | import com.czbix.v2ex.db.CommentDao
5 | import com.czbix.v2ex.db.TopicRecordDao
6 | import com.czbix.v2ex.db.V2Db
7 | import dagger.Module
8 | import dagger.Provides
9 | import javax.inject.Singleton
10 |
11 |
12 | @Module
13 | class DbModule {
14 | @Singleton
15 | @Provides
16 | fun provideDb(context: Context): V2Db {
17 | return V2Db.create(context)
18 | }
19 |
20 | @Provides
21 | fun provideTopicRecordDao(db: V2Db): TopicRecordDao {
22 | return db.topicRecords()
23 | }
24 |
25 | @Provides
26 | fun provideComments(db: V2Db): CommentDao {
27 | return db.comments()
28 | }
29 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/czbix/v2ex/inject/Injectable.kt:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.inject
2 |
3 | interface Injectable
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/czbix/v2ex/inject/MainFragmentModule.kt:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.inject
2 |
3 | import com.czbix.v2ex.ui.fragment.TopicListFragment
4 | import dagger.Module
5 | import dagger.android.ContributesAndroidInjector
6 |
7 | @Module
8 | abstract class MainFragmentModule {
9 | @ContributesAndroidInjector
10 | abstract fun topicListFragment(): TopicListFragment
11 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/czbix/v2ex/inject/NightModeModule.kt:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.inject
2 |
3 | import androidx.lifecycle.ViewModel
4 | import com.czbix.v2ex.ui.model.NightModeDelegate
5 | import com.czbix.v2ex.ui.model.NightModeDelegateImpl
6 | import com.czbix.v2ex.ui.model.NightModeViewModel
7 | import dagger.Binds
8 | import dagger.Module
9 | import dagger.multibindings.IntoMap
10 | import javax.inject.Singleton
11 |
12 | @Module
13 | abstract class NightModeModule {
14 | @Singleton
15 | @Binds
16 | abstract fun provideNightModeDelegate(impl: NightModeDelegateImpl): NightModeDelegate
17 |
18 | @Binds
19 | @IntoMap
20 | @ViewModelKey(NightModeViewModel::class)
21 | abstract fun provideNightModeViewModel(viewModel: NightModeViewModel): ViewModel
22 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/czbix/v2ex/inject/Scoped.kt:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.inject
2 |
3 | import javax.inject.Scope
4 |
5 | @Scope
6 | @Retention(AnnotationRetention.RUNTIME)
7 | annotation class ActivityScoped
8 |
9 | @Scope
10 | @Retention(AnnotationRetention.RUNTIME)
11 | @Target(AnnotationTarget.TYPE, AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
12 | annotation class FragmentScoped
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/czbix/v2ex/inject/TopicFragmentModule.kt:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.inject
2 |
3 | import androidx.lifecycle.ViewModel
4 | import com.czbix.v2ex.ui.fragment.TopicFragment
5 | import com.czbix.v2ex.ui.model.TopicViewModel
6 | import dagger.Binds
7 | import dagger.Module
8 | import dagger.android.ContributesAndroidInjector
9 | import dagger.multibindings.IntoMap
10 |
11 | @Module
12 | abstract class TopicFragmentModule {
13 | @Binds
14 | @IntoMap
15 | @ViewModelKey(TopicViewModel::class)
16 | abstract fun bindTopicViewModel(viewModel: TopicViewModel): ViewModel
17 |
18 | @FragmentScoped
19 | @ContributesAndroidInjector
20 | abstract fun topicFragment(): TopicFragment
21 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/czbix/v2ex/inject/ViewModelKey.kt:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.inject
2 |
3 | import androidx.lifecycle.ViewModel
4 | import dagger.MapKey
5 | import kotlin.reflect.KClass
6 |
7 | @Target(
8 | AnnotationTarget.FUNCTION,
9 | AnnotationTarget.PROPERTY_GETTER,
10 | AnnotationTarget.PROPERTY_SETTER
11 | )
12 | @Retention(AnnotationRetention.RUNTIME)
13 | @MapKey
14 | annotation class ViewModelKey(val value: KClass)
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/czbix/v2ex/inject/ViewModelModule.kt:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.inject
2 |
3 |
4 | import androidx.lifecycle.ViewModelProvider
5 | import com.czbix.v2ex.model.ViewModelFactory
6 | import dagger.Binds
7 | import dagger.Module
8 |
9 | @Module
10 | abstract class ViewModelModule {
11 | @Binds
12 | abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory
13 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/czbix/v2ex/model/AbsentLiveData.kt:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.model
2 |
3 | import androidx.lifecycle.LiveData
4 |
5 | /**
6 | * A LiveData class that has `null` value.
7 | */
8 | class AbsentLiveData(
9 | data: T? = null
10 | ): LiveData() {
11 | init {
12 | // use post instead of set since this can be created on any thread
13 | postValue(data)
14 | }
15 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/czbix/v2ex/model/ContentBlock.kt:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.model
2 |
3 | import android.os.Parcelable
4 | import kotlinx.android.parcel.Parcelize
5 |
6 | sealed class ContentBlock : Parcelable {
7 | @Parcelize
8 | data class TextBlock(val id: Int, val text: CharSequence) : ContentBlock()
9 |
10 | @Parcelize
11 | data class ImageBlock(val id: Int, val source: String) : ContentBlock()
12 |
13 | @Parcelize
14 | data class PreBlock(val id: Int, val text: CharSequence) : ContentBlock()
15 | }
16 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/czbix/v2ex/model/EmptyLiveData.kt:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.model
2 |
3 | import androidx.lifecycle.LiveData
4 |
5 | class EmptyLiveData : LiveData() {
6 | companion object {
7 | fun create(): EmptyLiveData {
8 | return EmptyLiveData()
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/czbix/v2ex/model/Postscript.kt:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.model
2 |
3 | import android.os.Parcelable
4 | import kotlinx.android.parcel.Parcelize
5 |
6 | @Parcelize
7 | data class Postscript(
8 | val content: List,
9 | val time: String
10 | ) : Parcelable
11 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/czbix/v2ex/model/Resource.kt:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.model
2 |
3 | sealed class Resource(
4 | val status: Status,
5 | open val data: T?,
6 | open val exception: Exception?
7 | ) {
8 | enum class Status {
9 | LOADING,
10 | SUCCESS,
11 | FAILED,
12 | }
13 |
14 | fun map(block: (T) -> R): Resource {
15 | return when (this) {
16 | is Loading -> Loading(data?.let(block))
17 | is Success -> Success(data.let(block))
18 | is Failed -> Failed(exception, data?.let(block))
19 | }
20 | }
21 |
22 | data class Loading(
23 | override val data: T? = null
24 | ) : Resource(Status.LOADING, data, null)
25 |
26 | data class Success(
27 | override val data: T
28 | ) : Resource(Status.SUCCESS, data, null)
29 |
30 | data class Failed(
31 | override val exception: Exception,
32 | override val data: T? = null
33 | ) : Resource(Status.FAILED, data, exception)
34 |
35 | override fun toString(): String {
36 | return when (this) {
37 | is Success -> "Success[data=$data]"
38 | is Failed -> "Failed[exception=$exception]"
39 | is Loading -> "Loading"
40 | }
41 | }
42 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/czbix/v2ex/model/ServerConfig.kt:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.model
2 |
3 | import com.czbix.v2ex.common.UpdateInfo
4 |
5 | data class ServerConfig(
6 | val version: UpdateInfo.VersionInfo
7 | )
8 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/czbix/v2ex/model/TopicResponse.kt:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.model
2 |
3 | import com.czbix.v2ex.db.CommentAndMember
4 |
5 | class TopicResponse(
6 | val topic: Topic,
7 | val comments: List,
8 | val curPage: Int,
9 | val maxPage: Int,
10 | val csrfToken: String?,
11 | val onceToken: String?
12 | ) {
13 | val nextPage = if (curPage < maxPage) curPage + 1 else 0
14 | }
15 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/czbix/v2ex/model/ViewModelFactory.kt:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.model
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.ViewModelProvider
5 | import javax.inject.Inject
6 | import javax.inject.Provider
7 |
8 | class ViewModelFactory @Inject constructor(
9 | private val creators: @JvmSuppressWildcards Map, Provider>
10 | ) : ViewModelProvider.Factory {
11 | override fun create(modelClass: Class): T {
12 | val found = creators[modelClass] ?: creators.entries.find {
13 | modelClass.isAssignableFrom(it.key)
14 | }?.value
15 |
16 | requireNotNull(found) {
17 | "Unknown model class $modelClass"
18 | }
19 |
20 | try {
21 | @Suppress("UNCHECKED_CAST")
22 | return found.get() as T
23 | } catch (e: Exception) {
24 | throw RuntimeException(e)
25 | }
26 | }
27 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/czbix/v2ex/model/loader/GooglePhotoUrlLoader.kt:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.model.loader
2 |
3 | import android.annotation.SuppressLint
4 | import com.bumptech.glide.load.Options
5 | import com.bumptech.glide.load.model.GlideUrl
6 | import com.bumptech.glide.load.model.ModelLoader
7 | import com.bumptech.glide.load.model.ModelLoaderFactory
8 | import com.bumptech.glide.load.model.MultiModelLoaderFactory
9 | import com.bumptech.glide.load.model.stream.BaseGlideUrlLoader
10 | import java.io.InputStream
11 | import kotlin.random.Random
12 |
13 | class GooglePhotoUrlLoader constructor(concreteLoader: ModelLoader) : BaseGlideUrlLoader(concreteLoader) {
14 | @SuppressLint("DefaultLocale")
15 | override fun getUrl(s: String, width: Int, height: Int, options: Options): String {
16 | return String.format("https://lh%d.%s=w%d-h%d", Random.nextInt(3,7), s, width, height)
17 | }
18 |
19 | override fun handles(s: String): Boolean {
20 | return s.startsWith("ggpht.com/")
21 | }
22 |
23 | class Factory : ModelLoaderFactory {
24 | override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader {
25 | return GooglePhotoUrlLoader(multiFactory.build(GlideUrl::class.java, InputStream::class.java))
26 | }
27 |
28 | override fun teardown() {
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/czbix/v2ex/network/NetworkState.kt:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.network
2 |
3 | enum class NetworkState {
4 | RUNNING,
5 | SUCCESS,
6 | FAILED,
7 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/czbix/v2ex/network/repository/BaseRepository.kt:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.network.repository
2 |
3 | import androidx.lifecycle.LiveData
4 | import com.czbix.v2ex.model.NetworkBoundResource
5 | import com.czbix.v2ex.model.Resource
6 | import com.czbix.v2ex.util.liveData
7 |
8 | open class BaseRepository {
9 | protected fun wrapCall(call: suspend () -> T): LiveData> {
10 | return liveData {
11 | try {
12 | val result = call()
13 | Resource.Success(result)
14 | } catch (e: Exception) {
15 | Resource.Failed(e)
16 | }
17 | }
18 | }
19 |
20 | protected inline fun networkOnlyCall(crossinline call: suspend () -> T): LiveData> {
21 | return NetworkBoundResource.NetworkOnlyResource {
22 | wrapCall {
23 | call()
24 | }
25 | }.asLiveData()
26 | }
27 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/czbix/v2ex/network/repository/TopicRepository.kt:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.network.repository
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.lifecycle.MutableLiveData
5 | import androidx.paging.PagedList
6 | import androidx.paging.toLiveData
7 | import com.czbix.v2ex.db.CommentAndMember
8 | import com.czbix.v2ex.db.CommentDao
9 | import com.czbix.v2ex.model.*
10 | import com.czbix.v2ex.network.V2exService
11 | import kotlinx.coroutines.runBlocking
12 | import javax.inject.Inject
13 | import javax.inject.Singleton
14 |
15 | @Singleton
16 | class TopicRepository @Inject constructor(
17 | private val service: V2exService,
18 | private val commentDao: CommentDao
19 | ) : BaseRepository() {
20 |
21 | fun loadTopic(topic: Topic, page: Int): LiveData> {
22 | return object : NetworkBoundResource() {
23 | private val liveData = MutableLiveData(null)
24 |
25 | override fun saveCallResult(item: TopicResponse) {
26 | runBlocking {
27 | commentDao.updateCommentAndMembers(item.comments, CommentDao.CommentPage(topic.id, page))
28 | }
29 | liveData.postValue(item)
30 | }
31 |
32 | override fun shouldFetch(data: TopicResponse?): Boolean {
33 | return true
34 | }
35 |
36 | override fun loadFromDb(): LiveData {
37 | return liveData
38 | }
39 |
40 | override fun createCall(): LiveData> {
41 | return wrapCall {
42 | service.getTopic(topic, page)
43 | }
44 | }
45 | }.asLiveData()
46 | }
47 |
48 | fun loadLocalTopicComments(topicId: Int): LiveData> {
49 | return commentDao.getCommentsByTopicId(topicId).toLiveData(pageSize = 100)
50 | }
51 |
52 | fun postComment(topic: Topic, content: String, onceToken: String) = networkOnlyCall {
53 | service.postComment(topic, content, onceToken)
54 | }
55 |
56 | fun favTopic(topic: Topic, bool: Boolean, csrfToken: String) = networkOnlyCall {
57 | service.favor(topic, bool, csrfToken)
58 | }
59 |
60 | fun thank(thankable: Thankable, onceToken: String) = networkOnlyCall {
61 | service.thank(thankable, onceToken)
62 | }
63 |
64 | fun ignore(ignorable: Ignorable, onceToken: String) = networkOnlyCall {
65 | service.ignore(ignorable, onceToken)
66 | }
67 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/czbix/v2ex/ui/AutoClearedValue.kt:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.ui
2 |
3 | import androidx.fragment.app.Fragment
4 | import androidx.lifecycle.Lifecycle
5 | import androidx.lifecycle.LifecycleObserver
6 | import androidx.lifecycle.OnLifecycleEvent
7 | import kotlin.properties.ReadWriteProperty
8 | import kotlin.reflect.KProperty
9 |
10 | /**
11 | * A lazy property that gets cleaned up when the fragment is destroyed.
12 | *
13 | * Accessing this variable in a destroyed fragment will throw NPE.
14 | */
15 | class AutoClearedValue(val fragment: Fragment) : ReadWriteProperty {
16 | private var _value: T? = null
17 |
18 | init {
19 | fragment.lifecycle.addObserver(object : LifecycleObserver {
20 | @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
21 | fun onDestroy() {
22 | _value = null
23 | }
24 | })
25 | }
26 |
27 | override fun getValue(thisRef: Fragment, property: KProperty<*>): T {
28 | return _value ?: throw IllegalStateException(
29 | "should never call auto-cleared-value get when it might not be available"
30 | )
31 | }
32 |
33 | override fun setValue(thisRef: Fragment, property: KProperty<*>, value: T) {
34 | _value = value
35 | }
36 | }
37 |
38 | /**
39 | * Creates an [AutoClearedValue] associated with this fragment.
40 | */
41 | fun Fragment.autoCleared() = AutoClearedValue(this)
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/czbix/v2ex/ui/BindingAdapters.kt:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.ui
2 |
3 | import android.view.View
4 | import androidx.databinding.BindingAdapter
5 |
6 | @BindingAdapter("visibility")
7 | fun View.setVisibility(value: Boolean) {
8 | visibility = if (value) View.VISIBLE else View.GONE
9 | }
10 |
11 | @BindingAdapter("visible")
12 | fun View.setVisible(value: Boolean) {
13 | visibility = if (value) View.VISIBLE else View.INVISIBLE
14 | }
15 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/czbix/v2ex/ui/DebugActivity.kt:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.ui
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.Intent
5 | import android.os.Bundle
6 | import androidx.appcompat.app.AppCompatActivity
7 | import android.view.View
8 | import android.widget.Button
9 | import android.widget.LinearLayout
10 |
11 | import com.czbix.v2ex.model.Topic
12 |
13 | class DebugActivity : AppCompatActivity() {
14 | private lateinit var mLayout: LinearLayout
15 |
16 | override fun onCreate(savedInstanceState: Bundle?) {
17 | super.onCreate(savedInstanceState)
18 | mLayout = LinearLayout(this)
19 | setContentView(mLayout)
20 |
21 | initDebugItem()
22 | }
23 |
24 | @SuppressLint("SetTextI18n")
25 | private fun initDebugItem() {
26 | val button = Button(this)
27 | button.text = "Sandbox Topic"
28 | button.setOnClickListener {
29 | // val intent = Intent(this@DebugActivity, TopicActivity::class.java)
30 | }
31 | mLayout.addView(button)
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/czbix/v2ex/ui/ExHolder.kt:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.ui
2 |
3 | import android.view.View
4 | import androidx.annotation.CallSuper
5 | import androidx.annotation.IdRes
6 | import com.airbnb.epoxy.EpoxyHolder
7 | import com.bumptech.glide.Glide
8 | import kotlin.properties.ReadOnlyProperty
9 | import kotlin.reflect.KProperty
10 |
11 | abstract class ExHolder : EpoxyHolder() {
12 | lateinit var view: T
13 | val glide by lazy(LazyThreadSafetyMode.NONE) { Glide.with(view) }
14 |
15 | @CallSuper
16 | override fun bindView(itemView: View) {
17 | @Suppress("UNCHECKED_CAST")
18 | view = itemView as T
19 |
20 | onCreate()
21 | }
22 |
23 | protected open fun onCreate() {
24 | }
25 |
26 | protected fun bind(@IdRes id: Int): ReadOnlyProperty, V> {
27 | return Lazy { holder: ExHolder, prop ->
28 | holder.view.findViewById(id) as V?
29 | ?: throw IllegalStateException("View ID $id for '${prop.name}' not found.")
30 | }
31 | }
32 |
33 | /**
34 | * Taken from Kotterknife.
35 | * https://github.com/JakeWharton/kotterknife
36 | */
37 | private class Lazy(
38 | private val initializer: (ExHolder, KProperty<*>) -> V
39 | ) : ReadOnlyProperty, V> {
40 | private object EMPTY
41 |
42 | private var value: Any? = EMPTY
43 |
44 | override fun getValue(thisRef: ExHolder, property: KProperty<*>): V {
45 | if (value == EMPTY) {
46 | value = initializer(thisRef, property)
47 | }
48 | @Suppress("UNCHECKED_CAST")
49 | return value as V
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/czbix/v2ex/ui/SettingsActivity.kt:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.ui
2 |
3 | import android.os.Bundle
4 | import android.view.MenuItem
5 | import androidx.activity.viewModels
6 | import androidx.lifecycle.ViewModelProvider
7 | import com.czbix.v2ex.ui.model.NightModeViewModel
8 | import com.czbix.v2ex.ui.settings.PrefsFragment
9 | import javax.inject.Inject
10 |
11 | class SettingsActivity : BaseActivity() {
12 | @Inject
13 | lateinit var viewModelFactory: ViewModelProvider.Factory
14 |
15 | private val nightModeViewModel: NightModeViewModel by viewModels {
16 | viewModelFactory
17 | }
18 |
19 | override fun onCreate(savedInstanceState: Bundle?) {
20 | super.onCreate(savedInstanceState)
21 |
22 | initNightMode(nightModeViewModel.nightMode)
23 |
24 | supportFragmentManager.beginTransaction().replace(android.R.id.content, PrefsFragment()).commit()
25 | supportActionBar!!.setDisplayHomeAsUpEnabled(true)
26 | }
27 |
28 | override fun onOptionsItemSelected(item: MenuItem): Boolean {
29 | if (item.itemId == android.R.id.home) {
30 | onBackPressed()
31 | return true
32 | }
33 |
34 | return super.onOptionsItemSelected(item)
35 | }
36 |
37 | // override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
38 | // super.onActivityResult(requestCode, resultCode, data)
39 | // mFragment.onActivityResult(requestCode, resultCode, data)
40 | // }
41 | }
42 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/czbix/v2ex/ui/adapter/TopicController.kt:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.ui.adapter
2 |
3 | import android.graphics.drawable.Drawable
4 | import com.airbnb.epoxy.EpoxyAttribute
5 | import com.airbnb.epoxy.EpoxyModelClass
6 | import com.airbnb.epoxy.EpoxyModelWithHolder
7 | import com.airbnb.epoxy.Typed2EpoxyController
8 | import com.airbnb.epoxy.preload.Preloadable
9 | import com.bumptech.glide.RequestBuilder
10 | import com.bumptech.glide.RequestManager
11 | import com.czbix.v2ex.R
12 | import com.czbix.v2ex.model.Topic
13 | import com.czbix.v2ex.ui.ExHolder
14 | import com.czbix.v2ex.ui.widget.AvatarView
15 | import com.czbix.v2ex.ui.widget.TopicView
16 |
17 | class TopicController(private val mListener: TopicView.OnTopicActionListener)
18 | : Typed2EpoxyController, Set>() {
19 | var data: List = emptyList()
20 | var readedSet: Set = emptySet()
21 |
22 | override fun buildModels(data: List, readedSet: Set) {
23 | this.data = data
24 | this.readedSet = readedSet
25 | if (data.isEmpty()) {
26 | return
27 | }
28 |
29 | data.forEach { topic ->
30 | topicControllerTopic {
31 | id(topic.id)
32 | listener(this@TopicController.mListener)
33 | topic(topic)
34 | readed(topic.id in readedSet)
35 | }
36 | }
37 | }
38 |
39 | @EpoxyModelClass(layout = R.layout.view_topic)
40 | abstract class TopicModel : EpoxyModelWithHolder() {
41 | @EpoxyAttribute
42 | lateinit var topic: Topic
43 |
44 | @EpoxyAttribute
45 | var readed: Boolean = false
46 |
47 | @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
48 | lateinit var listener: TopicView.OnTopicActionListener
49 |
50 | override fun bind(holder: Holder) {
51 | holder.view.setListener(listener)
52 | holder.view.fillData(holder.glide, topic, readed)
53 | }
54 |
55 | override fun unbind(holder: Holder) {
56 | holder.view.clear(holder.glide)
57 | }
58 |
59 | fun getImgRequest(glide: RequestManager, avatarView: AvatarView): RequestBuilder {
60 | return avatarView.getImgRequest(glide, topic.member!!.avatar)
61 | }
62 |
63 | inner class Holder : ExHolder(), Preloadable {
64 | override val viewsToPreload by lazy {
65 | listOf(view.mAvatar)
66 | }
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/czbix/v2ex/ui/common/RetryCallback.kt:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.ui.common
2 |
3 | interface RetryCallback {
4 | fun retry()
5 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/czbix/v2ex/ui/fragment/TwoFactorAuthDialog.kt:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.ui.fragment
2 |
3 | import android.app.Dialog
4 | import android.content.DialogInterface
5 | import android.os.Bundle
6 | import android.widget.EditText
7 | import androidx.appcompat.app.AlertDialog
8 | import androidx.fragment.app.DialogFragment
9 | import com.czbix.v2ex.R
10 | import com.czbix.v2ex.event.BaseEvent
11 | import com.czbix.v2ex.helper.RxBus
12 |
13 | class TwoFactorAuthDialog : DialogFragment(), DialogInterface.OnClickListener {
14 | private lateinit var editText: EditText
15 |
16 | override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
17 | val inflater = activity!!.layoutInflater
18 | val layout = inflater.inflate(R.layout.view_edittext, null)
19 | editText = layout.findViewById(R.id.edit_text)
20 |
21 | return AlertDialog.Builder(context!!).apply {
22 | setTitle(R.string.title_two_factor_auth)
23 | setView(layout)
24 | setNegativeButton(R.string.action_cancel, this@TwoFactorAuthDialog)
25 | // set click listener later to avoid auto dismiss
26 | setPositiveButton(R.string.action_sign_in, null)
27 | }.create()
28 | }
29 |
30 | override fun onStart() {
31 | super.onStart()
32 |
33 | val dialog = dialog as AlertDialog
34 |
35 | dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener {
36 | onClick(dialog, DialogInterface.BUTTON_POSITIVE)
37 | }
38 | }
39 |
40 | override fun onClick(dialog: DialogInterface, which: Int) {
41 | when (which) {
42 | DialogInterface.BUTTON_NEGATIVE -> dialog.cancel()
43 | DialogInterface.BUTTON_POSITIVE -> {
44 | val code = editText.text.toString()
45 | if (code.length != 6) {
46 | editText.error = getString(R.string.error_invalid_auth_code)
47 | return
48 | }
49 |
50 | // dialog will auto close after click, check code format at outside
51 | RxBus.post(TwoFactorAuthEvent(code))
52 | dismiss()
53 | }
54 | }
55 | }
56 |
57 | override fun onCancel(dialog: DialogInterface) {
58 | super.onCancel(dialog)
59 |
60 | RxBus.post(TwoFactorAuthEvent())
61 | }
62 |
63 | class TwoFactorAuthEvent(val code: String?) : BaseEvent() {
64 | constructor() : this(null)
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/czbix/v2ex/ui/loader/TopicListLoader.kt:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.ui.loader
2 |
3 | import android.content.Context
4 | import androidx.collection.ArraySet
5 | import com.czbix.v2ex.db.TopicRecordDao
6 | import com.czbix.v2ex.model.Page
7 | import com.czbix.v2ex.model.Topic
8 | import com.czbix.v2ex.network.RequestHelper
9 | import kotlinx.coroutines.runBlocking
10 |
11 | class TopicListLoader(
12 | context: Context,
13 | private val mPage: Page,
14 | private val dao: TopicRecordDao
15 | ) : AsyncTaskLoader(context) {
16 | @Throws(Exception::class)
17 | override fun loadInBackgroundWithException(): TopicList {
18 | val topics = runBlocking {
19 | RequestHelper.getTopics(mPage)
20 | }
21 |
22 | val readed = topics.filter { topic ->
23 | val lastRead = dao.getLastReadComment(topic.id) ?: 0
24 | lastRead >= topic.replyCount
25 | }.map {
26 | it.id
27 | }
28 |
29 | topics.readed = ArraySet(readed)
30 |
31 | return topics
32 | }
33 |
34 | class TopicList(
35 | list: List,
36 | val isFavorited: Boolean,
37 | val onceToken: String? = null
38 | ): List by list {
39 | lateinit var readed: Set
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/czbix/v2ex/ui/model/NightModeViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.ui.model
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.lifecycle.ViewModel
5 | import androidx.lifecycle.map
6 | import com.czbix.v2ex.model.PreferenceStorage
7 | import javax.inject.Inject
8 |
9 | enum class NightMode(val key: String) {
10 | LIGHT("light"),
11 | DARK("dark"),
12 | SYSTEM("system");
13 |
14 | companion object {
15 | fun fromKey(key: String): NightMode {
16 | for (mode in values()) {
17 | if (mode.key == key) {
18 | return mode
19 | }
20 | }
21 |
22 | error("Unknown key: $key")
23 | }
24 | }
25 | }
26 |
27 | class NightModeViewModel @Inject constructor(
28 | private val nightModeDelegate: NightModeDelegate
29 | ) : ViewModel(), NightModeDelegate by nightModeDelegate
30 |
31 | interface NightModeDelegate {
32 | val nightMode: LiveData
33 | val currentNightMode: NightMode
34 | }
35 |
36 | class NightModeDelegateImpl @Inject constructor(
37 | private val prefs: PreferenceStorage
38 | ) : NightModeDelegate {
39 | override val nightMode by lazy {
40 | prefs.nightModeObservable.map {
41 | NightMode.fromKey(it)
42 | }
43 | }
44 |
45 | override val currentNightMode
46 | get() = NightMode.fromKey(prefs.nightMode)
47 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/czbix/v2ex/ui/settings/SettingsModule.kt:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.ui.settings
2 |
3 | import androidx.lifecycle.ViewModel
4 | import com.czbix.v2ex.inject.FragmentScoped
5 | import com.czbix.v2ex.inject.ViewModelKey
6 | import dagger.Binds
7 | import dagger.Module
8 | import dagger.android.ContributesAndroidInjector
9 | import dagger.multibindings.IntoMap
10 |
11 | @Module
12 | abstract class SettingsModule {
13 | @FragmentScoped
14 | @ContributesAndroidInjector
15 | abstract fun prefsFragment(): PrefsFragment
16 |
17 | @Binds
18 | @IntoMap
19 | @ViewModelKey(SettingsViewModel::class)
20 | abstract fun bindSettingsViewModel(viewModel: SettingsViewModel): ViewModel
21 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/czbix/v2ex/ui/settings/SettingsViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.ui.settings
2 |
3 | import androidx.lifecycle.ViewModel
4 | import com.czbix.v2ex.model.PreferenceStorage
5 | import com.czbix.v2ex.ui.model.NightModeDelegate
6 | import javax.inject.Inject
7 |
8 | class SettingsViewModel @Inject constructor(
9 | private val nightModeDelegate: NightModeDelegate,
10 | private val prefs: PreferenceStorage
11 | ) : ViewModel() {
12 | val nightMode
13 | get() = nightModeDelegate.currentNightMode.key
14 |
15 | fun setNightMode(key: String) {
16 | prefs.nightMode = key
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/czbix/v2ex/ui/widget/AvatarView.kt:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.ui.widget
2 |
3 | import android.content.Context
4 | import android.graphics.drawable.Drawable
5 | import android.util.AttributeSet
6 | import androidx.appcompat.widget.AppCompatImageView
7 | import com.airbnb.epoxy.preload.ViewMetadata
8 | import com.bumptech.glide.RequestBuilder
9 | import com.bumptech.glide.RequestManager
10 | import com.czbix.v2ex.R
11 | import com.czbix.v2ex.db.Member
12 | import com.czbix.v2ex.model.Avatar
13 | import com.czbix.v2ex.util.withCrossFade
14 |
15 | class AvatarView @JvmOverloads constructor(
16 | context: Context,
17 | attrs: AttributeSet? = null,
18 | defStyleAttr: Int = 0
19 | ) : AppCompatImageView(context, attrs, defStyleAttr) {
20 | private val realSize by lazy {
21 | layoutParams.width - paddingTop * 2
22 | }
23 |
24 | fun setAvatar(glide: RequestManager, avatar: Avatar?) {
25 | if (avatar == null) {
26 | glide.clear(this)
27 | return
28 | }
29 |
30 | getImgRequest(glide, avatar).into(this)
31 | }
32 |
33 | fun getImgRequest(glide: RequestManager, avatar: Avatar): RequestBuilder {
34 | val size = realSize
35 | val url = avatar.getUrlByPx(size)
36 |
37 | return glide.load(url).placeholder(R.drawable.avatar_default)
38 | .override(size, size).fitCenter()
39 | .withCrossFade()
40 | }
41 |
42 | interface OnAvatarActionListener {
43 | fun onMemberClick(member: Member)
44 | }
45 |
46 | class Metadata(
47 | val avatarView: AvatarView
48 | ) : ViewMetadata
49 | }
50 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/czbix/v2ex/util/CoroutineUtils.kt:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.util
2 |
3 | import kotlinx.coroutines.*
4 | import kotlinx.coroutines.channels.ReceiveChannel
5 | import kotlinx.coroutines.channels.consumeEach
6 | import kotlinx.coroutines.channels.produce
7 | import kotlin.coroutines.CoroutineContext
8 | import kotlin.coroutines.EmptyCoroutineContext
9 |
10 |
11 | suspend fun ReceiveChannel.debounce(
12 | wait: Long,
13 | scope: CoroutineScope = GlobalScope,
14 | context: CoroutineContext = EmptyCoroutineContext
15 | ): ReceiveChannel = scope.produce(context) {
16 | var lastTimeout: Job? = null
17 | consumeEach {
18 | lastTimeout?.cancel()
19 | lastTimeout = launch {
20 | delay(wait)
21 | send(it)
22 | }
23 | }
24 | lastTimeout?.join()
25 | }
26 |
27 | fun ReceiveChannel.throttle(
28 | wait: Long,
29 | scope: CoroutineScope = GlobalScope,
30 | context: CoroutineContext = EmptyCoroutineContext
31 | ): ReceiveChannel = scope.produce(context) {
32 | var nextTime = 0L
33 | consumeEach {
34 | val curTime = System.currentTimeMillis()
35 | if (curTime >= nextTime) {
36 | nextTime = curTime + wait
37 | send(it)
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/czbix/v2ex/util/CrashlyticsTree.kt:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.util
2 |
3 | import android.util.Log
4 | import com.google.firebase.crashlytics.FirebaseCrashlytics
5 | import timber.log.Timber
6 |
7 | class CrashlyticsTree : Timber.Tree() {
8 | override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
9 | if (priority == Log.VERBOSE) {
10 | return
11 | }
12 |
13 | val crashlytics = FirebaseCrashlytics.getInstance()
14 | crashlytics.log(message)
15 | if (t != null) {
16 | crashlytics.recordException(t)
17 | }
18 | }
19 | }
20 |
21 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/czbix/v2ex/util/ExceptionUtils.kt:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.util
2 |
3 | import android.widget.Toast
4 | import androidx.fragment.app.Fragment
5 | import com.czbix.v2ex.R
6 | import com.czbix.v2ex.common.exception.*
7 | import com.czbix.v2ex.network.HttpStatus
8 | import timber.log.Timber
9 |
10 | object ExceptionUtils {
11 | /**
12 | * warp exception in [.handleException] with [FatalException]
13 | */
14 | @JvmStatic
15 | @Deprecated("Should not crash the app")
16 | fun handleExceptionNoCatch(fragment: Fragment, ex: Exception): Boolean {
17 | try {
18 | return handleException(fragment, ex)
19 | } catch (e: Exception) {
20 | throw FatalException(e)
21 | }
22 | }
23 |
24 | @JvmStatic
25 | fun handleException(fragment: Fragment, e: Exception): Boolean {
26 | val activity = fragment.activity
27 | var needFinishActivity = false
28 | val stringId: Int
29 |
30 | when (e) {
31 | is RestrictedException -> {
32 | needFinishActivity = true
33 | stringId = R.string.toast_access_restricted
34 | }
35 | is UnauthorizedException -> {
36 | needFinishActivity = true
37 | stringId = R.string.toast_need_sign_in
38 | }
39 | is ConnectionException -> {
40 | stringId = R.string.toast_connection_exception
41 | }
42 | is RemoteException -> {
43 | stringId = R.string.toast_remote_exception
44 | }
45 | is RequestException -> {
46 | if (e.isShouldLogged) {
47 | Timber.e(e)
48 | }
49 | stringId = when (e.code) {
50 | HttpStatus.SC_FORBIDDEN -> R.string.toast_access_denied
51 | else -> R.string.toast_unknown_error
52 | }
53 | }
54 | is IllegalStateException -> {
55 | var logException = true
56 | if (e is ExIllegalStateException) {
57 | logException = e.shouldLogged
58 | }
59 | if (logException) {
60 | Timber.e(e)
61 | } else {
62 | Timber.i(e)
63 | }
64 |
65 | stringId = R.string.toast_parse_failed
66 | }
67 | is RuntimeException -> {
68 | Timber.e(e)
69 | stringId = R.string.toast_parse_failed
70 | }
71 | else -> {
72 | Timber.e(e,"Unknown error")
73 | stringId = R.string.toast_unknown_error
74 | }
75 | }
76 |
77 | Toast.makeText(activity, stringId, Toast.LENGTH_LONG).show()
78 |
79 | return needFinishActivity
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/czbix/v2ex/util/GsonUtils.kt:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.util
2 |
3 | import com.google.gson.FieldNamingStrategy
4 | import com.google.gson.Gson
5 | import com.google.gson.GsonBuilder
6 | import com.google.gson.reflect.TypeToken
7 | import java.io.Reader
8 | import java.lang.reflect.Field
9 | import java.util.regex.Pattern
10 |
11 | private class AndroidFieldNamingStrategy : FieldNamingStrategy {
12 | override fun translateName(f: Field): String {
13 | if (f.name.startsWith("m")) {
14 | return handleWords(f.name.substring(1))
15 | } else {
16 | return f.name
17 | }
18 | }
19 |
20 | private fun handleWords(fieldName: String): String {
21 | val words = UPPERCASE_PATTERN.split(fieldName)
22 | val sb = StringBuilder()
23 | for (word in words) {
24 | if (sb.length > 0) {
25 | sb.append(JSON_WORD_DELIMITER)
26 | }
27 | sb.append(word.toLowerCase())
28 | }
29 | return sb.toString()
30 | }
31 |
32 | companion object {
33 | private val JSON_WORD_DELIMITER = "_"
34 |
35 | private val UPPERCASE_PATTERN = Pattern.compile("(?=\\p{Lu})")
36 | }
37 | }
38 |
39 |
40 | val GSON: Gson = GsonBuilder().apply {
41 | setFieldNamingStrategy(AndroidFieldNamingStrategy())
42 | }.create()
43 |
44 | inline fun String.fromJson(): T {
45 | return GSON.fromJson(this, T::class.java)
46 | }
47 |
48 | @Suppress("UNUSED_PARAMETER")
49 | inline fun String.fromJson(isGenericType: Boolean): T {
50 | return GSON.fromJson(this, object : TypeToken(){}.type)
51 | }
52 |
53 | inline fun Reader.fromJson(): T {
54 | return GSON.fromJson(this, T::class.java)
55 | }
56 |
57 | @Suppress("UNUSED_PARAMETER")
58 | inline fun Reader.fromJson(isGenericType: Boolean): T {
59 | return GSON.fromJson(this, object : TypeToken(){}.type)
60 | }
61 |
62 | inline fun T.toJson(): String {
63 | return GSON.toJson(this, T::class.java)
64 | }
65 |
66 | @Suppress("UNUSED_PARAMETER")
67 | inline fun T.toJson(isGenericType: Boolean): String {
68 | return GSON.toJson(this, object : TypeToken(){}.type)
69 | }
70 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/czbix/v2ex/util/IoUtils.kt:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.util
2 |
3 | import com.czbix.v2ex.AppCtx
4 |
5 | import java.io.File
6 |
7 | object IoUtils {
8 | private var cacheDir: File
9 |
10 | init {
11 | cacheDir = AppCtx.instance.cacheDir
12 | }
13 |
14 | @JvmStatic
15 | val webCachePath: File by lazy {
16 | File(cacheDir, "webCache")
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/czbix/v2ex/util/LiveDataUtils.kt:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.util
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.lifecycle.MediatorLiveData
5 | import androidx.lifecycle.MutableLiveData
6 | import com.czbix.v2ex.model.EmptyLiveData
7 | import com.czbix.v2ex.model.Resource
8 | import kotlinx.coroutines.CoroutineScope
9 | import kotlinx.coroutines.GlobalScope
10 | import kotlinx.coroutines.launch
11 | import kotlin.coroutines.CoroutineContext
12 | import kotlin.coroutines.EmptyCoroutineContext
13 |
14 |
15 | fun liveData(
16 | scope: CoroutineScope = GlobalScope,
17 | context: CoroutineContext = EmptyCoroutineContext,
18 | block: suspend () -> T
19 | ): LiveData {
20 | val liveData = MutableLiveData()
21 | scope.launch(context) {
22 | val data = block()
23 | liveData.postValue(data)
24 | }
25 |
26 | return liveData
27 | }
28 |
29 | fun emptyLiveData() = EmptyLiveData.create()
30 |
31 | fun LiveData.then(condition: ((T) -> Boolean)? = null, block: () -> LiveData): LiveData {
32 | var conditionMeet = false
33 | var valueEmitted = false
34 |
35 | val result = MediatorLiveData()
36 | val liveData = block()
37 |
38 | val emit = { it: R? ->
39 | if (conditionMeet && valueEmitted) {
40 | result.value = it ?: liveData.value
41 | }
42 | }
43 |
44 | result.addSource(this) {
45 | if (condition == null || condition(it)) {
46 | result.removeSource(this)
47 |
48 | conditionMeet = true
49 | emit(null)
50 | }
51 | }
52 | result.addSource(liveData) {
53 | if (!valueEmitted) {
54 | valueEmitted = true
55 | }
56 | emit(it)
57 | }
58 |
59 | return result
60 | }
61 |
62 | fun LiveData>.then(block: () -> LiveData): LiveData {
63 | return this.then({ it.status == Resource.Status.SUCCESS}, block)
64 | }
65 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/czbix/v2ex/util/RxUtils.kt:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.util
2 |
3 | import io.reactivex.Single
4 | import io.reactivex.android.schedulers.AndroidSchedulers
5 | import io.reactivex.disposables.Disposable
6 | import io.reactivex.internal.observers.ConsumerSingleObserver
7 |
8 | fun Single.await(onSuccess: (T) -> Unit): Disposable {
9 | return this.observeOn(AndroidSchedulers.mainThread()).subscribe(onSuccess)
10 | }
11 |
12 | fun Single.await(onSuccess: (T) -> Unit, onError: (Throwable) -> Unit): Disposable {
13 | return this.observeOn(AndroidSchedulers.mainThread()).subscribe(onSuccess, onError)
14 | }
15 |
16 | fun MutableList.dispose() {
17 | this.forEach { it.dispose() }
18 | this.clear()
19 | }
20 |
21 | /**
22 | * @see io.reactivex.exceptions.Exceptions.propagate
23 | */
24 | fun Single.result(): T {
25 | try {
26 | return this.blockingGet()
27 | } catch (e: RuntimeException) {
28 | // unwarp RuntimeException for Exceptions.propagate
29 | val cause = e.cause
30 | when {
31 | cause == null -> throw e
32 | cause === e -> throw e
33 | else -> throw cause
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/app/src/main/res/color/btn_reply_submit.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-anydpi/ic_arrow_back_black_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-anydpi/ic_close_black_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-anydpi/ic_dashboard_black_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-anydpi/ic_explore_black_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-anydpi/ic_favorite_black_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-anydpi/ic_feedback_black_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-anydpi/ic_info_outline_black_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-anydpi/ic_notifications_black_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-anydpi/ic_notifications_none_black_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-anydpi/ic_send_black_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-anydpi/ic_settings_black_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi-v23/window_background_statusbar_toolbar_tab.9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-hdpi-v23/window_background_statusbar_toolbar_tab.9.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_card_giftcard_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-hdpi/ic_card_giftcard_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_favorite_border_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-hdpi/ic_favorite_border_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_favorite_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-hdpi/ic_favorite_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_keyboard_arrow_down_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-hdpi/ic_keyboard_arrow_down_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_notifications_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-hdpi/ic_notifications_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_refresh_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-hdpi/ic_refresh_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_search_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-hdpi/ic_search_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_sync_disabled_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-hdpi/ic_sync_disabled_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_sync_problem_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-hdpi/ic_sync_problem_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_sync_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-hdpi/ic_sync_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/window_background_statusbar_toolbar_tab.9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-hdpi/window_background_statusbar_toolbar_tab.9.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi-v23/window_background_statusbar_toolbar_tab.9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-mdpi-v23/window_background_statusbar_toolbar_tab.9.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/avatar_default.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-mdpi/avatar_default.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_card_giftcard_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-mdpi/ic_card_giftcard_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_favorite_border_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-mdpi/ic_favorite_border_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_favorite_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-mdpi/ic_favorite_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_keyboard_arrow_down_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-mdpi/ic_keyboard_arrow_down_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_notifications_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-mdpi/ic_notifications_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_refresh_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-mdpi/ic_refresh_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_search_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-mdpi/ic_search_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_sync_disabled_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-mdpi/ic_sync_disabled_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_sync_problem_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-mdpi/ic_sync_problem_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_sync_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-mdpi/ic_sync_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/window_background_statusbar_toolbar_tab.9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-mdpi/window_background_statusbar_toolbar_tab.9.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-sw600dp-hdpi-v23/window_background_statusbar_toolbar_tab.9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-sw600dp-hdpi-v23/window_background_statusbar_toolbar_tab.9.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-sw600dp-hdpi/window_background_statusbar_toolbar_tab.9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-sw600dp-hdpi/window_background_statusbar_toolbar_tab.9.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-sw600dp-mdpi-v23/window_background_statusbar_toolbar_tab.9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-sw600dp-mdpi-v23/window_background_statusbar_toolbar_tab.9.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-sw600dp-mdpi/window_background_statusbar_toolbar_tab.9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-sw600dp-mdpi/window_background_statusbar_toolbar_tab.9.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-sw600dp-xhdpi-v23/window_background_statusbar_toolbar_tab.9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-sw600dp-xhdpi-v23/window_background_statusbar_toolbar_tab.9.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-sw600dp-xhdpi/window_background_statusbar_toolbar_tab.9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-sw600dp-xhdpi/window_background_statusbar_toolbar_tab.9.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-sw600dp-xxhdpi-v23/window_background_statusbar_toolbar_tab.9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-sw600dp-xxhdpi-v23/window_background_statusbar_toolbar_tab.9.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-sw600dp-xxhdpi/window_background_statusbar_toolbar_tab.9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-sw600dp-xxhdpi/window_background_statusbar_toolbar_tab.9.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-sw600dp-xxxhdpi-v23/window_background_statusbar_toolbar_tab.9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-sw600dp-xxxhdpi-v23/window_background_statusbar_toolbar_tab.9.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-sw600dp-xxxhdpi/window_background_statusbar_toolbar_tab.9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-sw600dp-xxxhdpi/window_background_statusbar_toolbar_tab.9.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi-v23/window_background_statusbar_toolbar_tab.9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-xhdpi-v23/window_background_statusbar_toolbar_tab.9.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_card_giftcard_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-xhdpi/ic_card_giftcard_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_favorite_border_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-xhdpi/ic_favorite_border_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_favorite_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-xhdpi/ic_favorite_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_keyboard_arrow_down_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-xhdpi/ic_keyboard_arrow_down_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_notifications_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-xhdpi/ic_notifications_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_refresh_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-xhdpi/ic_refresh_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_search_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-xhdpi/ic_search_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_sync_disabled_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-xhdpi/ic_sync_disabled_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_sync_problem_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-xhdpi/ic_sync_problem_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_sync_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-xhdpi/ic_sync_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/window_background_statusbar_toolbar_tab.9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-xhdpi/window_background_statusbar_toolbar_tab.9.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi-v23/window_background_statusbar_toolbar_tab.9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-xxhdpi-v23/window_background_statusbar_toolbar_tab.9.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_card_giftcard_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-xxhdpi/ic_card_giftcard_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_favorite_border_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-xxhdpi/ic_favorite_border_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_favorite_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-xxhdpi/ic_favorite_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_keyboard_arrow_down_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-xxhdpi/ic_keyboard_arrow_down_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_notifications_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-xxhdpi/ic_notifications_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_refresh_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-xxhdpi/ic_refresh_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_search_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-xxhdpi/ic_search_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_sync_disabled_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-xxhdpi/ic_sync_disabled_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_sync_problem_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-xxhdpi/ic_sync_problem_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_sync_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-xxhdpi/ic_sync_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/window_background_statusbar_toolbar_tab.9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-xxhdpi/window_background_statusbar_toolbar_tab.9.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi-v23/window_background_statusbar_toolbar_tab.9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-xxxhdpi-v23/window_background_statusbar_toolbar_tab.9.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/ic_card_giftcard_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-xxxhdpi/ic_card_giftcard_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/ic_favorite_border_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-xxxhdpi/ic_favorite_border_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/ic_favorite_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-xxxhdpi/ic_favorite_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/ic_keyboard_arrow_down_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-xxxhdpi/ic_keyboard_arrow_down_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/ic_notifications_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-xxxhdpi/ic_notifications_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/ic_refresh_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-xxxhdpi/ic_refresh_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/ic_search_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-xxxhdpi/ic_search_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/ic_sync_disabled_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-xxxhdpi/ic_sync_disabled_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/ic_sync_problem_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-xxxhdpi/ic_sync_problem_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/ic_sync_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-xxxhdpi/ic_sync_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/window_background_statusbar_toolbar_tab.9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/drawable-xxxhdpi/window_background_statusbar_toolbar_tab.9.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/bg_author_flag.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
10 |
14 |
15 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/btn_jump_back_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/count_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
11 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_account_circle.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_brightness_medium_outline.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_cancel_outline_black_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_comment_black_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_comment_outline.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_content_copy_outline_black_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_get_app_black_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_image_outline.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_info_outline_black_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_person_add_outline.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_share_black_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_star_black_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_star_border_black_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_undo_black_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_update_black_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/img_topic_image_loading.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/radius_box.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_loading.xml:
--------------------------------------------------------------------------------
1 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
7 |
12 |
13 |
19 |
20 |
21 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_topic.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
16 |
17 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_topic_edit.xml:
--------------------------------------------------------------------------------
1 |
11 |
12 |
16 |
24 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/app_bar_layout.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/container_fragment.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_node_list.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_notification_list.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_topic.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
16 |
17 |
22 |
23 |
24 |
37 |
38 |
48 |
49 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_topic_list.xml:
--------------------------------------------------------------------------------
1 |
6 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/model_comments_footer.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
10 |
13 |
14 |
15 |
18 |
19 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/model_postscript.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
10 |
13 |
16 |
17 |
18 |
25 |
26 |
32 |
33 |
44 |
45 |
55 |
56 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/nav_header.xml:
--------------------------------------------------------------------------------
1 |
2 |
12 |
19 |
20 |
28 |
29 |
43 |
44 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/nav_view.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/recycle_view.xml:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/tab_layout.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
12 |
16 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/toolbar_action.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/view_alert_dialog.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/view_comment.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/view_content_image.xml:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/view_content_text.xml:
--------------------------------------------------------------------------------
1 |
2 |
16 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/view_edittext.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
16 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/view_float_topic.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
13 |
14 |
19 |
20 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/view_node.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
11 |
19 |
28 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/view_reply_form.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
15 |
16 |
28 |
29 |
40 |
41 |
51 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/view_search_box.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
15 |
26 |
27 |
37 |
38 |
50 |
51 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/view_select_node.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
9 |
10 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/view_topic.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/menu_comment.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/menu_drawer.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/menu_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/menu_nodes.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/menu_topic.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/menu_topic_edit.xml:
--------------------------------------------------------------------------------
1 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/menu_topic_list.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/values-night/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #444
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values-w820dp/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 64dp
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values-zh/arrays.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | - 浅色
5 | - 深色
6 | - 系统
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/values/arrays.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | - Light
5 | - Dark
6 | - System
7 |
8 |
9 |
10 | - light
11 | - dark
12 | - system
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #4caf50
4 | #607d8b
5 | #455a64
6 | #263238
7 | #448aff
8 | @color/material_blue_grey_500
9 | #88000000
10 | #eee
11 | @color/placeholder
12 |
--------------------------------------------------------------------------------
/app/src/main/res/values/config.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | AEdPqrEAAAAIA-7Ap1EsED8jjzKblgxW7060iEU8t2tCiviIRA
4 | W/"a57d2379cb65e3aee54669369f755513fac12400"
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 16dp
5 | 48dp
6 |
7 | 64dp
8 | 48dp
9 | 48dp
10 | 48dp
11 |
12 | 8dp
13 |
14 | 40dp
15 | 4dp
16 | 8dp
17 |
18 |
--------------------------------------------------------------------------------
/app/src/main/res/values/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #E0E0E0
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/ids.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 100
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/values/intent_filter.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | http
4 | https
5 |
6 | v2ex.com
7 | www.v2ex.com
8 |
9 | /t/
10 | /go/
11 | /member/
12 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings_activity_login.xml:
--------------------------------------------------------------------------------
1 |
2 | Sign in
3 |
4 |
5 | Account
6 | Password
7 | Captcha
8 | Load Captcha
9 | Sign in
10 | Sign up
11 | Reset password
12 | V2EX = way to explore\n一个关于分享和探索的地方
13 | Welcome back, %s!
14 |
15 | Two factor auth
16 | This field is required
17 | Invalid auth code
18 | Load Captcha failed!
19 | Sign in failed!
20 | Google Sign In
21 |
22 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings_activity_settings.xml:
--------------------------------------------------------------------------------
1 |
2 | Settings
3 | General
4 | User info
5 | Log out
6 | Sign in
7 | User
8 | Customize tabs to show
9 | Long press to sort
10 | Receive notifications
11 | Receive notifications via FCM service
12 | Google Play services not available,\n error code: %s
13 | Always show reply form
14 | 2.5D Touch (Experimental)
15 | Quick preview topic by deeply press
16 | Content be selectable
17 | May cause crash on some devices
18 | Enable undo
19 | Delay before send request, so you may cancel it.
20 | Version
21 |
22 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
12 |
13 |
16 |
17 |
24 |
25 |
28 |
29 |
33 |
34 |
38 |
39 |
43 |
44 |
49 |
50 |
53 |
54 |
58 |
59 |
64 |
65 |
--------------------------------------------------------------------------------
/app/src/test/kotlin/com/czbix/v2ex/util/MiscUtilsTest.kt:
--------------------------------------------------------------------------------
1 | package com.czbix.v2ex.util
2 |
3 | import io.kotlintest.data.forall
4 | import io.kotlintest.shouldBe
5 | import io.kotlintest.specs.FunSpec
6 | import io.kotlintest.tables.row
7 |
8 | class MiscUtilsTest : FunSpec({
9 | test("decode cf email") {
10 | forall(
11 | row("99aeaba9e9d9aeafa1a9ffe9ea", "720p@7680fps"),
12 | row("19282921296959202f297f696a", "1080p@960fps")
13 | ) { encoded, decoded ->
14 | MiscUtils.decodeCfEmail(encoded) shouldBe decoded
15 | }
16 | }
17 | })
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 | repositories {
5 | google()
6 | mavenCentral()
7 | }
8 | dependencies {
9 | classpath 'com.android.tools.build:gradle:8.3.0'
10 |
11 | // NOTE: Do not place your application dependencies here; they belong
12 | // in the individual module build.gradle files
13 | }
14 | }
15 |
16 | allprojects {
17 | buildscript {
18 | repositories {
19 | google()
20 | mavenCentral()
21 | }
22 | }
23 |
24 | repositories {
25 | google()
26 | mavenCentral()
27 | }
28 | }
29 |
30 | task clean(type: Delete) {
31 | delete rootProject.buildDir
32 | }
33 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m
13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
14 |
15 | # When configured, Gradle will run in incubating parallel mode.
16 | # This option should only be used with decoupled projects. More details, visit
17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
18 | # org.gradle.parallel=true
19 | android.enableJetifier=true
20 | android.nonFinalResIds=false
21 | android.nonTransitiveRClass=false
22 | android.useAndroidX=true
23 | org.gradle.jvmargs=-Xmx2g
24 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CzBiX/v2ex-android/e18b6722babeaf771a51c5b714d234edbf6f6fa1/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
--------------------------------------------------------------------------------