├── .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 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/article_list_search_bottom_navigation_menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/carete_article_comment_type_menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/home_bottom_navigation_menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/post_article_bottom_navigation_menu2.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/post_article_bottom_navigation_menu3.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/post_article_rank_menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/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 |
--------------------------------------------------------------------------------