├── .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 | 
2 |
3 | 
4 | [](../../releases)
5 | 
6 | [](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