├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── pr_ci.yml │ └── push_ci.yml ├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── app.iml ├── build.gradle.kts ├── debug.jks ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── cc │ │ └── ptt │ │ └── android │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── ic_launcher-web.png │ ├── java │ │ └── cc │ │ │ └── ptt │ │ │ └── android │ │ │ ├── HomeActivity.kt │ │ │ ├── Navigation.kt │ │ │ ├── PttApplication.kt │ │ │ ├── articlelist │ │ │ ├── ArticleListAdapter.kt │ │ │ ├── ArticleListFragment.kt │ │ │ ├── ArticleListViewModel.kt │ │ │ └── viewholder │ │ │ │ ├── DeletedViewHolder.kt │ │ │ │ └── PostViewHolder.kt │ │ │ ├── articleread │ │ │ ├── ArticleReadAdapter.kt │ │ │ ├── ArticleReadFragment.kt │ │ │ └── ArticleReadViewModel.kt │ │ │ ├── articlesearch │ │ │ └── ArticleListSearchFragment.kt │ │ │ ├── base │ │ │ ├── BaseActivity.kt │ │ │ └── BaseFragment.kt │ │ │ ├── common │ │ │ ├── ClickFix.kt │ │ │ ├── CustomLinearLayoutManager.kt │ │ │ ├── GeneralFragmentStatePagerAdapter.kt │ │ │ ├── KeyboardUtils.kt │ │ │ ├── RecyclerItemClickListener.kt │ │ │ ├── ResourcesUtils.kt │ │ │ ├── TextViewMovementMethod.kt │ │ │ ├── UIUtils.kt │ │ │ ├── WebUtils.kt │ │ │ ├── dragitemmove │ │ │ │ ├── ItemMoveCallback.kt │ │ │ │ └── StartDragListener.kt │ │ │ ├── event │ │ │ │ └── Event.kt │ │ │ ├── extension │ │ │ │ ├── ArgumentsDelegateExtensions.kt │ │ │ │ ├── LifecycleExtentions.kt │ │ │ │ └── NavExt.kt │ │ │ └── stickyheader │ │ │ │ ├── StickyAdapter.kt │ │ │ │ └── StickyHeaderItemDecorator.kt │ │ │ ├── di │ │ │ └── ViewModelModules.kt │ │ │ ├── home │ │ │ ├── EmptyFragment.kt │ │ │ ├── HomeFragment.kt │ │ │ ├── favoriteboards │ │ │ │ ├── FavoriteBoardsFragment.kt │ │ │ │ ├── FavoriteBoardsListAdapter.kt │ │ │ │ └── FavoriteBoardsViewModel.kt │ │ │ ├── hotarticle │ │ │ │ ├── HotArticleFilterAdapter.kt │ │ │ │ ├── HotArticleFilterFragment.kt │ │ │ │ ├── HotArticleListAdapter.kt │ │ │ │ ├── HotArticleListFragment.kt │ │ │ │ └── HotArticleListViewModel.kt │ │ │ ├── hotboard │ │ │ │ ├── HotBoardsFragment.kt │ │ │ │ ├── HotBoardsListAdapter.kt │ │ │ │ └── HotBoardsViewModel.kt │ │ │ ├── personalpage │ │ │ │ ├── PersonInfoFragment.kt │ │ │ │ └── PersonalPageFragment.kt │ │ │ └── setting │ │ │ │ ├── SettingAdapter.kt │ │ │ │ ├── SettingFragment.kt │ │ │ │ └── SettingViewModel.kt │ │ │ ├── login │ │ │ ├── LoginPageFragment.kt │ │ │ └── LoginPageViewModel.kt │ │ │ ├── postarticle │ │ │ └── PostArticleFragment.kt │ │ │ └── searchboards │ │ │ ├── SearchBoardsAdapter.kt │ │ │ ├── SearchBoardsFragment.kt │ │ │ └── SearchBoardsModel.kt │ └── res │ │ ├── anim │ │ ├── fade_in.xml │ │ ├── fade_out.xml │ │ ├── no_anim.xml │ │ ├── slide_in_right_250.xml │ │ └── slide_out_right_250.xml │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable-xxhdpi │ │ └── ic_launcher.png │ │ ├── drawable │ │ ├── article_list_search_bar_background.xml │ │ ├── botton_background.xml │ │ ├── botton_background_selector.xml │ │ ├── botton_background_selector_re.xml │ │ ├── botton_state_pressed.xml │ │ ├── buttom_background.xml │ │ ├── clock.png │ │ ├── commit.png │ │ ├── component_10_3.png │ │ ├── dialog_background.xml │ │ ├── down.png │ │ ├── edit2.png │ │ ├── edit_bar_background.xml │ │ ├── edittext_background.xml │ │ ├── home_botton_item_color_selector.xml │ │ ├── ic_baseline_more_horiz_24.xml │ │ ├── ic_baseline_person_24.xml │ │ ├── ic_baseline_visibility_24.xml │ │ ├── ic_baseline_visibility_off_24.xml │ │ ├── ic_baseline_whatshot_24.xml │ │ ├── ic_check_black_24dp.xml │ │ ├── ic_chevron_left_black_24dp.xml │ │ ├── ic_clear_black_24dp.xml │ │ ├── ic_close_black_24dp.xml │ │ ├── ic_component_10_.xml │ │ ├── ic_component_10_2.xml │ │ ├── ic_component_8.xml │ │ ├── ic_dehaze_black_24dp.xml │ │ ├── ic_delete_black_24dp.xml │ │ ├── ic_edit_24px.xml │ │ ├── ic_expand_more_black_24dp.xml │ │ ├── ic_home_fragment_item_default_24dp.xml │ │ ├── ic_home_fragment_item_favorite_24dp.xml │ │ ├── ic_image_black_24dp.xml │ │ ├── ic_keyboard_arrow_right_black_24dp.xml │ │ ├── ic_keyboard_hide_black_24dp.xml │ │ ├── ic_launcher_background.xml │ │ ├── ic_like.xml │ │ ├── ic_mode_edit_black_24dp.xml │ │ ├── ic_more_24.xml │ │ ├── ic_more_horiz_black_24dp.xml │ │ ├── ic_more_vert_black_24dp.xml │ │ ├── ic_post_add_24px.xml │ │ ├── ic_replay_24px__1_.xml │ │ ├── ic_reply_black_24dp.xml │ │ ├── ic_save_black_24dp.xml │ │ ├── ic_search_24.xml │ │ ├── ic_search_black_24dp.xml │ │ ├── ic_send_black_24dp.xml │ │ ├── ic_share_black_24dp.xml │ │ ├── ic_sharp_drag_handle_24.xml │ │ ├── image_click.xml │ │ ├── more.png │ │ ├── person.png │ │ ├── person_picture.jpeg │ │ ├── rectangle.png │ │ ├── rectangle2.png │ │ ├── search_bar_background.xml │ │ ├── shadow_left.png │ │ ├── shadow_right.png │ │ ├── splash_drawable.xml │ │ ├── tab_state_select.xml │ │ ├── tab_state_selector.xml │ │ ├── tab_state_unselect.xml │ │ ├── un_fav.png │ │ ├── up.png │ │ ├── user1.png │ │ └── user2.png │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── article_list_fragment_layout.xml │ │ ├── article_list_item.xml │ │ ├── article_list_item_delete.xml │ │ ├── article_list_search_fragment_layout.xml │ │ ├── article_read_fragment_layout.xml │ │ ├── article_read_item_center_bar.xml │ │ ├── article_read_item_commit.xml │ │ ├── article_read_item_commit_bar.xml │ │ ├── article_read_item_commit_sort.xml │ │ ├── article_read_item_content.xml │ │ ├── article_read_item_header.xml │ │ ├── article_read_item_image.xml │ │ ├── edit_dialog_layout.xml │ │ ├── empty_fragment_layout.xml │ │ ├── favorite_boards_fragment_layout.xml │ │ ├── fragment_setting.xml │ │ ├── home_fragment_layout.xml │ │ ├── hot_article_list_fragment_layout.xml │ │ ├── hot_article_list_item.xml │ │ ├── hot_article_list_item_more.xml │ │ ├── hot_article_list_item_title.xml │ │ ├── hot_article_list_item_title_subitem.xml │ │ ├── hot_article_list_item_title_top.xml │ │ ├── hot_boards_fragment_layout.xml │ │ ├── hot_boards_list_item.xml │ │ ├── hot_boards_list_item_edit.xml │ │ ├── item_setting.xml │ │ ├── login_page_fragment.xml │ │ ├── login_page_fragment_layout.xml │ │ ├── persional_page_fragment_layout.xml │ │ ├── personal_info_fragment_item_infobar.xml │ │ ├── personal_info_fragment_layout.xml │ │ ├── post_article_fragment_layout.xml │ │ ├── search_boards_fragment_layout.xml │ │ ├── search_boards_item.xml │ │ └── search_boards_item_2.xml │ │ ├── menu │ │ ├── article_list_bottom_navigation_menu.xml │ │ ├── article_list_search_bottom_navigation_menu.xml │ │ ├── carete_article_comment_type_menu.xml │ │ ├── home_bottom_navigation_menu.xml │ │ ├── post_article_bottom_navigation_menu.xml │ │ ├── post_article_bottom_navigation_menu2.xml │ │ ├── post_article_bottom_navigation_menu3.xml │ │ └── post_article_rank_menu.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── navigation │ │ ├── include_login.xml │ │ └── nav_graph.xml │ │ ├── values │ │ ├── attrs.xml │ │ ├── ic_launcher_background.xml │ │ ├── strings.xml │ │ └── styles.xml │ │ └── xml │ │ └── network_security_config.xml │ └── test │ ├── java │ └── cc │ │ └── ptt │ │ └── android │ │ └── ExampleUnitTest.kt │ └── resources │ └── api │ └── board │ ├── articles.json │ ├── popular_boards.json │ └── search_boards.json ├── build.gradle.kts ├── buildSrc ├── build.gradle.kts └── src │ └── main │ └── java │ └── Versions.kt ├── common ├── .gitignore ├── build.gradle.kts ├── consumer-rules.pro ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── cc │ │ └── ptt │ │ └── android │ │ └── common │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ └── java │ │ └── cc │ │ └── ptt │ │ └── android │ │ └── common │ │ ├── ResourcesProvider.kt │ │ ├── StaticValue.kt │ │ ├── StringUtils.kt │ │ ├── date │ │ ├── DateFormatUtils.kt │ │ └── DatePatternConstants.kt │ │ ├── di │ │ └── CommonModules.kt │ │ ├── extensions │ │ └── GsonExtensions.kt │ │ ├── logger │ │ ├── PttLogger.kt │ │ └── PttLoggerImpl.kt │ │ ├── network │ │ ├── api │ │ │ ├── ApiAnnotations.kt │ │ │ ├── ApiException.kt │ │ │ ├── ApiResponseInterceptor.kt │ │ │ ├── RetrofitServiceProvider.kt │ │ │ ├── TokenInterceptor.kt │ │ │ └── apihelper │ │ │ │ └── ApiHelper.kt │ │ └── retrofit │ │ │ ├── RetrofitBodyCallAdapter.kt │ │ │ ├── RetrofitFlowCallAdapterFactory.kt │ │ │ └── RetrofitResponseCallAdapter.kt │ │ ├── ptt │ │ ├── AidBean.kt │ │ ├── AidConverter.kt │ │ └── PttColor.kt │ │ └── security │ │ ├── AESKeyStoreHelper.kt │ │ └── AESKeyStoreHelperImpl.kt │ └── test │ └── java │ └── cc │ └── ptt │ └── android │ └── common │ ├── ExampleUnitTest.java │ ├── StringUtilsTest.kt │ └── ptt │ └── AidBeanTest.kt ├── data ├── .gitignore ├── build.gradle.kts ├── consumer-rules.pro ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── cc │ │ └── ptt │ │ └── android │ │ └── data │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── cc │ │ │ └── ptt │ │ │ └── android │ │ │ └── data │ │ │ ├── ApiHelperImpl.kt │ │ │ ├── TokenInterceptorImpl.kt │ │ │ ├── apiservices │ │ │ ├── article │ │ │ │ └── ArticleApi.kt │ │ │ ├── board │ │ │ │ └── BoardApi.kt │ │ │ └── user │ │ │ │ └── UserApi.kt │ │ │ ├── di │ │ │ ├── ApiModules.kt │ │ │ ├── LocalDataSourceModules.kt │ │ │ ├── PreferenceModules.kt │ │ │ ├── RemoteDataSourceModules.kt │ │ │ └── RepositoryModules.kt │ │ │ ├── model │ │ │ └── remote │ │ │ │ ├── Comment.kt │ │ │ │ ├── Post.kt │ │ │ │ ├── PostRank.kt │ │ │ │ ├── ServerMessage.kt │ │ │ │ ├── article │ │ │ │ ├── ArticleComment.kt │ │ │ │ ├── ArticleCommentsList.kt │ │ │ │ ├── ArticleDetail.kt │ │ │ │ ├── ArticleRank.kt │ │ │ │ ├── Color.kt │ │ │ │ ├── Content.kt │ │ │ │ └── hotarticle │ │ │ │ │ ├── HotArticle.kt │ │ │ │ │ └── HotArticleList.kt │ │ │ │ ├── board │ │ │ │ ├── article │ │ │ │ │ ├── Article.kt │ │ │ │ │ └── ArticleList.kt │ │ │ │ ├── hotboard │ │ │ │ │ ├── BoardList.kt │ │ │ │ │ ├── HotBoardTemp.kt │ │ │ │ │ └── HotBoardsItem.kt │ │ │ │ └── searchboard │ │ │ │ │ └── SearchBoardsItem.kt │ │ │ │ └── user │ │ │ │ ├── existuser │ │ │ │ ├── ExistUser.kt │ │ │ │ └── ExistUserRequest.kt │ │ │ │ ├── login │ │ │ │ ├── LoginEntity.kt │ │ │ │ └── LoginRequest.kt │ │ │ │ └── userid │ │ │ │ ├── UserIdEntity.kt │ │ │ │ └── UserIdRequest.kt │ │ │ ├── preference │ │ │ ├── MainPreferences.kt │ │ │ ├── MainPreferencesImpl.kt │ │ │ ├── UserInfoPreferences.kt │ │ │ └── UserInfoPreferencesImpl.kt │ │ │ ├── repository │ │ │ ├── article │ │ │ │ ├── ArticleRepository.kt │ │ │ │ └── ArticleRepositoryImpl.kt │ │ │ ├── board │ │ │ │ ├── BoardRepository.kt │ │ │ │ └── BoardRepositoryImpl.kt │ │ │ ├── populararticles │ │ │ │ ├── PopularArticlesRepository.kt │ │ │ │ └── PopularArticlesRepositoryImpl.kt │ │ │ ├── search │ │ │ │ ├── SearchBoardRepository.kt │ │ │ │ └── SearchBoardRepositoryImpl.kt │ │ │ └── user │ │ │ │ ├── UserRepository.kt │ │ │ │ └── UserRepositoryImpl.kt │ │ │ └── source │ │ │ ├── local │ │ │ ├── LoginLocalDataSource.kt │ │ │ └── LoginLocalDataSourceImpl.kt │ │ │ └── remote │ │ │ ├── article │ │ │ ├── ArticleRemoteDataSource.kt │ │ │ └── ArticleRemoteDataSourceImpl.kt │ │ │ ├── board │ │ │ ├── BoardRemoteDataSource.kt │ │ │ └── BoardRemoteDataSourceImpl.kt │ │ │ ├── search │ │ │ ├── SearchBoardRemoteDataSource.kt │ │ │ └── SearchBoardRemoteDataSourceImpl.kt │ │ │ └── user │ │ │ ├── UserRemoteDataSource.kt │ │ │ └── UserRemoteDataSourceImpl.kt │ └── res │ │ ├── values-night │ │ └── colors.xml │ │ ├── values-notnight │ │ └── colors.xml │ │ └── values │ │ └── colors.xml │ └── test │ └── java │ └── cc │ └── ptt │ └── android │ └── data │ ├── ApiTestBase.kt │ ├── ExampleUnitTest.kt │ ├── KoinTestBase.kt │ ├── KoinTestRule.kt │ ├── MainCoroutineRule.kt │ ├── MockAESKeyStoreHelperImpl.kt │ ├── TestApiHelperImpl.kt │ └── repository │ ├── board │ └── BoardRepositoryTest.kt │ ├── search │ └── SearchBoardRepositoryTest.kt │ └── user │ └── UserRepositoryTest.kt ├── domain ├── .gitignore ├── build.gradle.kts ├── consumer-rules.pro ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── cc │ │ └── ptt │ │ └── android │ │ └── domain │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ └── java │ │ └── cc │ │ └── ptt │ │ └── android │ │ └── domain │ │ ├── base │ │ └── UseCaseBase.kt │ │ ├── di │ │ └── UseCaseModules.kt │ │ ├── model │ │ ├── UserType.kt │ │ └── ui │ │ │ ├── article │ │ │ ├── ArticleInfo.kt │ │ │ ├── ArticleReadInfo.kt │ │ │ └── PostRankMark.kt │ │ │ ├── hotarticle │ │ │ └── HotArticleUI.kt │ │ │ └── user │ │ │ └── UserInfo.kt │ │ └── usecase │ │ ├── GetPopularArticlesUIUseCase.kt │ │ ├── article │ │ ├── CreateArticleCommentUseCase.kt │ │ └── GetArticleUseCase.kt │ │ ├── board │ │ ├── BoardUseCase.kt │ │ └── BoardUseCaseImpl.kt │ │ └── user │ │ └── UserUseCase.kt │ └── test │ └── java │ └── cc │ └── ptt │ └── android │ └── domain │ └── ExampleUnitTest.kt ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── script └── pre_commit_format.sh ├── settings.gradle.kts └── shared ├── .gitignore ├── build.gradle.kts └── src ├── androidDeviceTest └── kotlin │ └── cc │ └── ptt │ └── android │ └── shared │ └── ExampleInstrumentedTest.kt ├── androidHostTest └── kotlin │ └── cc │ └── ptt │ └── android │ └── shared │ └── ExampleUnitTest.kt ├── androidMain ├── AndroidManifest.xml └── kotlin │ └── cc │ └── ptt │ └── android │ └── shared │ └── Platform.android.kt ├── commonMain └── kotlin │ └── cc │ └── ptt │ └── android │ └── shared │ └── Platform.kt └── iosMain └── kotlin └── cc └── ptt └── android └── shared └── Platform.ios.kt /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/pr_ci.yml: -------------------------------------------------------------------------------- 1 | name: Android CI for PR check 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - 'master' 7 | - 'release*' 8 | - 'dev' 9 | 10 | concurrency: 11 | group: ${{ github.head_ref }} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | test: 16 | name: Run Build Tests 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - uses: actions/checkout@v3 21 | - name: set up JDK 21 22 | uses: actions/setup-java@v3 23 | with: 24 | java-version: '21' 25 | distribution: 'temurin' 26 | cache: gradle 27 | 28 | - name: Setup Android SDK 29 | uses: android-actions/setup-android@v3.2.2 30 | 31 | - name: Grant execute permission for gradlew 32 | run: chmod +x gradlew 33 | 34 | - name: Set up environment 35 | env: 36 | HOST: ${{ secrets.HOST }} 37 | TEST_ACCOUNT: ${{ secrets.TEST_ACCOUNT }} 38 | TEST_PASSWORD: ${{ secrets.TEST_PASSWORD }} 39 | shell: bash 40 | run: echo -e "HOST=$HOST\nACCOUNT=$TEST_ACCOUNT\nPASSWORD=$TEST_PASSWORD" > ./local.properties 41 | 42 | - name: Spotless Check 43 | run: bash ./gradlew spotlessCheck 44 | 45 | - name: Build Test 46 | run: bash ./gradlew clean assembleStaging 47 | 48 | - name: App layer Unit Test 49 | run: bash ./gradlew app:testStagingDebugUnitTest 50 | 51 | - name: Domain layer Unit Test 52 | run: bash ./gradlew domain:testStagingDebugUnitTest 53 | 54 | - name: Data layer Unit Test 55 | run: bash ./gradlew data:testStagingDebugUnitTest 56 | 57 | - name: Common layer Unit Test 58 | run: bash ./gradlew common:testDebugUnitTest 59 | 60 | - name: Upload Reports 61 | uses: actions/upload-artifact@v4.6.2 62 | with: 63 | name: Test-Reports 64 | path: ~/**/**/build/reports 65 | if: always() 66 | -------------------------------------------------------------------------------- /.github/workflows/push_ci.yml: -------------------------------------------------------------------------------- 1 | name: Android CI for Push 2 | 3 | on: 4 | push: 5 | branches: 6 | - '*' 7 | - '!master' 8 | - '!release*' 9 | 10 | concurrency: 11 | group: ${{ github.ref }} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | test: 16 | name: Run Build Tests 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - uses: actions/checkout@v3 21 | - name: set up JDK 21 22 | uses: actions/setup-java@v3 23 | with: 24 | java-version: '21' 25 | distribution: 'temurin' 26 | cache: gradle 27 | 28 | - name: Setup Android SDK 29 | uses: android-actions/setup-android@v2 30 | 31 | - name: Grant execute permission for gradlew 32 | run: chmod +x gradlew 33 | 34 | - name: Set up environment 35 | env: 36 | HOST: ${{ secrets.HOST }} 37 | TEST_ACCOUNT: ${{ secrets.TEST_ACCOUNT }} 38 | TEST_PASSWORD: ${{ secrets.TEST_PASSWORD }} 39 | shell: bash 40 | run: echo -e "HOST=$HOST\nACCOUNT=$TEST_ACCOUNT\nPASSWORD=$TEST_PASSWORD" > ./local.properties 41 | 42 | - name: Spotless Check 43 | run: bash ./gradlew spotlessCheck 44 | 45 | - name: Build Test 46 | run: bash ./gradlew clean assembleStaging 47 | 48 | - name: App layer Unit Test 49 | run: bash ./gradlew app:testStagingDebugUnitTest 50 | 51 | - name: Domain layer Unit Test 52 | run: bash ./gradlew domain:testStagingDebugUnitTest 53 | 54 | - name: Data layer Unit Test 55 | run: bash ./gradlew data:testStagingDebugUnitTest 56 | 57 | - name: Common layer Unit Test 58 | run: bash ./gradlew common:testDebugUnitTest 59 | 60 | - name: Upload Reports 61 | uses: actions/upload-artifact@v4.6.2 62 | with: 63 | name: Test-Reports 64 | path: ~/**/**/build/reports 65 | if: always() 66 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.aar 4 | *.ap_ 5 | *.aab 6 | 7 | # Files for the ART/Dalvik VM 8 | *.dex 9 | 10 | # Java class files 11 | *.class 12 | 13 | # Generated files 14 | bin/ 15 | gen/ 16 | out/ 17 | # Uncomment the following line in case you need and you don't have the release build type files in your app 18 | # release/ 19 | 20 | # Gradle files 21 | .gradle/ 22 | build/ 23 | 24 | # Local configuration file (sdk path, etc) 25 | local.properties 26 | api.properties 27 | 28 | # Proguard folder generated by Eclipse 29 | proguard/ 30 | 31 | # Log Files 32 | *.log 33 | 34 | # Android Studio Navigation editor temp files 35 | .navigation/ 36 | 37 | # Android Studio captures folder 38 | captures/ 39 | 40 | # IntelliJ 41 | *.iml 42 | .idea 43 | 44 | # Keystore files 45 | # Uncomment the following lines if you do not want to check your keystore files in. 46 | #*.jks 47 | #*.keystore 48 | 49 | # External native build folder generated in Android Studio 2.2 and later 50 | .externalNativeBuild 51 | .cxx/ 52 | 53 | # Google Services (e.g. APIs or Firebase) 54 | # google-services.json 55 | 56 | # Freeline 57 | freeline.py 58 | freeline/ 59 | freeline_project_description.json 60 | 61 | # fastlane 62 | fastlane/report.xml 63 | fastlane/Preview.html 64 | fastlane/screenshots 65 | fastlane/test_output 66 | fastlane/readme.md 67 | 68 | # Version control 69 | vcs.xml 70 | 71 | # lint 72 | lint/intermediates/ 73 | lint/generated/ 74 | lint/outputs/ 75 | lint/tmp/ 76 | # lint/reports/ 77 | 78 | # Android Profiling 79 | *.hprof 80 | 81 | # Mac OX X 82 | *.DS_Store 83 | .kotlin/ 84 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/debug.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ptt-official-app/Ptt-Android/6a4198cbab9497774dac1579a04518142c66b543/app/debug.jks -------------------------------------------------------------------------------- /app/src/androidTest/java/cc/ptt/android/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import org.junit.Test 6 | import org.junit.runner.RunWith 7 | 8 | /** 9 | * Instrumented test, which will execute on an Android device. 10 | * 11 | * @see [Testing documentation](http://d.android.com/tools/testing) 12 | */ 13 | @RunWith(AndroidJUnit4::class) 14 | class ExampleInstrumentedTest { 15 | @Test 16 | fun useAppContext() { 17 | // Context of the app under test. 18 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 19 | assert(appContext.packageName.startsWith("cc.ptt.android")) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 18 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ptt-official-app/Ptt-Android/6a4198cbab9497774dac1579a04518142c66b543/app/src/main/ic_launcher-web.png -------------------------------------------------------------------------------- /app/src/main/java/cc/ptt/android/PttApplication.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android 2 | 3 | import android.app.Application 4 | import cc.ptt.android.common.di.commonModules 5 | import cc.ptt.android.data.di.apiModules 6 | import cc.ptt.android.data.di.localDataSourceModules 7 | import cc.ptt.android.data.di.preferenceModules 8 | import cc.ptt.android.data.di.remoteDataSourceModules 9 | import cc.ptt.android.data.di.repositoryModules 10 | import cc.ptt.android.di.viewModelModules 11 | import cc.ptt.android.domain.di.useCaseModules 12 | import kotlinx.coroutines.FlowPreview 13 | import org.koin.android.ext.koin.androidContext 14 | import org.koin.core.context.GlobalContext.startKoin 15 | 16 | class PttApplication : Application() { 17 | @FlowPreview 18 | override fun onCreate() { 19 | super.onCreate() 20 | startKoin { 21 | // declare used modules 22 | androidContext(this@PttApplication) 23 | modules( 24 | listOf( 25 | apiModules, 26 | remoteDataSourceModules, 27 | localDataSourceModules, 28 | repositoryModules, 29 | useCaseModules, 30 | viewModelModules, 31 | commonModules, 32 | preferenceModules, 33 | ), 34 | ) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/cc/ptt/android/articlelist/viewholder/DeletedViewHolder.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.articlelist.viewholder 2 | 3 | import androidx.recyclerview.widget.RecyclerView 4 | import cc.ptt.android.R 5 | import cc.ptt.android.common.ResourcesUtils 6 | import cc.ptt.android.data.model.remote.board.article.Article 7 | import cc.ptt.android.databinding.ArticleListItemDeleteBinding 8 | 9 | class DeletedViewHolder constructor( 10 | private val binding: ArticleListItemDeleteBinding, 11 | ) : RecyclerView.ViewHolder(binding.root) { 12 | fun onBind(data: Article) { 13 | binding.apply { 14 | articleListItemTextViewTitle.text = data.title 15 | if (adapterPosition % 2 == 0) { 16 | articleListItemMain.setBackgroundColor(ResourcesUtils.getColor(itemView.context, R.attr.darkGreyTwo)) 17 | } else { 18 | articleListItemMain.setBackgroundColor(ResourcesUtils.getColor(itemView.context, R.attr.black)) 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/cc/ptt/android/base/BaseActivity.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.base 2 | 3 | import androidx.appcompat.app.AppCompatActivity 4 | 5 | open class BaseActivity : AppCompatActivity() 6 | -------------------------------------------------------------------------------- /app/src/main/java/cc/ptt/android/base/BaseFragment.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.base 2 | 3 | import android.view.View 4 | import androidx.fragment.app.Fragment 5 | import cc.ptt.android.common.KeyboardUtils 6 | import cc.ptt.android.common.logger.PttLogger 7 | import org.koin.android.ext.android.inject 8 | 9 | open class BaseFragment : Fragment() { 10 | protected val TAG get() = this::class.java.simpleName 11 | 12 | private var isFirstStart = false 13 | protected val logger: PttLogger by inject() 14 | 15 | @Deprecated("", ReplaceWith("", "")) 16 | fun findViewById(id: Int): T? = view?.findViewById(id) as T 17 | 18 | protected open fun onAnimFinished() { 19 | logger.d(TAG, "onAnimOver") 20 | } 21 | 22 | override fun onResume() { 23 | super.onResume() 24 | if (!isFirstStart) { 25 | onAnimFinished() 26 | isFirstStart = true 27 | } 28 | } 29 | 30 | override fun onDestroyView() { 31 | super.onDestroyView() 32 | hideSoftInput() 33 | } 34 | 35 | protected fun hideSoftInput() { 36 | KeyboardUtils.hideSoftInput(requireActivity()) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/cc/ptt/android/common/ClickFix.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.common 2 | 3 | import java.util.Date 4 | import kotlin.math.abs 5 | 6 | class ClickFix( 7 | private var defaultTime: Long = 500L, 8 | ) { 9 | private var lastClickTime: Long = 0 10 | val isFastDoubleClick: Boolean 11 | get() = isFastDoubleClick(defaultTime) 12 | 13 | fun isFastDoubleClick(time2: Long): Boolean { 14 | val time = Date().time 15 | if (abs(time - lastClickTime) < time2) { 16 | return true 17 | } 18 | lastClickTime = time 19 | return false 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/cc/ptt/android/common/GeneralFragmentStatePagerAdapter.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.common 2 | 3 | import androidx.fragment.app.Fragment 4 | import androidx.fragment.app.FragmentActivity 5 | import androidx.viewpager2.adapter.FragmentStateAdapter 6 | 7 | class GeneralFragmentStatePagerAdapter( 8 | fm: FragmentActivity, 9 | private val fragmentArrayList: List, 10 | ) : FragmentStateAdapter(fm) { 11 | override fun createFragment(position: Int): Fragment = fragmentArrayList[position] 12 | 13 | override fun getItemCount(): Int = fragmentArrayList.size 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/cc/ptt/android/common/KeyboardUtils.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.common 2 | 3 | import android.app.Activity 4 | import android.view.View 5 | import android.view.inputmethod.InputMethodManager 6 | 7 | object KeyboardUtils { 8 | fun hideSoftInput(activity: Activity) { 9 | val inputMethodManager: InputMethodManager = 10 | activity.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager 11 | ?: return 12 | val view = activity.currentFocus ?: View(activity) 13 | inputMethodManager.hideSoftInputFromWindow(view.windowToken, 0) 14 | } 15 | 16 | fun showSoftInput(activity: Activity) { 17 | val inputMethodManager: InputMethodManager = 18 | activity.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager 19 | ?: return 20 | val view = activity.currentFocus ?: View(activity) 21 | inputMethodManager.showSoftInput(view, 0) 22 | inputMethodManager.toggleSoftInput(InputMethodManager.SHOW_FORCED, InputMethodManager.HIDE_IMPLICIT_ONLY) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/cc/ptt/android/common/ResourcesUtils.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.common 2 | 3 | import android.content.Context 4 | import android.util.TypedValue 5 | import androidx.annotation.ColorInt 6 | 7 | object ResourcesUtils { 8 | @ColorInt 9 | fun getColor( 10 | context: Context, 11 | attrResId: Int, 12 | ): Int { 13 | val typedValue = TypedValue() 14 | val theme = context.theme 15 | theme.resolveAttribute(attrResId, typedValue, true) 16 | return typedValue.data 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/cc/ptt/android/common/UIUtils.kt: -------------------------------------------------------------------------------- 1 | @file:JvmName("UIUtils") 2 | 3 | package cc.ptt.android.utils 4 | 5 | import android.content.Context 6 | import android.content.Intent 7 | 8 | /** 9 | * 使用Android分享選單分享 10 | * 11 | * @param context 12 | * @param subject 主題 13 | * @param body 內文 14 | * @param chooserTitle 選擇器標題 15 | */ 16 | fun shareTo( 17 | context: Context, 18 | subject: String, 19 | body: String, 20 | chooserTitle: String, 21 | ) { 22 | val sharingIntent = 23 | Intent(Intent.ACTION_SEND).apply { 24 | type = "text/plain" 25 | putExtra(Intent.EXTRA_SUBJECT, subject) 26 | putExtra(Intent.EXTRA_TEXT, body) 27 | } 28 | context.startActivity(Intent.createChooser(sharingIntent, chooserTitle)) 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/cc/ptt/android/common/WebUtils.kt: -------------------------------------------------------------------------------- 1 | @file:JvmName("WebUtils") 2 | 3 | package cc.ptt.android.utils 4 | 5 | import android.content.Context 6 | import android.content.Intent 7 | import android.net.ConnectivityManager 8 | import android.net.NetworkInfo 9 | import android.net.Uri 10 | import androidx.browser.customtabs.CustomTabsIntent 11 | import cc.ptt.android.R 12 | 13 | fun turnOnUrl( 14 | context: Context, 15 | url: String?, 16 | ) { 17 | val sharingIntent = Intent(Intent.ACTION_SEND) 18 | sharingIntent.type = "text/plain" 19 | sharingIntent.putExtra(Intent.EXTRA_SUBJECT, url) 20 | sharingIntent.putExtra(Intent.EXTRA_TEXT, url) 21 | val builder = CustomTabsIntent.Builder() 22 | builder.addDefaultShareMenuItem() 23 | builder.setToolbarColor(context.resources.getColor(cc.ptt.android.data.R.color.black)) 24 | builder.setShowTitle(true) 25 | val customTabsIntent = builder.build() 26 | customTabsIntent.launchUrl(context, Uri.parse(url)) 27 | } 28 | 29 | fun isConnected(context: Context): Boolean { 30 | val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager 31 | val activeNetwork: NetworkInfo? = cm.activeNetworkInfo 32 | 33 | return (activeNetwork?.isConnectedOrConnecting == true) 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/cc/ptt/android/common/dragitemmove/StartDragListener.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.common.dragitemmove 2 | 3 | import androidx.recyclerview.widget.RecyclerView 4 | 5 | interface StartDragListener { 6 | fun requestDrag(viewHolder: RecyclerView.ViewHolder?) 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/cc/ptt/android/common/event/Event.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.common.event 2 | 3 | import androidx.lifecycle.Observer 4 | 5 | /** 6 | * 這是一個再次封裝 [LiveData] 數據的封裝類,目的是為了讓 [LiveData] 的數據流可以過濾掉一些因為生命週期的關係而重複發送的資料, 7 | * 例如: LiveData 先顯示一個 Toast ,旋轉螢幕後 Activity/Fragment 會 destroy & create ,這時這個 Toast 的 LiveData 就會重新發射數據流並被 [Observer] 接收,把之前顯示的訊息再次顯示出來。 8 | */ 9 | open class Event( 10 | private val content: T, 11 | ) { 12 | var hasBeenHandled = false 13 | private set 14 | 15 | fun getContentIfNotHandled(): T? = 16 | if (hasBeenHandled) { 17 | null 18 | } else { 19 | hasBeenHandled = true 20 | content 21 | } 22 | 23 | fun getContent(): T = content 24 | } 25 | 26 | /** 27 | * 簡化使用 [Event] 時的 [Observer] 操作行為 28 | */ 29 | class EventObserver( 30 | private val onEventUnhandledContent: (T) -> Unit, 31 | ) : Observer> { 32 | override fun onChanged(value: Event) { 33 | value.getContentIfNotHandled()?.let { 34 | onEventUnhandledContent(it) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/cc/ptt/android/common/extension/ArgumentsDelegateExtensions.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.common.extension 2 | 3 | import android.app.Activity 4 | import android.os.Bundle 5 | import androidx.fragment.app.Fragment 6 | import kotlin.reflect.KProperty 7 | 8 | interface ArgumentProvider { 9 | operator fun provideDelegate( 10 | thisRef: E, 11 | prop: KProperty<*>, 12 | ): Lazy 13 | } 14 | 15 | inline fun argumentDelegate(crossinline provideArgument: (F) -> Bundle): ArgumentProvider = 16 | object : ArgumentProvider { 17 | override fun provideDelegate( 18 | thisRef: F, 19 | prop: KProperty<*>, 20 | ) = lazy { 21 | val bundle = provideArgument(thisRef) 22 | bundle[prop.name] as T 23 | } 24 | } 25 | 26 | inline fun Activity.bundleDelegate() = 27 | argumentDelegate { 28 | it.intent?.extras ?: throw RuntimeException("No arguments passed") 29 | } 30 | 31 | inline fun Fragment.bundleDelegate() = 32 | argumentDelegate { 33 | it.arguments ?: throw RuntimeException("No arguments passed") 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/cc/ptt/android/common/extension/LifecycleExtentions.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.utils 2 | 3 | import androidx.lifecycle.LifecycleOwner 4 | import androidx.lifecycle.LiveData 5 | import androidx.lifecycle.MutableLiveData 6 | import cc.ptt.android.common.event.Event 7 | import cc.ptt.android.common.event.EventObserver 8 | 9 | fun LifecycleOwner.observe( 10 | liveData: LiveData, 11 | block: ((it: T?) -> Unit), 12 | ) { 13 | liveData.observe( 14 | this, 15 | { 16 | block(it) 17 | }, 18 | ) 19 | } 20 | 21 | fun LifecycleOwner.observeNotNull( 22 | liveData: LiveData, 23 | block: ((it: T) -> Unit), 24 | ) { 25 | liveData.observe( 26 | this, 27 | { 28 | block(it) 29 | }, 30 | ) 31 | } 32 | 33 | inline fun LiveData>.observeEvent( 34 | owner: LifecycleOwner, 35 | crossinline eventObserver: (T?) -> Unit, 36 | ) { 37 | this.observe(owner, EventObserver { eventObserver(it) }) 38 | } 39 | 40 | inline fun LiveData>.observeEventNotNull( 41 | owner: LifecycleOwner, 42 | crossinline eventObserver: (T) -> Unit, 43 | ) { 44 | this.observe(owner, EventObserver { it?.run(eventObserver) }) 45 | } 46 | 47 | inline fun MutableLiveData.forceRefresh() { 48 | this.value = this.value 49 | } 50 | -------------------------------------------------------------------------------- /app/src/main/java/cc/ptt/android/common/extension/NavExt.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.common.extension 2 | 3 | import android.os.Bundle 4 | import androidx.navigation.NavController 5 | import androidx.navigation.NavOptions 6 | import cc.ptt.android.R 7 | 8 | fun NavController.defaultNavigateForward(resId: Int) = 9 | navigateForward( 10 | resId, 11 | useDefaultAnim = false, 12 | ) 13 | 14 | fun NavController.defaultNavigateForward( 15 | resId: Int, 16 | args: Bundle? = null, 17 | isSingleTop: Boolean = true, 18 | ) = navigateForward(resId, args, isSingleTop, useDefaultAnim = true) 19 | 20 | fun NavController.navigateForward( 21 | resId: Int, 22 | args: Bundle? = null, 23 | isSingleTop: Boolean = true, 24 | useDefaultAnim: Boolean = false, 25 | ) = navigate( 26 | resId, 27 | args, 28 | NavOptions 29 | .Builder() 30 | .setLaunchSingleTop(isSingleTop) 31 | .setDefaultAnim(useDefaultAnim) 32 | .build(), 33 | ) 34 | 35 | fun NavOptions.Builder.setDefaultAnim(useDefaultAnim: Boolean) = 36 | apply { 37 | if (useDefaultAnim) { 38 | setEnterAnim(R.anim.slide_in_right_250) 39 | setExitAnim(R.anim.fade_out) 40 | setPopEnterAnim(R.anim.fade_in) 41 | setPopExitAnim(R.anim.slide_out_right_250) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/java/cc/ptt/android/di/ViewModelModules.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.di 2 | 3 | import cc.ptt.android.articlelist.ArticleListViewModel 4 | import cc.ptt.android.articleread.ArticleReadViewModel 5 | import cc.ptt.android.home.favoriteboards.FavoriteBoardsViewModel 6 | import cc.ptt.android.home.hotarticle.HotArticleListViewModel 7 | import cc.ptt.android.home.hotboard.HotBoardsViewModel 8 | import cc.ptt.android.home.setting.SettingViewModel 9 | import cc.ptt.android.login.LoginPageViewModel 10 | import cc.ptt.android.searchboards.SearchBoardsModel 11 | import kotlinx.coroutines.FlowPreview 12 | import org.koin.androidx.viewmodel.dsl.viewModel 13 | import org.koin.dsl.module 14 | 15 | @FlowPreview 16 | val viewModelModules = 17 | module { 18 | viewModel { ArticleListViewModel(get(), get()) } 19 | viewModel { ArticleReadViewModel(get(), get(), get(), get(), get()) } 20 | viewModel { FavoriteBoardsViewModel(get(), get()) } 21 | viewModel { HotArticleListViewModel(get()) } 22 | viewModel { HotBoardsViewModel(get()) } 23 | viewModel { SettingViewModel(get(), get(), get()) } 24 | viewModel { LoginPageViewModel(get(), get()) } 25 | viewModel { SearchBoardsModel(get()) } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/cc/ptt/android/home/personalpage/PersonInfoFragment.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.home.personalpage 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.recyclerview.widget.RecyclerView 8 | import cc.ptt.android.R 9 | import cc.ptt.android.base.BaseFragment 10 | import cc.ptt.android.common.CustomLinearLayoutManager 11 | 12 | class PersonInfoFragment : BaseFragment() { 13 | private var recyclerView: RecyclerView? = null 14 | 15 | override fun onCreateView( 16 | inflater: LayoutInflater, 17 | container: ViewGroup?, 18 | savedInstanceState: Bundle?, 19 | ): View? { 20 | val view = inflater.inflate(R.layout.personal_info_fragment_layout, container, false) 21 | recyclerView = findViewById(R.id.persion_info_fragment_recyclerView) 22 | val layoutManager = CustomLinearLayoutManager(context) 23 | layoutManager.orientation = RecyclerView.VERTICAL 24 | recyclerView?.setHasFixedSize(true) 25 | recyclerView?.layoutManager = layoutManager 26 | return view 27 | } 28 | 29 | override fun onAnimFinished() {} 30 | 31 | companion object { 32 | fun newInstance(): PersonInfoFragment { 33 | val args = Bundle() 34 | val fragment = PersonInfoFragment() 35 | fragment.arguments = args 36 | return fragment 37 | } 38 | 39 | fun newInstance(args: Bundle?): PersonInfoFragment { 40 | val fragment = PersonInfoFragment() 41 | fragment.arguments = args 42 | return fragment 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/java/cc/ptt/android/home/setting/SettingAdapter.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.home.setting 2 | 3 | import android.view.LayoutInflater 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import androidx.recyclerview.widget.RecyclerView 7 | import cc.ptt.android.databinding.ItemSettingBinding 8 | 9 | class SettingAdapter( 10 | private val dataList: List, 11 | private var mOnItemClickListener: OnItemClickListener, 12 | ) : RecyclerView.Adapter() { 13 | class ViewHolder( 14 | private val binding: ItemSettingBinding, 15 | ) : RecyclerView.ViewHolder(binding.root) { 16 | fun onBind(data: SettingFragment.SettingItem) { 17 | binding.apply { 18 | textView.text = itemView.resources.getString(data.titleResId) 19 | } 20 | } 21 | } 22 | 23 | override fun onCreateViewHolder( 24 | parent: ViewGroup, 25 | viewType: Int, 26 | ): ViewHolder = 27 | ViewHolder(ItemSettingBinding.inflate(LayoutInflater.from(parent.context), parent, false)).apply { 28 | itemView.setOnClickListener { 29 | mOnItemClickListener.onItemClick(it, dataList[adapterPosition]) 30 | } 31 | } 32 | 33 | override fun onBindViewHolder( 34 | viewHolder: ViewHolder, 35 | position: Int, 36 | ) { 37 | viewHolder.onBind(dataList[position]) 38 | } 39 | 40 | override fun getItemCount(): Int = dataList.size 41 | 42 | interface OnItemClickListener { 43 | fun onItemClick( 44 | view: View, 45 | data: SettingFragment.SettingItem, 46 | ) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/java/cc/ptt/android/home/setting/SettingViewModel.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.home.setting 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import cc.ptt.android.common.logger.PttLogger 6 | import cc.ptt.android.data.repository.user.UserRepository 7 | import cc.ptt.android.domain.usecase.user.UserUseCase 8 | import kotlinx.coroutines.launch 9 | 10 | class SettingViewModel constructor( 11 | private val userRepository: UserRepository, 12 | private val userUseCase: UserUseCase, 13 | private val logger: PttLogger, 14 | ) : ViewModel() { 15 | val loginState get() = userUseCase.userType 16 | 17 | fun isLogin(): Boolean = userRepository.isLogin() 18 | 19 | fun logout() = 20 | viewModelScope.launch { 21 | userUseCase.logout().collect { 22 | logger.d("SettingViewModel", "logout success") 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/res/anim/fade_in.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/anim/fade_out.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/anim/no_anim.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/anim/slide_in_right_250.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/anim/slide_out_right_250.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ptt-official-app/Ptt-Android/6a4198cbab9497774dac1579a04518142c66b543/app/src/main/res/drawable-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/article_list_search_bar_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/botton_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/botton_background_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/botton_background_selector_re.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/botton_state_pressed.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/buttom_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | 18 | 19 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/clock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ptt-official-app/Ptt-Android/6a4198cbab9497774dac1579a04518142c66b543/app/src/main/res/drawable/clock.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/commit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ptt-official-app/Ptt-Android/6a4198cbab9497774dac1579a04518142c66b543/app/src/main/res/drawable/commit.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/component_10_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ptt-official-app/Ptt-Android/6a4198cbab9497774dac1579a04518142c66b543/app/src/main/res/drawable/component_10_3.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/dialog_background.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ptt-official-app/Ptt-Android/6a4198cbab9497774dac1579a04518142c66b543/app/src/main/res/drawable/down.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/edit2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ptt-official-app/Ptt-Android/6a4198cbab9497774dac1579a04518142c66b543/app/src/main/res/drawable/edit2.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/edit_bar_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/edittext_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | 18 | 19 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/home_botton_item_color_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_more_horiz_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_person_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_visibility_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_visibility_off_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_whatshot_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_check_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_chevron_left_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_clear_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_close_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_component_10_2.xml: -------------------------------------------------------------------------------- 1 | 3 | 5 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_component_8.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 10 | 13 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_dehaze_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_delete_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_edit_24px.xml: -------------------------------------------------------------------------------- 1 | 6 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_expand_more_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_home_fragment_item_default_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_home_fragment_item_favorite_24dp.xml: -------------------------------------------------------------------------------- 1 | 3 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_image_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_keyboard_arrow_right_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_keyboard_hide_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_like.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_mode_edit_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_more_24.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_more_horiz_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_more_vert_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_post_add_24px.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_replay_24px__1_.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_reply_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_save_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_search_24.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_search_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_send_black_24dp.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_sharp_drag_handle_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/image_click.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/more.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ptt-official-app/Ptt-Android/6a4198cbab9497774dac1579a04518142c66b543/app/src/main/res/drawable/more.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/person.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ptt-official-app/Ptt-Android/6a4198cbab9497774dac1579a04518142c66b543/app/src/main/res/drawable/person.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/person_picture.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ptt-official-app/Ptt-Android/6a4198cbab9497774dac1579a04518142c66b543/app/src/main/res/drawable/person_picture.jpeg -------------------------------------------------------------------------------- /app/src/main/res/drawable/rectangle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ptt-official-app/Ptt-Android/6a4198cbab9497774dac1579a04518142c66b543/app/src/main/res/drawable/rectangle.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/rectangle2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ptt-official-app/Ptt-Android/6a4198cbab9497774dac1579a04518142c66b543/app/src/main/res/drawable/rectangle2.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/search_bar_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/shadow_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ptt-official-app/Ptt-Android/6a4198cbab9497774dac1579a04518142c66b543/app/src/main/res/drawable/shadow_left.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/shadow_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ptt-official-app/Ptt-Android/6a4198cbab9497774dac1579a04518142c66b543/app/src/main/res/drawable/shadow_right.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/splash_drawable.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 11 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/tab_state_select.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/tab_state_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/tab_state_unselect.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/un_fav.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ptt-official-app/Ptt-Android/6a4198cbab9497774dac1579a04518142c66b543/app/src/main/res/drawable/un_fav.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ptt-official-app/Ptt-Android/6a4198cbab9497774dac1579a04518142c66b543/app/src/main/res/drawable/up.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/user1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ptt-official-app/Ptt-Android/6a4198cbab9497774dac1579a04518142c66b543/app/src/main/res/drawable/user1.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/user2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ptt-official-app/Ptt-Android/6a4198cbab9497774dac1579a04518142c66b543/app/src/main/res/drawable/user2.png -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/layout/article_list_item_delete.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 17 | 18 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/layout/article_read_item_commit.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 17 | 18 | 21 | 22 | 33 | 34 | 37 | 38 | -------------------------------------------------------------------------------- /app/src/main/res/layout/article_read_item_commit_sort.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 18 | 19 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/layout/article_read_item_content.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/layout/article_read_item_image.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/layout/edit_dialog_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/layout/empty_fragment_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 19 | 20 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_setting.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 15 | 16 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/layout/home_fragment_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 14 | 15 | 16 | 17 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/res/layout/hot_article_list_fragment_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 15 | 16 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/layout/hot_article_list_item_more.xml: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 16 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/layout/hot_article_list_item_title.xml: -------------------------------------------------------------------------------- 1 | 2 | 15 | 16 | 26 | 27 | 37 | 38 | -------------------------------------------------------------------------------- /app/src/main/res/layout/hot_article_list_item_title_subitem.xml: -------------------------------------------------------------------------------- 1 | 2 | 15 | 16 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/res/layout/hot_article_list_item_title_top.xml: -------------------------------------------------------------------------------- 1 | 2 | 15 | 16 | 26 | 27 | 38 | 39 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_setting.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 17 | 18 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/layout/personal_info_fragment_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 12 | 13 | 17 | 18 | 19 | 20 | 21 | 22 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/res/layout/search_boards_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 19 | 20 | 30 | 31 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /app/src/main/res/menu/article_list_bottom_navigation_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 17 | 18 | 24 | 30 | 36 | 37 | -------------------------------------------------------------------------------- /app/src/main/res/menu/article_list_search_bottom_navigation_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 17 | 18 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/menu/carete_article_comment_type_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 12 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/menu/home_bottom_navigation_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 15 | 16 | 21 | 26 | 31 | 32 | -------------------------------------------------------------------------------- /app/src/main/res/menu/post_article_bottom_navigation_menu2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 17 | 22 | 23 | 28 | 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/res/menu/post_article_bottom_navigation_menu3.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 17 | 22 | 23 | 28 | 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/res/menu/post_article_rank_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 12 | 16 | 17 | -------------------------------------------------------------------------------- /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/Ptt-official-app/Ptt-Android/6a4198cbab9497774dac1579a04518142c66b543/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ptt-official-app/Ptt-Android/6a4198cbab9497774dac1579a04518142c66b543/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ptt-official-app/Ptt-Android/6a4198cbab9497774dac1579a04518142c66b543/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ptt-official-app/Ptt-Android/6a4198cbab9497774dac1579a04518142c66b543/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ptt-official-app/Ptt-Android/6a4198cbab9497774dac1579a04518142c66b543/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ptt-official-app/Ptt-Android/6a4198cbab9497774dac1579a04518142c66b543/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ptt-official-app/Ptt-Android/6a4198cbab9497774dac1579a04518142c66b543/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ptt-official-app/Ptt-Android/6a4198cbab9497774dac1579a04518142c66b543/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ptt-official-app/Ptt-Android/6a4198cbab9497774dac1579a04518142c66b543/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ptt-official-app/Ptt-Android/6a4198cbab9497774dac1579a04518142c66b543/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ptt-official-app/Ptt-Android/6a4198cbab9497774dac1579a04518142c66b543/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ptt-official-app/Ptt-Android/6a4198cbab9497774dac1579a04518142c66b543/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ptt-official-app/Ptt-Android/6a4198cbab9497774dac1579a04518142c66b543/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ptt-official-app/Ptt-Android/6a4198cbab9497774dac1579a04518142c66b543/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ptt-official-app/Ptt-Android/6a4198cbab9497774dac1579a04518142c66b543/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/navigation/include_login.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFFFFF 4 | -------------------------------------------------------------------------------- /app/src/main/res/xml/network_security_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 140.136.149.223 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/test/java/cc/ptt/android/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android 2 | 3 | import org.junit.Assert 4 | import org.junit.Test 5 | 6 | /** 7 | * Example local unit test, which will execute on the development machine (host). 8 | * 9 | * @see [Testing documentation](http://d.android.com/tools/testing) 10 | */ 11 | class ExampleUnitTest { 12 | @Test 13 | fun addition_isCorrect() { 14 | Assert.assertEquals(4, 2 + 2.toLong()) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/src/test/resources/api/board/popular_boards.json: -------------------------------------------------------------------------------- 1 | { 2 | "list": [ 3 | { 4 | "bid": "string", 5 | "brdname": "string", 6 | "title": "string", 7 | "flag": 0, 8 | "type": "string", 9 | "class": "string", 10 | "nuser": 0, 11 | "moderators": [ 12 | "string" 13 | ], 14 | "reason": "string", 15 | "read": true, 16 | "total": 0, 17 | "last_post_time": 0, 18 | "stat_attr": 0, 19 | "level_idx": "string" 20 | } 21 | ], 22 | "next_idx": "string" 23 | } -------------------------------------------------------------------------------- /app/src/test/resources/api/board/search_boards.json: -------------------------------------------------------------------------------- 1 | { 2 | "list": [ 3 | { 4 | "bid": "string", 5 | "brdname": "string", 6 | "title": "string", 7 | "flag": 0, 8 | "type": "string", 9 | "class": "string", 10 | "nuser": 0, 11 | "moderators": [ 12 | "string" 13 | ], 14 | "reason": "string", 15 | "read": true, 16 | "total": 0, 17 | "last_post_time": 0, 18 | "stat_attr": 0, 19 | "level_idx": "string" 20 | } 21 | ], 22 | "next_idx": "string" 23 | } -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | plugins { 3 | alias(libs.plugins.spotless) 4 | alias(libs.plugins.kotlin.android) apply false 5 | alias(libs.plugins.kotlin.multiplatform) apply false 6 | alias(libs.plugins.android.application) apply false 7 | alias(libs.plugins.android.library) apply false 8 | alias(libs.plugins.compose.multiplatform) apply false 9 | alias(libs.plugins.compose.compiler) apply false 10 | alias(libs.plugins.androidKotlinMultiplatformLibrary) apply false 11 | } 12 | 13 | allprojects { 14 | repositories { 15 | google() 16 | mavenCentral() 17 | maven(url = "https://plugins.gradle.org/m2/") 18 | maven(url = "https://cdn.reproio.com/android") 19 | } 20 | } 21 | 22 | subprojects { 23 | apply(plugin = "com.diffplug.spotless") 24 | configure { 25 | kotlin { 26 | target ("**/*.kt") 27 | targetExclude("${layout.buildDirectory}/**/*.kt") 28 | targetExclude("bin/**/*.kt") 29 | trimTrailingWhitespace() 30 | endWithNewline() 31 | // 允許 import 路徑使用萬用字元 32 | ktlint("1.5.0").userData(mapOf("disabled_rules" to "no-wildcard-imports")) 33 | } 34 | java { 35 | target ("src/*/java/**/*.java") 36 | googleJavaFormat("1.21.0").aosp() 37 | // 移除沒用到的 Import 38 | removeUnusedImports() 39 | // 刪除多餘的空白 40 | trimTrailingWhitespace() 41 | importOrder("android", "androidx", "com", "junit", "net", "org", "java", "javax") 42 | } 43 | } 44 | 45 | task("format") { 46 | dependsOn("spotlessApply") 47 | group = "Verification" 48 | } 49 | task("formatCheck") { 50 | dependsOn("spotlessCheck") 51 | group = "Verification" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /buildSrc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins{ 2 | `kotlin-dsl` 3 | } 4 | 5 | repositories { 6 | mavenCentral() 7 | } -------------------------------------------------------------------------------- /buildSrc/src/main/java/Versions.kt: -------------------------------------------------------------------------------- 1 | import org.gradle.api.JavaVersion 2 | 3 | object Versions { 4 | const val majorVersion = 0 5 | const val minorVersion = 18 6 | const val patchVersion = 4 7 | const val spotless = "6.22.0" 8 | } 9 | 10 | object GlobalConfig { 11 | const val ANDROID_BUILD_SDK_VERSION = 35 12 | const val ANDROID_BUILD_MIN_SDK_VERSION = 23 13 | const val ANDROID_BUILD_TARGET_SDK_VERSION = 35 14 | const val ANDROID_BUILD_TOOLS_VERSION = "35.0.0" 15 | 16 | const val applicationId: String = "cc.ptt.android" 17 | const val versionCode: Int = Versions.majorVersion * 1000000 + Versions.minorVersion * 10000 + Versions.patchVersion * 100 18 | const val versionName = "${Versions.majorVersion}.${Versions.minorVersion}.${Versions.patchVersion}" 19 | 20 | const val testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 21 | 22 | val JDKVersion = JavaVersion.VERSION_21 23 | 24 | const val BUILD_CONFIG_KEY_FOR_API_HOST = "API_HOST" 25 | const val BUILD_CONFIG_KEY_FOR_TEST_ACCOUNT = "TEST_ACCOUNT" 26 | const val BUILD_CONFIG_KEY_FOR_TEST_PASSWORD = "TEST_PASSWORD" 27 | } -------------------------------------------------------------------------------- /common/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /common/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.library") 3 | kotlin("android") 4 | } 5 | 6 | android { 7 | namespace = "cc.ptt.android.common" 8 | compileSdk = GlobalConfig.ANDROID_BUILD_SDK_VERSION 9 | 10 | buildFeatures.buildConfig = true 11 | 12 | defaultConfig { 13 | minSdk = GlobalConfig.ANDROID_BUILD_MIN_SDK_VERSION 14 | 15 | testInstrumentationRunner = GlobalConfig.testInstrumentationRunner 16 | consumerProguardFiles("consumer-rules.pro") 17 | } 18 | 19 | buildTypes { 20 | getByName("release") { 21 | isMinifyEnabled = true 22 | proguardFiles( 23 | getDefaultProguardFile("proguard-android-optimize.txt"), 24 | "proguard-rules.pro" 25 | ) 26 | } 27 | } 28 | 29 | compileOptions { 30 | sourceCompatibility = GlobalConfig.JDKVersion 31 | targetCompatibility = GlobalConfig.JDKVersion 32 | } 33 | 34 | buildFeatures { 35 | buildConfig = true 36 | } 37 | } 38 | 39 | dependencies { 40 | implementation(libs.androidx.appcompat) 41 | implementation(libs.google.material) 42 | // Koin Core features 43 | implementation(libs.koin.core) 44 | // Koin main features for Android 45 | implementation(libs.koin.android) 46 | // Kotlin 47 | implementation(libs.coroutines.core) 48 | implementation(libs.coroutines.android) 49 | // Square 50 | implementation(libs.square.okhttp) 51 | implementation(libs.square.log) 52 | implementation(libs.square.okio) 53 | implementation(libs.square.retrofit.core) 54 | implementation(libs.square.retrofit.gson.converter) 55 | implementation(libs.androidx.core) 56 | 57 | testImplementation(libs.junit) 58 | 59 | androidTestImplementation(libs.androidx.test.junit) 60 | androidTestImplementation(libs.androidx.test.espresso) 61 | } -------------------------------------------------------------------------------- /common/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ptt-official-app/Ptt-Android/6a4198cbab9497774dac1579a04518142c66b543/common/consumer-rules.pro -------------------------------------------------------------------------------- /common/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /common/src/androidTest/java/cc/ptt/android/common/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.common 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import org.junit.Test 6 | import org.junit.runner.RunWith 7 | 8 | /** 9 | * Instrumented test, which will execute on an Android device. 10 | * 11 | * @see [Testing documentation](http://d.android.com/tools/testing) 12 | */ 13 | @RunWith(AndroidJUnit4::class) 14 | class ExampleInstrumentedTest { 15 | @Test 16 | fun useAppContext() { 17 | // Context of the app under test. 18 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 19 | assert(appContext.packageName.startsWith("cc.ptt.android.common")) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /common/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /common/src/main/java/cc/ptt/android/common/ResourcesProvider.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.common 2 | 3 | import android.content.Context 4 | 5 | class ResourcesProvider( 6 | private val context: Context, 7 | ) { 8 | fun getString(resTd: Int): String = context.getString(resTd) 9 | } 10 | -------------------------------------------------------------------------------- /common/src/main/java/cc/ptt/android/common/date/DateFormatUtils.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.common.date 2 | 3 | import android.annotation.SuppressLint 4 | import java.text.SimpleDateFormat 5 | import java.util.Date 6 | import java.util.TimeZone 7 | 8 | object DateFormatUtils { 9 | @SuppressLint("SimpleDateFormat") 10 | fun secondsToDateTime( 11 | seconds: Long, 12 | pattern: String, 13 | timeZone: TimeZone = TimeZone.getDefault(), 14 | ): String { 15 | val dateFormat = 16 | SimpleDateFormat(pattern).also { 17 | it.timeZone = timeZone 18 | } 19 | return dateFormat.format(Date(seconds * 1000)) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /common/src/main/java/cc/ptt/android/common/date/DatePatternConstants.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.common.date 2 | 3 | object DatePatternConstants { 4 | const val ARTICLE_DATE_TIME = "yyyy-MM-dd / HH:mm:ss" 5 | const val ARTICLE_COMMENT_DATE_TIME = "MM/dd HH:mm" 6 | } 7 | -------------------------------------------------------------------------------- /common/src/main/java/cc/ptt/android/common/di/CommonModules.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.common.di 2 | 3 | import cc.ptt.android.common.ResourcesProvider 4 | import cc.ptt.android.common.logger.PttLogger 5 | import cc.ptt.android.common.logger.PttLoggerImpl 6 | import org.koin.android.ext.koin.androidContext 7 | import org.koin.dsl.module 8 | 9 | val commonModules = 10 | module { 11 | single { PttLoggerImpl() } 12 | single { ResourcesProvider(androidContext()) } 13 | } 14 | -------------------------------------------------------------------------------- /common/src/main/java/cc/ptt/android/common/extensions/GsonExtensions.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.common.extensions 2 | 3 | import com.google.gson.Gson 4 | import com.google.gson.reflect.TypeToken 5 | 6 | /** 7 | * Created by Michael.Lien 8 | * on 2020/12/25 9 | */ 10 | inline fun Gson.fromJson(json: String): T = fromJson(json, object : TypeToken() {}.type) 11 | -------------------------------------------------------------------------------- /common/src/main/java/cc/ptt/android/common/logger/PttLogger.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.common.logger 2 | 3 | interface PttLogger { 4 | companion object { 5 | val TAG: String = PttLogger::class.java.simpleName 6 | } 7 | 8 | fun d( 9 | tag: String? = TAG, 10 | msg: String? = null, 11 | t: Throwable? = null, 12 | ) 13 | 14 | fun i( 15 | tag: String? = TAG, 16 | msg: String? = null, 17 | t: Throwable? = null, 18 | ) 19 | 20 | fun w( 21 | tag: String? = TAG, 22 | msg: String? = null, 23 | t: Throwable? = null, 24 | ) 25 | 26 | fun e( 27 | tag: String? = TAG, 28 | msg: String? = null, 29 | t: Throwable? = null, 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /common/src/main/java/cc/ptt/android/common/logger/PttLoggerImpl.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.common.logger 2 | 3 | import android.util.Log 4 | import cc.ptt.android.common.BuildConfig 5 | 6 | class PttLoggerImpl : PttLogger { 7 | override fun d( 8 | tag: String?, 9 | msg: String?, 10 | t: Throwable?, 11 | ) { 12 | if (BuildConfig.DEBUG) { 13 | Log.d(tag, msg, t) 14 | } 15 | } 16 | 17 | override fun i( 18 | tag: String?, 19 | msg: String?, 20 | t: Throwable?, 21 | ) { 22 | if (BuildConfig.DEBUG) { 23 | Log.i(tag, msg, t) 24 | } 25 | } 26 | 27 | override fun w( 28 | tag: String?, 29 | msg: String?, 30 | t: Throwable?, 31 | ) { 32 | if (BuildConfig.DEBUG) { 33 | Log.w(tag, msg, t) 34 | } 35 | } 36 | 37 | override fun e( 38 | tag: String?, 39 | msg: String?, 40 | t: Throwable?, 41 | ) { 42 | if (BuildConfig.DEBUG) { 43 | Log.e(tag, msg, t) 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /common/src/main/java/cc/ptt/android/common/network/api/ApiAnnotations.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.common.network.api 2 | 3 | @Target(AnnotationTarget.CLASS) 4 | annotation class ApiMaxLogLevel( 5 | val level: MaxLogLevel, 6 | ) 7 | 8 | enum class MaxLogLevel { 9 | NONE, 10 | BASIC, 11 | HEADERS, 12 | BODY, 13 | } 14 | -------------------------------------------------------------------------------- /common/src/main/java/cc/ptt/android/common/network/api/ApiException.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.common.network.api 2 | 3 | import java.io.IOException 4 | 5 | class ApiException( 6 | val code: Int, 7 | private val msg: String, 8 | ) : IOException(msg) 9 | -------------------------------------------------------------------------------- /common/src/main/java/cc/ptt/android/common/network/api/ApiResponseInterceptor.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.common.network.api 2 | 3 | import okhttp3.Interceptor 4 | import okhttp3.Response 5 | import java.io.IOException 6 | 7 | class ApiResponseInterceptor : Interceptor { 8 | @Throws(IOException::class) 9 | override fun intercept(chain: Interceptor.Chain): Response { 10 | val request = chain.request() 11 | val response = chain.proceed(request) 12 | if (response.isSuccessful) { 13 | return response 14 | } else { 15 | throw ApiException(response.code, response.body?.string().orEmpty()) 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /common/src/main/java/cc/ptt/android/common/network/api/TokenInterceptor.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.common.network.api 2 | 3 | import okhttp3.Interceptor 4 | 5 | interface TokenInterceptor : Interceptor 6 | -------------------------------------------------------------------------------- /common/src/main/java/cc/ptt/android/common/network/api/apihelper/ApiHelper.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.common.network.api.apihelper 2 | 3 | interface ApiHelper { 4 | fun getHost(): String 5 | 6 | fun getClientId(): String 7 | 8 | fun getClientSecret(): String 9 | 10 | companion object { 11 | const val CLIENT_ID = "test_client_id" 12 | const val CLIENT_SECRET = "test_client_secret" 13 | const val API_HOST = "https://api.devptt.dev" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /common/src/main/java/cc/ptt/android/common/network/retrofit/RetrofitFlowCallAdapterFactory.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.common.network.retrofit 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | import retrofit2.CallAdapter 5 | import retrofit2.Response 6 | import retrofit2.Retrofit 7 | import java.lang.reflect.ParameterizedType 8 | import java.lang.reflect.Type 9 | 10 | // Ref: https://github.com/MohammadSianaki/Retrofit2-Flow-Call-Adapter/blob/master/FlowAdapter/src/main/java/me/sianaki/flowretrofitadapter/FlowCallAdapterFactory.kt 11 | class RetrofitFlowCallAdapterFactory : CallAdapter.Factory() { 12 | override fun get( 13 | returnType: Type, 14 | annotations: Array, 15 | retrofit: Retrofit, 16 | ): CallAdapter<*, *>? { 17 | if (getRawType(returnType) != Flow::class.java) { 18 | return null 19 | } 20 | 21 | check(returnType is ParameterizedType) { "Flow return type must be parameterized as Flow or Flow" } 22 | 23 | val responseType = getParameterUpperBound(0, returnType) 24 | val rawFlowType = getRawType(responseType) 25 | 26 | return if (rawFlowType == Response::class.java) { 27 | check(responseType is ParameterizedType) { "Response must be parameterized as Response or Response" } 28 | 29 | RetrofitResponseCallAdapter(getParameterUpperBound(0, responseType)) 30 | } else { 31 | RetrofitBodyCallAdapter(responseType) 32 | } 33 | } 34 | 35 | companion object { 36 | @JvmStatic 37 | fun create() = RetrofitFlowCallAdapterFactory() 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /common/src/main/java/cc/ptt/android/common/network/retrofit/RetrofitResponseCallAdapter.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.common.network.retrofit 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | import kotlinx.coroutines.flow.flow 5 | import kotlinx.coroutines.suspendCancellableCoroutine 6 | import retrofit2.Call 7 | import retrofit2.CallAdapter 8 | import retrofit2.Callback 9 | import retrofit2.Response 10 | import java.lang.reflect.Type 11 | import kotlin.coroutines.resume 12 | import kotlin.coroutines.resumeWithException 13 | 14 | class RetrofitResponseCallAdapter( 15 | private val responseType: Type, 16 | ) : CallAdapter>> { 17 | override fun adapt(call: Call): Flow> = 18 | flow { 19 | emit( 20 | suspendCancellableCoroutine { continuation -> 21 | call.enqueue( 22 | object : Callback { 23 | override fun onFailure( 24 | call: Call, 25 | t: Throwable, 26 | ) { 27 | continuation.resumeWithException(t) 28 | } 29 | 30 | override fun onResponse( 31 | call: Call, 32 | response: Response, 33 | ) { 34 | continuation.resume(response) 35 | } 36 | }, 37 | ) 38 | continuation.invokeOnCancellation { call.cancel() } 39 | }, 40 | ) 41 | } 42 | 43 | override fun responseType() = responseType 44 | } 45 | -------------------------------------------------------------------------------- /common/src/main/java/cc/ptt/android/common/ptt/AidBean.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.common.ptt 2 | 3 | data class AidBean( 4 | val boardName: String? = null, 5 | val aid: String? = null, 6 | ) { 7 | fun isEmpty(): Boolean = boardName.isNullOrEmpty() || aid.isNullOrEmpty() 8 | 9 | fun toUrl(): String = AidConverter.aidToUrl(this) 10 | 11 | companion object { 12 | fun parse(url: String): AidBean = AidConverter.urlToAid(url) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /common/src/main/java/cc/ptt/android/common/security/AESKeyStoreHelper.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.common.security 2 | 3 | interface AESKeyStoreHelper { 4 | fun encrypt(plainText: String?): String 5 | 6 | fun decrypt(encryptedText: String?): String 7 | } 8 | -------------------------------------------------------------------------------- /common/src/test/java/cc/ptt/android/common/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.common; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | 5 | import org.junit.Test; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /common/src/test/java/cc/ptt/android/common/StringUtilsTest.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.common 2 | 3 | import org.junit.After 4 | import org.junit.Assert 5 | import org.junit.Before 6 | import org.junit.Test 7 | 8 | class StringUtilsTest { 9 | @Before 10 | fun setUp() { 11 | } 12 | 13 | @After 14 | fun tearDown() { 15 | } 16 | 17 | @Test 18 | fun notNullString() { 19 | Assert.assertEquals("", StringUtils.notNullString(null)) 20 | Assert.assertEquals("ptt", StringUtils.notNullString("ptt")) 21 | } 22 | 23 | @Test 24 | fun isAccount() { 25 | Assert.assertTrue(StringUtils.isAccount("ptt")) 26 | Assert.assertTrue(StringUtils.isAccount("PTT")) 27 | Assert.assertFalse(StringUtils.isAccount("P")) 28 | Assert.assertTrue(StringUtils.isAccount("P111111111111")) 29 | } 30 | 31 | @Test 32 | fun getImgUrl() { 33 | val expected = listOf("https://i.imgur.com/MbkBGEG.jpg", "https://i.imgur.com/MbkBGEG.jpg") 34 | Assert.assertArrayEquals( 35 | expected.toTypedArray(), 36 | StringUtils.getImgUrl("https://i.imgur.com/MbkBGEG.jpg 嗨 https://i.imgur.com/MbkBGEG.jpg").toTypedArray(), 37 | ) 38 | } 39 | 40 | @Test 41 | fun notNullImageString() { 42 | Assert.assertEquals("https://i.imgur.com/MbkBGEG.jpg", StringUtils.notNullImageString("https://i.imgur.com/MbkBGEG.jpg")) 43 | } 44 | 45 | @Test 46 | fun clearStart() { 47 | Assert.assertEquals("ptt", StringUtils.clearStart(" ptt")) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /common/src/test/java/cc/ptt/android/common/ptt/AidBeanTest.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.common.ptt 2 | 3 | import org.junit.Assert 4 | import org.junit.Test 5 | 6 | class AidBeanTest { 7 | private val aid = "1Xpy6SRj" 8 | private val boardName = "Test" 9 | private val url = "https://www.ptt.cc/bbs/Test/M.1641005468.A.6ED.html" 10 | 11 | @Test 12 | fun `Test isEmpty when both of AidBean are empty then return true`() { 13 | val aidBean = AidBean() 14 | Assert.assertEquals(true, aidBean.isEmpty()) 15 | } 16 | 17 | @Test 18 | fun `Test isEmpty when board name of AidBean is empty then return true`() { 19 | val aidBean = AidBean(null, aid) 20 | Assert.assertEquals(true, aidBean.isEmpty()) 21 | } 22 | 23 | @Test 24 | fun `Test isEmpty when aid of AidBean is empty then return true`() { 25 | val aidBean = AidBean(boardName, null) 26 | Assert.assertEquals(true, aidBean.isEmpty()) 27 | } 28 | 29 | @Test 30 | fun `Test toUrl when data is correct then converter correct`() { 31 | val aidBean = AidBean(boardName, aid) 32 | Assert.assertEquals(url, aidBean.toUrl()) 33 | } 34 | 35 | @Test 36 | fun `Test toUrl when data is incorrect then converter error`() { 37 | val aidBean = AidBean(boardName, "aid") 38 | Assert.assertNotEquals(url, aidBean.toUrl()) 39 | } 40 | 41 | @Test 42 | fun `Test parse when data is correct then converter correct`() { 43 | val oriAidBean = AidBean(boardName, aid) 44 | val aidBean = AidBean.parse(url) 45 | Assert.assertEquals(oriAidBean, aidBean) 46 | } 47 | 48 | @Test 49 | fun `Test parse when data is incorrect then converter error`() { 50 | val aidBean = AidBean.parse("https://www.ptt.cc/bbs/Test/M.1641005468.A.6ED") 51 | Assert.assertEquals(true, aidBean.isEmpty()) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /data/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /data/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ptt-official-app/Ptt-Android/6a4198cbab9497774dac1579a04518142c66b543/data/consumer-rules.pro -------------------------------------------------------------------------------- /data/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /data/src/androidTest/java/cc/ptt/android/data/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.data 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import org.junit.Test 6 | import org.junit.runner.RunWith 7 | 8 | /** 9 | * Instrumented test, which will execute on an Android device. 10 | * 11 | * @see [Testing documentation](http://d.android.com/tools/testing) 12 | */ 13 | @RunWith(AndroidJUnit4::class) 14 | class ExampleInstrumentedTest { 15 | @Test 16 | fun useAppContext() { 17 | // Context of the app under test. 18 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 19 | assert(appContext.packageName.startsWith("cc.ptt.android.data")) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /data/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /data/src/main/java/cc/ptt/android/data/ApiHelperImpl.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.data 2 | 3 | import cc.ptt.android.common.network.api.apihelper.ApiHelper 4 | import cc.ptt.android.common.network.api.apihelper.ApiHelper.Companion.CLIENT_ID 5 | import cc.ptt.android.common.network.api.apihelper.ApiHelper.Companion.CLIENT_SECRET 6 | import cc.ptt.android.data.preference.MainPreferences 7 | 8 | class ApiHelperImpl constructor( 9 | private val mainPreferences: MainPreferences, 10 | ) : ApiHelper { 11 | override fun getHost(): String = mainPreferences.getApiDomain().ifBlank { defaultHost() } 12 | 13 | override fun getClientId(): String = CLIENT_ID 14 | 15 | override fun getClientSecret(): String = CLIENT_SECRET 16 | 17 | private fun defaultHost(): String = 18 | BuildConfig.API_HOST.ifBlank { 19 | ApiHelper.API_HOST 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /data/src/main/java/cc/ptt/android/data/TokenInterceptorImpl.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.data 2 | 3 | import cc.ptt.android.common.network.api.TokenInterceptor 4 | import cc.ptt.android.data.source.local.LoginLocalDataSource 5 | import okhttp3.Interceptor 6 | import okhttp3.Response 7 | 8 | class TokenInterceptorImpl constructor( 9 | private val loginLocalDataSource: LoginLocalDataSource, 10 | ) : TokenInterceptor { 11 | override fun intercept(chain: Interceptor.Chain): Response { 12 | val requestBuilder = chain.request().newBuilder() 13 | 14 | if (loginLocalDataSource.isLogin()) { 15 | loginLocalDataSource.getUserInfo()?.let { 16 | requestBuilder.addHeader("Authorization", "${it.tokenType} ${it.accessToken}") 17 | } 18 | } 19 | 20 | return chain.proceed(requestBuilder.build()) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /data/src/main/java/cc/ptt/android/data/apiservices/board/BoardApi.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.data.apiservices.board 2 | 3 | import cc.ptt.android.data.model.remote.board.article.ArticleList 4 | import cc.ptt.android.data.model.remote.board.hotboard.BoardList 5 | import kotlinx.coroutines.flow.Flow 6 | import retrofit2.http.GET 7 | import retrofit2.http.Path 8 | import retrofit2.http.Query 9 | 10 | interface BoardApi { 11 | @GET("api/boards/popular") 12 | fun getPopularBoard(): Flow 13 | 14 | @GET("api/board/{bid}/articles") 15 | fun getArticles( 16 | @Path("bid") boardId: String, 17 | @Query("title") title: String, 18 | @Query("start_idx") startIndex: String, 19 | @Query("limit") limit: Int, 20 | @Query("desc") desc: Boolean, 21 | ): Flow 22 | 23 | @GET("api/boards") 24 | fun searchBoards( 25 | @Query("keyword") keyword: String, 26 | @Query("start_idx") start_idx: String, 27 | @Query("limit") limit: Int, 28 | @Query("asc") asc: Boolean, 29 | ): Flow 30 | 31 | @GET("api/user/{user_id}/favorites") 32 | fun favoriteBoards( 33 | @Path("user_id") user_id: String, 34 | @Query("level_idx") level_idx: String, 35 | @Query("start_idx") start_idx: String, 36 | @Query("limit") limit: Int, 37 | @Query("asc") asc: Boolean, 38 | ): Flow 39 | } 40 | -------------------------------------------------------------------------------- /data/src/main/java/cc/ptt/android/data/apiservices/user/UserApi.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.data.apiservices.user 2 | 3 | import cc.ptt.android.common.network.api.ApiMaxLogLevel 4 | import cc.ptt.android.common.network.api.MaxLogLevel 5 | import cc.ptt.android.data.model.remote.user.existuser.ExistUser 6 | import cc.ptt.android.data.model.remote.user.login.LoginEntity 7 | import cc.ptt.android.data.model.remote.user.userid.UserIdEntity 8 | import kotlinx.coroutines.flow.Flow 9 | import okhttp3.RequestBody 10 | import retrofit2.http.Body 11 | import retrofit2.http.GET 12 | import retrofit2.http.Headers 13 | import retrofit2.http.POST 14 | 15 | @ApiMaxLogLevel(MaxLogLevel.HEADERS) 16 | interface UserApi { 17 | @Headers("Content-Type: text/plain; charset=utf-8") 18 | @POST("api/account/login/") 19 | fun login( 20 | @Body body: RequestBody, 21 | ): Flow 22 | 23 | @Headers("Content-Type: text/plain; charset=utf-8") 24 | @POST("api/account/existsuser/") 25 | fun existUser( 26 | @Body body: RequestBody, 27 | ): Flow 28 | 29 | @Headers("Content-Type: text/plain; charset=utf-8") 30 | @GET("api/userid/") 31 | fun userId(): Flow 32 | } 33 | -------------------------------------------------------------------------------- /data/src/main/java/cc/ptt/android/data/di/ApiModules.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.data.di 2 | 3 | import cc.ptt.android.common.network.api.RetrofitServiceProvider 4 | import cc.ptt.android.common.network.api.TokenInterceptor 5 | import cc.ptt.android.common.network.api.apihelper.ApiHelper 6 | import cc.ptt.android.common.security.AESKeyStoreHelper 7 | import cc.ptt.android.common.security.AESKeyStoreHelperImpl 8 | import cc.ptt.android.data.ApiHelperImpl 9 | import cc.ptt.android.data.TokenInterceptorImpl 10 | import cc.ptt.android.data.apiservices.article.ArticleApi 11 | import cc.ptt.android.data.apiservices.board.BoardApi 12 | import cc.ptt.android.data.apiservices.user.UserApi 13 | import org.koin.dsl.module 14 | 15 | val apiModules = 16 | module { 17 | factory { AESKeyStoreHelperImpl(get()) } 18 | factory { ApiHelperImpl(get()) } 19 | single { TokenInterceptorImpl(get()) } 20 | factory { RetrofitServiceProvider(get(), get()) } 21 | single { get().create(BoardApi::class.java) } 22 | single { get().create(UserApi::class.java) } 23 | single { get().create(ArticleApi::class.java) } 24 | } 25 | -------------------------------------------------------------------------------- /data/src/main/java/cc/ptt/android/data/di/LocalDataSourceModules.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.data.di 2 | 3 | import cc.ptt.android.data.source.local.LoginLocalDataSource 4 | import cc.ptt.android.data.source.local.LoginLocalDataSourceImpl 5 | import org.koin.dsl.module 6 | 7 | val localDataSourceModules = 8 | module { 9 | single { LoginLocalDataSourceImpl(get()) } 10 | } 11 | -------------------------------------------------------------------------------- /data/src/main/java/cc/ptt/android/data/di/PreferenceModules.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.data.di 2 | 3 | import cc.ptt.android.data.preference.MainPreferences 4 | import cc.ptt.android.data.preference.MainPreferencesImpl 5 | import cc.ptt.android.data.preference.UserInfoPreferences 6 | import cc.ptt.android.data.preference.UserInfoPreferencesImpl 7 | import org.koin.android.ext.koin.androidContext 8 | import org.koin.dsl.module 9 | 10 | val preferenceModules = 11 | module { 12 | single { UserInfoPreferencesImpl(androidContext(), get()) } 13 | single { MainPreferencesImpl(androidContext()) } 14 | } 15 | -------------------------------------------------------------------------------- /data/src/main/java/cc/ptt/android/data/di/RemoteDataSourceModules.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.data.di 2 | 3 | import cc.ptt.android.data.source.remote.article.ArticleRemoteDataSource 4 | import cc.ptt.android.data.source.remote.article.ArticleRemoteDataSourceImpl 5 | import cc.ptt.android.data.source.remote.board.BoardRemoteDataSource 6 | import cc.ptt.android.data.source.remote.board.BoardRemoteDataSourceImpl 7 | import cc.ptt.android.data.source.remote.search.SearchBoardRemoteDataSource 8 | import cc.ptt.android.data.source.remote.search.SearchBoardRemoteDataSourceImpl 9 | import cc.ptt.android.data.source.remote.user.UserRemoteDataSource 10 | import cc.ptt.android.data.source.remote.user.UserRemoteDataSourceImpl 11 | import org.koin.dsl.module 12 | 13 | val remoteDataSourceModules = 14 | module { 15 | factory { BoardRemoteDataSourceImpl(get()) } 16 | factory { SearchBoardRemoteDataSourceImpl(get()) } 17 | factory { ArticleRemoteDataSourceImpl(get()) } 18 | factory { UserRemoteDataSourceImpl(get()) } 19 | } 20 | -------------------------------------------------------------------------------- /data/src/main/java/cc/ptt/android/data/di/RepositoryModules.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.data.di 2 | 3 | import cc.ptt.android.data.repository.article.ArticleRepository 4 | import cc.ptt.android.data.repository.article.ArticleRepositoryImpl 5 | import cc.ptt.android.data.repository.board.BoardRepository 6 | import cc.ptt.android.data.repository.board.BoardRepositoryImpl 7 | import cc.ptt.android.data.repository.populararticles.PopularArticlesRepository 8 | import cc.ptt.android.data.repository.populararticles.PopularArticlesRepositoryImpl 9 | import cc.ptt.android.data.repository.search.SearchBoardRepository 10 | import cc.ptt.android.data.repository.search.SearchBoardRepositoryImpl 11 | import cc.ptt.android.data.repository.user.UserRepository 12 | import cc.ptt.android.data.repository.user.UserRepositoryImpl 13 | import org.koin.dsl.module 14 | 15 | val repositoryModules = 16 | module { 17 | factory { PopularArticlesRepositoryImpl(get()) } 18 | factory { ArticleRepositoryImpl(get()) } 19 | factory { BoardRepositoryImpl(get()) } 20 | factory { UserRepositoryImpl(get(), get()) } 21 | factory { SearchBoardRepositoryImpl(get()) } 22 | } 23 | -------------------------------------------------------------------------------- /data/src/main/java/cc/ptt/android/data/model/remote/Comment.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.data.model.remote 2 | 3 | data class Comment( 4 | val userid: String, 5 | val tag: String, 6 | val content: String, 7 | val ip: String, 8 | val date: String, 9 | ) 10 | -------------------------------------------------------------------------------- /data/src/main/java/cc/ptt/android/data/model/remote/Post.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.data.model.remote 2 | 3 | data class Post( 4 | val title: String, 5 | val classString: String, 6 | val date: String, 7 | val auth: String, 8 | val authNickName: String, 9 | val content: String, 10 | val comments: List, 11 | ) 12 | -------------------------------------------------------------------------------- /data/src/main/java/cc/ptt/android/data/model/remote/PostRank.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.data.model.remote 2 | 3 | data class PostRank( 4 | val board: String, 5 | val aid: String, 6 | val goup: Int, 7 | val down: Int, 8 | ) { 9 | fun getLike(): Int = goup - down 10 | } 11 | -------------------------------------------------------------------------------- /data/src/main/java/cc/ptt/android/data/model/remote/ServerMessage.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.data.model.remote 2 | 3 | import cc.ptt.android.common.network.api.ApiException 4 | import com.google.gson.Gson 5 | import com.google.gson.annotations.SerializedName 6 | 7 | data class ServerMessage( 8 | @SerializedName("Msg") 9 | val msg: String, 10 | ) 11 | 12 | val ApiException.serverMsg: ServerMessage get() = 13 | try { 14 | Gson().fromJson(this.message, ServerMessage::class.java) 15 | } catch (_: Throwable) { 16 | ServerMessage(this.message.orEmpty()) 17 | } 18 | -------------------------------------------------------------------------------- /data/src/main/java/cc/ptt/android/data/model/remote/article/ArticleComment.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.data.model.remote.article 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class ArticleComment( 6 | @SerializedName("aid") 7 | val articleId: String, 8 | @SerializedName("bid") 9 | val boardId: String, 10 | @SerializedName("cid") 11 | val commentId: String, 12 | @SerializedName("content") 13 | val content: List>?, 14 | @SerializedName("create_time") 15 | val createTime: Int, 16 | @SerializedName("deleted") 17 | val deleted: Boolean, 18 | @SerializedName("host") 19 | val host: String, 20 | @SerializedName("ip") 21 | val ip: String, 22 | @SerializedName("owner") 23 | val owner: String, 24 | /** 25 | * 是在回應哪個 comment. 第 0 層是 '' 26 | */ 27 | @SerializedName("refid") 28 | val refId: String, 29 | /** 30 | * 推/噓/→ 31 | */ 32 | @SerializedName("type") 33 | val type: Int, 34 | ) { 35 | val articleCommentType get() = 36 | ArticleCommentType.parse( 37 | type, 38 | ) 39 | } 40 | 41 | enum class ArticleCommentType( 42 | val value: Int, 43 | ) { 44 | PUSH(1), 45 | HUSH(2), 46 | COMMENT(3), 47 | ; 48 | 49 | companion object { 50 | fun parse(value: Int): ArticleCommentType = values().find { it.value == value } ?: COMMENT 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /data/src/main/java/cc/ptt/android/data/model/remote/article/ArticleCommentsList.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.data.model.remote.article 2 | import com.google.gson.annotations.SerializedName 3 | 4 | data class ArticleCommentsList( 5 | @SerializedName("list") 6 | val list: List, 7 | @SerializedName("next_idx") 8 | val nextIndex: String, 9 | ) 10 | -------------------------------------------------------------------------------- /data/src/main/java/cc/ptt/android/data/model/remote/article/ArticleDetail.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.data.model.remote.article 2 | import com.google.gson.annotations.SerializedName 3 | 4 | data class ArticleDetail( 5 | @SerializedName("aid") 6 | val articleId: String, 7 | @SerializedName("bbs") 8 | val bbs: String, 9 | @SerializedName("bid") 10 | val boardId: String, 11 | @SerializedName("brdname") 12 | val boardName: String, 13 | @SerializedName("class") 14 | val classX: String, 15 | @SerializedName("content") 16 | val content: List>, 17 | @SerializedName("create_time") 18 | val createTime: Int, 19 | @SerializedName("deleted") 20 | val deleted: Boolean, 21 | @SerializedName("host") 22 | val host: String, 23 | @SerializedName("ip") 24 | val ip: String, 25 | @SerializedName("mode") 26 | val mode: Int, 27 | @SerializedName("modified") 28 | val modified: Int, 29 | @SerializedName("money") 30 | val money: Int, 31 | /** 32 | * 文章回覆數 33 | */ 34 | @SerializedName("n_comments") 35 | val nComments: Int, 36 | @SerializedName("owner") 37 | val owner: String, 38 | @SerializedName("read") 39 | val read: Boolean, 40 | /** 41 | * PTT 推噓數量 42 | */ 43 | @SerializedName("recommend") 44 | val recommend: Int, 45 | @SerializedName("title") 46 | val title: String, 47 | @SerializedName("url") 48 | val url: String, 49 | /** 50 | * 文章按讚數量 51 | */ 52 | @SerializedName("rank") 53 | val rank: Int, 54 | ) 55 | -------------------------------------------------------------------------------- /data/src/main/java/cc/ptt/android/data/model/remote/article/ArticleRank.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.data.model.remote.article 2 | 3 | data class ArticleRank( 4 | val rank: Int, 5 | ) 6 | -------------------------------------------------------------------------------- /data/src/main/java/cc/ptt/android/data/model/remote/article/Color.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.data.model.remote.article 2 | 3 | import cc.ptt.android.common.ptt.PttColor 4 | import com.google.gson.annotations.SerializedName 5 | 6 | data class Color( 7 | @SerializedName("background") 8 | val background: Int, 9 | @SerializedName("blink") 10 | val blink: Boolean, 11 | @SerializedName("foreground") 12 | val foreground: Int, 13 | @SerializedName("highlight") 14 | val highlight: Boolean, 15 | @SerializedName("reset") 16 | val reset: Boolean, 17 | ) { 18 | val backgroundColor: Int 19 | get() = PttColor.backgroundColor(background) 20 | 21 | val foregroundColor: Int 22 | get() = PttColor.foregroundColor(foreground, highlight) 23 | } 24 | -------------------------------------------------------------------------------- /data/src/main/java/cc/ptt/android/data/model/remote/article/Content.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.data.model.remote.article 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class Content( 6 | @SerializedName("color0") 7 | val color0: Color, 8 | @SerializedName("color1") 9 | val color1: Color, 10 | @SerializedName("text") 11 | val text: String, 12 | ) 13 | -------------------------------------------------------------------------------- /data/src/main/java/cc/ptt/android/data/model/remote/article/hotarticle/HotArticle.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.data.model.remote.article.hotarticle 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class HotArticle( 6 | @SerializedName("aid") 7 | var aid: String, 8 | @SerializedName("bid") 9 | var bid: String, 10 | @SerializedName("deleted") 11 | var deleted: Boolean, 12 | @SerializedName("filename") 13 | var filename: String, 14 | @SerializedName("create_time") 15 | var createTime: Int, 16 | @SerializedName("modified") 17 | var modified: Int, 18 | @SerializedName("recommend") 19 | var recommend: Int, 20 | @SerializedName("n_comments") 21 | var nComments: Int, 22 | @SerializedName("owner") 23 | var owner: String, 24 | @SerializedName("date") 25 | var date: String, 26 | @SerializedName("title") 27 | var title: String, 28 | @SerializedName("money") 29 | var money: Int, 30 | @SerializedName("type") 31 | var type: String, 32 | @SerializedName("class") 33 | var `class`: String, 34 | @SerializedName("mode") 35 | var mode: Int, 36 | @SerializedName("url") 37 | var url: String, 38 | @SerializedName("read") 39 | var read: Boolean, 40 | @SerializedName("idx") 41 | var idx: String, 42 | @SerializedName("rank") 43 | var rank: Int, 44 | @SerializedName("subject_type") 45 | var subjectType: String, 46 | ) 47 | -------------------------------------------------------------------------------- /data/src/main/java/cc/ptt/android/data/model/remote/article/hotarticle/HotArticleList.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.data.model.remote.article.hotarticle 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class HotArticleList( 6 | @SerializedName("list") 7 | var list: ArrayList, 8 | @SerializedName("next_idx") 9 | var nextIdx: String, 10 | ) 11 | -------------------------------------------------------------------------------- /data/src/main/java/cc/ptt/android/data/model/remote/board/article/Article.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.data.model.remote.board.article 2 | 3 | import android.os.Parcelable 4 | import com.google.gson.annotations.SerializedName 5 | import kotlinx.parcelize.Parcelize 6 | 7 | /** 8 | * Created by Michael.Lien 9 | * on 2021/2/18 10 | */ 11 | @Parcelize 12 | data class Article( 13 | @SerializedName("aid") 14 | val articleId: String, 15 | @SerializedName("bid") 16 | val boardId: String, 17 | @SerializedName("class") 18 | val classX: String, 19 | @SerializedName("create_time") 20 | val createTime: Int, 21 | val deleted: Boolean, 22 | @SerializedName("idx") 23 | val index: String, 24 | val mode: Int, 25 | val modified: Int, 26 | val money: Int, 27 | @SerializedName("n_comments") 28 | val nComments: Int, 29 | val owner: String, 30 | var read: Boolean, 31 | val recommend: Int, 32 | val title: String, 33 | val url: String, 34 | ) : Parcelable 35 | -------------------------------------------------------------------------------- /data/src/main/java/cc/ptt/android/data/model/remote/board/article/ArticleList.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.data.model.remote.board.article 2 | import com.google.gson.annotations.SerializedName 3 | 4 | /** 5 | * Created by Michael.Lien 6 | * on 2021/2/18 7 | */ 8 | data class ArticleList( 9 | val list: List
, 10 | @SerializedName("next_create_time") 11 | val nextCreateTime: Int, 12 | @SerializedName("next_idx") 13 | val nextIndex: String, 14 | @SerializedName("start_num_idx") 15 | val startNumIndex: Int, 16 | ) 17 | -------------------------------------------------------------------------------- /data/src/main/java/cc/ptt/android/data/model/remote/board/hotboard/BoardList.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.data.model.remote.board.hotboard 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class BoardList( 6 | @SerializedName("list") 7 | val list: List, 8 | @SerializedName("next_idx") 9 | val nextId: String, 10 | ) 11 | 12 | data class Board( 13 | @SerializedName("bid") 14 | val boardId: String, 15 | @SerializedName("brdname") 16 | val boardName: String, 17 | @SerializedName("class") 18 | val boardClass: String, 19 | @SerializedName("flag") 20 | val boardAttributes: Int, 21 | @SerializedName("last_post_time") 22 | val lastPostTime: Int, 23 | @SerializedName("moderators") 24 | val moderators: List, 25 | @SerializedName("nuser") 26 | val onlineUser: Int, 27 | @SerializedName("read") 28 | val read: Boolean, 29 | @SerializedName("reason") 30 | val reason: String, 31 | @SerializedName("stat_attr") 32 | val stateAttributes: Int, 33 | @SerializedName("title") 34 | val title: String, 35 | @SerializedName("total") 36 | val totalArticles: Int, 37 | @SerializedName("type") 38 | val type: String, 39 | ) 40 | -------------------------------------------------------------------------------- /data/src/main/java/cc/ptt/android/data/model/remote/board/hotboard/HotBoardTemp.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.data.model.remote.board.hotboard 2 | 3 | data class HotBoardTemp( 4 | val number: Int, 5 | val title: String, 6 | val subtitle: String, 7 | val boardType: Int, 8 | val online: Int, 9 | val onlineColor: String, 10 | ) 11 | -------------------------------------------------------------------------------- /data/src/main/java/cc/ptt/android/data/model/remote/board/hotboard/HotBoardsItem.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.data.model.remote.board.hotboard 2 | 3 | data class HotBoardsItem( 4 | val boardId: String = "", 5 | val boardName: String = "", 6 | val subtitle: String = "", 7 | val online: String = "", 8 | val onlineColor: String = "", 9 | ) 10 | -------------------------------------------------------------------------------- /data/src/main/java/cc/ptt/android/data/model/remote/board/searchboard/SearchBoardsItem.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.data.model.remote.board.searchboard 2 | 3 | data class SearchBoardsItem( 4 | val boardId: String = "", 5 | val number: Int = 0, 6 | val title: String = "", 7 | val subtitle: String = "", 8 | val boardType: Int = 0, 9 | val like: Boolean = false, 10 | val moderators: String = "", 11 | val _class: String = "", 12 | val online: Int = 0, 13 | val onlineColor: Int = 7, 14 | ) 15 | -------------------------------------------------------------------------------- /data/src/main/java/cc/ptt/android/data/model/remote/user/existuser/ExistUser.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.data.model.remote.user.existuser 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class ExistUser( 6 | @SerializedName("is_exists") 7 | val isExist: Boolean, 8 | ) 9 | -------------------------------------------------------------------------------- /data/src/main/java/cc/ptt/android/data/model/remote/user/existuser/ExistUserRequest.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.data.model.remote.user.existuser 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class ExistUserRequest( 6 | @SerializedName("client_id") 7 | val clientId: String, 8 | @SerializedName("client_secret") 9 | val clientSecret: String, 10 | @SerializedName("username") 11 | val userName: String, 12 | ) 13 | -------------------------------------------------------------------------------- /data/src/main/java/cc/ptt/android/data/model/remote/user/login/LoginEntity.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.data.model.remote.user.login 2 | import com.google.gson.annotations.SerializedName 3 | 4 | data class LoginEntity( 5 | @SerializedName("access_token") 6 | val accessToken: String, 7 | @SerializedName("token_type") 8 | val tokenType: String, 9 | @SerializedName("user_id") 10 | val userId: String, 11 | ) 12 | -------------------------------------------------------------------------------- /data/src/main/java/cc/ptt/android/data/model/remote/user/login/LoginRequest.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.data.model.remote.user.login 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class LoginRequest( 6 | @SerializedName("client_id") 7 | val clientId: String, 8 | @SerializedName("client_secret") 9 | val clientSecret: String, 10 | @SerializedName("username") 11 | val userName: String, 12 | @SerializedName("password") 13 | val password: String, 14 | ) 15 | -------------------------------------------------------------------------------- /data/src/main/java/cc/ptt/android/data/model/remote/user/userid/UserIdEntity.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.data.model.remote.user.userid 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class UserIdEntity( 6 | @SerializedName("user_id") 7 | val userId: String, 8 | ) 9 | -------------------------------------------------------------------------------- /data/src/main/java/cc/ptt/android/data/model/remote/user/userid/UserIdRequest.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.data.model.remote.user.userid 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class UserIdRequest( 6 | @SerializedName("client_id") 7 | val clientId: String, 8 | @SerializedName("client_secret") 9 | val clientSecret: String, 10 | ) 11 | -------------------------------------------------------------------------------- /data/src/main/java/cc/ptt/android/data/preference/MainPreferences.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.data.preference 2 | 3 | interface MainPreferences { 4 | fun setApiDomain(text: String?) 5 | 6 | fun getApiDomain(): String 7 | 8 | fun setThemeType(type: Int) 9 | 10 | fun getThemeType(): Int 11 | 12 | fun setSearchStyle(type: Int) 13 | 14 | fun getSearchStyle(): Int 15 | 16 | fun setPostBottomStyle(type: Int) 17 | 18 | fun getPostBottomStyle(): Int 19 | } 20 | -------------------------------------------------------------------------------- /data/src/main/java/cc/ptt/android/data/preference/UserInfoPreferences.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.data.preference 2 | 3 | import cc.ptt.android.data.model.remote.user.login.LoginEntity 4 | 5 | interface UserInfoPreferences { 6 | fun setLogin(loginEntity: LoginEntity?) 7 | 8 | fun getLogin(): LoginEntity? 9 | } 10 | -------------------------------------------------------------------------------- /data/src/main/java/cc/ptt/android/data/repository/article/ArticleRepository.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.data.repository.article 2 | 3 | import cc.ptt.android.data.model.remote.article.ArticleComment 4 | import cc.ptt.android.data.model.remote.article.ArticleCommentsList 5 | import cc.ptt.android.data.model.remote.article.ArticleDetail 6 | import cc.ptt.android.data.model.remote.article.ArticleRank 7 | import kotlinx.coroutines.flow.Flow 8 | 9 | interface ArticleRepository { 10 | fun getArticleDetail( 11 | boardId: String, 12 | articleId: String, 13 | ): Flow 14 | 15 | fun getArticleComments( 16 | boardId: String, 17 | articleId: String, 18 | desc: Boolean = false, 19 | ): Flow 20 | 21 | fun createArticleComment( 22 | bid: String, 23 | aid: String, 24 | type: Int, 25 | content: String, 26 | ): Flow 27 | 28 | fun postArticleRank( 29 | rank: Int, 30 | boardId: String, 31 | articleId: String, 32 | ): Flow 33 | } 34 | -------------------------------------------------------------------------------- /data/src/main/java/cc/ptt/android/data/repository/article/ArticleRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.data.repository.article 2 | 3 | import cc.ptt.android.data.model.remote.article.ArticleComment 4 | import cc.ptt.android.data.model.remote.article.ArticleCommentsList 5 | import cc.ptt.android.data.model.remote.article.ArticleDetail 6 | import cc.ptt.android.data.model.remote.article.ArticleRank 7 | import cc.ptt.android.data.source.remote.article.ArticleRemoteDataSource 8 | import kotlinx.coroutines.Dispatchers 9 | import kotlinx.coroutines.flow.Flow 10 | import kotlinx.coroutines.flow.flowOn 11 | 12 | class ArticleRepositoryImpl constructor( 13 | private val articleRemoteDataSource: ArticleRemoteDataSource, 14 | ) : ArticleRepository { 15 | override fun getArticleDetail( 16 | boardId: String, 17 | articleId: String, 18 | ): Flow = articleRemoteDataSource.getArticleDetail(boardId, articleId).flowOn(Dispatchers.IO) 19 | 20 | override fun getArticleComments( 21 | boardId: String, 22 | articleId: String, 23 | desc: Boolean, 24 | ): Flow = articleRemoteDataSource.getArticleComments(boardId, articleId, desc).flowOn(Dispatchers.IO) 25 | 26 | override fun createArticleComment( 27 | bid: String, 28 | aid: String, 29 | type: Int, 30 | content: String, 31 | ): Flow = articleRemoteDataSource.createArticleComment(bid, aid, type, content).flowOn(Dispatchers.IO) 32 | 33 | override fun postArticleRank( 34 | rank: Int, 35 | boardId: String, 36 | articleId: String, 37 | ): Flow = articleRemoteDataSource.postArticleRank(rank, boardId, articleId).flowOn(Dispatchers.IO) 38 | } 39 | -------------------------------------------------------------------------------- /data/src/main/java/cc/ptt/android/data/repository/board/BoardRepository.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.data.repository.board 2 | 3 | import cc.ptt.android.data.model.remote.board.article.ArticleList 4 | import cc.ptt.android.data.model.remote.board.hotboard.BoardList 5 | import kotlinx.coroutines.flow.Flow 6 | 7 | interface BoardRepository { 8 | fun getPopularBoards(): Flow 9 | 10 | fun getBoardArticles( 11 | boardId: String, 12 | title: String = "", 13 | startIndex: String = "", 14 | limit: Int = 200, 15 | desc: Boolean = true, 16 | ): Flow 17 | 18 | fun getFavoriteBoards( 19 | userid: String = "", 20 | level_idx: String = "", 21 | startIndex: String = "", 22 | limit: Int = 200, 23 | aces: Boolean = true, 24 | ): Flow 25 | } 26 | -------------------------------------------------------------------------------- /data/src/main/java/cc/ptt/android/data/repository/board/BoardRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.data.repository.board 2 | 3 | import cc.ptt.android.data.model.remote.board.article.ArticleList 4 | import cc.ptt.android.data.model.remote.board.hotboard.BoardList 5 | import cc.ptt.android.data.source.remote.board.BoardRemoteDataSource 6 | import kotlinx.coroutines.Dispatchers 7 | import kotlinx.coroutines.flow.Flow 8 | import kotlinx.coroutines.flow.flowOn 9 | 10 | class BoardRepositoryImpl constructor( 11 | private val boardRemoteDataSource: BoardRemoteDataSource, 12 | ) : BoardRepository { 13 | override fun getPopularBoards(): Flow = boardRemoteDataSource.getPopularBoards().flowOn(Dispatchers.IO) 14 | 15 | override fun getBoardArticles( 16 | boardId: String, 17 | title: String, 18 | startIndex: String, 19 | limit: Int, 20 | desc: Boolean, 21 | ): Flow = boardRemoteDataSource.getBoardArticles(boardId, title, startIndex, limit, desc).flowOn(Dispatchers.IO) 22 | 23 | override fun getFavoriteBoards( 24 | userid: String, 25 | level_idx: String, 26 | startIndex: String, 27 | limit: Int, 28 | aces: Boolean, 29 | ): Flow = boardRemoteDataSource.getFavoriteBoards(userid, level_idx, startIndex, limit, aces).flowOn(Dispatchers.IO) 30 | } 31 | -------------------------------------------------------------------------------- /data/src/main/java/cc/ptt/android/data/repository/populararticles/PopularArticlesRepository.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.data.repository.populararticles 2 | 3 | import cc.ptt.android.data.model.remote.article.hotarticle.HotArticleList 4 | import kotlinx.coroutines.flow.Flow 5 | 6 | interface PopularArticlesRepository { 7 | fun getPopularArticles( 8 | startIndex: String, 9 | limit: Int, 10 | desc: Boolean, 11 | ): Flow 12 | } 13 | -------------------------------------------------------------------------------- /data/src/main/java/cc/ptt/android/data/repository/populararticles/PopularArticlesRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.data.repository.populararticles 2 | 3 | import cc.ptt.android.data.apiservices.article.ArticleApi 4 | import cc.ptt.android.data.model.remote.article.hotarticle.HotArticleList 5 | import kotlinx.coroutines.flow.Flow 6 | 7 | class PopularArticlesRepositoryImpl constructor( 8 | private val articleApi: ArticleApi, 9 | ) : PopularArticlesRepository { 10 | override fun getPopularArticles( 11 | startIndex: String, 12 | limit: Int, 13 | desc: Boolean, 14 | ): Flow = articleApi.getPopularArticles(startIndex = startIndex, limit = limit, desc = desc) 15 | } 16 | -------------------------------------------------------------------------------- /data/src/main/java/cc/ptt/android/data/repository/search/SearchBoardRepository.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.data.repository.search 2 | 3 | import cc.ptt.android.data.model.remote.board.hotboard.BoardList 4 | import kotlinx.coroutines.flow.Flow 5 | 6 | interface SearchBoardRepository { 7 | fun searchBoardByKeyword( 8 | keyword: String, 9 | startIndex: String = "", 10 | limit: Int = 200, 11 | aces: Boolean = true, 12 | ): Flow 13 | } 14 | -------------------------------------------------------------------------------- /data/src/main/java/cc/ptt/android/data/repository/search/SearchBoardRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.data.repository.search 2 | 3 | import cc.ptt.android.data.model.remote.board.hotboard.BoardList 4 | import cc.ptt.android.data.source.remote.search.SearchBoardRemoteDataSource 5 | import kotlinx.coroutines.Dispatchers 6 | import kotlinx.coroutines.flow.Flow 7 | import kotlinx.coroutines.flow.flowOn 8 | 9 | class SearchBoardRepositoryImpl constructor( 10 | private val searchBoardRemoteDataSource: SearchBoardRemoteDataSource, 11 | ) : SearchBoardRepository { 12 | override fun searchBoardByKeyword( 13 | keyword: String, 14 | startIndex: String, 15 | limit: Int, 16 | aces: Boolean, 17 | ): Flow = searchBoardRemoteDataSource.searchBoardByKeyword(keyword, startIndex, limit, aces).flowOn(Dispatchers.IO) 18 | } 19 | -------------------------------------------------------------------------------- /data/src/main/java/cc/ptt/android/data/repository/user/UserRepository.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.data.repository.user 2 | 3 | import cc.ptt.android.data.model.remote.user.existuser.ExistUser 4 | import cc.ptt.android.data.model.remote.user.login.LoginEntity 5 | import kotlinx.coroutines.flow.Flow 6 | 7 | interface UserRepository { 8 | fun login( 9 | clientId: String, 10 | clientSecret: String, 11 | userName: String, 12 | password: String, 13 | ): Flow 14 | 15 | fun logout(): Flow 16 | 17 | fun existUser( 18 | clientId: String, 19 | clientSecret: String, 20 | userName: String, 21 | ): Flow 22 | 23 | fun isLogin(): Boolean 24 | 25 | fun isGuest(): Boolean 26 | 27 | fun getUserInfo(): LoginEntity? 28 | 29 | fun userId(): Flow 30 | } 31 | -------------------------------------------------------------------------------- /data/src/main/java/cc/ptt/android/data/source/local/LoginLocalDataSource.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.data.source.local 2 | 3 | import cc.ptt.android.data.model.remote.user.login.LoginEntity 4 | 5 | interface LoginLocalDataSource { 6 | fun isLogin(): Boolean 7 | 8 | fun cleanUserInfo() 9 | 10 | fun setUserInfo(userInfo: LoginEntity) 11 | 12 | fun getUserInfo(): LoginEntity? 13 | } 14 | -------------------------------------------------------------------------------- /data/src/main/java/cc/ptt/android/data/source/local/LoginLocalDataSourceImpl.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.data.source.local 2 | 3 | import cc.ptt.android.data.model.remote.user.login.LoginEntity 4 | import cc.ptt.android.data.preference.UserInfoPreferences 5 | 6 | class LoginLocalDataSourceImpl constructor( 7 | private val userInfoPreferences: UserInfoPreferences, 8 | ) : LoginLocalDataSource { 9 | override fun isLogin(): Boolean = 10 | getUserInfo()?.let { 11 | true 12 | } ?: false 13 | 14 | override fun cleanUserInfo() { 15 | userInfoPreferences.setLogin(null) 16 | } 17 | 18 | override fun setUserInfo(loginEntity: LoginEntity) { 19 | userInfoPreferences.setLogin(loginEntity) 20 | } 21 | 22 | override fun getUserInfo(): LoginEntity? = userInfoPreferences.getLogin() 23 | } 24 | -------------------------------------------------------------------------------- /data/src/main/java/cc/ptt/android/data/source/remote/article/ArticleRemoteDataSource.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.data.source.remote.article 2 | 3 | import cc.ptt.android.data.model.remote.article.ArticleComment 4 | import cc.ptt.android.data.model.remote.article.ArticleCommentsList 5 | import cc.ptt.android.data.model.remote.article.ArticleDetail 6 | import cc.ptt.android.data.model.remote.article.ArticleRank 7 | import kotlinx.coroutines.flow.Flow 8 | 9 | interface ArticleRemoteDataSource { 10 | fun getArticleDetail( 11 | boardId: String, 12 | articleId: String, 13 | ): Flow 14 | 15 | fun getArticleComments( 16 | boardId: String, 17 | articleId: String, 18 | desc: Boolean = false, 19 | ): Flow 20 | 21 | fun postArticleRank( 22 | rank: Int, 23 | boardId: String, 24 | articleId: String, 25 | ): Flow 26 | 27 | fun createArticleComment( 28 | bid: String, 29 | aid: String, 30 | type: Int, 31 | content: String, 32 | ): Flow 33 | } 34 | -------------------------------------------------------------------------------- /data/src/main/java/cc/ptt/android/data/source/remote/board/BoardRemoteDataSource.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.data.source.remote.board 2 | 3 | import cc.ptt.android.data.model.remote.board.article.ArticleList 4 | import cc.ptt.android.data.model.remote.board.hotboard.BoardList 5 | import kotlinx.coroutines.flow.Flow 6 | 7 | interface BoardRemoteDataSource { 8 | fun getPopularBoards(): Flow 9 | 10 | fun getBoardArticles( 11 | boardId: String, 12 | title: String = "", 13 | startIndex: String = "", 14 | limit: Int = 200, 15 | desc: Boolean = true, 16 | ): Flow 17 | 18 | fun getFavoriteBoards( 19 | userid: String = "", 20 | level_idx: String = "", 21 | startIndex: String = "", 22 | limit: Int = 200, 23 | aces: Boolean = true, 24 | ): Flow 25 | } 26 | -------------------------------------------------------------------------------- /data/src/main/java/cc/ptt/android/data/source/remote/board/BoardRemoteDataSourceImpl.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.data.source.remote.board 2 | 3 | import cc.ptt.android.data.apiservices.board.BoardApi 4 | import cc.ptt.android.data.model.remote.board.article.ArticleList 5 | import cc.ptt.android.data.model.remote.board.hotboard.BoardList 6 | import kotlinx.coroutines.flow.Flow 7 | 8 | class BoardRemoteDataSourceImpl constructor( 9 | private val boardApi: BoardApi, 10 | ) : BoardRemoteDataSource { 11 | override fun getPopularBoards(): Flow = boardApi.getPopularBoard() 12 | 13 | override fun getBoardArticles( 14 | boardId: String, 15 | title: String, 16 | startIndex: String, 17 | limit: Int, 18 | desc: Boolean, 19 | ): Flow = 20 | boardApi.getArticles( 21 | boardId, 22 | title, 23 | startIndex, 24 | limit, 25 | desc, 26 | ) 27 | 28 | override fun getFavoriteBoards( 29 | userid: String, 30 | level_idx: String, 31 | startIndex: String, 32 | limit: Int, 33 | aces: Boolean, 34 | ): Flow = boardApi.favoriteBoards(userid, level_idx, startIndex, limit, aces) 35 | } 36 | -------------------------------------------------------------------------------- /data/src/main/java/cc/ptt/android/data/source/remote/search/SearchBoardRemoteDataSource.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.data.source.remote.search 2 | 3 | import cc.ptt.android.data.model.remote.board.hotboard.BoardList 4 | import kotlinx.coroutines.flow.Flow 5 | 6 | interface SearchBoardRemoteDataSource { 7 | fun searchBoardByKeyword( 8 | keyword: String, 9 | startIndex: String = "", 10 | limit: Int = 200, 11 | aces: Boolean = true, 12 | ): Flow 13 | } 14 | -------------------------------------------------------------------------------- /data/src/main/java/cc/ptt/android/data/source/remote/search/SearchBoardRemoteDataSourceImpl.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.data.source.remote.search 2 | 3 | import cc.ptt.android.data.apiservices.board.BoardApi 4 | import cc.ptt.android.data.model.remote.board.hotboard.BoardList 5 | import kotlinx.coroutines.flow.Flow 6 | 7 | class SearchBoardRemoteDataSourceImpl constructor( 8 | private val boardApi: BoardApi, 9 | ) : SearchBoardRemoteDataSource { 10 | override fun searchBoardByKeyword( 11 | keyword: String, 12 | startIndex: String, 13 | limit: Int, 14 | aces: Boolean, 15 | ): Flow = 16 | boardApi.searchBoards( 17 | keyword = keyword, 18 | start_idx = startIndex, 19 | limit = limit, 20 | asc = aces, 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /data/src/main/java/cc/ptt/android/data/source/remote/user/UserRemoteDataSource.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.data.source.remote.user 2 | 3 | import cc.ptt.android.data.model.remote.user.existuser.ExistUser 4 | import cc.ptt.android.data.model.remote.user.login.LoginEntity 5 | import cc.ptt.android.data.model.remote.user.userid.UserIdEntity 6 | import kotlinx.coroutines.flow.Flow 7 | 8 | interface UserRemoteDataSource { 9 | fun login( 10 | clientId: String, 11 | clientSecret: String, 12 | userName: String, 13 | password: String, 14 | ): Flow 15 | 16 | fun existUser( 17 | clientId: String, 18 | clientSecret: String, 19 | userName: String, 20 | ): Flow 21 | 22 | fun userId(): Flow 23 | } 24 | -------------------------------------------------------------------------------- /data/src/main/res/values-night/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #171717 4 | #171717 5 | #FF9F0A 6 | 7 | #FF9F0A 8 | #5E5E66 9 | #000000 10 | #171717 11 | #2A2A2E 12 | #888894 13 | #F0F0F7 14 | #CBCBD6 15 | #c2c2cc 16 | #1C1C1F 17 | #38383D 18 | #44444a 19 | #aaaab3 20 | #e4e4eb 21 | #0a84ff 22 | #30d158 23 | #5e5ce6 24 | #ff453a 25 | #64d2ff 26 | #ff375f 27 | #bf5af2 28 | #ffd60a 29 | #131314 30 | -------------------------------------------------------------------------------- /data/src/main/res/values-notnight/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | #171717 5 | #171717 6 | #FF9F0A 7 | 8 | #FF9F0A 9 | #747474 10 | #C2C2C2 11 | #FFFFFF 12 | #B9B9B9 13 | #888888 14 | #161616 15 | #ACACAC 16 | #c2c2cc 17 | #ECECEC 18 | #A0A0A0 19 | #44444a 20 | #CECECE 21 | #e4e4eb 22 | #0a84ff 23 | #30d158 24 | #5e5ce6 25 | #ff453a 26 | #64d2ff 27 | #ff375f 28 | #bf5af2 29 | #ffd60a 30 | #F3F3F3 31 | -------------------------------------------------------------------------------- /data/src/test/java/cc/ptt/android/data/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.data 2 | 3 | import org.junit.Assert 4 | import org.junit.Test 5 | 6 | /** 7 | * Example local unit test, which will execute on the development machine (host). 8 | * 9 | * @see [Testing documentation](http://d.android.com/tools/testing) 10 | */ 11 | class ExampleUnitTest { 12 | @Test 13 | fun addition_isCorrect() { 14 | Assert.assertEquals(4, 2 + 2.toLong()) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /data/src/test/java/cc/ptt/android/data/KoinTestBase.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.data 2 | 3 | import cc.ptt.android.common.network.api.apihelper.ApiHelper 4 | import cc.ptt.android.common.security.AESKeyStoreHelper 5 | import cc.ptt.android.data.di.apiModules 6 | import cc.ptt.android.data.di.localDataSourceModules 7 | import cc.ptt.android.data.di.remoteDataSourceModules 8 | import cc.ptt.android.data.di.repositoryModules 9 | import io.mockk.mockkClass 10 | import kotlinx.coroutines.FlowPreview 11 | import org.junit.Rule 12 | import org.koin.dsl.module 13 | import org.koin.test.KoinTest 14 | import org.koin.test.mock.MockProviderRule 15 | 16 | @FlowPreview 17 | open class KoinTestBase : KoinTest { 18 | companion object { 19 | @JvmStatic 20 | val testAppModules = 21 | module { 22 | factory { MockAESKeyStoreHelperImpl() } 23 | factory { TestApiHelperImpl() } 24 | } 25 | 26 | @JvmStatic 27 | val koinModules = 28 | listOf( 29 | apiModules, 30 | testAppModules, 31 | remoteDataSourceModules, 32 | localDataSourceModules, 33 | repositoryModules, 34 | ) 35 | } 36 | 37 | @get:Rule 38 | val koinTestRule = 39 | KoinTestRule( 40 | modules = koinModules, 41 | ) 42 | 43 | @get:Rule 44 | val mockProvider = 45 | MockProviderRule.create { clazz -> 46 | // Your way to build a Mock here 47 | mockkClass(clazz) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /data/src/test/java/cc/ptt/android/data/KoinTestRule.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.data 2 | 3 | import org.junit.rules.TestWatcher 4 | import org.junit.runner.Description 5 | import org.koin.core.context.GlobalContext.startKoin 6 | import org.koin.core.context.GlobalContext.stopKoin 7 | import org.koin.core.module.Module 8 | 9 | class KoinTestRule( 10 | private val modules: List, 11 | ) : TestWatcher() { 12 | override fun starting(description: Description) { 13 | startKoin { 14 | modules(modules) 15 | } 16 | } 17 | 18 | override fun finished(description: Description) { 19 | stopKoin() 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /data/src/test/java/cc/ptt/android/data/MainCoroutineRule.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.data 2 | 3 | import kotlinx.coroutines.Dispatchers 4 | import kotlinx.coroutines.ExperimentalCoroutinesApi 5 | import kotlinx.coroutines.test.StandardTestDispatcher 6 | import kotlinx.coroutines.test.TestScope 7 | import kotlinx.coroutines.test.resetMain 8 | import kotlinx.coroutines.test.setMain 9 | import org.junit.rules.TestWatcher 10 | import org.junit.runner.Description 11 | 12 | /** 13 | * Created by Michael.Lien 14 | * on 2021/2/1 15 | */ 16 | @ExperimentalCoroutinesApi 17 | class MainCoroutineRule : TestWatcher() { 18 | private val dispatcher = StandardTestDispatcher() 19 | val scope = TestScope(dispatcher) 20 | 21 | override fun starting(description: Description?) { 22 | super.starting(description) 23 | Dispatchers.setMain(dispatcher) 24 | } 25 | 26 | override fun finished(description: Description?) { 27 | super.finished(description) 28 | Dispatchers.resetMain() 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /data/src/test/java/cc/ptt/android/data/MockAESKeyStoreHelperImpl.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.data 2 | 3 | import cc.ptt.android.common.security.AESKeyStoreHelper 4 | 5 | class MockAESKeyStoreHelperImpl : AESKeyStoreHelper { 6 | override fun encrypt(plainText: String?): String = plainText.orEmpty() 7 | 8 | override fun decrypt(encryptedText: String?): String = encryptedText.toString() 9 | } 10 | -------------------------------------------------------------------------------- /data/src/test/java/cc/ptt/android/data/TestApiHelperImpl.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.data 2 | 3 | import cc.ptt.android.common.network.api.apihelper.ApiHelper 4 | 5 | class TestApiHelperImpl : ApiHelper { 6 | override fun getHost(): String = 7 | BuildConfig.API_HOST.ifBlank { 8 | ApiHelper.API_HOST 9 | } 10 | 11 | override fun getClientId(): String = ApiHelper.CLIENT_ID 12 | 13 | override fun getClientSecret(): String = ApiHelper.CLIENT_SECRET 14 | } 15 | -------------------------------------------------------------------------------- /data/src/test/java/cc/ptt/android/data/repository/board/BoardRepositoryTest.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.data.repository.board 2 | 3 | import cc.ptt.android.data.ApiTestBase 4 | import kotlinx.coroutines.flow.catch 5 | import kotlinx.coroutines.runBlocking 6 | import org.junit.Test 7 | import org.koin.test.inject 8 | 9 | class BoardRepositoryTest : ApiTestBase(needLogin = true) { 10 | private val boardRepository: BoardRepository by inject() 11 | 12 | @Test 13 | fun testFetchPopularBoards() = 14 | runBlocking { 15 | boardRepository 16 | .getPopularBoards() 17 | .catch { 18 | assert(false) 19 | }.collect { 20 | assert(it.list.isNotEmpty()) 21 | } 22 | } 23 | 24 | @Test 25 | fun testFetchBoardArticles() = 26 | runBlocking { 27 | val boardId = "Baseball" 28 | val limit = 200 29 | boardRepository 30 | .getBoardArticles(boardId = boardId, limit = limit) 31 | .catch { 32 | assert(false) 33 | }.collect { 34 | assert(it.list.isNotEmpty()) 35 | assert(it.list.size <= limit) 36 | it.list.forEach { article -> 37 | assert(article.title.isNotEmpty()) 38 | assert(article.boardId.isNotEmpty()) 39 | assert(article.articleId.isNotEmpty()) 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /data/src/test/java/cc/ptt/android/data/repository/search/SearchBoardRepositoryTest.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.data.repository.search 2 | 3 | import cc.ptt.android.data.ApiTestBase 4 | import kotlinx.coroutines.flow.catch 5 | import kotlinx.coroutines.runBlocking 6 | import org.junit.Test 7 | import org.koin.test.inject 8 | 9 | class SearchBoardRepositoryTest : ApiTestBase(needLogin = true) { 10 | private val searchBoardRepository: SearchBoardRepository by inject() 11 | 12 | @Test 13 | fun testFetchSearchBoardByKeyword() = 14 | runBlocking { 15 | val keyWord = "e" 16 | val limit = 200 17 | searchBoardRepository 18 | .searchBoardByKeyword(keyword = keyWord, limit = limit) 19 | .catch { 20 | assert(false) 21 | }.collect { 22 | assert(it.list.isNotEmpty()) 23 | assert(it.list.size <= limit) 24 | it.list.forEach { article -> 25 | assert(article.boardId.isNotEmpty()) 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /data/src/test/java/cc/ptt/android/data/repository/user/UserRepositoryTest.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.data.repository.user 2 | 3 | import cc.ptt.android.common.network.api.apihelper.ApiHelper 4 | import cc.ptt.android.data.ApiTestBase 5 | import cc.ptt.android.data.BuildConfig 6 | import kotlinx.coroutines.flow.catch 7 | import kotlinx.coroutines.runBlocking 8 | import org.junit.Test 9 | import org.koin.test.inject 10 | import kotlin.test.assertEquals 11 | 12 | class UserRepositoryTest : ApiTestBase(needLogin = false) { 13 | private val userRepository: UserRepository by inject() 14 | private val apiHelper: ApiHelper by inject() 15 | 16 | @Test 17 | fun testLogin() = 18 | runBlocking { 19 | userRepository 20 | .login( 21 | apiHelper.getClientId(), 22 | apiHelper.getClientSecret(), 23 | BuildConfig.TEST_ACCOUNT, 24 | BuildConfig.TEST_PASSWORD, 25 | ).catch { 26 | assert(false) 27 | }.collect { 28 | assert(it.accessToken.isNotBlank()) 29 | assert(it.tokenType.isNotBlank()) 30 | assertEquals(BuildConfig.TEST_ACCOUNT, it.userId) 31 | } 32 | } 33 | 34 | @Test 35 | fun testUserId() = 36 | runBlocking { 37 | login() 38 | userRepository 39 | .userId() 40 | .catch { 41 | assert(false) 42 | }.collect { 43 | assertEquals(BuildConfig.TEST_ACCOUNT, it) 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /domain/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /domain/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ptt-official-app/Ptt-Android/6a4198cbab9497774dac1579a04518142c66b543/domain/consumer-rules.pro -------------------------------------------------------------------------------- /domain/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /domain/src/androidTest/java/cc/ptt/android/domain/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.domain 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import org.junit.Test 6 | import org.junit.runner.RunWith 7 | 8 | /** 9 | * Instrumented test, which will execute on an Android device. 10 | * 11 | * @see [Testing documentation](http://d.android.com/tools/testing) 12 | */ 13 | @RunWith(AndroidJUnit4::class) 14 | class ExampleInstrumentedTest { 15 | @Test 16 | fun useAppContext() { 17 | // Context of the app under test. 18 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 19 | assert(appContext.packageName.startsWith("cc.ptt.android.domain")) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /domain/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /domain/src/main/java/cc/ptt/android/domain/base/UseCaseBase.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.domain.base 2 | 3 | import kotlinx.coroutines.CoroutineScope 4 | import kotlinx.coroutines.MainScope 5 | import org.koin.core.component.KoinComponent 6 | 7 | open class UseCaseBase : 8 | KoinComponent, 9 | CoroutineScope by MainScope() 10 | -------------------------------------------------------------------------------- /domain/src/main/java/cc/ptt/android/domain/di/UseCaseModules.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.domain.di 2 | 3 | import cc.ptt.android.domain.usecase.GetPopularArticlesUIUseCase 4 | import cc.ptt.android.domain.usecase.article.CreateArticleCommentUseCase 5 | import cc.ptt.android.domain.usecase.article.GetArticleUseCase 6 | import cc.ptt.android.domain.usecase.board.BoardUseCase 7 | import cc.ptt.android.domain.usecase.board.BoardUseCaseImpl 8 | import cc.ptt.android.domain.usecase.user.UserUseCase 9 | import kotlinx.coroutines.FlowPreview 10 | import org.koin.dsl.module 11 | 12 | @FlowPreview 13 | val useCaseModules = 14 | module { 15 | factory { CreateArticleCommentUseCase(get()) } 16 | factory { GetArticleUseCase(get(), get()) } 17 | single { UserUseCase(get(), get()) } 18 | factory { GetPopularArticlesUIUseCase(get()) } 19 | factory { BoardUseCaseImpl(get(), get()) } 20 | } 21 | -------------------------------------------------------------------------------- /domain/src/main/java/cc/ptt/android/domain/model/UserType.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.domain.model 2 | 3 | import cc.ptt.android.domain.model.ui.user.UserInfo 4 | 5 | sealed class UserType { 6 | object Guest : UserType() 7 | 8 | data class Login( 9 | val userInfo: UserInfo, 10 | ) : UserType() 11 | } 12 | -------------------------------------------------------------------------------- /domain/src/main/java/cc/ptt/android/domain/model/ui/article/ArticleInfo.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.domain.model.ui.article 2 | 3 | data class ArticleInfo( 4 | val contentList: List, 5 | val rank: Int, 6 | ) 7 | -------------------------------------------------------------------------------- /domain/src/main/java/cc/ptt/android/domain/model/ui/article/ArticleReadInfo.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.domain.model.ui.article 2 | 3 | import android.text.SpannableStringBuilder 4 | 5 | sealed class ArticleReadInfo { 6 | data class HeaderInfo( 7 | val title: String, 8 | val auth: String, 9 | val date: String, 10 | val type: String, 11 | val board: String, 12 | ) : ArticleReadInfo() 13 | 14 | data class ContentLineInfo( 15 | val text: String, 16 | val richText: SpannableStringBuilder, 17 | ) : ArticleReadInfo() 18 | 19 | data class ImageInfo( 20 | val index: Int, 21 | val url: String, 22 | ) : ArticleReadInfo() 23 | 24 | data class CenterBarInfo( 25 | val like: String, 26 | val floor: String, 27 | ) : ArticleReadInfo() 28 | 29 | data class CommentInfo( 30 | val index: Int, 31 | val text: String, 32 | val auth: String, 33 | ) : ArticleReadInfo() 34 | 35 | data class CommentBarInfo( 36 | val index: Int, 37 | val time: String, 38 | val floor: String, 39 | val like: String, 40 | ) : ArticleReadInfo() 41 | } 42 | -------------------------------------------------------------------------------- /domain/src/main/java/cc/ptt/android/domain/model/ui/article/PostRankMark.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.domain.model.ui.article 2 | 3 | enum class PostRankMark( 4 | val value: Int, 5 | ) { 6 | Like(1), 7 | Dislike(-1), 8 | None(0), 9 | } 10 | -------------------------------------------------------------------------------- /domain/src/main/java/cc/ptt/android/domain/model/ui/user/UserInfo.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.domain.model.ui.user 2 | 3 | data class UserInfo( 4 | val id: String, 5 | val accessToken: String, 6 | val tokenType: String, 7 | ) 8 | -------------------------------------------------------------------------------- /domain/src/main/java/cc/ptt/android/domain/usecase/article/CreateArticleCommentUseCase.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.domain.usecase.article 2 | 3 | import cc.ptt.android.data.model.remote.article.ArticleComment 4 | import cc.ptt.android.data.model.remote.article.ArticleCommentType 5 | import cc.ptt.android.data.repository.article.ArticleRepository 6 | import cc.ptt.android.domain.base.UseCaseBase 7 | import kotlinx.coroutines.flow.Flow 8 | 9 | class CreateArticleCommentUseCase constructor( 10 | private val articleRepository: ArticleRepository, 11 | ) : UseCaseBase() { 12 | fun createArticleComment( 13 | bid: String, 14 | aid: String, 15 | type: ArticleCommentType, 16 | content: String, 17 | ): Flow = articleRepository.createArticleComment(bid, aid, type.value, content) 18 | } 19 | -------------------------------------------------------------------------------- /domain/src/main/java/cc/ptt/android/domain/usecase/board/BoardUseCase.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.domain.usecase.board 2 | 3 | import cc.ptt.android.data.model.remote.board.article.ArticleList 4 | import cc.ptt.android.data.model.remote.board.hotboard.BoardList 5 | import kotlinx.coroutines.flow.Flow 6 | 7 | interface BoardUseCase { 8 | fun getPopularBoards(): Flow 9 | 10 | fun getBoardArticles( 11 | boardId: String, 12 | title: String = "", 13 | startIndex: String = "", 14 | limit: Int = 200, 15 | desc: Boolean = true, 16 | ): Flow 17 | 18 | fun getFavoriteBoards( 19 | level_idx: String, 20 | startIndex: String, 21 | limit: Int, 22 | aces: Boolean, 23 | ): Flow 24 | } 25 | -------------------------------------------------------------------------------- /domain/src/main/java/cc/ptt/android/domain/usecase/board/BoardUseCaseImpl.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.domain.usecase.board 2 | 3 | import cc.ptt.android.data.model.remote.board.article.ArticleList 4 | import cc.ptt.android.data.model.remote.board.hotboard.BoardList 5 | import cc.ptt.android.data.repository.board.BoardRepository 6 | import cc.ptt.android.data.repository.user.UserRepository 7 | import kotlinx.coroutines.FlowPreview 8 | import kotlinx.coroutines.flow.Flow 9 | import kotlinx.coroutines.flow.flatMapMerge 10 | 11 | class BoardUseCaseImpl constructor( 12 | private val boardRepository: BoardRepository, 13 | private val userRepository: UserRepository, 14 | ) : BoardUseCase { 15 | override fun getPopularBoards(): Flow = boardRepository.getPopularBoards() 16 | 17 | override fun getBoardArticles( 18 | boardId: String, 19 | title: String, 20 | startIndex: String, 21 | limit: Int, 22 | desc: Boolean, 23 | ): Flow = boardRepository.getBoardArticles(boardId, title, startIndex, limit, desc) 24 | 25 | @OptIn(FlowPreview::class) 26 | override fun getFavoriteBoards( 27 | level_idx: String, 28 | startIndex: String, 29 | limit: Int, 30 | aces: Boolean, 31 | ): Flow = 32 | userRepository.userId().flatMapMerge { 33 | boardRepository.getFavoriteBoards(it, level_idx, startIndex, limit, aces) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /domain/src/main/java/cc/ptt/android/domain/usecase/user/UserUseCase.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.domain.usecase.user 2 | 3 | import cc.ptt.android.common.network.api.apihelper.ApiHelper 4 | import cc.ptt.android.data.repository.user.UserRepository 5 | import cc.ptt.android.domain.base.UseCaseBase 6 | import cc.ptt.android.domain.model.UserType 7 | import cc.ptt.android.domain.model.ui.user.UserInfo 8 | import kotlinx.coroutines.flow.Flow 9 | import kotlinx.coroutines.flow.MutableStateFlow 10 | import kotlinx.coroutines.flow.StateFlow 11 | import kotlinx.coroutines.flow.asStateFlow 12 | import kotlinx.coroutines.flow.map 13 | import kotlinx.coroutines.flow.onEach 14 | import kotlinx.coroutines.launch 15 | 16 | class UserUseCase( 17 | private val userRepository: UserRepository, 18 | private val apiHelper: ApiHelper, 19 | ) : UseCaseBase() { 20 | private val _userType = MutableStateFlow(UserType.Guest) 21 | val userType: StateFlow = _userType.asStateFlow() 22 | 23 | init { 24 | initUserType() 25 | } 26 | 27 | private fun initUserType() = 28 | launch { 29 | userRepository.getUserInfo()?.let { 30 | _userType.emit(UserType.Login(UserInfo(it.userId, it.accessToken, it.tokenType))) 31 | } ?: run { 32 | _userType.emit(UserType.Guest) 33 | } 34 | } 35 | 36 | fun login( 37 | id: String, 38 | password: String, 39 | ): Flow = 40 | userRepository 41 | .login(apiHelper.getClientId(), apiHelper.getClientSecret(), id, password) 42 | .map { 43 | UserInfo(it.userId, it.accessToken, it.tokenType) 44 | }.onEach { initUserType() } 45 | 46 | fun logout(): Flow = userRepository.logout().onEach { initUserType() } 47 | 48 | fun getUserInfo(): UserInfo? = userRepository.getUserInfo()?.let { UserInfo(it.userId, it.accessToken, it.tokenType) } 49 | } 50 | -------------------------------------------------------------------------------- /domain/src/test/java/cc/ptt/android/domain/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.domain 2 | 3 | import org.junit.Assert 4 | import org.junit.Test 5 | 6 | /** 7 | * Example local unit test, which will execute on the development machine (host). 8 | * 9 | * @see [Testing documentation](http://d.android.com/tools/testing) 10 | */ 11 | class ExampleUnitTest { 12 | @Test 13 | fun addition_isCorrect() { 14 | Assert.assertEquals(4, 2 + 2.toLong()) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx1536m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | 21 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ptt-official-app/Ptt-Android/6a4198cbab9497774dac1579a04518142c66b543/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Dec 19 18:31:05 CST 2024 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /script/pre_commit_format.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | mkdir .git/hooks/ 3 | echo "#!/usr/bin/env sh 4 | # 5 | # An example hook script to verify what is about to be committed. 6 | # Called by \"git commit\" with no arguments. The hook should 7 | # exit with non-zero status after issuing an appropriate message if 8 | # it wants to stop the commit. 9 | # 10 | # To enable this hook, rename this file to \"pre-commit\". 11 | ./gradlew formatCheck 12 | result=\$? 13 | SLASH='"\\"' 14 | RED='"\033[31m"' 15 | GREEN='"\033[32m"' 16 | NC='"\033[0m"' 17 | printf \"the spotlessCheck result code is \$result\" 18 | if [[ \"\$result\" = 0 ]] ; then 19 | printf \"\$SLASH\$GREEN 20 | .... 21 | .... 22 | SpotlessCheck Pass!! 23 | .... 24 | .... 25 | \$SLASH\$NC\" 26 | exit 0 27 | else 28 | ./gradlew format 29 | printf \"\$SLASH\$RED 30 | .... 31 | .... 32 | SpotlessCheck Failed!! 33 | There are some format violations in the commit files. 34 | .... 35 | Already auto format files, please review the difference and commit again. 36 | .... 37 | .... 38 | \$SLASH\$NC\" 39 | exit 1 40 | fi 41 | " >> .git/hooks/pre-commit 42 | chmod +x .git/hooks/pre-commit -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | maven(url = "https://plugins.gradle.org/m2/") 6 | maven(url = "https://cdn.reproio.com/android") 7 | maven(url = "https://maven.pkg.jetbrains.space/public/p/compose/dev") 8 | } 9 | } 10 | rootProject.name = "Ptt" 11 | include(":app") 12 | include(":common") 13 | include(":data") 14 | include(":domain") 15 | include(":shared") 16 | -------------------------------------------------------------------------------- /shared/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /shared/src/androidDeviceTest/kotlin/cc/ptt/android/shared/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.shared 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import org.junit.Assert.assertEquals 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | /** 10 | * Instrumented test, which will execute on an Android device. 11 | * 12 | * See [testing documentation](http://d.android.com/tools/testing). 13 | */ 14 | @RunWith(AndroidJUnit4::class) 15 | class ExampleInstrumentedTest { 16 | @Test 17 | fun useAppContext() { 18 | // Context of the app under test. 19 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 20 | assertEquals("cc.ptt.android.shared.test", appContext.packageName) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /shared/src/androidHostTest/kotlin/cc/ptt/android/shared/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.shared 2 | 3 | import kotlin.test.Test 4 | import kotlin.test.assertEquals 5 | 6 | /** 7 | * Example local unit test, which will execute on the development machine (host). 8 | * 9 | * See [testing documentation](http://d.android.com/tools/testing). 10 | */ 11 | class ExampleUnitTest { 12 | @Test 13 | fun addition_isCorrect() { 14 | assertEquals(4, 2 + 2) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /shared/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /shared/src/androidMain/kotlin/cc/ptt/android/shared/Platform.android.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.shared 2 | 3 | actual fun platform() = "Android" 4 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/cc/ptt/android/shared/Platform.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.shared 2 | 3 | expect fun platform(): String 4 | -------------------------------------------------------------------------------- /shared/src/iosMain/kotlin/cc/ptt/android/shared/Platform.ios.kt: -------------------------------------------------------------------------------- 1 | package cc.ptt.android.shared 2 | 3 | actual fun platform() = "iOS" 4 | --------------------------------------------------------------------------------