├── .github ├── stale.yml └── workflows │ ├── automerge-action.yml │ ├── build.yml │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.MD ├── Screenshots ├── 0.png ├── 1.png ├── 2.png ├── 3.png ├── 4.png ├── 5.png └── app_icon.webp ├── app ├── .gitignore ├── build.gradle.kts ├── dictionary.txt ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── kotlin │ └── cn │ │ └── xihan │ │ └── age │ │ ├── MyApp.kt │ │ ├── base │ │ ├── BaseDao.kt │ │ └── BaseViewModel.kt │ │ ├── component │ │ ├── AgeScaffold.kt │ │ ├── AnimatedIcons.kt │ │ ├── AnywhereDropdown.kt │ │ ├── BottomBar.kt │ │ ├── BottomSheetDialog.kt │ │ ├── Card.kt │ │ ├── Dialog.kt │ │ ├── Grid.kt │ │ ├── Navigation.kt │ │ ├── Placeholder.kt │ │ ├── Placeholder3.kt │ │ ├── SearchBar.kt │ │ └── TopBar.kt │ │ ├── di │ │ ├── AppModule.kt │ │ ├── CoroutinesQualifiers.kt │ │ ├── DispatcherModule.kt │ │ ├── NetworkModule.kt │ │ ├── PersistenceModule.kt │ │ └── RepositoryModule.kt │ │ ├── initializer │ │ └── AppInitializer.kt │ │ ├── model │ │ ├── AlertDialogModel.kt │ │ ├── AnimeDetailModel.kt │ │ ├── CatalogModel.kt │ │ ├── Category.kt │ │ ├── CommentModel.kt │ │ ├── HomeModel.kt │ │ ├── LoginResponseModel.kt │ │ ├── OthenModel.kt │ │ ├── RankingModel.kt │ │ └── SearchModel.kt │ │ ├── paging │ │ ├── CatalogPagingSource.kt │ │ ├── CommentPagingSource.kt │ │ ├── GeneralizedPagingSource.kt │ │ └── SearchPagingSource.kt │ │ ├── persistence │ │ ├── AppDatabase.kt │ │ └── MyConverter.kt │ │ ├── repository │ │ ├── LocalRepository.kt │ │ ├── RemoteRepository.kt │ │ └── Repository.kt │ │ ├── ui │ │ ├── category │ │ │ ├── CatalogNavigation.kt │ │ │ ├── CatalogScreen.kt │ │ │ └── CatalogViewModel.kt │ │ ├── generalized │ │ │ ├── GeneralizedNavigation.kt │ │ │ ├── GeneralizedScreen.kt │ │ │ └── GeneralizedViewModel.kt │ │ ├── main │ │ │ ├── MainActivity.kt │ │ │ ├── MainViewModel.kt │ │ │ ├── home │ │ │ │ ├── HomeNavigation.kt │ │ │ │ ├── HomeScreen.kt │ │ │ │ └── HomeViewModel.kt │ │ │ ├── mine │ │ │ │ ├── MineNavigation.kt │ │ │ │ ├── MineScreen.kt │ │ │ │ └── MineScreenViewModel.kt │ │ │ └── schedule │ │ │ │ ├── ScheduleNavigation.kt │ │ │ │ ├── ScheduleScreen.kt │ │ │ │ └── ScheduleViewModel.kt │ │ ├── player │ │ │ ├── AgeAnimePlayer.kt │ │ │ ├── AnimePlayScreen.kt │ │ │ ├── AnimePlayViewModel.kt │ │ │ └── PlayerNavigation.kt │ │ ├── rank │ │ │ ├── RankNavigation.kt │ │ │ ├── RankScreen.kt │ │ │ └── RankingViewModel.kt │ │ ├── search │ │ │ ├── SearchNavigation.kt │ │ │ ├── SearchScreen.kt │ │ │ └── SearchViewModel.kt │ │ └── theme │ │ │ ├── Color.kt │ │ │ ├── Icons.kt │ │ │ ├── Theme.kt │ │ │ └── Type.kt │ │ └── util │ │ ├── Api.kt │ │ ├── Exception.kt │ │ ├── KStore.kt │ │ ├── Logger.kt │ │ ├── MMKV.kt │ │ ├── SSLSocketClient.kt │ │ ├── Settings.kt │ │ ├── SystemUiController.kt │ │ ├── Thread.kt │ │ ├── Utils.kt │ │ └── WhatIf.kt │ └── res │ ├── drawable │ ├── broken_image.xml │ ├── error.webp │ ├── fast_forward.xml │ ├── fast_rewind.xml │ ├── ic_arrow_left.xml │ ├── ic_arrow_right.xml │ ├── ic_auto_mode.xml │ ├── ic_back.xml │ ├── ic_brand.xml │ ├── ic_cast.xml │ ├── ic_category.xml │ ├── ic_close.xml │ ├── ic_dark.xml │ ├── ic_date.xml │ ├── ic_date2.xml │ ├── ic_download.xml │ ├── ic_download_video.xml │ ├── ic_episode.xml │ ├── ic_expand.xml │ ├── ic_filter.xml │ ├── ic_filter2.xml │ ├── ic_history.xml │ ├── ic_launcher_background.xml │ ├── ic_launcher_foreground.xml │ ├── ic_leaderboard.xml │ ├── ic_light.xml │ ├── ic_lock.xml │ ├── ic_logo.xml │ ├── ic_love.xml │ ├── ic_more.xml │ ├── ic_pause.xml │ ├── ic_person.xml │ ├── ic_play.xml │ ├── ic_play_next.xml │ ├── ic_search.xml │ ├── ic_splash.xml │ ├── ic_status.xml │ ├── ic_tag.xml │ ├── ic_thumb.xml │ ├── ic_trash.xml │ ├── ic_trash2.xml │ ├── ic_tv.xml │ ├── loading.webp │ ├── parsing_error_bg.9.png │ ├── qmui_icon_tip_new.png │ ├── update_popover_header_bg.xml │ └── working_in_progress.webp │ ├── mipmap │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── raw │ ├── ic_arrow_up_down.json │ ├── ic_calendar.json │ ├── ic_follow.json │ ├── ic_home.json │ ├── ic_my.json │ ├── ic_radio.json │ ├── ic_switch.json │ ├── loading.json │ └── v_loading.json │ ├── values │ └── strings.xml │ └── xml │ └── network_security_config.xml ├── build.gradle.kts ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── renovate.json └── settings.gradle.kts /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - security 9 | # Label to use when marking an issue as stale 10 | staleLabel: wontfix 11 | # Comment to post when marking an issue as stale. Set to `false` to disable 12 | markComment: > 13 | This issue has been automatically marked as stale because it has not had 14 | recent activity. It will be closed if no further activity occurs. Thank you 15 | for your contributions. 16 | # Comment to post when closing a stale issue. Set to `false` to disable 17 | closeComment: false -------------------------------------------------------------------------------- /.github/workflows/automerge-action.yml: -------------------------------------------------------------------------------- 1 | name: automerge 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - labeled 7 | - unlabeled 8 | - synchronize 9 | - opened 10 | - edited 11 | - ready_for_review 12 | - reopened 13 | - unlocked 14 | pull_request_review: 15 | types: 16 | - submitted 17 | check_suite: 18 | types: 19 | - completed 20 | status: {} 21 | 22 | jobs: 23 | automerge: 24 | runs-on: ubuntu-latest 25 | steps: 26 | - id: automerge 27 | name: automerge 28 | uses: "pascalgn/automerge-action@v0.15.6" 29 | env: 30 | GITHUB_TOKEN: '${{ secrets.RELEASE_TOKEN }}' 31 | MERGE_LABELS: '' 32 | MERGE_FILTER_AUTHOR: 'xihan123' 33 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Android CI 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | tags: 7 | - "v*" 8 | 9 | jobs: 10 | build: 11 | name: Build on ${{ matrix.os }} 12 | runs-on: ${{ matrix.os }} 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | os: [ ubuntu-latest ] 17 | 18 | steps: 19 | - name: Check Commit Message [skip CI] 20 | env: 21 | COMMIT_FILTER: "[skip ci]" 22 | if: contains(github.event.head_commit.message, '[skip ci]') 23 | run: | 24 | echo "no 'skip ci' in commit message" 25 | exit 2 26 | 27 | - name: Checkout 28 | uses: actions/checkout@v4 29 | with: 30 | submodules: 'recursive' 31 | fetch-depth: 0 32 | 33 | - name: base64-to-file 34 | id: write_file 35 | uses: timheuer/base64-to-file@v1.2 36 | if: ${{ ( github.event_name != 'pull_request' && github.ref == 'refs/heads/master' ) || github.ref_type == 'tag' }} 37 | with: 38 | fileName: 'key.jks' 39 | encodedString: ${{ secrets.SIGNING_KEY }} 40 | 41 | - name: Write key 42 | if: ${{ ( github.event_name != 'pull_request' && github.ref == 'refs/heads/master' ) || github.ref_type == 'tag' }} 43 | run: | 44 | touch keystore.properties 45 | echo storePassword='${{ secrets.KEY_STORE_PASSWORD }}' >> keystore.properties 46 | echo keyAlias='${{ secrets.ALIAS }}' >> keystore.properties 47 | echo keyPassword='${{ secrets.KEY_PASSWORD }}' >> keystore.properties 48 | echo storeFile='${{ steps.write_file.outputs.filePath }}' >> keystore.properties 49 | 50 | - name: Set up JDK 17 51 | uses: actions/setup-java@v3 52 | with: 53 | java-version: '17' 54 | distribution: 'temurin' 55 | cache: 'gradle' 56 | 57 | - name: Cache gradle dependencies 58 | uses: actions/cache@v3 59 | with: 60 | path: | 61 | ~/.gradle/caches 62 | ~/.gradle/wrapper 63 | !~/.gradle/caches/build-cache-* 64 | key: gradle-deps-core-${{ hashFiles('**/build.gradle.kts') }} 65 | restore-keys: | 66 | gradle-deps 67 | 68 | - name: Build with Gradle 69 | run: | 70 | [ $(du -s ~/.gradle/wrapper | awk '{ print $1 }') -gt 250000 ] && rm -rf ~/.gradle/wrapper/* || true 71 | chmod +x gradlew 72 | ./gradlew assembleRelease 73 | 74 | - name: Upload assets to a Release 75 | uses: meeDamian/github-release@v2.0.3 76 | with: 77 | files: app/build/outputs/apk/release/*.apk 78 | token: ${{ secrets.RELEASE_TOKEN }} 79 | allow_override: true 80 | gzip: false -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | 6 | name: Release 7 | 8 | jobs: 9 | release-please: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Check Commit Message [skip CI] 13 | env: 14 | COMMIT_FILTER: "[skip ci]" 15 | if: contains(github.event.head_commit.message, '[skip ci]') 16 | run: | 17 | echo "no 'skip ci' in commit message" 18 | exit 2 19 | 20 | - uses: GoogleCloudPlatform/release-please-action@v3 21 | id: release 22 | with: 23 | token: ${{ secrets.RELEASE_TOKEN }} 24 | release-type: node 25 | package-name: release-please-action 26 | release-as: 1.0.3 27 | changelog-types: '[{"type":"types","section":"Types","hidden":false},{"type":"revert","section":"Reverts","hidden":false},{"type":"feat","section":"Features","hidden":false},{"type":"fix","section":"Bug Fixes","hidden":false},{"type":"improvement","section":"Feature Improvements","hidden":false},{"type":"docs","section":"Docs","hidden":false},{"type":"ci","section":"CI","hidden":false},{"type":"chore","section":"Miscellaneous","hidden":false}]' 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | .cxx 10 | local.properties 11 | /keystore.properties -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [1.0.3](https://github.com/xihan123/AGE/compare/v1.0.2...v1.0.3) (2023-12-06) 4 | 5 | 6 | ### Features 7 | 8 | * 新增自定义API设置(仅支持官方API) ([083d511](https://github.com/xihan123/AGE/commit/083d5118703a00ddfc2d22581384c477fa5655bf)) 9 | 10 | 11 | ### Bug Fixes 12 | 13 | * 轮播图导致的崩溃 ([f4cf47d](https://github.com/xihan123/AGE/commit/f4cf47da5bb94d85cb4b04fec9587c23f1c05e1a)) 14 | 15 | 16 | ### Miscellaneous 17 | 18 | * 更新依赖库 ([c0e5292](https://github.com/xihan123/AGE/commit/c0e529201cf70ac67825ac91cdf4ab73eafd6a13)) 19 | 20 | ## [1.0.2](https://github.com/xihan123/AGE/compare/v1.0.1...v1.0.2) (2023-10-05) 21 | 22 | 23 | ### Miscellaneous 24 | 25 | * 更新依赖库 ([b1d1da2](https://github.com/xihan123/AGE/commit/b1d1da287a62ef8f7d073844ad3233763b29507e)) 26 | 27 | ## 1.0.1 (2023-10-01) 28 | 29 | 30 | ### Miscellaneous 31 | 32 | * Initialize Commit ([d93da88](https://github.com/xihan123/AGE/commit/d93da88c2d76b0d15ab3d2c446a6bc7974476b50)) 33 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | ![AGE](https://socialify.git.ci/xihan123/AGE/image?description=1&forks=1&issues=1&language=1&logo=https%3A%2F%2Ft3.picb.cc%2F2023%2F06%2F06%2FI05QQe.png&name=1&owner=1&pulls=1&stargazers=1&theme=Light) 2 | 3 | ![](https://img.shields.io/badge/Android-7.0%20or%20above-brightgreen.svg) 4 | [![Latest Release](https://img.shields.io/github/release/xihan123/AGE.svg)](../../releases) 5 | ![](https://img.shields.io/github/downloads/xihan123/AGE/total) 6 | [![Powered-by](https://img.shields.io/badge/powered%20by-AGE%E5%8A%A8%E6%BC%AB-ea5c7b)](https://github.com/agefanscom/website) 7 | 8 | 9 | 10 | # 使用 [AGE动漫](https://www.age.tv/) API 编写的第三方客户端。 11 | 12 | ## 目前自动播放暂时还不太行,需要手动选集 13 | 14 | --- 15 | ## TODO 16 | 17 | * [√] 完整播放功能 18 | * [√] AGE登录相关 19 | 20 | --- 21 | 22 | ### 应用截图 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 |
首页分类推荐时间表播放我的
42 | 43 | --- 44 | ## 请您遵守使用规则 45 | 46 | 本软件仅供学习交流使用, 本软件或本软件的拓展, 个人或企业不可用于商业用途, 不可上架任何商店 47 | 48 | 拓展包括但是不限于以下内容 49 | 50 | - 使用本软件进行继续开发形成的软件。 51 | - 引入本软件部分内容为依赖/参考本软件/使用本软件内代码的同时, 包含本软件内一致内容或功能的软件。 52 | - 直接对本软件进行打包发布 53 | 54 | 软件副本分发规则 55 | 56 | - 不要在任何其他 **二次元软件** 的 **聊天社区** 或 **开发社区** 内, 发布有关本软件的链接或信息 57 | - 不要发送本软件安装包到 **任何社区内** , 不要将APK发送包括任何聊天软件内的群聊功能。 分享本软件时, 在社区中使用Github中提供的Releases页面的链接, 或使用私聊窗口发送。 58 | - 作者不对分发软件承担任何后果, 请您遵守当地以及副本接受社区或副本接收人所在地区的法律。 -------------------------------------------------------------------------------- /Screenshots/0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xihan123/AGE/fed9c4e2a792e05923396adf2f2cbafadd6b474b/Screenshots/0.png -------------------------------------------------------------------------------- /Screenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xihan123/AGE/fed9c4e2a792e05923396adf2f2cbafadd6b474b/Screenshots/1.png -------------------------------------------------------------------------------- /Screenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xihan123/AGE/fed9c4e2a792e05923396adf2f2cbafadd6b474b/Screenshots/2.png -------------------------------------------------------------------------------- /Screenshots/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xihan123/AGE/fed9c4e2a792e05923396adf2f2cbafadd6b474b/Screenshots/3.png -------------------------------------------------------------------------------- /Screenshots/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xihan123/AGE/fed9c4e2a792e05923396adf2f2cbafadd6b474b/Screenshots/4.png -------------------------------------------------------------------------------- /Screenshots/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xihan123/AGE/fed9c4e2a792e05923396adf2f2cbafadd6b474b/Screenshots/5.png -------------------------------------------------------------------------------- /Screenshots/app_icon.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xihan123/AGE/fed9c4e2a792e05923396adf2f2cbafadd6b474b/Screenshots/app_icon.webp -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /schemas 3 | machinet.conf 4 | mapping.txt 5 | setting.txt 6 | unused.txt -------------------------------------------------------------------------------- /app/dictionary.txt: -------------------------------------------------------------------------------- 1 | # Reserved Java names 2 | abstract 3 | assert 4 | boolean 5 | break 6 | byte 7 | case 8 | catch 9 | char 10 | class 11 | const 12 | continue 13 | default 14 | do 15 | double 16 | else 17 | enum 18 | extends 19 | false 20 | final 21 | finally 22 | float 23 | for 24 | goto 25 | if 26 | implements 27 | import 28 | instanceof 29 | int 30 | interface 31 | long 32 | native 33 | new 34 | null 35 | package 36 | private 37 | protected 38 | public 39 | return 40 | short 41 | static 42 | strictfp 43 | super 44 | switch 45 | synchronized 46 | this 47 | throw 48 | throws 49 | transient 50 | true 51 | try 52 | void 53 | volatile 54 | while 55 | 56 | 57 | # This obfuscation dictionary contains names that are not allowed as file names 58 | # in Windows, not even with extensions like .class or .java. They can however 59 | # be used without problems in jar archives, which just begs to apply them as 60 | # obfuscated class names. Trying to unpack the obfuscated archives in Windows 61 | # will probably generate some sparks. 62 | 63 | aux 64 | Aux 65 | aUx 66 | AUx 67 | auX 68 | AuX 69 | aUX 70 | AUX 71 | con 72 | Con 73 | cOn 74 | COn 75 | coN 76 | CoN 77 | cON 78 | CON 79 | nul 80 | Nul 81 | nUl 82 | NUl 83 | nuL 84 | NuL 85 | nUL 86 | NUL 87 | prn 88 | Prn 89 | pRn 90 | PRn 91 | prN 92 | PrN 93 | pRN 94 | PRN 95 | com1 96 | Com1 97 | cOm1 98 | COm1 99 | coM1 100 | CoM1 101 | cOM1 102 | COM1 103 | com2 104 | Com2 105 | cOm2 106 | COm2 107 | coM2 108 | CoM2 109 | cOM2 110 | COM2 111 | com3 112 | Com3 113 | cOm3 114 | COm3 115 | coM3 116 | CoM3 117 | cOM3 118 | COM3 119 | com4 120 | Com4 121 | cOm4 122 | COm4 123 | coM4 124 | CoM4 125 | cOM4 126 | COM4 127 | com5 128 | Com5 129 | cOm5 130 | COm5 131 | coM5 132 | CoM5 133 | cOM5 134 | COM5 135 | com6 136 | Com6 137 | cOm6 138 | COm6 139 | coM6 140 | CoM6 141 | cOM6 142 | COM6 143 | com7 144 | Com7 145 | cOm7 146 | COm7 147 | coM7 148 | CoM7 149 | cOM7 150 | COM7 151 | com8 152 | Com8 153 | cOm8 154 | COm8 155 | coM8 156 | CoM8 157 | cOM8 158 | COM8 159 | com9 160 | Com9 161 | cOm9 162 | COm9 163 | coM9 164 | CoM9 165 | cOM9 166 | COM9 167 | lpt1 168 | Lpt1 169 | lPt1 170 | LPt1 171 | lpT1 172 | LpT1 173 | lPT1 174 | LPT1 175 | lpt2 176 | Lpt2 177 | lPt2 178 | LPt2 179 | lpT2 180 | LpT2 181 | lPT2 182 | LPT2 183 | lpt3 184 | Lpt3 185 | lPt3 186 | LPt3 187 | lpT3 188 | LpT3 189 | lPT3 190 | LPT3 191 | lpt4 192 | Lpt4 193 | lPt4 194 | LPt4 195 | lpT4 196 | LpT4 197 | lPT4 198 | LPT4 199 | lpt5 200 | Lpt5 201 | lPt5 202 | LPt5 203 | lpT5 204 | LpT5 205 | lPT5 206 | LPT5 207 | lpt6 208 | Lpt6 209 | lPt6 210 | LPt6 211 | lpT6 212 | LpT6 213 | lPT6 214 | LPT6 215 | lpt7 216 | Lpt7 217 | lPt7 218 | LPt7 219 | lpT7 220 | LpT7 221 | lPT7 222 | LPT7 223 | lpt8 224 | Lpt8 225 | lPt8 226 | LPt8 227 | lpT8 228 | LpT8 229 | lPT8 230 | LPT8 231 | lpt9 232 | Lpt9 233 | lPt9 234 | LPt9 235 | lpT9 236 | LpT9 237 | lPT9 238 | LPT9 239 | 240 | ### CUSTOM ### 241 | 242 | # Java-like names 243 | next 244 | size 245 | length 246 | toString 247 | hashCode 248 | String 249 | Array 250 | File 251 | Exception 252 | java 253 | lang 254 | util 255 | javax 256 | IOException 257 | android 258 | os 259 | Class 260 | app 261 | internal 262 | Integer 263 | Boolean 264 | Long 265 | Activity 266 | Application 267 | Context 268 | Thread 269 | RuntimeException 270 | 271 | # Phrases 272 | stop_looking_at_my_code 273 | did_you_understand 274 | pls_do_it_for_me 275 | hi_pls_stop 276 | hey_you 277 | stop_reversing 278 | who_am_i_to_stop_you 279 | stop_it 280 | 281 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 15 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 32 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 44 | 45 | 46 | 47 | 50 | 51 | 52 | 53 | 56 | 59 | 60 | 61 | 62 | 65 | 68 | 71 | 72 | 73 | 74 | 75 | 78 | -------------------------------------------------------------------------------- /app/src/main/kotlin/cn/xihan/age/MyApp.kt: -------------------------------------------------------------------------------- 1 | package cn.xihan.age 2 | 3 | import androidx.multidex.MultiDexApplication 4 | import dagger.hilt.android.HiltAndroidApp 5 | 6 | /** 7 | * @项目名 : AGE动漫 8 | * @作者 : MissYang 9 | * @创建时间 : 2023/9/17 20:33 10 | * @介绍 : 11 | */ 12 | @HiltAndroidApp 13 | class MyApp : MultiDexApplication() -------------------------------------------------------------------------------- /app/src/main/kotlin/cn/xihan/age/base/BaseDao.kt: -------------------------------------------------------------------------------- 1 | package cn.xihan.age.base 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Delete 5 | import androidx.room.Insert 6 | import androidx.room.OnConflictStrategy 7 | import androidx.room.Update 8 | import androidx.room.Upsert 9 | 10 | /** 11 | * @项目名 : AGE动漫 12 | * @作者 : MissYang 13 | * @创建时间 : 2023/5/27 7:46 14 | * @介绍 : 15 | */ 16 | @Dao 17 | interface BaseDao { 18 | @Insert(onConflict = OnConflictStrategy.REPLACE) 19 | suspend fun insert(vararg entity: T) 20 | 21 | @Upsert 22 | suspend fun upsert(vararg entity: T) 23 | 24 | @Update(onConflict = OnConflictStrategy.REPLACE) 25 | suspend fun update(vararg entity: T) 26 | 27 | @Delete 28 | suspend fun delete(vararg entity: T) 29 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/cn/xihan/age/base/BaseViewModel.kt: -------------------------------------------------------------------------------- 1 | package cn.xihan.age.base 2 | 3 | import androidx.annotation.Keep 4 | import androidx.lifecycle.ViewModel 5 | import androidx.lifecycle.viewModelScope 6 | import cn.xihan.age.util.AgeException 7 | import cn.xihan.age.util.logError 8 | import kotlinx.coroutines.CoroutineExceptionHandler 9 | import kotlinx.coroutines.CoroutineScope 10 | import kotlinx.coroutines.Dispatchers 11 | import kotlinx.coroutines.launch 12 | import org.orbitmvi.orbit.Container 13 | import org.orbitmvi.orbit.ContainerHost 14 | import org.orbitmvi.orbit.viewmodel.container 15 | 16 | /** 17 | * @项目名 : age-anime 18 | * @作者 : MissYang 19 | * @创建时间 : 2023/9/17 16:58 20 | * @介绍 : 21 | */ 22 | @Keep 23 | sealed interface LoadingIntent : IUiIntent { 24 | data object IDLE : LoadingIntent 25 | data object LOADING : LoadingIntent 26 | data class FAILURE(val error: AgeException) : LoadingIntent 27 | } 28 | 29 | @Keep 30 | interface IUiState { 31 | var loading: Boolean 32 | var refreshing: Boolean 33 | var error: AgeException? 34 | } 35 | 36 | 37 | @Keep 38 | interface IUiIntent 39 | 40 | /** 41 | * BaseViewModel is an abstract class that implements the MVI pattern for ViewModels. 42 | * 43 | * @param S Generic type that must extend IUiState to represent UI state 44 | * @param I Generic type that must extend IUiIntent for intents 45 | * 46 | * Contains: 47 | * - coroutineExceptionHandler - Handles exceptions in coroutines 48 | * - container - Holds the MVI container for state and intents 49 | * - initViewState() - Abstract function to provide initial state, must be implemented 50 | * - hideError() - Can override to hide errors 51 | * - showError() - Can override to show errors 52 | * - mainLaunch() - Launches coroutine on main thread 53 | * - ioLaunch() - Launches coroutine on IO thread 54 | * 55 | * All coroutines will use the exception handler. 56 | */ 57 | 58 | abstract class BaseViewModel : ContainerHost, ViewModel() { 59 | 60 | protected val coroutineExceptionHandler = CoroutineExceptionHandler { _, exception -> 61 | logError(exception.message) 62 | val errorResponse = if (exception is AgeException) { 63 | exception 64 | } else { 65 | AgeException.SnackBarException(message = exception.message ?: "未知错误") 66 | } 67 | showError(errorResponse) 68 | } 69 | 70 | override val container: Container by lazy { 71 | container( 72 | initialState = initViewState(), 73 | buildSettings = { 74 | exceptionHandler = coroutineExceptionHandler 75 | } 76 | ) 77 | } 78 | 79 | abstract fun initViewState(): S 80 | 81 | open fun hideError() {} 82 | 83 | open fun showError(error: AgeException) {} 84 | 85 | /** 86 | * 主线程执行 87 | */ 88 | fun mainLaunch(block: suspend CoroutineScope.() -> Unit) { 89 | viewModelScope.launch(Dispatchers.Main + coroutineExceptionHandler) { 90 | block.invoke(this) 91 | } 92 | } 93 | 94 | /** 95 | * IO线程执行 96 | */ 97 | fun ioLaunch(block: suspend CoroutineScope.() -> Unit) { 98 | viewModelScope.launch(Dispatchers.IO + coroutineExceptionHandler) { 99 | block.invoke(this) 100 | } 101 | } 102 | 103 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/cn/xihan/age/component/AnimatedIcons.kt: -------------------------------------------------------------------------------- 1 | package cn.xihan.age.component 2 | 3 | import androidx.compose.animation.core.LinearEasing 4 | import androidx.compose.animation.core.animateFloatAsState 5 | import androidx.compose.animation.core.tween 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.runtime.getValue 8 | import androidx.compose.ui.Modifier 9 | import cn.xihan.age.ui.theme.AgeAnimeIcons 10 | import com.airbnb.lottie.compose.LottieAnimation 11 | import com.airbnb.lottie.compose.LottieCompositionSpec 12 | import com.airbnb.lottie.compose.rememberLottieComposition 13 | 14 | 15 | @Composable 16 | fun AnimatedFollowIcon( 17 | modifier: Modifier = Modifier, isFollowed: Boolean 18 | ) { 19 | val followIcon by rememberLottieComposition( 20 | LottieCompositionSpec.RawRes(AgeAnimeIcons.Animated.follow) 21 | ) 22 | val animationProgress by animateFloatAsState( 23 | targetValue = if (isFollowed) 1f else 0f, animationSpec = tween( 24 | 800, easing = LinearEasing 25 | ), label = "" 26 | ) 27 | 28 | LottieAnimation(modifier = modifier, 29 | composition = followIcon, 30 | progress = { if (isFollowed) animationProgress else 0f }) 31 | } 32 | 33 | @Composable 34 | fun AnimatedRadioButton( 35 | modifier: Modifier = Modifier, 36 | selected: Boolean, 37 | durationMillis: Int = 600, 38 | ) { 39 | val radioButton by rememberLottieComposition( 40 | LottieCompositionSpec.RawRes(AgeAnimeIcons.Animated.radio) 41 | ) 42 | 43 | val selectProgress by animateFloatAsState( 44 | targetValue = if (selected) 0.5f else 0f, 45 | animationSpec = tween(durationMillis, easing = LinearEasing), 46 | label = "" 47 | ) 48 | 49 | LottieAnimation(modifier = modifier, 50 | composition = radioButton, 51 | progress = { if (selected) selectProgress else 0f }) 52 | } 53 | 54 | @Composable 55 | fun AnimatedSwitchButton( 56 | modifier: Modifier = Modifier, 57 | checked: Boolean?, 58 | ) { 59 | val switchButton by rememberLottieComposition( 60 | LottieCompositionSpec.RawRes(AgeAnimeIcons.Animated.switch) 61 | ) 62 | 63 | if (checked == null) { 64 | LottieAnimation(modifier = modifier, composition = switchButton, progress = { 1f }) 65 | } else { 66 | val animationProgress by animateFloatAsState( 67 | targetValue = if (checked) 1f else 0f, 68 | animationSpec = tween(400, easing = LinearEasing), 69 | label = "" 70 | ) 71 | 72 | LottieAnimation( 73 | modifier = modifier, 74 | composition = switchButton, 75 | progress = { animationProgress }) 76 | } 77 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/cn/xihan/age/component/AnywhereDropdown.kt: -------------------------------------------------------------------------------- 1 | package cn.xihan.age.component 2 | 3 | import androidx.compose.foundation.ExperimentalFoundationApi 4 | import androidx.compose.foundation.LocalIndication 5 | import androidx.compose.foundation.combinedClickable 6 | import androidx.compose.foundation.interaction.MutableInteractionSource 7 | import androidx.compose.foundation.interaction.PressInteraction 8 | import androidx.compose.foundation.layout.Box 9 | import androidx.compose.foundation.layout.ColumnScope 10 | import androidx.compose.foundation.layout.PaddingValues 11 | import androidx.compose.material3.DropdownMenu 12 | import androidx.compose.material3.DropdownMenuItem 13 | import androidx.compose.material3.MenuDefaults 14 | import androidx.compose.material3.MenuItemColors 15 | import androidx.compose.runtime.Composable 16 | import androidx.compose.runtime.LaunchedEffect 17 | import androidx.compose.runtime.MutableState 18 | import androidx.compose.runtime.collectAsState 19 | import androidx.compose.runtime.getValue 20 | import androidx.compose.runtime.remember 21 | import androidx.compose.runtime.setValue 22 | import androidx.compose.ui.Modifier 23 | import androidx.compose.ui.geometry.Offset 24 | import androidx.compose.ui.platform.LocalDensity 25 | import androidx.compose.ui.unit.DpOffset 26 | import cn.xihan.age.util.rememberMutableInteractionSource 27 | import cn.xihan.age.util.rememberMutableStateOf 28 | 29 | /** 30 | * @项目名 : AGE动漫 31 | * @作者 : MissYang 32 | * @创建时间 : 2023/9/27 0:21 33 | * @介绍 : 34 | */ 35 | @OptIn(ExperimentalFoundationApi::class) 36 | @Composable 37 | fun AnywhereDropdown( 38 | modifier: Modifier = Modifier, 39 | enabled: Boolean = true, 40 | expanded: Boolean, 41 | onDismissRequest: () -> Unit, 42 | onClick: () -> Unit, 43 | onLongClick: (() -> Unit)? = null, 44 | surface: @Composable () -> Unit, 45 | content: @Composable ColumnScope.() -> Unit 46 | ) { 47 | val indication = LocalIndication.current 48 | val interactionSource = rememberMutableInteractionSource() 49 | val state by interactionSource.interactions.collectAsState(null) 50 | var offset by rememberMutableStateOf(Offset.Zero) 51 | val dpOffset = with(LocalDensity.current) { 52 | DpOffset(offset.x.toDp(), offset.y.toDp()) 53 | } 54 | 55 | LaunchedEffect(state) { 56 | if (state is PressInteraction.Press) { 57 | val i = state as PressInteraction.Press 58 | offset = i.pressPosition 59 | } 60 | if (state is PressInteraction.Release) { 61 | val i = state as PressInteraction.Release 62 | offset = i.press.pressPosition 63 | } 64 | } 65 | 66 | Box( 67 | modifier = modifier 68 | .combinedClickable( 69 | interactionSource = interactionSource, 70 | indication = indication, 71 | enabled = enabled, 72 | onClick = onClick, 73 | onLongClick = onLongClick 74 | ) 75 | ) { 76 | surface() 77 | Box { 78 | DropdownMenu( 79 | expanded = expanded, 80 | onDismissRequest = onDismissRequest, 81 | offset = dpOffset, 82 | content = content 83 | ) 84 | } 85 | } 86 | } 87 | 88 | @Composable 89 | fun MyDropdownMenuItem( 90 | topAppBarExpanded: MutableState, 91 | text: @Composable () -> Unit, 92 | onClick: () -> Unit, 93 | modifier: Modifier = Modifier, 94 | leadingIcon: @Composable (() -> Unit)? = null, 95 | trailingIcon: @Composable (() -> Unit)? = null, 96 | enabled: Boolean = true, 97 | colors: MenuItemColors = MenuDefaults.itemColors(), 98 | contentPadding: PaddingValues = MenuDefaults.DropdownMenuItemContentPadding, 99 | interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, 100 | ) { 101 | DropdownMenuItem( 102 | text = text, 103 | onClick = { 104 | topAppBarExpanded.value = false 105 | onClick() 106 | }, 107 | modifier = modifier, 108 | leadingIcon = leadingIcon, 109 | trailingIcon = trailingIcon, 110 | enabled = enabled, 111 | colors = colors, 112 | contentPadding = contentPadding, 113 | interactionSource = interactionSource, 114 | ) 115 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/cn/xihan/age/component/BottomBar.kt: -------------------------------------------------------------------------------- 1 | package cn.xihan.age.component 2 | 3 | import androidx.compose.animation.core.LinearEasing 4 | import androidx.compose.animation.core.animateFloatAsState 5 | import androidx.compose.animation.core.tween 6 | import androidx.compose.foundation.layout.Box 7 | import androidx.compose.foundation.layout.Column 8 | import androidx.compose.foundation.layout.Row 9 | import androidx.compose.foundation.layout.WindowInsets 10 | import androidx.compose.foundation.layout.fillMaxWidth 11 | import androidx.compose.foundation.layout.navigationBars 12 | import androidx.compose.foundation.layout.padding 13 | import androidx.compose.foundation.layout.width 14 | import androidx.compose.foundation.layout.windowInsetsPadding 15 | import androidx.compose.foundation.selection.selectable 16 | import androidx.compose.foundation.selection.selectableGroup 17 | import androidx.compose.material3.Surface 18 | import androidx.compose.material3.Text 19 | import androidx.compose.runtime.Composable 20 | import androidx.compose.runtime.getValue 21 | import androidx.compose.ui.Alignment 22 | import androidx.compose.ui.Modifier 23 | import androidx.compose.ui.draw.alpha 24 | import androidx.compose.ui.semantics.Role 25 | import androidx.compose.ui.unit.TextUnit 26 | import androidx.compose.ui.unit.TextUnitType 27 | import androidx.compose.ui.unit.dp 28 | import androidx.navigation.NavDestination 29 | import cn.xihan.age.ui.theme.darkPink30 30 | import cn.xihan.age.ui.theme.pink50 31 | import cn.xihan.age.util.rememberMutableInteractionSource 32 | import com.airbnb.lottie.compose.LottieAnimation 33 | import com.airbnb.lottie.compose.LottieCompositionSpec 34 | import com.airbnb.lottie.compose.rememberLottieComposition 35 | 36 | @Composable 37 | fun AgeAnimeBottomAppBar( 38 | modifier: Modifier = Modifier, 39 | currentDestination: NavDestination?, 40 | onNavigateTo: (TopLevelScreen) -> Unit, 41 | ) { 42 | Surface( 43 | modifier = modifier 44 | .windowInsetsPadding(WindowInsets.navigationBars) 45 | .alpha(0.95f) 46 | .fillMaxWidth() 47 | .selectableGroup(), tonalElevation = 2.dp 48 | ) { 49 | Row( 50 | modifier = Modifier.padding(8.dp) 51 | ) { 52 | TopLevelScreen.entries.forEach { destination -> 53 | BottomAppBarItem( 54 | modifier = Modifier 55 | .fillMaxWidth() 56 | .weight(1f), 57 | selected = destination.route == currentDestination?.route, 58 | onClick = { onNavigateTo(destination) }, 59 | iconId = destination.iconId, 60 | label = destination.label, 61 | ) 62 | } 63 | } 64 | } 65 | } 66 | 67 | @Composable 68 | fun BottomAppBarItem( 69 | selected: Boolean, 70 | onClick: () -> Unit, 71 | iconId: Int, 72 | label: String, 73 | modifier: Modifier = Modifier, 74 | ) { 75 | val animatedIcon by rememberLottieComposition(LottieCompositionSpec.RawRes(iconId)) 76 | val animationProgress: Float by animateFloatAsState( 77 | targetValue = if (selected) 1f else 0f, animationSpec = tween( 78 | 800, easing = LinearEasing 79 | ), label = "" 80 | ) 81 | 82 | Box( 83 | modifier.selectable( 84 | selected = selected, 85 | onClick = onClick, 86 | role = Role.Tab, 87 | interactionSource = rememberMutableInteractionSource(), 88 | indication = null, 89 | ), contentAlignment = Alignment.Center 90 | ) { 91 | Column( 92 | modifier = Modifier.width(60.dp), 93 | horizontalAlignment = Alignment.CenterHorizontally, 94 | ) { 95 | LottieAnimation(animatedIcon, progress = { 96 | if (selected) animationProgress else 0f 97 | }) 98 | Text( 99 | text = label, 100 | fontSize = TextUnit(9f, TextUnitType.Sp), 101 | color = if (selected) pink50 else darkPink30 102 | ) 103 | } 104 | } 105 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/cn/xihan/age/component/Grid.kt: -------------------------------------------------------------------------------- 1 | package cn.xihan.age.component 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.Modifier 5 | import androidx.compose.ui.layout.Layout 6 | import androidx.compose.ui.unit.Dp 7 | import androidx.compose.ui.unit.dp 8 | import cn.xihan.age.model.AnimeModel 9 | import cn.xihan.age.util.isTablet 10 | import kotlin.math.ceil 11 | import kotlin.math.floor 12 | 13 | /** 14 | * @项目名 : AGE动漫 15 | * @作者 : MissYang 16 | * @创建时间 : 2023/9/20 21:18 17 | * @介绍 : 18 | */ 19 | @Composable 20 | fun AnimeGrid( 21 | modifier: Modifier = Modifier, 22 | useExpandCardStyle: Boolean = false, 23 | horizontalSpacing: Dp = 12.dp, 24 | verticalSpacing: Dp = 5.dp, 25 | animeList: List, 26 | onAnimeClick: (Int) -> Unit, 27 | ) { 28 | val isTablet = isTablet() 29 | 30 | val minCardWidth: Dp = if (isTablet) 144.dp else 96.dp 31 | 32 | Layout(modifier = modifier, content = { 33 | animeList.forEach { 34 | if (it == null) { 35 | PlaceholderAnimeCard() 36 | } else if (useExpandCardStyle) { 37 | ExpandedAnimeCard(anime = it, onClick = onAnimeClick) 38 | } else { 39 | NarrowAnimeCard(anime = it, onClick = onAnimeClick) 40 | } 41 | } 42 | }, measurePolicy = { measures, constraints -> 43 | 44 | var colCnt = 45 | floor((constraints.maxWidth) / (minCardWidth.toPx() + horizontalSpacing.toPx())).toInt() 46 | .coerceIn(3, 6) 47 | if (colCnt == 5) colCnt = 4 48 | 49 | val width = (constraints.maxWidth - (colCnt - 1) * horizontalSpacing.roundToPx()) / colCnt 50 | 51 | val placeable = measures.map { it.measure(constraints.copy(maxWidth = width)) } 52 | val height = placeable.firstOrNull()?.height ?: 12 53 | val rowCnt = ceil(placeable.size / colCnt.toFloat()).toInt() 54 | 55 | layout( 56 | constraints.maxWidth, rowCnt * height + (rowCnt - 1) * verticalSpacing.roundToPx() 57 | ) { 58 | placeable.forEachIndexed { index, placeable -> 59 | val row = index / colCnt 60 | val col = index % colCnt 61 | placeable.placeRelative( 62 | col * (width + horizontalSpacing.roundToPx()), 63 | row * (height + verticalSpacing.roundToPx()) 64 | ) 65 | } 66 | } 67 | }) 68 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/cn/xihan/age/component/Navigation.kt: -------------------------------------------------------------------------------- 1 | package cn.xihan.age.component 2 | 3 | import androidx.compose.foundation.layout.WindowInsets 4 | import androidx.compose.foundation.layout.navigationBars 5 | import androidx.compose.material3.Scaffold 6 | import androidx.compose.ui.Modifier 7 | import androidx.compose.ui.zIndex 8 | import androidx.navigation.NavGraph.Companion.findStartDestination 9 | import androidx.navigation.NavGraphBuilder 10 | import androidx.navigation.NavHostController 11 | import androidx.navigation.compose.NavHost 12 | import androidx.navigation.compose.composable 13 | import androidx.navigation.compose.currentBackStackEntryAsState 14 | import cn.xihan.age.ui.main.home.HomeNavRoute 15 | import cn.xihan.age.ui.main.home.homeScreen 16 | import cn.xihan.age.ui.main.mine.MineNavRoute 17 | import cn.xihan.age.ui.main.mine.mineScreen 18 | import cn.xihan.age.ui.main.schedule.ScheduleNavRoute 19 | import cn.xihan.age.ui.main.schedule.scheduleScreen 20 | import cn.xihan.age.ui.theme.AgeAnimeIcons 21 | 22 | /** 23 | * @项目名 : AGE动漫 24 | * @作者 : MissYang 25 | * @创建时间 : 2023/9/17 19:07 26 | * @介绍 : 27 | */ 28 | const val TopLevelNavRoute = "top_level_route" 29 | 30 | fun NavGraphBuilder.topLevelScreen( 31 | navController: NavHostController, 32 | navigateToCategory: (label: String) -> Unit, 33 | navigateToAnimePlay: (Int) -> Unit, 34 | navigateToRank: () -> Unit, 35 | onShowSnackbar: (message: String) -> Unit, 36 | onNavigationClick: (String) -> Unit, 37 | ) { 38 | composable(TopLevelNavRoute) { 39 | 40 | val currentDestination = navController.currentBackStackEntryAsState().value?.destination 41 | 42 | Scaffold( 43 | bottomBar = { 44 | AgeAnimeBottomAppBar( 45 | currentDestination = currentDestination, 46 | onNavigateTo = { 47 | navController.navigate(it.route) { 48 | popUpTo(navController.graph.findStartDestination().id) { 49 | saveState = true 50 | } 51 | 52 | launchSingleTop = true 53 | restoreState = true 54 | } 55 | }) 56 | }, 57 | contentWindowInsets = WindowInsets.navigationBars 58 | ) { padding -> 59 | 60 | NavHost( 61 | modifier = Modifier.zIndex(1f), 62 | navController = navController, 63 | startDestination = HomeNavRoute, 64 | // contentAlignment = Alignment.Center, 65 | // enterTransition = { fadeIn() }, 66 | // exitTransition = { fadeOut() } 67 | ) { 68 | 69 | homeScreen( 70 | padding = padding, 71 | onCategoryClick = { navigateToCategory("") }, 72 | onAnimeClick = navigateToAnimePlay, 73 | onShowSnackbar = onShowSnackbar, 74 | onNavigationClick = onNavigationClick 75 | ) 76 | 77 | scheduleScreen( 78 | padding = padding, 79 | onAnimeClick = navigateToAnimePlay, 80 | onRankClick = navigateToRank, 81 | onShowSnackbar = onShowSnackbar 82 | ) 83 | 84 | mineScreen( 85 | padding = padding, 86 | onNavigationClick = onNavigationClick, 87 | onShowSnackbar = onShowSnackbar 88 | ) 89 | } 90 | } 91 | } 92 | } 93 | 94 | enum class TopLevelScreen( 95 | val route: String, 96 | val iconId: Int, 97 | val label: String, 98 | ) { 99 | HOME( 100 | HomeNavRoute, AgeAnimeIcons.Animated.home, "首页" 101 | ), 102 | SCHEDULE( 103 | ScheduleNavRoute, AgeAnimeIcons.Animated.calendar, "时间表" 104 | ), 105 | MINE( 106 | MineNavRoute, AgeAnimeIcons.Animated.mine, "我的" 107 | ) 108 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/cn/xihan/age/component/TopBar.kt: -------------------------------------------------------------------------------- 1 | package cn.xihan.age.component 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.clickable 5 | import androidx.compose.foundation.layout.Arrangement 6 | import androidx.compose.foundation.layout.Box 7 | import androidx.compose.foundation.layout.Row 8 | import androidx.compose.foundation.layout.WindowInsets 9 | import androidx.compose.foundation.layout.WindowInsetsSides 10 | import androidx.compose.foundation.layout.fillMaxWidth 11 | import androidx.compose.foundation.layout.navigationBars 12 | import androidx.compose.foundation.layout.only 13 | import androidx.compose.foundation.layout.padding 14 | import androidx.compose.foundation.layout.statusBarsPadding 15 | import androidx.compose.foundation.layout.windowInsetsPadding 16 | import androidx.compose.foundation.shape.CircleShape 17 | import androidx.compose.material3.Icon 18 | import androidx.compose.material3.MaterialTheme 19 | import androidx.compose.material3.Surface 20 | import androidx.compose.material3.Text 21 | import androidx.compose.runtime.Composable 22 | import androidx.compose.ui.Alignment 23 | import androidx.compose.ui.Modifier 24 | import androidx.compose.ui.draw.clip 25 | import androidx.compose.ui.graphics.Color 26 | import androidx.compose.ui.res.painterResource 27 | import androidx.compose.ui.semantics.Role 28 | import androidx.compose.ui.text.font.FontWeight 29 | import androidx.compose.ui.unit.dp 30 | import cn.xihan.age.ui.theme.AgeAnimeIcons 31 | import cn.xihan.age.ui.theme.pink95 32 | import cn.xihan.age.util.rememberMutableInteractionSource 33 | 34 | @Composable 35 | fun SolidTopBar( 36 | title: String, 37 | leftIconId: Int = AgeAnimeIcons.arrowLeft, 38 | onLeftIconClick: () -> Unit = {}, 39 | rightIconId: Int? = null, 40 | onRightIconClick: (() -> Unit) = {}, 41 | ) { 42 | 43 | val interactionResource = rememberMutableInteractionSource() 44 | 45 | Surface { 46 | Box( 47 | modifier = Modifier 48 | .fillMaxWidth() 49 | .windowInsetsPadding(WindowInsets.navigationBars.only(WindowInsetsSides.Horizontal)) 50 | .statusBarsPadding() 51 | .padding(15.dp, 12.dp), 52 | ) { 53 | Icon( 54 | modifier = Modifier 55 | .align(Alignment.CenterStart) 56 | .clickable( 57 | interactionSource = interactionResource, 58 | indication = null, 59 | role = Role.Button, 60 | onClick = onLeftIconClick 61 | ), 62 | painter = painterResource(leftIconId), 63 | contentDescription = null, 64 | ) 65 | Text( 66 | modifier = Modifier.align(Alignment.Center), 67 | text = title, 68 | style = MaterialTheme.typography.titleMedium 69 | ) 70 | if (rightIconId != null) { 71 | Icon( 72 | modifier = Modifier 73 | .align(Alignment.CenterEnd) 74 | .clickable( 75 | interactionSource = interactionResource, 76 | indication = null, 77 | role = Role.Button, 78 | onClick = onRightIconClick 79 | ), 80 | painter = painterResource(rightIconId), 81 | contentDescription = null, 82 | ) 83 | } 84 | } 85 | } 86 | } 87 | 88 | @Composable 89 | fun TransparentTopBar( 90 | title: String, 91 | iconId: Int, 92 | iconTint: Color = MaterialTheme.colorScheme.onSurface, 93 | onIconClick: () -> Unit 94 | ) { 95 | Row( 96 | modifier = Modifier 97 | .fillMaxWidth() 98 | .statusBarsPadding() 99 | .padding(20.dp, 12.dp), 100 | verticalAlignment = Alignment.CenterVertically, 101 | horizontalArrangement = Arrangement.SpaceBetween 102 | ) { 103 | Text( 104 | text = title, 105 | // fontFamily = AgeAnimeFontFamilies.heiFontFamily, 106 | fontWeight = FontWeight.Normal, 107 | style = MaterialTheme.typography.titleLarge 108 | ) 109 | Box( 110 | modifier = Modifier 111 | .clip(CircleShape) 112 | .background(pink95) 113 | .clickable(role = Role.Button, onClick = onIconClick) 114 | .padding(10.dp), 115 | contentAlignment = Alignment.Center 116 | ) { 117 | Icon( 118 | painter = painterResource(iconId), 119 | contentDescription = null, 120 | tint = iconTint 121 | ) 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /app/src/main/kotlin/cn/xihan/age/di/AppModule.kt: -------------------------------------------------------------------------------- 1 | package cn.xihan.age.di 2 | 3 | import android.content.Context 4 | import android.content.res.Resources 5 | import dagger.Module 6 | import dagger.Provides 7 | import dagger.hilt.InstallIn 8 | import dagger.hilt.android.qualifiers.ApplicationContext 9 | import dagger.hilt.components.SingletonComponent 10 | import javax.inject.Singleton 11 | 12 | /** 13 | * @项目名 : AGE动漫 14 | * @作者 : MissYang 15 | * @创建时间 : 2023/9/17 20:33 16 | * @介绍 : 17 | */ 18 | @InstallIn(SingletonComponent::class) 19 | @Module 20 | object AppModule { 21 | 22 | @Provides 23 | @Singleton 24 | fun resources(@ApplicationContext context: Context): Resources = context.resources 25 | 26 | 27 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/cn/xihan/age/di/CoroutinesQualifiers.kt: -------------------------------------------------------------------------------- 1 | package cn.xihan.age.di 2 | 3 | import javax.inject.Qualifier 4 | 5 | /** 6 | * @项目名 : AGE动漫 7 | * @作者 : MissYang 8 | * @创建时间 : 2022/11/7 12:42 9 | * @介绍 : 10 | */ 11 | @Retention(AnnotationRetention.BINARY) 12 | @Qualifier 13 | annotation class DefaultDispatcher 14 | 15 | @Retention(AnnotationRetention.BINARY) 16 | @Qualifier 17 | annotation class IoDispatcher 18 | 19 | @Retention(AnnotationRetention.BINARY) 20 | @Qualifier 21 | annotation class MainDispatcher 22 | 23 | @Retention(AnnotationRetention.BINARY) 24 | @Qualifier 25 | annotation class MainImmediateDispatcher 26 | 27 | @Retention(AnnotationRetention.BINARY) 28 | @Qualifier 29 | annotation class ApplicationScope 30 | -------------------------------------------------------------------------------- /app/src/main/kotlin/cn/xihan/age/di/DispatcherModule.kt: -------------------------------------------------------------------------------- 1 | package cn.xihan.age.di 2 | 3 | import dagger.Module 4 | import dagger.Provides 5 | import dagger.hilt.InstallIn 6 | import dagger.hilt.components.SingletonComponent 7 | import kotlinx.coroutines.CoroutineDispatcher 8 | import kotlinx.coroutines.Dispatchers 9 | 10 | /** 11 | * @项目名 : AGE动漫 12 | * @作者 : MissYang 13 | * @创建时间 : 2023/9/17 20:31 14 | * @介绍 : 15 | */ 16 | @InstallIn(SingletonComponent::class) 17 | @Module 18 | object DispatcherModule { 19 | @DefaultDispatcher 20 | @Provides 21 | fun providesDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default 22 | 23 | @IoDispatcher 24 | @Provides 25 | fun providesIoDispatcher(): CoroutineDispatcher = Dispatchers.IO 26 | 27 | @MainDispatcher 28 | @Provides 29 | fun providesMainDispatcher(): CoroutineDispatcher = Dispatchers.Main 30 | 31 | @MainImmediateDispatcher 32 | @Provides 33 | fun providesMainImmediateDispatcher(): CoroutineDispatcher = Dispatchers.Main.immediate 34 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/cn/xihan/age/di/PersistenceModule.kt: -------------------------------------------------------------------------------- 1 | package cn.xihan.age.di 2 | 3 | import android.content.Context 4 | import cn.xihan.age.model.FavoriteDao 5 | import cn.xihan.age.model.HistoryDao 6 | import cn.xihan.age.persistence.AppDatabase 7 | import dagger.Module 8 | import dagger.Provides 9 | import dagger.hilt.InstallIn 10 | import dagger.hilt.android.qualifiers.ApplicationContext 11 | import dagger.hilt.components.SingletonComponent 12 | import javax.inject.Singleton 13 | 14 | /** 15 | * @项目名 : AGE动漫 16 | * @作者 : MissYang 17 | * @创建时间 : 2023/9/22 17:37 18 | * @介绍 : 19 | */ 20 | @Module 21 | @InstallIn(SingletonComponent::class) 22 | object PersistenceModule { 23 | 24 | @Provides 25 | @Singleton 26 | fun provideRoomDataBase(@ApplicationContext context: Context): AppDatabase = 27 | AppDatabase.getInstance(context) 28 | 29 | // @Provides 30 | // @Singleton 31 | // fun provideAnimeInfoModelDao(appDatabase: AppDatabase) = 32 | // appDatabase.animeInfoModelDao() 33 | 34 | // @Provides 35 | // @Singleton 36 | // fun provideAnimeCatalogModelDao(appDatabase: AppDatabase) = 37 | // appDatabase.animeCatalogModelDao() 38 | 39 | @Provides 40 | @Singleton 41 | fun provideFavoriteDao(appDatabase: AppDatabase): FavoriteDao { 42 | return appDatabase.favouriteDao() 43 | } 44 | 45 | @Provides 46 | @Singleton 47 | fun provideHistoryDao(appDatabase: AppDatabase): HistoryDao { 48 | return appDatabase.historyDao() 49 | } 50 | 51 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/cn/xihan/age/di/RepositoryModule.kt: -------------------------------------------------------------------------------- 1 | package cn.xihan.age.di 2 | 3 | import android.content.Context 4 | import cn.xihan.age.model.FavoriteDao 5 | import cn.xihan.age.model.HistoryDao 6 | import cn.xihan.age.repository.LocalRepository 7 | import cn.xihan.age.repository.RemoteRepository 8 | import cn.xihan.age.util.JsoupService 9 | import cn.xihan.age.util.RemoteService 10 | import dagger.Module 11 | import dagger.Provides 12 | import dagger.hilt.InstallIn 13 | import dagger.hilt.android.qualifiers.ApplicationContext 14 | import dagger.hilt.components.SingletonComponent 15 | import kotlinx.coroutines.CoroutineDispatcher 16 | import okhttp3.OkHttpClient 17 | import javax.inject.Singleton 18 | 19 | /** 20 | * @项目名 : AGE动漫 21 | * @作者 : MissYang 22 | * @创建时间 : 2023/9/18 20:55 23 | * @介绍 : 24 | */ 25 | @Module 26 | @InstallIn(SingletonComponent::class) 27 | object RepositoryModule { 28 | 29 | @Provides 30 | @Singleton 31 | fun provideLocalRepository( 32 | @ApplicationContext context: Context, 33 | @IoDispatcher ioDispatcher: CoroutineDispatcher, 34 | // animeDetailModelDao: AnimeDetailDao, 35 | // animeCatalogModelDao: AnimeCatalogModelDao, 36 | favoriteDao: FavoriteDao, 37 | historyDao: HistoryDao 38 | ): LocalRepository { 39 | return LocalRepository( 40 | context = context, 41 | ioDispatcher = ioDispatcher, 42 | // animeDetailModelDao = animeDetailModelDao, 43 | // animeCatalogModelDao = animeCatalogModelDao, 44 | favoriteDao = favoriteDao, 45 | historyDao = historyDao 46 | ) 47 | } 48 | 49 | @Provides 50 | @Singleton 51 | fun provideRemoteRepository( 52 | @ApplicationContext context: Context, 53 | @IoDispatcher ioDispatcher: CoroutineDispatcher, 54 | okhttpClient: OkHttpClient, 55 | remoteService: RemoteService, 56 | jsoupService: JsoupService 57 | ): RemoteRepository { 58 | return RemoteRepository( 59 | context = context, 60 | ioDispatcher = ioDispatcher, 61 | okhttpClient = okhttpClient, 62 | remoteService = remoteService, 63 | jsoupService = jsoupService 64 | ) 65 | } 66 | 67 | 68 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/cn/xihan/age/initializer/AppInitializer.kt: -------------------------------------------------------------------------------- 1 | package cn.xihan.age.initializer 2 | 3 | import android.content.Context 4 | import androidx.appcompat.app.AppCompatDelegate 5 | import androidx.startup.Initializer 6 | import cn.xihan.age.BuildConfig 7 | import cn.xihan.age.util.Settings 8 | import cn.xihan.age.util.SimpleLoggerPrinter 9 | import cn.xihan.age.util.initLogger 10 | import cn.xihan.age.util.logDebug 11 | import com.kongzue.dialogx.DialogX 12 | import com.tencent.mmkv.MMKV 13 | import timber.log.Timber 14 | 15 | /** 16 | * @项目名 : age-anime 17 | * @作者 : MissYang 18 | * @创建时间 : 2023/9/17 18:30 19 | * @介绍 : 20 | */ 21 | class AppInitializer : Initializer { 22 | 23 | override fun create(context: Context) { 24 | MMKV.initialize(context) 25 | initLogger(BuildConfig.DEBUG, SimpleLoggerPrinter()) 26 | theme() 27 | if (BuildConfig.DEBUG) { 28 | Timber.plant(Timber.DebugTree()) 29 | logDebug("AppInitializer is initialized.") 30 | } 31 | } 32 | 33 | 34 | private fun theme() { 35 | when (Settings.themeMode) { 36 | 1 -> { 37 | DialogX.globalTheme = DialogX.THEME.LIGHT 38 | AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) 39 | } 40 | 41 | 2 -> { 42 | DialogX.globalTheme = DialogX.THEME.DARK 43 | AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) 44 | } 45 | 46 | else -> { 47 | DialogX.globalTheme = DialogX.THEME.AUTO 48 | AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) 49 | } 50 | } 51 | } 52 | 53 | override fun dependencies() = emptyList>>() 54 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/cn/xihan/age/model/AlertDialogModel.kt: -------------------------------------------------------------------------------- 1 | package cn.xihan.age.model 2 | 3 | /** 4 | * @项目名 : AGE动漫 5 | * @作者 : MissYang 6 | * @创建时间 : 2022/12/31 21:22 7 | * @介绍 : 8 | */ 9 | data class AlertDialogModel( 10 | val title: String, 11 | val message: String, 12 | val positiveMessage: String? = null, 13 | val positiveObject: Any? = null, 14 | val negativeMessage: String? = null, 15 | val negativeObject: Any? = null, 16 | ) 17 | -------------------------------------------------------------------------------- /app/src/main/kotlin/cn/xihan/age/model/AnimeDetailModel.kt: -------------------------------------------------------------------------------- 1 | package cn.xihan.age.model 2 | 3 | import android.os.Parcelable 4 | import kotlinx.parcelize.Parcelize 5 | import kotlinx.serialization.SerialName 6 | import kotlinx.serialization.Serializable 7 | 8 | /** 9 | * @项目名 : AGE动漫 10 | * @作者 : MissYang 11 | * @创建时间 : 2023/9/22 15:56 12 | * @介绍 : 13 | */ 14 | /** 15 | * 视频模型 16 | * 链接: https://app.age-api.com:8443/v2/detail/20230131 17 | */ 18 | @Parcelize 19 | @Serializable 20 | //@Entity 21 | data class Video( 22 | // @PrimaryKey 23 | val id: Int = 0, 24 | @SerialName("name_other") 25 | val nameOther: String = "", 26 | val company: String = "", 27 | val name: String = "", 28 | val type: String = "", 29 | val writer: String = "", 30 | @SerialName("name_original") 31 | val nameOriginal: String = "", 32 | val plot: String = "", 33 | @SerialName("plot_arr") 34 | val plotArr: List = emptyList(), 35 | val playlists: Map>> = mapOf(), 36 | val area: String = "", 37 | val letter: String = "", 38 | val website: String = "", 39 | val star: Int = 0, 40 | val status: String = "", 41 | @SerialName("uptodate") 42 | val upToDate: String = "", 43 | @SerialName("time_format_1") 44 | val timeFormat1: String = "", 45 | @SerialName("time_format_2") 46 | val timeFormat2: String = "", 47 | @SerialName("time_format_3") 48 | val timeFormat3: String = "", 49 | val time: Long = 0L, 50 | val tags: String = "", 51 | @SerialName("tags_arr") 52 | val tagsArr: List = emptyList(), 53 | val intro: String = "", 54 | @SerialName("intro_html") 55 | val introHtml: String = "", 56 | @SerialName("intro_clean") 57 | val introClean: String = "", 58 | val series: String = "", 59 | @SerialName("net_disk") 60 | val netDisk: String = "", 61 | val resource: String = "", 62 | val year: Int = 0, 63 | val season: Int = 0, 64 | val premiere: String = "", 65 | @SerialName("rank_cnt") 66 | val rankCnt: String = "", 67 | val cover: String = "", 68 | @SerialName("comment_cnt") 69 | val commentCnt: String = "", 70 | @SerialName("collect_cnt") 71 | val collectCnt: String = "" 72 | ) : Parcelable 73 | 74 | @Serializable 75 | @Parcelize 76 | data class PlayerJx( 77 | val vip: String = "https://43.240.74.134:8443/vip/?url=", 78 | val zj: String = "https://43.240.74.134:8443/m3u8/?url=" 79 | ) : Parcelable 80 | 81 | @Serializable 82 | @Parcelize 83 | data class AnimeDetailModel( 84 | val video: Video = Video(), 85 | val series: List = listOf(), 86 | val similar: List = listOf(), 87 | @SerialName("player_label_arr") 88 | val playerLabel: Map = emptyMap(),//PlayerLabel = PlayerLabel(), 89 | @SerialName("player_vip") 90 | val playerVip: String = "", 91 | @SerialName("player_jx") 92 | val playerJx: PlayerJx = PlayerJx() 93 | ) : Parcelable 94 | 95 | //@Dao 96 | //interface AnimeDetailDao: BaseDao