├── .gitignore
├── .idea
├── .gitignore
├── compiler.xml
├── deploymentTargetDropDown.xml
├── gradle.xml
├── inspectionProfiles
│ └── Project_Default.xml
└── misc.xml
├── LICENSE
├── README.md
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── fmt
│ │ └── compose
│ │ └── eyepetizer
│ │ └── ExampleInstrumentedTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── fmt
│ │ │ └── compose
│ │ │ └── eyepetizer
│ │ │ ├── App.kt
│ │ │ ├── MainActivity.kt
│ │ │ ├── MainViewModel.kt
│ │ │ ├── db
│ │ │ ├── CacheManager.kt
│ │ │ ├── Video.kt
│ │ │ └── VideoDao.kt
│ │ │ ├── ext
│ │ │ ├── AbsPagingViewModel.kt
│ │ │ ├── GsonExt.kt
│ │ │ ├── LazyPagingItemExt.kt
│ │ │ ├── PxExt.kt
│ │ │ └── ToastExt.kt
│ │ │ ├── http
│ │ │ ├── ApiService.kt
│ │ │ └── IApi.kt
│ │ │ ├── model
│ │ │ ├── BaseApiResult.kt
│ │ │ ├── Category.kt
│ │ │ ├── Daily.kt
│ │ │ ├── Follow.kt
│ │ │ ├── Issue.kt
│ │ │ ├── Item.kt
│ │ │ ├── News.kt
│ │ │ ├── Recommend.kt
│ │ │ ├── TabListInfo.kt
│ │ │ ├── Topic.kt
│ │ │ └── TopicDetail.kt
│ │ │ ├── pages
│ │ │ ├── daily
│ │ │ │ ├── DailyPage.kt
│ │ │ │ ├── SearchVideoPage.kt
│ │ │ │ ├── repository
│ │ │ │ │ └── DailyPagingSource.kt
│ │ │ │ └── viewmodel
│ │ │ │ │ └── DailyViewModel.kt
│ │ │ ├── detail
│ │ │ │ ├── NewsDetailActivity.kt
│ │ │ │ ├── TopicDetailActivity.kt
│ │ │ │ ├── TopicDetailPage.kt
│ │ │ │ ├── VideoDetailActivity.kt
│ │ │ │ ├── VideoDetailPage.kt
│ │ │ │ └── viewmodel
│ │ │ │ │ ├── TopicDetailViewModel.kt
│ │ │ │ │ └── VideoDetailModel.kt
│ │ │ ├── discover
│ │ │ │ ├── CategoryDetailActivity.kt
│ │ │ │ ├── CategoryDetailPage.kt
│ │ │ │ ├── CategoryPage.kt
│ │ │ │ ├── DiscoverPage.kt
│ │ │ │ ├── FollowPage.kt
│ │ │ │ ├── NewsPage.kt
│ │ │ │ ├── RecommendPage.kt
│ │ │ │ ├── TopicPage.kt
│ │ │ │ └── viewmodel
│ │ │ │ │ ├── CategoryDetailViewModel.kt
│ │ │ │ │ ├── CategoryViewModel.kt
│ │ │ │ │ ├── DiscoverViewModel.kt
│ │ │ │ │ ├── FollowViewModel.kt
│ │ │ │ │ ├── NewsViewModel.kt
│ │ │ │ │ ├── RecommendViewModel.kt
│ │ │ │ │ └── TopicViewModel.kt
│ │ │ ├── hot
│ │ │ │ ├── HotPage.kt
│ │ │ │ └── viewmodel
│ │ │ │ │ ├── HotTabViewModel.kt
│ │ │ │ │ └── HotViewModel.kt
│ │ │ └── person
│ │ │ │ ├── PersonPage.kt
│ │ │ │ ├── WatchRecordActivity.kt
│ │ │ │ ├── WatchRecordPage.kt
│ │ │ │ └── viewmodel
│ │ │ │ ├── PersonViewModel.kt
│ │ │ │ └── WatchRecordViewModel.kt
│ │ │ ├── ui
│ │ │ └── theme
│ │ │ │ ├── Color.kt
│ │ │ │ ├── Shape.kt
│ │ │ │ ├── Theme.kt
│ │ │ │ └── Type.kt
│ │ │ ├── util
│ │ │ ├── DateUtils.kt
│ │ │ └── ScreenUtils.kt
│ │ │ └── view
│ │ │ ├── Swipe.kt
│ │ │ └── TopAppBar.kt
│ └── res
│ │ ├── drawable-v24
│ │ └── ic_launcher_foreground.xml
│ │ ├── drawable
│ │ ├── ic_launcher_background.xml
│ │ └── launch_layer_list.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxhdpi
│ │ ├── home__ic_mine_normal.png
│ │ ├── home_ic_discovery_normal.png
│ │ ├── home_ic_discovery_selected.png
│ │ ├── home_ic_hot_normal.png
│ │ ├── home_ic_hot_selected.png
│ │ ├── home_ic_img_avatar.png
│ │ ├── home_ic_mine_selected.png
│ │ ├── home_ic_normal.png
│ │ ├── home_ic_selected.png
│ │ ├── home_launch_screen.jpg
│ │ ├── ic_head_bg.webp
│ │ ├── ic_launcher.png
│ │ ├── ic_launcher_round.png
│ │ └── ic_splash.jpg
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── values
│ │ ├── colors.xml
│ │ ├── strings.xml
│ │ └── themes.xml
│ │ └── xml
│ │ ├── backup_rules.xml
│ │ └── data_extraction_rules.xml
│ └── test
│ └── java
│ └── com
│ └── fmt
│ └── compose
│ └── eyepetizer
│ └── ExampleUnitTest.kt
├── build.gradle
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── settings.gradle
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | .DS_Store
11 | /build
12 | /captures
13 | .externalNativeBuild
14 | .cxx
15 | local.properties
16 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/deploymentTargetDropDown.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 fmtjava
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Compose_Eyepetizer
2 | 一款基于 Jetpack Compose 实现的精美仿开眼视频App(提供Kotlin、Flutter、React Native、小程序版本 😁 )
3 | Kotlin:[Jetpack_Kotlin_Eyepetizer](https://github.com/fmtjava/Jetpack_Kotlin_Eyepetizer)
4 | Flutter版:[flutter_eyepetizer](https://github.com/fmtjava/flutter_eyepetizer)
5 | ReactNative版:[ReactNative_Eyepetizer](https://github.com/fmtjava/ReactNative_Eyepetizer)
6 | 小程序版:[wx_eyepetizer](https://github.com/fmtjava/wx_eyepetizer)
7 |
8 | **如果喜欢的话希望给个 `Star` 或 `Fork` ^_^ ,谢谢**
9 |
10 | # 项目截图
11 |
16 |
17 |
18 |
19 |
24 |
25 |
26 |
31 |
32 |
33 |
38 |
39 |
44 |
45 |
50 |
51 | # 核心功能
52 |
53 |
54 |
55 |
56 | # 下载体验
57 | - 点击[](https://www.pgyer.com/2hmCf5)
58 | - 下方二维码下载(每日上限100次,如达到上限,还是 clone 源码吧!✧(≖ ◡ ≖✿)))
59 |
60 |
61 | # 更新日志
62 | ### v1.0
63 | * 初始化项目,完成开眼视频App核心功能,目前实现首页、发现、热门、分类、我的、视频详情、视频播放等功能,后续持续学习 Compose 高级的内容,继续完善项目
64 | # Thanks
65 | - [coil](https://github.com/coil-kt/coil)
66 | - [JiaoZiVideoPlayer](https://github.com/Jzvd/JZVideo)
67 | - [compose-collapsing-toolbar](https://github.com/onebone/compose-collapsing-toolbar)
68 | - [retrofit2](https://github.com/square/retrofit)
69 |
70 | # 关于我
71 | - WX:fmtjava
72 | - QQ:2694746499
73 | - Email:2694746499@qq.com
74 | - Github:https://github.com/fmtjava
75 |
76 | # 声明
77 | 项目中的 API 均来自开眼视频,纯属学习交流使用,不得用于商业用途!
78 |
79 | # License
80 |
81 | Copyright (c) 2023 fmtjava
82 |
83 | Permission is hereby granted, free of charge, to any person obtaining a copy
84 | of this software and associated documentation files (the "Software"), to deal
85 | in the Software without restriction, including without limitation the rights
86 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
87 | copies of the Software, and to permit persons to whom the Software is
88 | furnished to do so, subject to the following conditions:
89 |
90 | The above copyright notice and this permission notice shall be included in all
91 | copies or substantial portions of the Software.
92 |
93 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
94 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
95 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
96 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
97 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
98 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
99 | SOFTWARE.
100 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.application'
3 | id 'org.jetbrains.kotlin.android'
4 | id 'kotlin-kapt'
5 | id 'kotlin-parcelize'
6 | }
7 |
8 | android {
9 | namespace 'com.fmt.compose.eyepetizer'
10 | compileSdk 33
11 |
12 | defaultConfig {
13 | applicationId "com.fmt.compose.eyepetizer"
14 | minSdk 21
15 | targetSdk 32
16 | versionCode 1
17 | versionName "1.0"
18 |
19 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
20 | vectorDrawables {
21 | useSupportLibrary true
22 | }
23 | }
24 |
25 | buildTypes {
26 | release {
27 | minifyEnabled false
28 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
29 | }
30 | }
31 | compileOptions {
32 | sourceCompatibility JavaVersion.VERSION_1_8
33 | targetCompatibility JavaVersion.VERSION_1_8
34 | }
35 | kotlinOptions {
36 | jvmTarget = '1.8'
37 | }
38 | buildFeatures {
39 | compose true
40 | }
41 | composeOptions {
42 | kotlinCompilerExtensionVersion '1.4.0'
43 | }
44 | packagingOptions {
45 | resources {
46 | excludes += '/META-INF/{AL2.0,LGPL2.1}'
47 | }
48 | }
49 | }
50 |
51 | dependencies {
52 |
53 | implementation 'androidx.core:core-ktx:1.10.1'
54 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1'
55 | implementation 'androidx.activity:activity-compose:1.7.0'
56 | implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1'
57 | implementation platform('androidx.compose:compose-bom:2023.01.00')
58 | implementation "androidx.compose.ui:ui"
59 | implementation "androidx.compose.ui:ui-tooling-preview"
60 | implementation 'androidx.compose.foundation:foundation'
61 | implementation 'androidx.compose.material:material'
62 | implementation 'androidx.compose.material:material-icons-core'
63 | implementation 'androidx.compose.material:material-icons-extended'
64 |
65 | implementation("androidx.paging:paging-compose:1.0.0-alpha18")
66 |
67 | implementation("io.coil-kt:coil-compose:2.4.0")
68 |
69 | implementation 'com.squareup.retrofit2:retrofit:2.9.0'
70 | implementation "com.squareup.retrofit2:converter-gson:2.9.0"
71 |
72 | implementation "androidx.room:room-ktx:2.5.2"
73 | implementation 'androidx.appcompat:appcompat:1.4.1'
74 | implementation 'com.google.android.material:material:1.4.+'
75 | implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
76 | kapt "androidx.room:room-compiler:2.5.1"
77 |
78 | implementation 'cn.jzvd:jiaozivideoplayer:7.6.0'
79 | implementation "me.onebone:toolbar-compose:2.3.5"
80 | implementation 'com.github.chrisbanes:PhotoView:2.3.0'
81 |
82 | testImplementation 'junit:junit:4.13.2'
83 | androidTestImplementation 'androidx.test.ext:junit:1.1.3'
84 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
85 | androidTestImplementation "androidx.compose.ui:ui-test-junit4"
86 | debugImplementation "androidx.compose.ui:ui-tooling"
87 | debugImplementation "androidx.compose.ui:ui-test-manifest"
88 | }
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/fmt/compose/eyepetizer/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.fmt.compose.eyepetizer
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("com.fmt.compose.eyepetizer", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
19 |
24 |
29 |
34 |
39 |
44 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/app/src/main/java/com/fmt/compose/eyepetizer/App.kt:
--------------------------------------------------------------------------------
1 | package com.fmt.compose.eyepetizer
2 |
3 | import android.app.Application
4 |
5 | lateinit var mainApplication: Application
6 |
7 | class App : Application() {
8 |
9 | override fun onCreate() {
10 | super.onCreate()
11 | mainApplication = this
12 | }
13 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/fmt/compose/eyepetizer/MainActivity.kt:
--------------------------------------------------------------------------------
1 | @file:OptIn(ExperimentalFoundationApi::class)
2 |
3 | package com.fmt.compose.eyepetizer
4 |
5 | import android.os.Bundle
6 | import androidx.activity.ComponentActivity
7 | import androidx.activity.compose.setContent
8 | import androidx.annotation.DrawableRes
9 | import androidx.annotation.StringRes
10 | import androidx.compose.foundation.ExperimentalFoundationApi
11 | import androidx.compose.foundation.background
12 | import androidx.compose.foundation.layout.PaddingValues
13 | import androidx.compose.foundation.layout.fillMaxSize
14 | import androidx.compose.foundation.layout.padding
15 | import androidx.compose.foundation.layout.size
16 | import androidx.compose.foundation.pager.HorizontalPager
17 | import androidx.compose.foundation.pager.PagerState
18 | import androidx.compose.foundation.pager.rememberPagerState
19 | import androidx.compose.material.*
20 | import androidx.compose.runtime.*
21 | import androidx.compose.ui.Modifier
22 | import androidx.compose.ui.graphics.Color
23 | import androidx.compose.ui.res.painterResource
24 | import androidx.compose.ui.res.stringResource
25 | import androidx.compose.ui.unit.dp
26 | import androidx.core.view.WindowInsetsControllerCompat
27 | import androidx.lifecycle.viewmodel.compose.viewModel
28 | import com.fmt.compose.eyepetizer.pages.daily.DailyPage
29 | import com.fmt.compose.eyepetizer.pages.discover.DiscoverPage
30 | import com.fmt.compose.eyepetizer.pages.hot.HotPage
31 | import com.fmt.compose.eyepetizer.pages.person.PersonPage
32 | import com.fmt.compose.eyepetizer.ui.theme.SelectedItemColor
33 | import com.fmt.compose.eyepetizer.ui.theme.UnselectedItemColor
34 | import kotlinx.coroutines.launch
35 |
36 | class MainActivity : ComponentActivity() {
37 |
38 | override fun onCreate(savedInstanceState: Bundle?) {
39 | super.onCreate(savedInstanceState)
40 | window.statusBarColor = android.graphics.Color.WHITE
41 | WindowInsetsControllerCompat(window, window.decorView).apply {
42 | isAppearanceLightStatusBars = true
43 | }
44 | setContent {
45 | val pagerState = rememberPagerState()
46 | Scaffold(backgroundColor = Color.White, bottomBar = {
47 | BottomNavigationBar(pagerState)
48 | }) { padding ->
49 | ContentScreen(padding, pagerState)
50 | }
51 | }
52 | }
53 | }
54 |
55 | @Composable
56 | fun ContentScreen(padding: PaddingValues, pagerState: PagerState) {
57 | HorizontalPager(pageCount = 4,
58 | userScrollEnabled = false,
59 | state = pagerState,
60 | modifier = Modifier
61 | .fillMaxSize()
62 | .background(Color.White)
63 | .padding(padding)) { pageIndex ->
64 | when (pageIndex) {
65 | 0 -> DailyPage()
66 | 1 -> DiscoverPage()
67 | 2 -> HotPage()
68 | 3 -> PersonPage()
69 | }
70 | }
71 | }
72 |
73 | @Composable
74 | fun BottomNavigationBar(pagerState: PagerState) {
75 | val viewModel: MainViewModel = viewModel()
76 | val scope = rememberCoroutineScope()
77 |
78 | BottomNavigation(backgroundColor = Color.White) {
79 | viewModel.tabs.value.forEachIndexed { index, tabItem ->
80 | BottomNavigationItem(selected = viewModel.selectTabIndex.value == index, onClick = {
81 | viewModel.selectTabIndex.value = index
82 | scope.launch {
83 | pagerState.scrollToPage(index)
84 | }
85 | }, label = {
86 | Text(text = stringResource(id = tabItem.title),
87 | color = if (viewModel.selectTabIndex.value == index) SelectedItemColor else UnselectedItemColor)
88 | }, icon = {
89 | Icon(painter = painterResource(id = if (viewModel.selectTabIndex.value == index) tabItem.selectIcon else tabItem.normalIcon),
90 | contentDescription = null,
91 | modifier = Modifier.size(24.dp))
92 | })
93 | }
94 | }
95 | }
96 |
97 | data class TabItem(
98 | @StringRes val title: Int,
99 | @DrawableRes val normalIcon: Int,
100 | @DrawableRes val selectIcon: Int,
101 | )
102 |
--------------------------------------------------------------------------------
/app/src/main/java/com/fmt/compose/eyepetizer/MainViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.fmt.compose.eyepetizer
2 |
3 | import androidx.compose.runtime.mutableStateOf
4 | import androidx.lifecycle.ViewModel
5 |
6 | class MainViewModel : ViewModel() {
7 |
8 | val tabs =
9 | mutableStateOf(listOf(
10 | TabItem(R.string.daily_paper, R.mipmap.home_ic_normal, R.mipmap.home_ic_selected),
11 | TabItem(R.string.discover,
12 | R.mipmap.home_ic_discovery_normal,
13 | R.mipmap.home_ic_discovery_selected),
14 | TabItem(R.string.hot, R.mipmap.home_ic_hot_normal, R.mipmap.home_ic_hot_selected),
15 | TabItem(R.string.mime, R.mipmap.home__ic_mine_normal, R.mipmap.home_ic_mine_selected),
16 | ))
17 |
18 | var selectTabIndex = mutableStateOf(0)
19 |
20 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/fmt/compose/eyepetizer/db/CacheManager.kt:
--------------------------------------------------------------------------------
1 | package com.fmt.compose.eyepetizer.db
2 |
3 | import androidx.room.Database
4 | import androidx.room.Room
5 | import androidx.room.RoomDatabase
6 | import com.fmt.compose.eyepetizer.mainApplication
7 |
8 | @Database(entities = [Video::class], version = 1)
9 | abstract class CacheManager : RoomDatabase() {
10 | abstract val videoDao: VideoDao
11 |
12 | companion object {
13 | private val database =
14 | Room.databaseBuilder(mainApplication, CacheManager::class.java, "compose_cache")
15 | .build()
16 |
17 | @JvmStatic
18 | fun get(): CacheManager {
19 | return database
20 | }
21 | }
22 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/fmt/compose/eyepetizer/db/Video.kt:
--------------------------------------------------------------------------------
1 | package com.fmt.compose.eyepetizer.db
2 |
3 | import androidx.room.Entity
4 | import androidx.room.PrimaryKey
5 |
6 | @Entity(tableName = "video")
7 | data class Video(@PrimaryKey val videoId: Int, val content: String)
--------------------------------------------------------------------------------
/app/src/main/java/com/fmt/compose/eyepetizer/db/VideoDao.kt:
--------------------------------------------------------------------------------
1 | package com.fmt.compose.eyepetizer.db
2 |
3 | import androidx.room.*
4 |
5 | @Dao
6 | interface VideoDao {
7 |
8 | @Insert(onConflict = OnConflictStrategy.REPLACE)
9 | suspend fun save(video: Video): Long
10 |
11 | @Query("select * from video where videoId=:videoId")
12 | suspend fun getVideo(videoId: Int): Video?
13 |
14 | @Query("select * from video")
15 | suspend fun getVideoList(): List
16 |
17 | @Delete
18 | suspend fun delete(video: Video): Int
19 |
20 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/fmt/compose/eyepetizer/ext/AbsPagingViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.fmt.compose.eyepetizer.ext
2 |
3 | import androidx.compose.runtime.mutableStateOf
4 | import androidx.lifecycle.ViewModel
5 | import androidx.lifecycle.viewModelScope
6 | import androidx.paging.*
7 | import com.fmt.compose.eyepetizer.model.BaseApiResult
8 | import kotlinx.coroutines.delay
9 |
10 | abstract class AbsPagingViewModel : ViewModel() {
11 |
12 | var refreshing = mutableStateOf(false)
13 |
14 | val pageFlow = Pager(config = PagingConfig(pageSize = 10,
15 | initialLoadSize = 10,
16 | prefetchDistance = 1), pagingSourceFactory = {
17 | AbsPagingSource()
18 | }).flow.cachedIn(viewModelScope)
19 |
20 | inner class AbsPagingSource() : PagingSource() {
21 | override fun getRefreshKey(state: PagingState): String? = null
22 |
23 | override suspend fun load(params: LoadParams): LoadResult {
24 | return try {
25 | val pageKey: String? = params.key
26 | var nextKey: String? = null
27 | val nextPageUrl: String?
28 | val apiResult = if (pageKey.isNullOrEmpty()) {
29 | refreshing.value = true
30 | doLoadPage()
31 | } else {
32 | doLoadPage(pageKey)
33 | }
34 | nextPageUrl = apiResult.nextPageUrl
35 | if (!nextPageUrl.isNullOrEmpty()) {
36 | nextKey = nextPageUrl
37 | }
38 | refreshing.value = false
39 | LoadResult.Page(apiResult.itemList,
40 | null,
41 | nextKey)
42 | } catch (e: Exception) {
43 | delay(200)
44 | refreshing.value = false
45 | errorToast(e.message ?: "")
46 | LoadResult.Error(e)
47 | }
48 | }
49 | }
50 |
51 | abstract suspend fun doLoadPage(pageKey: String? = null): BaseApiResult
52 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/fmt/compose/eyepetizer/ext/GsonExt.kt:
--------------------------------------------------------------------------------
1 | package com.fmt.compose.eyepetizer.ext
2 |
3 | import com.google.gson.Gson
4 | import com.google.gson.reflect.TypeToken
5 |
6 | val gson by lazy { Gson() }
7 |
8 | fun toJson(obj: Any): String {
9 | return gson.toJson(obj)
10 | }
11 |
12 | inline fun fromJson(json: String): T {
13 | val type = object : TypeToken() {}.type
14 | return gson.fromJson(json, type)
15 | }
16 |
--------------------------------------------------------------------------------
/app/src/main/java/com/fmt/compose/eyepetizer/ext/LazyPagingItemExt.kt:
--------------------------------------------------------------------------------
1 | @file:OptIn(ExperimentalFoundationApi::class)
2 |
3 | package com.fmt.compose.eyepetizer.ext
4 |
5 | import android.annotation.SuppressLint
6 | import android.os.Parcel
7 | import android.os.Parcelable
8 | import androidx.compose.foundation.ExperimentalFoundationApi
9 | import androidx.compose.foundation.clickable
10 | import androidx.compose.foundation.layout.*
11 | import androidx.compose.foundation.lazy.LazyListScope
12 | import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridItemScope
13 | import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridScope
14 | import androidx.compose.material.CircularProgressIndicator
15 | import androidx.compose.material.Icon
16 | import androidx.compose.material.Text
17 | import androidx.compose.material.icons.Icons
18 | import androidx.compose.material.icons.outlined.Sync
19 | import androidx.compose.runtime.Composable
20 | import androidx.compose.ui.Alignment
21 | import androidx.compose.ui.Modifier
22 | import androidx.compose.ui.res.stringResource
23 | import androidx.compose.ui.unit.dp
24 | import androidx.paging.LoadState
25 | import androidx.paging.compose.LazyPagingItems
26 | import com.fmt.compose.eyepetizer.R
27 |
28 | @OptIn(ExperimentalFoundationApi::class)
29 | fun LazyStaggeredGridScope.items(
30 | items: LazyPagingItems,
31 | key: ((item: T) -> Any)? = null,
32 | itemContent: @Composable LazyStaggeredGridItemScope.(item: T) -> Unit,
33 | ) {
34 | items(
35 | count = items.itemCount,
36 | key = if (key == null) null else { index ->
37 | val item = items.peek(index)
38 | if (item == null) {
39 | PagingPlaceholderKey(index)
40 | } else {
41 | key(item)
42 | }
43 | }
44 | ) { index ->
45 | items[index]?.let {
46 | itemContent(it)
47 | }
48 | }
49 | }
50 |
51 | @SuppressLint("BanParcelableUsage")
52 | private data class PagingPlaceholderKey(private val index: Int) : Parcelable {
53 | override fun writeToParcel(parcel: Parcel, flags: Int) {
54 | parcel.writeInt(index)
55 | }
56 |
57 | override fun describeContents(): Int {
58 | return 0
59 | }
60 |
61 | companion object {
62 | @Suppress("unused")
63 | @JvmField
64 | val CREATOR: Parcelable.Creator =
65 | object : Parcelable.Creator {
66 | override fun createFromParcel(parcel: Parcel) =
67 | PagingPlaceholderKey(parcel.readInt())
68 |
69 | override fun newArray(size: Int) = arrayOfNulls(size)
70 | }
71 | }
72 | }
73 |
74 | fun LazyListScope.loadMoreView(pagingItems: LazyPagingItems) {
75 | when (pagingItems.loadState.append) {
76 | is LoadState.Loading -> item {
77 | Box(contentAlignment = Alignment.Center,
78 | modifier = Modifier.fillMaxWidth()) {
79 | CircularProgressIndicator()
80 | }
81 | }
82 | is LoadState.Error -> item {
83 | Row(horizontalArrangement = Arrangement.Center,
84 | verticalAlignment = Alignment.CenterVertically,
85 | modifier = Modifier
86 | .fillMaxWidth()
87 | .height(40.dp)
88 | .clickable {
89 | pagingItems.retry()
90 | }) {
91 | Text(stringResource(id = R.string.loading_error))
92 | Icon(imageVector = Icons.Outlined.Sync,
93 | contentDescription = null,
94 | modifier = Modifier.padding(horizontal = 5.dp))
95 | }
96 | }
97 | else -> {
98 |
99 | }
100 | }
101 | }
102 |
103 | fun LazyStaggeredGridScope.loadMoreView(pagingItems: LazyPagingItems) {
104 | when (pagingItems.loadState.append) {
105 | is LoadState.Loading -> item {
106 | Box(contentAlignment = Alignment.Center,
107 | modifier = Modifier.fillMaxWidth()) {
108 | CircularProgressIndicator()
109 | }
110 | }
111 | is LoadState.Error -> item {
112 | Row(horizontalArrangement = Arrangement.Center,
113 | verticalAlignment = Alignment.CenterVertically,
114 | modifier = Modifier
115 | .fillMaxWidth()
116 | .height(40.dp)
117 | .clickable {
118 | pagingItems.retry()
119 | }) {
120 | Text(stringResource(id = R.string.loading_error))
121 | Icon(imageVector = Icons.Outlined.Sync,
122 | contentDescription = null,
123 | modifier = Modifier.padding(horizontal = 5.dp))
124 | }
125 | }
126 | else -> {
127 |
128 | }
129 | }
130 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/fmt/compose/eyepetizer/ext/PxExt.kt:
--------------------------------------------------------------------------------
1 | package com.fmt.compose.eyepetizer.ext
2 |
3 | import com.fmt.compose.eyepetizer.mainApplication
4 |
5 | fun Int.toPx(): Int = (this * mainApplication.resources.displayMetrics.density + 0.5f).toInt()
--------------------------------------------------------------------------------
/app/src/main/java/com/fmt/compose/eyepetizer/ext/ToastExt.kt:
--------------------------------------------------------------------------------
1 | package com.fmt.compose.eyepetizer.ext
2 |
3 | import android.widget.Toast
4 | import com.fmt.compose.eyepetizer.mainApplication
5 |
6 | fun errorToast(errorMsg: String) {
7 | Toast.makeText(mainApplication, errorMsg, Toast.LENGTH_LONG).show()
8 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/fmt/compose/eyepetizer/http/ApiService.kt:
--------------------------------------------------------------------------------
1 | package com.fmt.compose.eyepetizer.http
2 |
3 | import retrofit2.Retrofit
4 | import retrofit2.converter.gson.GsonConverterFactory
5 |
6 | const val BASE_URL = "http://baobab.kaiyanapp.com/api/"
7 |
8 | private val retrofit by lazy {
9 | Retrofit.Builder()
10 | .baseUrl(
11 | BASE_URL
12 | )
13 | .addConverterFactory(GsonConverterFactory.create())
14 | .build()
15 | }
16 |
17 | object ApiService : IApi by retrofit.create(
18 | IApi::
19 | class.java
20 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/fmt/compose/eyepetizer/http/IApi.kt:
--------------------------------------------------------------------------------
1 | package com.fmt.compose.eyepetizer.http
2 |
3 | import com.fmt.compose.eyepetizer.model.*
4 | import retrofit2.http.GET
5 | import retrofit2.http.Path
6 | import retrofit2.http.Query
7 | import retrofit2.http.Url
8 |
9 | interface IApi {
10 |
11 | @GET("v2/feed?num=0")
12 | suspend fun getDaily(): Daily
13 |
14 | @GET
15 | suspend fun getDaily(@Url nextPageUrl: String): Daily
16 |
17 | @GET("v4/tabs/follow")
18 | suspend fun getFollowList(): Follow
19 |
20 | @GET
21 | suspend fun getFollowList(@Url url: String): Follow
22 |
23 | @GET("v4/rankList")
24 | suspend fun getHotTabList(): TabListInfo
25 |
26 | @GET
27 | suspend fun getHotTabData(@Url url: String): Issue
28 |
29 | @GET("v4/categories")
30 | suspend fun getCategoryList(): List
31 |
32 | @GET
33 | suspend fun getCategoryVideoList(@Url url: String): Issue
34 |
35 | @GET("v3/specialTopics")
36 | suspend fun getTopicList(): SpecialTopics
37 |
38 | @GET
39 | suspend fun getTopicList(@Url nextPageUrl: String): SpecialTopics
40 |
41 | @GET("v7/information/list?vc=6030000&deviceModel=Android")
42 | suspend fun getNewsList(): News
43 |
44 | @GET
45 | suspend fun getNewsList(@Url url: String): News
46 |
47 | @GET("v7/community/tab/rec")
48 | suspend fun getRecommendList(): Recommend
49 |
50 | @GET
51 | suspend fun getRecommendList(@Url url: String): Recommend
52 |
53 | @GET("v4/video/related")
54 | suspend fun getRelateVideoList(@Query("id") id: Int): Issue
55 |
56 | @GET("v3/lightTopics/internal/{topicId}")
57 | suspend fun getTopicDetail(@Path("topicId") topicId: Int): TopicDetail
58 |
59 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/fmt/compose/eyepetizer/model/BaseApiResult.kt:
--------------------------------------------------------------------------------
1 | package com.fmt.compose.eyepetizer.model
2 |
3 | open class BaseApiResult(
4 | val itemList: MutableList = mutableListOf(),
5 | val nextPageUrl: String? = null,
6 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/fmt/compose/eyepetizer/model/Category.kt:
--------------------------------------------------------------------------------
1 | package com.fmt.compose.eyepetizer.model
2 |
3 | data class Category(
4 | val id: Int,
5 | val name: String,
6 | val description: String,
7 | val bgPicture: String,
8 | val headerImage: String,
9 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/fmt/compose/eyepetizer/model/Daily.kt:
--------------------------------------------------------------------------------
1 | package com.fmt.compose.eyepetizer.model
2 |
3 | data class Daily(val issueList: MutableList, val nextPageUrl: String? = null)
--------------------------------------------------------------------------------
/app/src/main/java/com/fmt/compose/eyepetizer/model/Follow.kt:
--------------------------------------------------------------------------------
1 | package com.fmt.compose.eyepetizer.model
2 |
3 | class Follow : BaseApiResult- ()
--------------------------------------------------------------------------------
/app/src/main/java/com/fmt/compose/eyepetizer/model/Issue.kt:
--------------------------------------------------------------------------------
1 | package com.fmt.compose.eyepetizer.model
2 |
3 | class Issue : BaseApiResult
- ()
--------------------------------------------------------------------------------
/app/src/main/java/com/fmt/compose/eyepetizer/model/Item.kt:
--------------------------------------------------------------------------------
1 | package com.fmt.compose.eyepetizer.model
2 |
3 | import android.os.Parcelable
4 | import kotlinx.parcelize.Parcelize
5 |
6 | @Parcelize
7 | data class Item(
8 | val type: String = "",
9 | val data: ItemData? = null,
10 | ) : Parcelable
11 |
12 | @Parcelize
13 | data class ItemData(
14 | val id: Int,
15 | val dataType: String,
16 | val text: String? = "",
17 | val description: String = "",
18 | val title: String,
19 | val category: String,
20 | val author: Author?,
21 | val cover: Cover,
22 | val duration: Int,
23 | val header: Header?,
24 | val itemList: List
- ?,
25 | val width: Int,
26 | val height: Int,
27 | val owner: Owner?,
28 | val consumption: Consumption,
29 | val urls: List
?,
30 | val playUrl: String,
31 | ) : Parcelable
32 |
33 | @Parcelize
34 | data class Header(val id: Int, val icon: String, val title: String, val description: String) :
35 | Parcelable
36 |
37 | @Parcelize
38 | data class Author(
39 | val icon: String,
40 | val name: String,
41 | val description: String,
42 | val latestReleaseTime: Long,
43 | ) : Parcelable
44 |
45 | @Parcelize
46 | data class Cover(
47 | val feed: String,
48 | val blurred: String,
49 | val detail: String,
50 | ) : Parcelable
51 |
52 | @Parcelize
53 | data class Owner(
54 | val avatar: String,
55 | val nickname: String,
56 | ) : Parcelable
57 |
58 | @Parcelize
59 | data class Consumption(
60 | val collectionCount: Int,
61 | val shareCount: Int,
62 | val replyCount: Int,
63 | ) : Parcelable
--------------------------------------------------------------------------------
/app/src/main/java/com/fmt/compose/eyepetizer/model/News.kt:
--------------------------------------------------------------------------------
1 | package com.fmt.compose.eyepetizer.model
2 |
3 | class News : BaseApiResult()
4 |
5 | data class NewsItem(val type: String, val data: NewsItemData)
6 |
7 | data class NewsItemData(
8 | val text: String = "", val titleList: List? = null,
9 | val backgroundImage: String = "", val actionUrl: String = "",
10 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/fmt/compose/eyepetizer/model/Recommend.kt:
--------------------------------------------------------------------------------
1 | package com.fmt.compose.eyepetizer.model
2 |
3 | class Recommend : BaseApiResult()
4 |
5 | data class RecommendItem(val type: String, val data: RecommendItemData)
6 |
7 | data class RecommendItemData(val content: Item)
8 |
--------------------------------------------------------------------------------
/app/src/main/java/com/fmt/compose/eyepetizer/model/TabListInfo.kt:
--------------------------------------------------------------------------------
1 | package com.fmt.compose.eyepetizer.model
2 |
3 | data class TabListInfo(val tabInfo: TabInfo)
4 |
5 | data class TabInfo(val tabList: List)
6 |
7 | data class Tab(val name: String, val apiUrl: String)
--------------------------------------------------------------------------------
/app/src/main/java/com/fmt/compose/eyepetizer/model/Topic.kt:
--------------------------------------------------------------------------------
1 | package com.fmt.compose.eyepetizer.model
2 |
3 | class SpecialTopics : BaseApiResult()
4 |
5 | data class Topic(val data: TopicData)
6 |
7 | data class TopicData(val id: Int, val image: String, val actionUrl: String)
--------------------------------------------------------------------------------
/app/src/main/java/com/fmt/compose/eyepetizer/model/TopicDetail.kt:
--------------------------------------------------------------------------------
1 | package com.fmt.compose.eyepetizer.model
2 |
3 | data class TopicDetail(
4 | val headerImage: String = "",
5 | val brief: String = "",
6 | val text: String = "",
7 | val itemList: List = listOf(),
8 | )
9 |
10 | data class TopicItem(val data: TopicItemData)
11 |
12 | data class TopicItemData(val content: Item)
--------------------------------------------------------------------------------
/app/src/main/java/com/fmt/compose/eyepetizer/pages/daily/DailyPage.kt:
--------------------------------------------------------------------------------
1 | @file:OptIn(ExperimentalFoundationApi::class, ExperimentalMaterialApi::class)
2 |
3 | package com.fmt.compose.eyepetizer.pages.daily
4 |
5 | import androidx.compose.foundation.ExperimentalFoundationApi
6 | import androidx.compose.foundation.Image
7 | import androidx.compose.foundation.background
8 | import androidx.compose.foundation.clickable
9 | import androidx.compose.foundation.layout.*
10 | import androidx.compose.foundation.lazy.LazyColumn
11 | import androidx.compose.foundation.pager.HorizontalPager
12 | import androidx.compose.foundation.pager.rememberPagerState
13 | import androidx.compose.foundation.shape.CircleShape
14 | import androidx.compose.foundation.shape.RoundedCornerShape
15 | import androidx.compose.material.*
16 | import androidx.compose.material.icons.Icons
17 | import androidx.compose.material.icons.filled.Search
18 | import androidx.compose.material.icons.filled.Share
19 | import androidx.compose.material.pullrefresh.PullRefreshIndicator
20 | import androidx.compose.material.pullrefresh.pullRefresh
21 | import androidx.compose.material.pullrefresh.rememberPullRefreshState
22 | import androidx.compose.runtime.Composable
23 | import androidx.compose.runtime.DisposableEffect
24 | import androidx.compose.runtime.rememberCoroutineScope
25 | import androidx.compose.ui.Alignment
26 | import androidx.compose.ui.Modifier
27 | import androidx.compose.ui.draw.clip
28 | import androidx.compose.ui.graphics.Color
29 | import androidx.compose.ui.graphics.graphicsLayer
30 | import androidx.compose.ui.layout.ContentScale
31 | import androidx.compose.ui.platform.LocalContext
32 | import androidx.compose.ui.res.stringResource
33 | import androidx.compose.ui.text.font.FontWeight
34 | import androidx.compose.ui.text.style.TextAlign
35 | import androidx.compose.ui.text.style.TextOverflow
36 | import androidx.compose.ui.unit.dp
37 | import androidx.compose.ui.unit.sp
38 | import androidx.compose.ui.zIndex
39 | import androidx.paging.compose.collectAsLazyPagingItems
40 | import androidx.paging.compose.itemsIndexed
41 | import coil.compose.rememberAsyncImagePainter
42 | import com.fmt.compose.eyepetizer.R
43 | import com.fmt.compose.eyepetizer.ext.loadMoreView
44 | import com.fmt.compose.eyepetizer.model.Item
45 | import com.fmt.compose.eyepetizer.model.ItemData
46 | import com.fmt.compose.eyepetizer.pages.daily.viewmodel.DailyViewModel
47 | import com.fmt.compose.eyepetizer.pages.detail.VideoDetailActivity
48 | import com.fmt.compose.eyepetizer.ui.theme.*
49 | import com.fmt.compose.eyepetizer.util.DateUtils
50 | import kotlinx.coroutines.launch
51 | import java.util.*
52 |
53 | const val TEXT_HEADER_TYPE = "textHeader"
54 |
55 | @Composable
56 | fun DailyPage(viewModel: DailyViewModel = androidx.lifecycle.viewmodel.compose.viewModel()) {
57 | val lazyPagingItems = viewModel.pageFlow.collectAsLazyPagingItems()
58 | val pullRefreshState =
59 | rememberPullRefreshState(refreshing = viewModel.refreshing.value, onRefresh = {
60 | lazyPagingItems.refresh()
61 | })
62 |
63 | Column(Modifier.fillMaxSize()) {
64 | TitleBarWidget()
65 | Box(modifier = Modifier
66 | .pullRefresh(pullRefreshState)
67 | .zIndex(-1f)) {
68 | LazyColumn(Modifier.fillMaxSize()) {
69 | itemsIndexed(lazyPagingItems) { index, pagingItem ->
70 | if (index == 0) {
71 | SwiperWidget(viewModel.bannerList)
72 | } else if (pagingItem?.type == TEXT_HEADER_TYPE) {
73 | TitleItemWidget(pagingItem.data!!)
74 | } else {
75 | RankItemWidget(pagingItem?.data!!)
76 | }
77 | }
78 | loadMoreView(lazyPagingItems)
79 | }
80 |
81 | PullRefreshIndicator(refreshing = viewModel.refreshing.value,
82 | state = pullRefreshState,
83 | modifier = Modifier
84 | .align(Alignment.TopCenter)
85 | .graphicsLayer {
86 | translationY = -160f
87 | })
88 | }
89 | }
90 | }
91 |
92 | @Composable
93 | internal fun TitleBarWidget() {
94 | TopAppBar(title = {
95 | Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxWidth()) {
96 | Text(text = stringResource(id = R.string.daily_paper),
97 | fontSize = 18.sp,
98 | fontWeight = FontWeight.Bold,
99 | color = Color.Black,
100 | textAlign = TextAlign.Center)
101 | }
102 | }, actions = {
103 | IconButton(onClick = { }) {
104 | Icon(imageVector = Icons.Default.Search, contentDescription = null, tint = Black_87)
105 | }
106 | }, backgroundColor = Color.White)
107 | }
108 |
109 | @Composable
110 | fun SwiperWidget(banners: List- ) {
111 | val virtualCount = Int.MAX_VALUE
112 | val actualCount = banners.size
113 | val initialIndex = virtualCount / 2
114 |
115 | val pagerState = rememberPagerState(initialPage = initialIndex)
116 | val coroutineScope = rememberCoroutineScope()
117 | val context = LocalContext.current
118 |
119 | Box(modifier = Modifier
120 | .padding(15.dp)
121 | .fillMaxWidth()
122 | .clip(RoundedCornerShape(4.dp))) {
123 |
124 | HorizontalPager(
125 | pageCount = virtualCount,
126 | state = pagerState,
127 | ) { index ->
128 | val actualIndex = (index - initialIndex).floorMod(actualCount)
129 | Image(painter = rememberAsyncImagePainter(model = banners[actualIndex].data!!.cover.feed),
130 | contentDescription = null,
131 | Modifier
132 | .fillMaxWidth()
133 | .aspectRatio(7 / 3f)
134 | .clickable {
135 | VideoDetailActivity.start(context, banners[actualIndex].data!!)
136 | },
137 | contentScale = ContentScale.Crop)
138 |
139 | DisposableEffect(Unit) {
140 | val timer = Timer()
141 | timer.schedule(object : TimerTask() {
142 | override fun run() {
143 | coroutineScope.launch {
144 | pagerState.animateScrollToPage(pagerState.currentPage + 1)
145 | }
146 | }
147 | }, 3000, 3000)
148 | onDispose {
149 | timer.cancel()
150 | }
151 | }
152 | }
153 |
154 | Row(Modifier
155 | .height(30.dp)
156 | .fillMaxWidth()
157 | .background(Black_12)
158 | .padding(start = 10.dp, end = 10.dp)
159 | .align(Alignment.BottomCenter),
160 | horizontalArrangement = Arrangement.SpaceBetween,
161 | verticalAlignment = Alignment.CenterVertically) {
162 | val actualIndex = (pagerState.currentPage - initialIndex).floorMod(actualCount)
163 | Text(text = banners[actualIndex].data!!.title, fontSize = 12.sp, color = Color.White)
164 | Row(horizontalArrangement = Arrangement.Center) {
165 | repeat(banners.size) { iteration ->
166 | val color = if (actualIndex == iteration) Color.White else White_12
167 | Box(modifier = Modifier
168 | .padding(2.dp)
169 | .clip(CircleShape)
170 | .background(color)
171 | .size(8.dp))
172 | }
173 | }
174 | }
175 | }
176 | }
177 |
178 | @Composable
179 | fun TitleItemWidget(itemData: ItemData) {
180 | Box(modifier = Modifier
181 | .fillMaxWidth()
182 | .padding(top = 5.dp, bottom = 5.dp),
183 | contentAlignment = Alignment.Center) {
184 | Text(text = itemData.text ?: "",
185 | color = Black_87,
186 | fontSize = 18.sp,
187 | fontWeight = FontWeight.Bold)
188 | }
189 | }
190 |
191 | @Composable
192 | fun RankItemWidget(itemData: ItemData) {
193 | val context = LocalContext.current
194 | Column(modifier = Modifier
195 | .padding(start = 15.dp, top = 5.dp, end = 15.dp, bottom = 10.dp)
196 | .clickable {
197 | VideoDetailActivity.start(context, itemData)
198 | }) {
199 | Box {
200 | Image(painter = rememberAsyncImagePainter(model = itemData.cover.feed),
201 | contentDescription = null, modifier = Modifier
202 | .fillMaxWidth()
203 | .height(200.dp)
204 | .clip(
205 | RoundedCornerShape(4.dp)), contentScale = ContentScale.Crop)
206 | Box(modifier = Modifier
207 | .padding(start = 15.dp, top = 10.dp)
208 | .background(White_54, shape = CircleShape)
209 | .size(44.dp), contentAlignment = Alignment.Center) {
210 | Text(text = itemData.category, color = Color.White)
211 | }
212 | Box(modifier = Modifier
213 | .padding(end = 15.dp, bottom = 10.dp)
214 | .background(Black_54, shape = RoundedCornerShape(5.dp))
215 | .padding(5.dp)
216 | .align(Alignment.BottomEnd), contentAlignment = Alignment.Center) {
217 | Text(text = DateUtils.formatDateMsByMS((itemData.duration * 1000).toLong()),
218 | color = Color.White,
219 | fontWeight = FontWeight.Bold)
220 | }
221 | }
222 | Row(modifier = Modifier.padding(top = 10.dp),
223 | horizontalArrangement = Arrangement.Center,
224 | verticalAlignment = Alignment.CenterVertically) {
225 | Image(painter = rememberAsyncImagePainter(model = itemData.author?.icon),
226 | contentDescription = null, modifier = Modifier
227 | .size(40.dp)
228 | .clip(CircleShape))
229 | Column(modifier = Modifier
230 | .weight(1f)
231 | .padding(start = 10.dp)) {
232 | Text(text = itemData.title,
233 | color = Color.Black,
234 | fontSize = 14.sp,
235 | fontWeight = FontWeight.Bold)
236 | Text(text = itemData.author?.name ?: "",
237 | fontSize = 12.sp,
238 | color = UnselectedItemColor,
239 | modifier = Modifier.padding(top = 2.dp),
240 | overflow = TextOverflow.Ellipsis,
241 | maxLines = 2)
242 | }
243 | IconButton(onClick = { }) {
244 | Icon(imageVector = Icons.Default.Share, contentDescription = null, tint = Black_38)
245 | }
246 | }
247 | Divider(thickness = 0.5.dp, modifier = Modifier.padding(top = 5.dp))
248 | }
249 | }
250 |
251 | fun Int.floorMod(other: Int): Int = when (other) {
252 | 0 -> this
253 | else -> this - floorDiv(other = other) * other
254 | }
255 |
256 |
--------------------------------------------------------------------------------
/app/src/main/java/com/fmt/compose/eyepetizer/pages/daily/SearchVideoPage.kt:
--------------------------------------------------------------------------------
1 | package com.fmt.compose.eyepetizer.pages.daily
2 |
3 |
--------------------------------------------------------------------------------
/app/src/main/java/com/fmt/compose/eyepetizer/pages/daily/repository/DailyPagingSource.kt:
--------------------------------------------------------------------------------
1 | package com.fmt.compose.eyepetizer.pages.daily.repository
2 |
3 | import androidx.compose.runtime.MutableState
4 | import androidx.paging.PagingSource
5 | import androidx.paging.PagingState
6 | import com.fmt.compose.eyepetizer.ext.errorToast
7 | import com.fmt.compose.eyepetizer.http.ApiService
8 | import com.fmt.compose.eyepetizer.model.Item
9 | import kotlinx.coroutines.delay
10 |
11 | class DailyPagingSource(
12 | private val bannerList: MutableList
- ,
13 | private var refreshing: MutableState
,
14 | ) : PagingSource() {
15 |
16 | override fun getRefreshKey(state: PagingState): String? = null
17 |
18 | override suspend fun load(params: LoadParams): LoadResult {
19 | return try {
20 | val pageKey: String? = params.key
21 | var nextKey: String? = null
22 | val nextPageUrl: String?
23 | val daily = if (pageKey.isNullOrEmpty()) {
24 | refreshing.value = true
25 | ApiService.getDaily()
26 | } else {
27 | ApiService.getDaily(pageKey)
28 | }
29 | nextPageUrl = daily.nextPageUrl
30 | val itemList = daily.issueList[0].itemList
31 | itemList.removeAll {
32 | it.type == "banner2"
33 | }
34 | if (pageKey.isNullOrEmpty()) {
35 | bannerList.clear()
36 | bannerList.addAll(itemList)
37 | }
38 | if (!nextPageUrl.isNullOrEmpty()) {
39 | nextKey = nextPageUrl
40 | }
41 | refreshing.value = false
42 | LoadResult.Page(if (pageKey.isNullOrEmpty()) listOf(Item()) else itemList,
43 | null,
44 | nextKey)
45 | } catch (e: Exception) {
46 | delay(200)
47 | refreshing.value = false
48 | errorToast(e.message ?: "")
49 | LoadResult.Error(e)
50 | }
51 | }
52 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/fmt/compose/eyepetizer/pages/daily/viewmodel/DailyViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.fmt.compose.eyepetizer.pages.daily.viewmodel
2 |
3 | import androidx.compose.runtime.*
4 | import androidx.lifecycle.ViewModel
5 | import androidx.lifecycle.viewModelScope
6 | import androidx.paging.Pager
7 | import androidx.paging.PagingConfig
8 | import androidx.paging.cachedIn
9 | import com.fmt.compose.eyepetizer.model.Item
10 | import com.fmt.compose.eyepetizer.pages.daily.repository.DailyPagingSource
11 |
12 | class DailyViewModel : ViewModel() {
13 |
14 | var bannerList = mutableStateListOf- ()
15 | var refreshing = mutableStateOf(false)
16 |
17 | val pageFlow = Pager(config = PagingConfig(pageSize = 10,
18 | initialLoadSize = 10,
19 | prefetchDistance = 1), pagingSourceFactory = {
20 | DailyPagingSource(bannerList, refreshing)
21 | }).flow.cachedIn(viewModelScope)
22 |
23 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/fmt/compose/eyepetizer/pages/detail/NewsDetailActivity.kt:
--------------------------------------------------------------------------------
1 | package com.fmt.compose.eyepetizer.pages.detail
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.os.Bundle
6 | import android.view.ViewGroup
7 | import android.webkit.WebSettings
8 | import android.webkit.WebView
9 | import android.webkit.WebViewClient
10 | import androidx.activity.ComponentActivity
11 | import androidx.activity.compose.setContent
12 | import androidx.compose.foundation.layout.Column
13 | import androidx.compose.foundation.layout.fillMaxSize
14 | import androidx.compose.ui.Modifier
15 | import androidx.compose.ui.viewinterop.AndroidView
16 | import androidx.core.view.WindowInsetsControllerCompat
17 | import com.fmt.compose.eyepetizer.view.TopTitleAppBar
18 |
19 | class NewsDetailActivity : ComponentActivity() {
20 | override fun onCreate(savedInstanceState: Bundle?) {
21 | super.onCreate(savedInstanceState)
22 | window.statusBarColor = android.graphics.Color.WHITE
23 | WindowInsetsControllerCompat(window, window.decorView).apply {
24 | isAppearanceLightStatusBars = true
25 | }
26 | val webUrl = intent.getStringExtra(KEY_URL) ?: ""
27 | val title = intent.getStringExtra(KEY_TITLE) ?: ""
28 | setContent {
29 | Column(Modifier.fillMaxSize()) {
30 | TopTitleAppBar(title = title) {
31 | finish()
32 | }
33 | AndroidView(factory = {
34 | WebView(this@NewsDetailActivity).apply {
35 | layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
36 | ViewGroup.LayoutParams.MATCH_PARENT)
37 | webViewClient = WebViewClient()
38 | settings.javaScriptEnabled = true
39 | settings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
40 | loadUrl(webUrl)
41 | }
42 | })
43 | }
44 | }
45 | }
46 |
47 | companion object {
48 | const val KEY_URL = "url"
49 | const val KEY_TITLE = "title"
50 | fun start(context: Context, url: String, title: String) {
51 | val intent = Intent(context, NewsDetailActivity::class.java)
52 | intent.putExtra(KEY_URL, url)
53 | intent.putExtra(KEY_TITLE, title)
54 | context.startActivity(intent)
55 | }
56 | }
57 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/fmt/compose/eyepetizer/pages/detail/TopicDetailActivity.kt:
--------------------------------------------------------------------------------
1 | package com.fmt.compose.eyepetizer.pages.detail
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.os.Bundle
6 | import androidx.activity.ComponentActivity
7 | import androidx.activity.compose.setContent
8 | import androidx.core.view.WindowInsetsControllerCompat
9 |
10 | class TopicDetailActivity : ComponentActivity() {
11 |
12 | override fun onCreate(savedInstanceState: Bundle?) {
13 | super.onCreate(savedInstanceState)
14 | window.statusBarColor = android.graphics.Color.WHITE
15 | WindowInsetsControllerCompat(window, window.decorView).apply {
16 | isAppearanceLightStatusBars = true
17 | }
18 | val topicId = intent.getIntExtra(KEY_TOPIC_ID, 0)
19 | setContent {
20 | TopicDetailPage(topicId)
21 | }
22 | }
23 |
24 | companion object {
25 | private const val KEY_TOPIC_ID = "topicId"
26 | fun start(context: Context, topicId: Int) {
27 | val intent = Intent(context, TopicDetailActivity::class.java)
28 | intent.putExtra(KEY_TOPIC_ID, topicId)
29 | context.startActivity(intent)
30 | }
31 | }
32 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/fmt/compose/eyepetizer/pages/detail/TopicDetailPage.kt:
--------------------------------------------------------------------------------
1 | @file:OptIn(ExperimentalMaterialApi::class)
2 |
3 | package com.fmt.compose.eyepetizer.pages.detail
4 |
5 | import android.app.Activity
6 | import androidx.compose.foundation.Image
7 | import androidx.compose.foundation.background
8 | import androidx.compose.foundation.border
9 | import androidx.compose.foundation.layout.*
10 | import androidx.compose.foundation.lazy.LazyColumn
11 | import androidx.compose.foundation.lazy.items
12 | import androidx.compose.foundation.shape.RoundedCornerShape
13 | import androidx.compose.material.*
14 | import androidx.compose.material.pullrefresh.PullRefreshIndicator
15 | import androidx.compose.material.pullrefresh.pullRefresh
16 | import androidx.compose.material.pullrefresh.rememberPullRefreshState
17 | import androidx.compose.runtime.Composable
18 | import androidx.compose.runtime.LaunchedEffect
19 | import androidx.compose.ui.Alignment
20 | import androidx.compose.ui.Modifier
21 | import androidx.compose.ui.draw.clip
22 | import androidx.compose.ui.graphics.Color
23 | import androidx.compose.ui.layout.ContentScale
24 | import androidx.compose.ui.platform.LocalContext
25 | import androidx.compose.ui.text.font.FontWeight
26 | import androidx.compose.ui.unit.dp
27 | import androidx.compose.ui.unit.sp
28 | import androidx.compose.ui.zIndex
29 | import androidx.lifecycle.viewmodel.compose.viewModel
30 | import coil.compose.rememberAsyncImagePainter
31 | import com.fmt.compose.eyepetizer.model.TopicDetail
32 | import com.fmt.compose.eyepetizer.pages.daily.RankItemWidget
33 | import com.fmt.compose.eyepetizer.pages.detail.viewmodel.TopicDetailViewModel
34 | import com.fmt.compose.eyepetizer.ui.theme.Black_12
35 | import com.fmt.compose.eyepetizer.ui.theme.Border_bg
36 | import com.fmt.compose.eyepetizer.ui.theme.UnselectedItemColor
37 | import com.fmt.compose.eyepetizer.view.TopTitleAppBar
38 |
39 | @Composable
40 | fun TopicDetailPage(topicId: Int) {
41 | val context = LocalContext.current
42 | val viewModel: TopicDetailViewModel = viewModel()
43 |
44 | LaunchedEffect(Unit) {
45 | viewModel.getTopicDetail(topicId)
46 | }
47 | val pullRefreshState =
48 | rememberPullRefreshState(refreshing = viewModel.refreshing.value, onRefresh = {
49 | viewModel.getTopicDetail(topicId)
50 | })
51 | val topicDetail = viewModel.topicDetail.value
52 | Column(modifier = Modifier
53 | .background(Color.White)
54 | .fillMaxSize()) {
55 | TopTitleAppBar(topicDetail.brief) {
56 | if (context is Activity) {
57 | context.finish()
58 | }
59 | }
60 | Box(Modifier
61 | .fillMaxWidth()
62 | .weight(1f)
63 | .zIndex(-1f)) {
64 | LazyColumn(modifier = Modifier
65 | .fillMaxSize()
66 | .pullRefresh(pullRefreshState)) {
67 | item {
68 | TopicDetailHeadWidget(topicDetail)
69 | }
70 | items(topicDetail.itemList) {
71 | it.data.content.data?.run {
72 | RankItemWidget(itemData = it.data.content.data)
73 | }
74 | }
75 | }
76 | PullRefreshIndicator(refreshing = viewModel.refreshing.value,
77 | state = pullRefreshState,
78 | modifier = Modifier.align(Alignment.TopCenter))
79 | }
80 | }
81 | }
82 |
83 | @Composable
84 | fun TopicDetailHeadWidget(topicDetail: TopicDetail) {
85 | Box(modifier = Modifier
86 | .fillMaxWidth()
87 | .wrapContentHeight()) {
88 | Column(modifier = Modifier.fillMaxWidth()) {
89 | Image(modifier = Modifier
90 | .fillMaxWidth()
91 | .height(250.dp),
92 | painter = rememberAsyncImagePainter(model = topicDetail.headerImage),
93 | contentDescription = null,
94 | contentScale = ContentScale.FillWidth)
95 | Text(modifier = Modifier
96 | .padding(start = 20.dp,
97 | top = 40.dp,
98 | end = 20.dp,
99 | bottom = 10.dp),
100 | text = topicDetail.text,
101 | fontSize = 12.sp,
102 | color = UnselectedItemColor)
103 | Spacer(modifier = Modifier
104 | .fillMaxWidth()
105 | .height(5.dp)
106 | .background(Black_12))
107 | }
108 | Box(modifier = Modifier
109 | .fillMaxWidth()
110 | .padding(start = 20.dp, end = 20.dp, top = 230.dp)
111 | .height(50.dp)
112 | .clip(RoundedCornerShape(4.dp))
113 | .background(Color.White)
114 | .border(1.dp, Border_bg),
115 | contentAlignment = Alignment.Center) {
116 | Text(text = topicDetail.brief,
117 | fontSize = 14.sp,
118 | fontWeight = FontWeight.Bold,
119 | color = Color.Black)
120 | }
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/app/src/main/java/com/fmt/compose/eyepetizer/pages/detail/VideoDetailActivity.kt:
--------------------------------------------------------------------------------
1 | package com.fmt.compose.eyepetizer.pages.detail
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.os.Bundle
6 | import androidx.activity.ComponentActivity
7 | import androidx.activity.compose.setContent
8 | import androidx.core.view.WindowInsetsControllerCompat
9 | import cn.jzvd.Jzvd
10 | import com.fmt.compose.eyepetizer.R
11 | import com.fmt.compose.eyepetizer.model.ItemData
12 |
13 | class VideoDetailActivity : ComponentActivity() {
14 |
15 | override fun onCreate(savedInstanceState: Bundle?) {
16 | super.onCreate(savedInstanceState)
17 | window.statusBarColor = android.graphics.Color.BLACK
18 | WindowInsetsControllerCompat(window, window.decorView).apply {
19 | isAppearanceLightStatusBars = false
20 | }
21 | val itemData = intent.getParcelableExtra
(KEY_ITEM_DATA)
22 | setContent {
23 | VideoDetailPage(itemData)
24 | }
25 | }
26 |
27 | override fun onBackPressed() {
28 | if (Jzvd.backPress()) {
29 | return
30 | }
31 | super.onBackPressed()
32 | }
33 |
34 | override fun onPause() {
35 | super.onPause()
36 | Jzvd.releaseAllVideos()
37 | }
38 |
39 | companion object {
40 | const val KEY_ITEM_DATA = "key_item_data"
41 | fun start(context: Context, itemData: ItemData) {
42 | val intent = Intent(context, VideoDetailActivity::class.java)
43 | intent.putExtra(KEY_ITEM_DATA, itemData)
44 | context.startActivity(intent)
45 | }
46 | }
47 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/fmt/compose/eyepetizer/pages/detail/VideoDetailPage.kt:
--------------------------------------------------------------------------------
1 | @file:OptIn(ExperimentalMaterialApi::class)
2 |
3 | package com.fmt.compose.eyepetizer.pages.detail
4 |
5 | import android.view.ViewGroup
6 | import androidx.compose.foundation.Image
7 | import androidx.compose.foundation.background
8 | import androidx.compose.foundation.clickable
9 | import androidx.compose.foundation.layout.*
10 | import androidx.compose.foundation.lazy.LazyColumn
11 | import androidx.compose.foundation.lazy.items
12 | import androidx.compose.foundation.shape.CircleShape
13 | import androidx.compose.foundation.shape.RoundedCornerShape
14 | import androidx.compose.material.Divider
15 | import androidx.compose.material.ExperimentalMaterialApi
16 | import androidx.compose.material.Icon
17 | import androidx.compose.material.Text
18 | import androidx.compose.material.icons.Icons
19 | import androidx.compose.material.icons.outlined.Comment
20 | import androidx.compose.material.icons.outlined.Favorite
21 | import androidx.compose.material.icons.outlined.Share
22 | import androidx.compose.material.pullrefresh.PullRefreshIndicator
23 | import androidx.compose.material.pullrefresh.pullRefresh
24 | import androidx.compose.material.pullrefresh.rememberPullRefreshState
25 | import androidx.compose.runtime.Composable
26 | import androidx.compose.runtime.LaunchedEffect
27 | import androidx.compose.ui.Alignment
28 | import androidx.compose.ui.Modifier
29 | import androidx.compose.ui.draw.clip
30 | import androidx.compose.ui.graphics.Color
31 | import androidx.compose.ui.layout.ContentScale
32 | import androidx.compose.ui.platform.LocalContext
33 | import androidx.compose.ui.res.stringResource
34 | import androidx.compose.ui.text.font.FontWeight
35 | import androidx.compose.ui.unit.dp
36 | import androidx.compose.ui.unit.sp
37 | import androidx.compose.ui.viewinterop.AndroidView
38 | import androidx.compose.ui.zIndex
39 | import androidx.core.view.isVisible
40 | import androidx.lifecycle.viewmodel.compose.viewModel
41 | import cn.jzvd.JzvdStd
42 | import coil.compose.rememberAsyncImagePainter
43 | import com.fmt.compose.eyepetizer.ext.toPx
44 | import com.fmt.compose.eyepetizer.model.ItemData
45 | import com.fmt.compose.eyepetizer.pages.detail.viewmodel.VideoDetailModel
46 | import com.fmt.compose.eyepetizer.ui.theme.Black_54
47 | import com.fmt.compose.eyepetizer.util.DateUtils
48 | import com.fmt.compose.eyepetizer.util.ScreenUtils
49 |
50 | const val VIDEO_SMALL_CARD_TYPE = "videoSmallCard"
51 |
52 | @Composable
53 | internal fun VideoDetailPage(itemData: ItemData?) {
54 | val context = LocalContext.current
55 | val viewModel: VideoDetailModel = viewModel()
56 | val pullRefreshState =
57 | rememberPullRefreshState(refreshing = viewModel.refreshing.value, onRefresh = {
58 | itemData?.run {
59 | viewModel.getRelateVideoList(itemData.id)
60 | }
61 | })
62 | itemData?.run {
63 | LaunchedEffect(key1 = Unit) {
64 | viewModel.getRelateVideoList(itemData.id)
65 | viewModel.saveVideo(itemData)
66 | }
67 | }
68 | Box(modifier = Modifier.fillMaxSize()) {
69 | itemData?.run {
70 | Image(painter = rememberAsyncImagePainter(model = "${itemData.cover.blurred}/thumbnail/${ScreenUtils.instance.getScreenHeight()}x${ScreenUtils.instance.getScreenWidth()}"),
71 | contentDescription = null,
72 | Modifier.fillMaxSize(),
73 | contentScale = ContentScale.FillHeight)
74 | }
75 | itemData?.run {
76 | Column(modifier = Modifier.fillMaxSize()) {
77 | AndroidView(
78 | factory = {
79 | val jzvdStd = JzvdStd(context)
80 | jzvdStd.layoutParams =
81 | ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 200.toPx())
82 | jzvdStd
83 | }
84 | ) {
85 | it.setUp(itemData.playUrl, itemData.title)
86 | it.startVideoAfterPreloading()
87 | }
88 | Box(modifier = Modifier
89 | .pullRefresh(pullRefreshState)
90 | .zIndex(-1f)) {
91 | LazyColumn {
92 | item {
93 | VideoInfoItemWidget(itemData)
94 | }
95 | items(viewModel.itemList) {
96 | if (it.type == VIDEO_SMALL_CARD_TYPE) {
97 | VideoRelateItemWidget(it.data)
98 | } else {
99 | Text(
100 | text = it.data?.text ?: "",
101 | color = Color.White,
102 | fontSize = 16.sp,
103 | fontWeight = FontWeight.Bold,
104 | modifier = Modifier.padding(10.dp)
105 | )
106 | }
107 | }
108 | }
109 | PullRefreshIndicator(refreshing = viewModel.refreshing.value,
110 | state = pullRefreshState,
111 | modifier = Modifier.align(Alignment.TopCenter))
112 | }
113 | }
114 | }
115 | }
116 | }
117 |
118 | @Composable
119 | fun VideoInfoItemWidget(itemData: ItemData?) {
120 | itemData?.run {
121 | Column(Modifier.fillMaxWidth()) {
122 | Text(text = itemData.title,
123 | color = Color.White,
124 | fontSize = 18.sp,
125 | fontWeight = FontWeight.Bold,
126 | modifier = Modifier.padding(start = 10.dp, top = 10.dp))
127 | Text(text = "${itemData.category} / ${DateUtils.formatDateMsByYMDHM(itemData.author?.latestReleaseTime ?: 0)}",
128 | color = Color.White,
129 | fontSize = 12.sp,
130 | modifier = Modifier.padding(start = 10.dp, top = 10.dp))
131 | Text(text = itemData.description,
132 | color = Color.White,
133 | fontSize = 14.sp,
134 | modifier = Modifier.padding(start = 10.dp, top = 10.dp))
135 | Row(modifier = Modifier.padding(start = 10.dp, top = 10.dp),
136 | verticalAlignment = Alignment.CenterVertically) {
137 | Icon(imageVector = Icons.Outlined.Favorite,
138 | contentDescription = null,
139 | modifier = Modifier.size(14.dp),
140 | tint = Color.White)
141 | Text(text = "${itemData.consumption.collectionCount}",
142 | color = Color.White,
143 | fontSize = 14.sp,
144 | modifier = Modifier.padding(start = 3.dp))
145 | Icon(imageVector = Icons.Outlined.Share,
146 | contentDescription = null,
147 | modifier = Modifier
148 | .padding(start = 30.dp)
149 | .size(14.dp),
150 | tint = Color.White)
151 | Text(text = "${itemData.consumption.shareCount}",
152 | color = Color.White,
153 | fontSize = 14.sp,
154 | modifier = Modifier.padding(start = 3.dp))
155 | Icon(imageVector = Icons.Outlined.Comment,
156 | contentDescription = null,
157 | modifier = Modifier
158 | .padding(start = 30.dp)
159 | .size(14.dp),
160 | tint = Color.White)
161 | Text(text = "${itemData.consumption.replyCount}",
162 | color = Color.White,
163 | fontSize = 14.sp,
164 | modifier = Modifier.padding(start = 3.dp))
165 | }
166 | Divider(thickness = 0.5.dp,
167 | color = Color.White,
168 | modifier = Modifier.padding(top = 10.dp))
169 | Row(modifier = Modifier.fillMaxWidth(),
170 | verticalAlignment = Alignment.CenterVertically) {
171 | Image(painter = rememberAsyncImagePainter(model = itemData.author?.icon),
172 | contentDescription = null,
173 | contentScale = ContentScale.Crop,
174 | modifier = Modifier
175 | .padding(10.dp)
176 | .size(40.dp)
177 | .clip(CircleShape))
178 | Column(modifier = Modifier
179 | .weight(1f)
180 | .padding(vertical = 10.dp)) {
181 | Text(text = itemData.author?.name ?: "",
182 | color = Color.White,
183 | fontSize = 15.sp)
184 | Text(text = itemData.author?.description ?: "",
185 | color = Color.White,
186 | fontSize = 13.sp, modifier = Modifier.padding(top = 3.dp))
187 | }
188 | Text(text = stringResource(id = com.fmt.compose.eyepetizer.R.string.add_follow),
189 | color = Color.Gray,
190 | fontSize = 12.sp,
191 | fontWeight = FontWeight.Bold,
192 | modifier = Modifier
193 | .padding(horizontal = 10.dp)
194 | .clip(RoundedCornerShape(5.dp))
195 | .background(Color.White)
196 | .padding(5.dp))
197 | }
198 | Divider(thickness = 0.5.dp,
199 | color = Color.White)
200 | }
201 | }
202 | }
203 |
204 | @Composable
205 | fun VideoRelateItemWidget(itemData: ItemData?) {
206 | val context = LocalContext.current
207 | itemData?.run {
208 |
209 | Row(modifier = Modifier
210 | .padding(start = 10.dp, top = 10.dp, end = 10.dp, bottom = 5.dp)
211 | .clickable {
212 | VideoDetailActivity.start(context, itemData)
213 | }) {
214 | Box {
215 | Image(painter = rememberAsyncImagePainter(model = itemData.cover.detail),
216 | contentDescription = null, modifier = Modifier
217 | .width(135.dp)
218 | .height(80.dp)
219 | .clip(RoundedCornerShape(5.dp)), contentScale = ContentScale.Crop)
220 | Text(
221 | text = DateUtils.formatDateMsByMS(itemData.duration.toLong() * 1000),
222 | color = Color.White,
223 | fontWeight = FontWeight.Bold,
224 | fontSize = 10.sp,
225 | modifier = Modifier
226 | .padding(end = 5.dp, bottom = 5.dp)
227 | .clip(RoundedCornerShape(5.dp))
228 | .background(Black_54)
229 | .padding(3.dp)
230 | .align(Alignment.BottomEnd)
231 | )
232 | }
233 | Column(modifier = Modifier
234 | .padding(10.dp)
235 | .weight(1f)) {
236 | Text(
237 | text = itemData.title,
238 | color = Color.White,
239 | fontWeight = FontWeight.Bold,
240 | fontSize = 14.sp,
241 | )
242 | Text(
243 | text = "#${itemData.category} / ${itemData.author?.name}",
244 | color = Color.White,
245 | fontSize = 12.sp,
246 | modifier = Modifier
247 | .padding(top = 15.dp)
248 | )
249 | }
250 | }
251 | }
252 | }
253 |
--------------------------------------------------------------------------------
/app/src/main/java/com/fmt/compose/eyepetizer/pages/detail/viewmodel/TopicDetailViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.fmt.compose.eyepetizer.pages.detail.viewmodel
2 |
3 | import androidx.compose.runtime.mutableStateOf
4 | import androidx.lifecycle.ViewModel
5 | import androidx.lifecycle.viewModelScope
6 | import com.fmt.compose.eyepetizer.ext.errorToast
7 | import com.fmt.compose.eyepetizer.http.ApiService
8 | import com.fmt.compose.eyepetizer.model.TopicDetail
9 | import kotlinx.coroutines.delay
10 | import kotlinx.coroutines.launch
11 |
12 | class TopicDetailViewModel : ViewModel() {
13 |
14 | var topicDetail = mutableStateOf(TopicDetail())
15 | var refreshing = mutableStateOf(false)
16 |
17 | fun getTopicDetail(topicId: Int) {
18 | viewModelScope.launch {
19 | runCatching {
20 | refreshing.value = true
21 | topicDetail.value = ApiService.getTopicDetail(topicId)
22 | refreshing.value = false
23 | }.onFailure {
24 | errorToast(it.message ?: "")
25 | delay(200)
26 | refreshing.value = false
27 | }
28 | }
29 | }
30 |
31 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/fmt/compose/eyepetizer/pages/detail/viewmodel/VideoDetailModel.kt:
--------------------------------------------------------------------------------
1 | package com.fmt.compose.eyepetizer.pages.detail.viewmodel
2 |
3 | import androidx.compose.runtime.mutableStateListOf
4 | import androidx.compose.runtime.mutableStateOf
5 | import androidx.lifecycle.ViewModel
6 | import androidx.lifecycle.viewModelScope
7 | import com.fmt.compose.eyepetizer.db.CacheManager
8 | import com.fmt.compose.eyepetizer.db.Video
9 | import com.fmt.compose.eyepetizer.ext.errorToast
10 | import com.fmt.compose.eyepetizer.ext.toJson
11 | import com.fmt.compose.eyepetizer.http.ApiService
12 | import com.fmt.compose.eyepetizer.model.Item
13 | import com.fmt.compose.eyepetizer.model.ItemData
14 | import kotlinx.coroutines.delay
15 | import kotlinx.coroutines.launch
16 |
17 | class VideoDetailModel : ViewModel() {
18 |
19 | val itemList = mutableStateListOf- ()
20 | var refreshing = mutableStateOf(false)
21 |
22 | fun getRelateVideoList(videoId: Int) {
23 | viewModelScope.launch {
24 | runCatching {
25 | refreshing.value = true
26 | val items = ApiService.getRelateVideoList(videoId).itemList
27 | if (items.isNotEmpty()) {
28 | itemList.clear()
29 | itemList.addAll(items)
30 | }
31 | refreshing.value = false
32 | }.onFailure {
33 | errorToast(it.message ?: "")
34 | delay(200)
35 | refreshing.value = false
36 | }
37 | }
38 | }
39 |
40 | fun saveVideo(itemData: ItemData) {
41 | viewModelScope.launch {
42 | runCatching {
43 | val video = CacheManager.get().videoDao.getVideo(itemData.id)
44 | if (video == null) {
45 | CacheManager.get().videoDao.save(Video(itemData.id, toJson(itemData)))
46 | }
47 | }
48 | }
49 | }
50 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/fmt/compose/eyepetizer/pages/discover/CategoryDetailActivity.kt:
--------------------------------------------------------------------------------
1 | package com.fmt.compose.eyepetizer.pages.discover
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.graphics.Color
6 | import android.os.Bundle
7 | import androidx.activity.ComponentActivity
8 | import androidx.activity.compose.setContent
9 | import androidx.core.view.WindowCompat
10 |
11 | class CategoryDetailActivity : ComponentActivity() {
12 |
13 | val defaultBg =
14 | "http://ali-img.kaiyanapp.com/bcf6da5da1363e140ddbd536147f9931.jpeg?imageMogr2/quality/60/format/jpg"
15 |
16 | override fun onCreate(savedInstanceState: Bundle?) {
17 | super.onCreate(savedInstanceState)
18 | // 沉浸式状态栏
19 | window.statusBarColor = Color.TRANSPARENT
20 | WindowCompat.setDecorFitsSystemWindows(window, false)
21 | val categoryId = intent.getIntExtra(KEY_CATEGORY_ID, 0)
22 | val categoryName = intent.getStringExtra(KEY_CATEGORY_NAME) ?: ""
23 | val headImage = intent.getStringExtra(KEY_HEAD_IMAGE) ?: ""
24 | setContent {
25 | CategoryDetailPage(categoryId, categoryName, headImage)
26 | }
27 | }
28 |
29 | companion object {
30 | private const val KEY_CATEGORY_ID = "categoryId"
31 | const val KEY_CATEGORY_NAME = "categoryName"
32 | const val KEY_HEAD_IMAGE = "headImage"
33 | fun start(context: Context, categoryId: Int, categoryName: String, headImage: String) {
34 | val intent = Intent(context, CategoryDetailActivity::class.java)
35 | intent.putExtra(KEY_CATEGORY_ID, categoryId)
36 | intent.putExtra(KEY_CATEGORY_NAME, categoryName)
37 | intent.putExtra(KEY_HEAD_IMAGE, headImage)
38 | context.startActivity(intent)
39 | }
40 | }
41 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/fmt/compose/eyepetizer/pages/discover/CategoryDetailPage.kt:
--------------------------------------------------------------------------------
1 | package com.fmt.compose.eyepetizer.pages.discover
2 |
3 | import android.app.Activity
4 | import androidx.compose.foundation.Image
5 | import androidx.compose.foundation.background
6 | import androidx.compose.foundation.layout.*
7 | import androidx.compose.foundation.lazy.LazyColumn
8 | import androidx.compose.material.Icon
9 | import androidx.compose.material.IconButton
10 | import androidx.compose.material.MaterialTheme
11 | import androidx.compose.material.Text
12 | import androidx.compose.material.icons.Icons
13 | import androidx.compose.material.icons.outlined.ArrowBack
14 | import androidx.compose.runtime.Composable
15 | import androidx.compose.ui.Alignment
16 | import androidx.compose.ui.Modifier
17 | import androidx.compose.ui.graphics.Color
18 | import androidx.compose.ui.graphics.graphicsLayer
19 | import androidx.compose.ui.layout.ContentScale
20 | import androidx.compose.ui.platform.LocalContext
21 | import androidx.compose.ui.unit.dp
22 | import androidx.compose.ui.unit.sp
23 | import androidx.lifecycle.viewmodel.compose.viewModel
24 | import androidx.paging.compose.collectAsLazyPagingItems
25 | import androidx.paging.compose.items
26 | import coil.compose.rememberAsyncImagePainter
27 | import com.fmt.compose.eyepetizer.pages.daily.RankItemWidget
28 | import com.fmt.compose.eyepetizer.pages.discover.viewmodel.CategoryDetailViewModel
29 | import me.onebone.toolbar.CollapsingToolbarScaffold
30 | import me.onebone.toolbar.ScrollStrategy
31 | import me.onebone.toolbar.rememberCollapsingToolbarScaffoldState
32 |
33 | @Composable
34 | internal fun CategoryDetailPage(categoryId: Int, categoryName: String, headImage: String) {
35 | val viewModel: CategoryDetailViewModel = viewModel()
36 | viewModel.categoryId = categoryId
37 | val lazyPagingItems = viewModel.pageFlow.collectAsLazyPagingItems()
38 | val context = LocalContext.current
39 | val state = rememberCollapsingToolbarScaffoldState()
40 | CollapsingToolbarScaffold(
41 | modifier = Modifier
42 | .fillMaxSize(),
43 | state = state,
44 | scrollStrategy = ScrollStrategy.ExitUntilCollapsed,
45 | toolbarModifier = Modifier.background(MaterialTheme.colors.primary),
46 | toolbar = {
47 | Image(
48 | painter =
49 | rememberAsyncImagePainter(model = headImage),
50 | modifier = Modifier
51 | .parallax(0.5f)
52 | .fillMaxWidth()
53 | .height(200.dp)
54 | .graphicsLayer {
55 | alpha = state.toolbarState.progress
56 | },
57 | contentScale = ContentScale.Crop,
58 | contentDescription = null
59 | )
60 | Row(
61 | modifier = Modifier
62 | .statusBarsPadding()
63 | .fillMaxWidth()
64 | .height(50.dp)
65 | .pin(),
66 | ) {
67 | IconButton(modifier = Modifier.align(Alignment.CenterVertically),
68 | onClick = {
69 | if (context is Activity) {
70 | context.finish()
71 | }
72 | }) {
73 | Icon(imageVector = Icons.Outlined.ArrowBack,
74 | tint = Color.White,
75 | contentDescription = null)
76 | }
77 | }
78 | Text(
79 | text = categoryName,
80 | modifier = Modifier
81 | .road(Alignment.CenterStart, Alignment.BottomStart)
82 | .padding(40.dp, 16.dp, 16.dp, 16.dp)
83 | .statusBarsPadding(),
84 | color = Color.White,
85 | fontSize = (18 + (30 - 18) * state.toolbarState.progress).sp
86 | )
87 | }
88 | ) {
89 | LazyColumn(
90 | modifier = Modifier
91 | .fillMaxSize()
92 | ) {
93 | items(lazyPagingItems) {
94 | it?.data?.run {
95 | RankItemWidget(itemData = it.data)
96 | }
97 | }
98 | }
99 | }
100 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/fmt/compose/eyepetizer/pages/discover/CategoryPage.kt:
--------------------------------------------------------------------------------
1 | @file:OptIn(ExperimentalMaterialApi::class)
2 |
3 | package com.fmt.compose.eyepetizer.pages.discover
4 |
5 | import androidx.compose.foundation.Image
6 | import androidx.compose.foundation.clickable
7 | import androidx.compose.foundation.layout.*
8 | import androidx.compose.foundation.lazy.grid.GridCells
9 | import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
10 | import androidx.compose.foundation.lazy.grid.items
11 | import androidx.compose.foundation.shape.RoundedCornerShape
12 | import androidx.compose.material.ExperimentalMaterialApi
13 | import androidx.compose.material.Text
14 | import androidx.compose.material.pullrefresh.PullRefreshIndicator
15 | import androidx.compose.material.pullrefresh.pullRefresh
16 | import androidx.compose.material.pullrefresh.rememberPullRefreshState
17 | import androidx.compose.runtime.Composable
18 | import androidx.compose.ui.Alignment
19 | import androidx.compose.ui.Modifier
20 | import androidx.compose.ui.draw.clip
21 | import androidx.compose.ui.graphics.Color
22 | import androidx.compose.ui.layout.ContentScale
23 | import androidx.compose.ui.platform.LocalContext
24 | import androidx.compose.ui.text.font.FontWeight
25 | import androidx.compose.ui.text.style.TextAlign
26 | import androidx.compose.ui.unit.dp
27 | import androidx.compose.ui.unit.sp
28 | import coil.compose.rememberAsyncImagePainter
29 | import com.fmt.compose.eyepetizer.model.Category
30 | import com.fmt.compose.eyepetizer.pages.discover.viewmodel.CategoryViewModel
31 |
32 | @Composable
33 | fun CategoryPage(viewModel: CategoryViewModel = androidx.lifecycle.viewmodel.compose.viewModel()) {
34 | val pullRefreshState =
35 | rememberPullRefreshState(refreshing = viewModel.refreshing.value, onRefresh = {
36 | viewModel.getCategoryList()
37 | })
38 | Box(modifier = Modifier
39 | .fillMaxSize()
40 | .pullRefresh(pullRefreshState)) {
41 | LazyVerticalGrid(modifier = Modifier.fillMaxSize(), columns = GridCells.Fixed(2),
42 | contentPadding = PaddingValues(horizontal = 5.dp, vertical = 5.dp)) {
43 | items(viewModel.categoryList) { item ->
44 | CategoryItemWidget(item)
45 | }
46 | }
47 | PullRefreshIndicator(refreshing = viewModel.refreshing.value,
48 | state = pullRefreshState,
49 | modifier = Modifier.align(Alignment.TopCenter))
50 | }
51 | }
52 |
53 | @Composable
54 | fun CategoryItemWidget(category: Category) {
55 | val context = LocalContext.current
56 | Box(Modifier
57 | .fillMaxWidth()
58 | .padding(horizontal = 5.dp, vertical = 5.dp)
59 | .clickable {
60 | CategoryDetailActivity.start(context, category.id, category.name, category.headerImage)
61 | },
62 | contentAlignment = Alignment.Center) {
63 | Image(painter = rememberAsyncImagePainter(model = category.bgPicture),
64 | contentDescription = null, modifier = Modifier
65 | .fillMaxWidth()
66 | .height(200.dp)
67 | .clip(RoundedCornerShape(4.dp)), contentScale = ContentScale.Crop)
68 | Text(text = "#${category.name}",
69 | fontSize = 22.sp,
70 | fontWeight = FontWeight.Bold,
71 | color = Color.White,
72 | textAlign = TextAlign.Center)
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/app/src/main/java/com/fmt/compose/eyepetizer/pages/discover/DiscoverPage.kt:
--------------------------------------------------------------------------------
1 | @file:OptIn(ExperimentalFoundationApi::class)
2 |
3 | package com.fmt.compose.eyepetizer.pages.discover
4 |
5 | import androidx.compose.foundation.ExperimentalFoundationApi
6 | import androidx.compose.foundation.layout.Box
7 | import androidx.compose.foundation.layout.Column
8 | import androidx.compose.foundation.layout.fillMaxSize
9 | import androidx.compose.foundation.layout.width
10 | import androidx.compose.foundation.pager.HorizontalPager
11 | import androidx.compose.foundation.pager.PagerState
12 | import androidx.compose.foundation.pager.rememberPagerState
13 | import androidx.compose.material.*
14 | import androidx.compose.material.TabRowDefaults.tabIndicatorOffset
15 | import androidx.compose.runtime.Composable
16 | import androidx.compose.runtime.LaunchedEffect
17 | import androidx.compose.runtime.rememberCoroutineScope
18 | import androidx.compose.ui.Alignment
19 | import androidx.compose.ui.Modifier
20 | import androidx.compose.ui.graphics.Color
21 | import androidx.compose.ui.res.stringResource
22 | import androidx.compose.ui.unit.dp
23 | import androidx.compose.ui.unit.sp
24 | import androidx.compose.ui.zIndex
25 | import com.fmt.compose.eyepetizer.R
26 | import com.fmt.compose.eyepetizer.pages.discover.viewmodel.DiscoverViewModel
27 | import com.fmt.compose.eyepetizer.pages.hot.TitleBarWidget
28 | import com.fmt.compose.eyepetizer.ui.theme.UnselectedItemColor
29 | import kotlinx.coroutines.launch
30 |
31 | val tabs = listOf("关注", "分类", "专题", "资讯", "推荐")
32 |
33 | @Composable
34 | fun DiscoverPage(
35 | viewModel: DiscoverViewModel = androidx.lifecycle.viewmodel.compose.viewModel(),
36 | ) {
37 | val pagerState = rememberPagerState()
38 | val coroutineScope = rememberCoroutineScope()
39 |
40 | LaunchedEffect(pagerState.currentPage) {
41 | viewModel.selectedIndex.value = pagerState.currentPage
42 | }
43 |
44 | Column(Modifier.fillMaxSize()) {
45 | TitleBarWidget(stringResource(id = R.string.discover))
46 | DiscoverTabRowWidget(viewModel.selectedIndex.value) { index ->
47 | coroutineScope.launch {
48 | viewModel.selectedIndex.value = index
49 | pagerState.animateScrollToPage(index)
50 | }
51 | }
52 | DiscoverTabPageWidget(pagerState, modifier = Modifier
53 | .weight(1f)
54 | .zIndex(-1f))
55 | }
56 | }
57 |
58 | @Composable
59 | fun DiscoverTabRowWidget(
60 | selectedIndex: Int,
61 | onTabClick: (Int) -> Unit,
62 | ) {
63 | TabRow(selectedTabIndex = selectedIndex,
64 | backgroundColor = Color.White,
65 | indicator = { tabPositions ->
66 | Box(modifier = Modifier.tabIndicatorOffset(tabPositions[selectedIndex]),
67 | contentAlignment = Alignment.Center) {
68 | Divider(modifier = Modifier.width(60.dp),
69 | thickness = 3.dp,
70 | color = LocalContentColor.current)
71 | }
72 | }) {
73 | tabs.forEachIndexed { index, title ->
74 | Tab(selected = selectedIndex == index, onClick = {
75 | onTabClick(index)
76 | }, text = {
77 | Text(text = title, fontSize = 14.sp)
78 | }, selectedContentColor = Color.Black, unselectedContentColor = UnselectedItemColor)
79 | }
80 | }
81 | }
82 |
83 | @Composable
84 | fun DiscoverTabPageWidget(
85 | pagerState: PagerState,
86 | modifier: Modifier = Modifier,
87 | ) {
88 | HorizontalPager(pageCount = tabs.size, state = pagerState, modifier = modifier) { pageIndex ->
89 | when (pageIndex) {
90 | 0 -> FollowPage()
91 | 1 -> CategoryPage()
92 | 2 -> TopicPage()
93 | 3 -> NewsPage()
94 | 4 -> RecommendPage()
95 | }
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/app/src/main/java/com/fmt/compose/eyepetizer/pages/discover/FollowPage.kt:
--------------------------------------------------------------------------------
1 | @file:OptIn(ExperimentalMaterialApi::class)
2 |
3 | package com.fmt.compose.eyepetizer.pages.discover
4 |
5 | import androidx.compose.foundation.Image
6 | import androidx.compose.foundation.background
7 | import androidx.compose.foundation.clickable
8 | import androidx.compose.foundation.layout.*
9 | import androidx.compose.foundation.lazy.LazyColumn
10 | import androidx.compose.foundation.lazy.LazyRow
11 | import androidx.compose.foundation.lazy.itemsIndexed
12 | import androidx.compose.foundation.shape.CircleShape
13 | import androidx.compose.foundation.shape.RoundedCornerShape
14 | import androidx.compose.material.*
15 | import androidx.compose.material.pullrefresh.PullRefreshIndicator
16 | import androidx.compose.material.pullrefresh.pullRefresh
17 | import androidx.compose.material.pullrefresh.rememberPullRefreshState
18 | import androidx.compose.runtime.*
19 | import androidx.compose.ui.Alignment
20 | import androidx.compose.ui.Modifier
21 | import androidx.compose.ui.draw.clip
22 | import androidx.compose.ui.graphics.Color
23 | import androidx.compose.ui.layout.ContentScale
24 | import androidx.compose.ui.platform.LocalContext
25 | import androidx.compose.ui.res.stringResource
26 | import androidx.compose.ui.text.TextStyle
27 | import androidx.compose.ui.text.font.FontWeight
28 | import androidx.compose.ui.text.style.TextOverflow
29 | import androidx.compose.ui.unit.dp
30 | import androidx.compose.ui.unit.sp
31 | import androidx.paging.compose.collectAsLazyPagingItems
32 | import androidx.paging.compose.itemsIndexed
33 | import coil.compose.rememberAsyncImagePainter
34 | import com.fmt.compose.eyepetizer.R
35 | import com.fmt.compose.eyepetizer.ext.loadMoreView
36 | import com.fmt.compose.eyepetizer.model.Item
37 | import com.fmt.compose.eyepetizer.pages.detail.VideoDetailActivity
38 | import com.fmt.compose.eyepetizer.pages.detail.VideoDetailPage
39 | import com.fmt.compose.eyepetizer.pages.discover.viewmodel.FollowViewModel
40 | import com.fmt.compose.eyepetizer.ui.theme.Black_26
41 | import com.fmt.compose.eyepetizer.ui.theme.Follow_bg
42 | import com.fmt.compose.eyepetizer.ui.theme.White_54
43 | import com.fmt.compose.eyepetizer.util.DateUtils
44 |
45 | @Composable
46 | fun FollowPage(viewModel: FollowViewModel = androidx.lifecycle.viewmodel.compose.viewModel()) {
47 | val lazyPagingItems = viewModel.pageFlow.collectAsLazyPagingItems()
48 | val pullRefreshState =
49 | rememberPullRefreshState(refreshing = viewModel.refreshing.value, onRefresh = {
50 | lazyPagingItems.refresh()
51 | })
52 |
53 | Box(modifier = Modifier
54 | .fillMaxSize()
55 | .pullRefresh(pullRefreshState)) {
56 | LazyColumn(modifier = Modifier.fillMaxSize(), contentPadding = PaddingValues(top = 10.dp)) {
57 | itemsIndexed(lazyPagingItems) { _, pagingItem ->
58 | FollowItemWidget(pagingItem!!)
59 | }
60 | loadMoreView(lazyPagingItems)
61 | }
62 | PullRefreshIndicator(refreshing = viewModel.refreshing.value,
63 | state = pullRefreshState,
64 | modifier = Modifier.align(Alignment.TopCenter))
65 | }
66 | }
67 |
68 | @Composable
69 | fun FollowItemWidget(item: Item) {
70 | Column(modifier = Modifier
71 | .padding(start = 15.dp, bottom = 10.dp)
72 | .fillMaxWidth()) {
73 | Row(modifier = Modifier
74 | .fillMaxWidth()
75 | .padding(bottom = 10.dp),
76 | verticalAlignment = Alignment.CenterVertically) {
77 | Image(painter = rememberAsyncImagePainter(model = item.data?.header?.icon),
78 | contentDescription = null,
79 | modifier = Modifier
80 | .size(40.dp)
81 | .clip(CircleShape))
82 |
83 | Column(modifier = Modifier
84 | .weight(1f)
85 | .padding(start = 10.dp)) {
86 | Text(text = item.data?.header?.title ?: "",
87 | style = TextStyle(color = Color.Black, fontSize = 14.sp))
88 | Text(text = item.data?.header?.description ?: "",
89 | style = TextStyle(color = Black_26, fontSize = 14.sp),
90 | maxLines = 1,
91 | overflow = TextOverflow.Ellipsis,
92 | modifier = Modifier.padding(top = 3.dp))
93 | }
94 | Text(text = stringResource(id = R.string.add_follow),
95 | fontSize = 12.sp,
96 | color = Color.Gray,
97 | modifier = Modifier
98 | .padding(end = 10.dp)
99 | .clip(RoundedCornerShape(5.dp))
100 | .background(Follow_bg)
101 | .padding(5.dp))
102 | }
103 | item.data?.itemList?.run {
104 | LazyRow {
105 | itemsIndexed(item.data.itemList) { _, itemData ->
106 | FollowHorizontalItemWidget(itemData)
107 | }
108 | }
109 | }
110 | Divider(thickness = 0.5.dp, modifier = Modifier.padding(top = 10.dp))
111 | }
112 | }
113 |
114 | @Composable
115 | fun FollowHorizontalItemWidget(item: Item) {
116 | val current = LocalContext.current
117 | Column(modifier = Modifier
118 | .padding(end = 15.dp)
119 | .clickable {
120 | item.data?.run {
121 | VideoDetailActivity.start(current, item.data)
122 | }
123 | }) {
124 | Box {
125 | Image(painter = rememberAsyncImagePainter(model = item.data?.cover?.feed),
126 | contentDescription = null,
127 | modifier = Modifier
128 | .width(300.dp)
129 | .height(180.dp)
130 | .clip(RoundedCornerShape(4.dp)),
131 | contentScale = ContentScale.Crop)
132 |
133 | Text(text = item.data?.category ?: "",
134 | fontSize = 13.sp,
135 | color = Color.Black,
136 | fontWeight = FontWeight.Bold,
137 | modifier = Modifier
138 | .padding(top = 8.dp, end = 8.dp)
139 | .clip(RoundedCornerShape(5.dp))
140 | .background(White_54)
141 | .padding(6.dp)
142 | .align(Alignment.TopEnd))
143 | }
144 | Text(text = item.data?.title ?: "",
145 | fontSize = 14.sp,
146 | color = Color.Black,
147 | maxLines = 1,
148 | overflow = TextOverflow.Ellipsis,
149 | fontWeight = FontWeight.Bold,
150 | modifier = Modifier.padding(top = 3.dp))
151 | Text(text = DateUtils.formatDateMsByYMDHM(item.data!!.author?.latestReleaseTime ?: 0),
152 | fontSize = 12.sp,
153 | color = Black_26,
154 | modifier = Modifier.padding(top = 3.dp))
155 | }
156 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/fmt/compose/eyepetizer/pages/discover/NewsPage.kt:
--------------------------------------------------------------------------------
1 | @file:OptIn(ExperimentalMaterialApi::class)
2 |
3 | package com.fmt.compose.eyepetizer.pages.discover
4 |
5 | import android.net.Uri
6 | import androidx.compose.foundation.Image
7 | import androidx.compose.foundation.clickable
8 | import androidx.compose.foundation.layout.*
9 | import androidx.compose.foundation.lazy.LazyColumn
10 | import androidx.compose.foundation.shape.RoundedCornerShape
11 | import androidx.compose.material.*
12 | import androidx.compose.material.pullrefresh.PullRefreshIndicator
13 | import androidx.compose.material.pullrefresh.pullRefresh
14 | import androidx.compose.material.pullrefresh.rememberPullRefreshState
15 | import androidx.compose.runtime.Composable
16 | import androidx.compose.ui.Alignment
17 | import androidx.compose.ui.Modifier
18 | import androidx.compose.ui.graphics.Color
19 | import androidx.compose.ui.layout.ContentScale
20 | import androidx.compose.ui.platform.LocalContext
21 | import androidx.compose.ui.text.font.FontWeight
22 | import androidx.compose.ui.text.style.TextOverflow
23 | import androidx.compose.ui.unit.dp
24 | import androidx.compose.ui.unit.sp
25 | import androidx.paging.compose.collectAsLazyPagingItems
26 | import androidx.paging.compose.itemsIndexed
27 | import coil.compose.rememberAsyncImagePainter
28 | import com.fmt.compose.eyepetizer.ext.loadMoreView
29 | import com.fmt.compose.eyepetizer.model.NewsItem
30 | import com.fmt.compose.eyepetizer.pages.detail.NewsDetailActivity
31 | import com.fmt.compose.eyepetizer.pages.discover.viewmodel.NewsViewModel
32 | import com.fmt.compose.eyepetizer.ui.theme.News_bg
33 |
34 | const val TEXT_CARD = "textCard"
35 |
36 | @Composable
37 | fun NewsPage(viewModel: NewsViewModel = androidx.lifecycle.viewmodel.compose.viewModel()) {
38 | val lazyPagingItems = viewModel.pageFlow.collectAsLazyPagingItems()
39 | val pullRefreshState =
40 | rememberPullRefreshState(refreshing = viewModel.refreshing.value, onRefresh = {
41 | lazyPagingItems.refresh()
42 | })
43 |
44 | Box(modifier = Modifier
45 | .fillMaxSize()
46 | .pullRefresh(pullRefreshState)) {
47 | LazyColumn(modifier = Modifier.fillMaxSize(), contentPadding = PaddingValues(top = 10.dp)) {
48 | itemsIndexed(lazyPagingItems) { _, pagingItem ->
49 | NewsItemWidget(pagingItem!!)
50 | }
51 | loadMoreView(pagingItems = lazyPagingItems)
52 | }
53 |
54 | PullRefreshIndicator(refreshing = viewModel.refreshing.value,
55 | state = pullRefreshState,
56 | modifier = Modifier
57 | .align(Alignment.TopCenter))
58 | }
59 | }
60 |
61 | @Composable
62 | fun NewsItemWidget(newsItem: NewsItem) {
63 | val context = LocalContext.current
64 | if (newsItem.type == TEXT_CARD) {
65 | Text(text = newsItem.data.text,
66 | fontSize = 22.sp,
67 | fontWeight = FontWeight.Bold,
68 | color = Color.Black, modifier = Modifier.padding(start = 10.dp, top = 5.dp))
69 | } else {
70 | Card(backgroundColor = News_bg, shape = RoundedCornerShape(4.dp), modifier = Modifier
71 | .padding(10.dp)
72 | .clickable {
73 | val actionUrl = newsItem.data.actionUrl
74 | val title = newsItem.data.titleList?.get(0) ?: ""
75 | var webUrl = Uri.decode(actionUrl.substring(actionUrl.indexOf("url")))
76 | webUrl = webUrl.substring(4)
77 | NewsDetailActivity.start(context, webUrl, title)
78 | }) {
79 | Column {
80 | Image(painter = rememberAsyncImagePainter(model = newsItem.data.backgroundImage),
81 | contentDescription = null,
82 | modifier = Modifier
83 | .fillMaxWidth()
84 | .height(140.dp),
85 | contentScale = ContentScale.FillWidth)
86 | newsItem.data.titleList?.run {
87 | newsItem.data.titleList.forEach { title ->
88 | Text(text = title,
89 | fontSize = 12.sp,
90 | maxLines = 3,
91 | overflow = TextOverflow.Ellipsis,
92 | modifier = Modifier.padding(start = 10.dp,
93 | top = 5.dp,
94 | end = 10.dp,
95 | bottom = 5.dp))
96 | }
97 | }
98 | }
99 | }
100 | }
101 | }
102 |
103 |
--------------------------------------------------------------------------------
/app/src/main/java/com/fmt/compose/eyepetizer/pages/discover/RecommendPage.kt:
--------------------------------------------------------------------------------
1 | @file:OptIn(ExperimentalMaterialApi::class, ExperimentalFoundationApi::class)
2 |
3 | package com.fmt.compose.eyepetizer.pages.discover
4 |
5 | import androidx.compose.foundation.ExperimentalFoundationApi
6 | import androidx.compose.foundation.Image
7 | import androidx.compose.foundation.layout.*
8 | import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
9 | import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
10 | import androidx.compose.foundation.shape.CircleShape
11 | import androidx.compose.foundation.shape.RoundedCornerShape
12 | import androidx.compose.material.Card
13 | import androidx.compose.material.ExperimentalMaterialApi
14 | import androidx.compose.material.Icon
15 | import androidx.compose.material.Text
16 | import androidx.compose.material.icons.Icons
17 | import androidx.compose.material.icons.filled.PhotoLibrary
18 | import androidx.compose.material.icons.filled.PlayCircle
19 | import androidx.compose.material.icons.filled.ThumbUp
20 | import androidx.compose.material.pullrefresh.PullRefreshIndicator
21 | import androidx.compose.material.pullrefresh.pullRefresh
22 | import androidx.compose.material.pullrefresh.rememberPullRefreshState
23 | import androidx.compose.runtime.Composable
24 | import androidx.compose.ui.Alignment
25 | import androidx.compose.ui.Modifier
26 | import androidx.compose.ui.draw.clip
27 | import androidx.compose.ui.graphics.Color
28 | import androidx.compose.ui.layout.ContentScale
29 | import androidx.compose.ui.text.style.TextOverflow
30 | import androidx.compose.ui.unit.dp
31 | import androidx.compose.ui.unit.sp
32 | import androidx.paging.compose.collectAsLazyPagingItems
33 | import coil.compose.rememberAsyncImagePainter
34 | import com.fmt.compose.eyepetizer.ext.items
35 | import com.fmt.compose.eyepetizer.ext.loadMoreView
36 | import com.fmt.compose.eyepetizer.model.RecommendItem
37 | import com.fmt.compose.eyepetizer.pages.discover.viewmodel.RecommendViewModel
38 | import com.fmt.compose.eyepetizer.ui.theme.Black_87
39 | import com.fmt.compose.eyepetizer.util.ScreenUtils
40 |
41 | const val VIDEO_TYPE = "video"
42 |
43 | @Composable
44 | fun RecommendPage(viewModel: RecommendViewModel = androidx.lifecycle.viewmodel.compose.viewModel()) {
45 | val lazyPagingItems = viewModel.pageFlow.collectAsLazyPagingItems()
46 | val pullRefreshState =
47 | rememberPullRefreshState(refreshing = viewModel.refreshing.value, onRefresh = {
48 | lazyPagingItems.refresh()
49 | })
50 | Box(modifier = Modifier
51 | .fillMaxSize()
52 | .pullRefresh(pullRefreshState)
53 | ) {
54 | LazyVerticalStaggeredGrid(modifier = Modifier.fillMaxSize(),
55 | contentPadding = PaddingValues(horizontal = 5.dp,
56 | vertical = 5.dp),
57 | columns = StaggeredGridCells.Fixed(2),
58 | verticalItemSpacing = 10.dp) {
59 | items(lazyPagingItems) { pagingItem ->
60 | RecommendItemWidget(pagingItem)
61 | }
62 | loadMoreView(lazyPagingItems)
63 | }
64 |
65 | PullRefreshIndicator(refreshing = viewModel.refreshing.value,
66 | state = pullRefreshState,
67 | modifier = Modifier
68 | .align(Alignment.TopCenter))
69 | }
70 | }
71 |
72 | @Composable
73 | fun RecommendItemWidget(recommendItem: RecommendItem) {
74 | Card(shape = RoundedCornerShape(5.dp),
75 | backgroundColor = Color.White,
76 | modifier = Modifier
77 | .padding(horizontal = 5.dp)) {
78 | val maxWidth = ScreenUtils.instance.getScreenWidth()
79 | val width =
80 | if (recommendItem.data.content.data!!.width == 0) maxWidth else recommendItem.data.content.data.width
81 | val height =
82 | if (recommendItem.data.content.data.height == 0) maxWidth else recommendItem.data.content.data.height
83 | Column {
84 | Box {
85 | Image(painter = rememberAsyncImagePainter(model = recommendItem.data.content.data.cover.feed
86 | ?: ""),
87 | contentDescription = null,
88 | modifier = Modifier
89 | .fillMaxWidth()
90 | .aspectRatio(width / height.toFloat()),
91 | contentScale = ContentScale.FillWidth)
92 | recommendItem.data.content.data.urls?.run {
93 | if (isNotEmpty()) {
94 | Icon(imageVector = if (recommendItem.data.content.type == VIDEO_TYPE) Icons.Default.PlayCircle else Icons.Default.PhotoLibrary,
95 | contentDescription = null,
96 | tint = Color.White,
97 | modifier = Modifier
98 | .size(18.dp)
99 | .padding(start = 5.dp, top = 5.dp))
100 | }
101 | }
102 | }
103 | Text(text = recommendItem.data.content.data.description,
104 | fontSize = 14.sp,
105 | color = Black_87,
106 | maxLines = 2,
107 | overflow = TextOverflow.Ellipsis,
108 | modifier = Modifier.padding(horizontal = 6.dp, vertical = 10.dp))
109 | Row(Modifier
110 | .padding(horizontal = 6.dp, vertical = 10.dp)
111 | .fillMaxWidth(),
112 | verticalAlignment = Alignment.CenterVertically,
113 | horizontalArrangement = Arrangement.SpaceBetween) {
114 | Row(verticalAlignment = Alignment.CenterVertically) {
115 | Image(painter = rememberAsyncImagePainter(model = recommendItem.data.content.data.owner?.avatar
116 | ?: ""), contentDescription = null, modifier = Modifier
117 | .size(24.dp)
118 | .clip(
119 | CircleShape))
120 | Text(text = recommendItem.data.content.data.owner?.nickname ?: "",
121 | fontSize = 12.sp,
122 | maxLines = 1, overflow = TextOverflow.Ellipsis,
123 | modifier = Modifier
124 | .width(80.dp)
125 | .padding(start = 3.dp))
126 | }
127 |
128 | Row(verticalAlignment = Alignment.CenterVertically) {
129 | Icon(imageVector = Icons.Default.ThumbUp,
130 | contentDescription = null,
131 | modifier = Modifier.size(14.dp),
132 | tint = Color.Gray)
133 | Text(text = "${recommendItem.data.content.data.consumption.collectionCount}",
134 | fontSize = 12.sp, modifier = Modifier.padding(start = 3.dp))
135 | }
136 | }
137 | }
138 | }
139 | }
140 |
141 |
--------------------------------------------------------------------------------
/app/src/main/java/com/fmt/compose/eyepetizer/pages/discover/TopicPage.kt:
--------------------------------------------------------------------------------
1 | @file:OptIn(ExperimentalMaterialApi::class)
2 |
3 | package com.fmt.compose.eyepetizer.pages.discover
4 |
5 | import androidx.compose.foundation.Image
6 | import androidx.compose.foundation.clickable
7 | import androidx.compose.foundation.layout.*
8 | import androidx.compose.foundation.lazy.LazyColumn
9 | import androidx.compose.foundation.shape.RoundedCornerShape
10 | import androidx.compose.material.*
11 | import androidx.compose.material.pullrefresh.PullRefreshIndicator
12 | import androidx.compose.material.pullrefresh.pullRefresh
13 | import androidx.compose.material.pullrefresh.rememberPullRefreshState
14 | import androidx.compose.runtime.Composable
15 | import androidx.compose.ui.Alignment
16 | import androidx.compose.ui.Modifier
17 | import androidx.compose.ui.draw.clip
18 | import androidx.compose.ui.layout.ContentScale
19 | import androidx.compose.ui.platform.LocalContext
20 | import androidx.compose.ui.unit.dp
21 | import androidx.paging.compose.collectAsLazyPagingItems
22 | import androidx.paging.compose.itemsIndexed
23 | import coil.compose.rememberAsyncImagePainter
24 | import com.fmt.compose.eyepetizer.ext.loadMoreView
25 | import com.fmt.compose.eyepetizer.model.Topic
26 | import com.fmt.compose.eyepetizer.pages.detail.TopicDetailActivity
27 | import com.fmt.compose.eyepetizer.pages.discover.viewmodel.TopicViewModel
28 |
29 | @Composable
30 | fun TopicPage(viewModel: TopicViewModel = androidx.lifecycle.viewmodel.compose.viewModel()) {
31 | val lazyPagingItems = viewModel.pageFlow.collectAsLazyPagingItems()
32 | val pullRefreshState =
33 | rememberPullRefreshState(refreshing = viewModel.refreshing.value, onRefresh = {
34 | lazyPagingItems.refresh()
35 | })
36 |
37 | Box(modifier = Modifier
38 | .pullRefresh(pullRefreshState)) {
39 | LazyColumn(modifier = Modifier.fillMaxSize()) {
40 | itemsIndexed(lazyPagingItems) { _, pagingItem ->
41 | TopicItemWidget(pagingItem!!)
42 | }
43 | loadMoreView(pagingItems = lazyPagingItems)
44 | }
45 |
46 | PullRefreshIndicator(refreshing = viewModel.refreshing.value,
47 | state = pullRefreshState,
48 | modifier = Modifier
49 | .align(Alignment.TopCenter))
50 | }
51 | }
52 |
53 | @Composable
54 | fun TopicItemWidget(topic: Topic) {
55 | val context = LocalContext.current
56 | Column(Modifier
57 | .fillMaxWidth()
58 | .clickable {
59 | TopicDetailActivity.start(context, topic.data.id)
60 | }) {
61 | Image(painter = rememberAsyncImagePainter(model = topic.data.image),
62 | contentDescription = null,
63 | modifier = Modifier
64 | .padding(10.dp)
65 | .fillMaxWidth()
66 | .height(200.dp)
67 | .clip(RoundedCornerShape(4.dp)), contentScale = ContentScale.Crop)
68 |
69 | Divider(thickness = 0.5.dp,
70 | modifier = Modifier.padding(start = 10.dp, end = 10.dp))
71 | }
72 | }
73 |
74 |
--------------------------------------------------------------------------------
/app/src/main/java/com/fmt/compose/eyepetizer/pages/discover/viewmodel/CategoryDetailViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.fmt.compose.eyepetizer.pages.discover.viewmodel
2 |
3 | import com.fmt.compose.eyepetizer.ext.AbsPagingViewModel
4 | import com.fmt.compose.eyepetizer.http.ApiService
5 | import com.fmt.compose.eyepetizer.http.BASE_URL
6 | import com.fmt.compose.eyepetizer.model.BaseApiResult
7 | import com.fmt.compose.eyepetizer.model.Item
8 |
9 | class CategoryDetailViewModel : AbsPagingViewModel
- () {
10 |
11 | var categoryId: Int = 0
12 |
13 | override suspend fun doLoadPage(pageKey: String?): BaseApiResult
- {
14 | return if (pageKey.isNullOrEmpty()) {
15 | val url =
16 | "${BASE_URL}v4/categories/videoList?id=${categoryId}&udid=d2807c895f0348a180148c9dfa6f2feeac0781b5&deviceModel=Android"
17 | ApiService.getCategoryVideoList(url)
18 | } else {
19 | ApiService.getFollowList("${pageKey}&udid=d2807c895f0348a180148c9dfa6f2feeac0781b5&deviceModel=Android")
20 | }
21 | }
22 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/fmt/compose/eyepetizer/pages/discover/viewmodel/CategoryViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.fmt.compose.eyepetizer.pages.discover.viewmodel
2 |
3 | import androidx.compose.runtime.mutableStateListOf
4 | import androidx.compose.runtime.mutableStateOf
5 | import androidx.lifecycle.ViewModel
6 | import androidx.lifecycle.viewModelScope
7 | import com.fmt.compose.eyepetizer.http.ApiService
8 | import com.fmt.compose.eyepetizer.model.Category
9 | import kotlinx.coroutines.launch
10 |
11 | class CategoryViewModel : ViewModel() {
12 |
13 | val categoryList = mutableStateListOf
()
14 | var refreshing = mutableStateOf(false)
15 |
16 | init {
17 | getCategoryList()
18 | }
19 |
20 | fun getCategoryList() {
21 | viewModelScope.launch {
22 | runCatching {
23 | refreshing.value = true
24 | categoryList.clear()
25 | categoryList.addAll(ApiService.getCategoryList())
26 | refreshing.value = false
27 | }.onFailure {
28 | refreshing.value = false
29 | }
30 | }
31 | }
32 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/fmt/compose/eyepetizer/pages/discover/viewmodel/DiscoverViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.fmt.compose.eyepetizer.pages.discover.viewmodel
2 |
3 | import androidx.compose.runtime.mutableStateOf
4 | import androidx.lifecycle.ViewModel
5 |
6 | class DiscoverViewModel : ViewModel() {
7 |
8 | var selectedIndex = mutableStateOf(0)
9 |
10 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/fmt/compose/eyepetizer/pages/discover/viewmodel/FollowViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.fmt.compose.eyepetizer.pages.discover.viewmodel
2 |
3 | import com.fmt.compose.eyepetizer.ext.AbsPagingViewModel
4 | import com.fmt.compose.eyepetizer.http.ApiService
5 | import com.fmt.compose.eyepetizer.model.BaseApiResult
6 | import com.fmt.compose.eyepetizer.model.Item
7 |
8 | class FollowViewModel : AbsPagingViewModel- () {
9 | override suspend fun doLoadPage(pageKey: String?): BaseApiResult
- {
10 | return if (pageKey.isNullOrEmpty()) {
11 | ApiService.getFollowList()
12 | } else {
13 | ApiService.getFollowList(pageKey)
14 | }
15 | }
16 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/fmt/compose/eyepetizer/pages/discover/viewmodel/NewsViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.fmt.compose.eyepetizer.pages.discover.viewmodel
2 |
3 | import com.fmt.compose.eyepetizer.ext.AbsPagingViewModel
4 | import com.fmt.compose.eyepetizer.http.ApiService
5 | import com.fmt.compose.eyepetizer.model.BaseApiResult
6 | import com.fmt.compose.eyepetizer.model.NewsItem
7 |
8 | class NewsViewModel : AbsPagingViewModel
() {
9 |
10 | override suspend fun doLoadPage(pageKey: String?): BaseApiResult {
11 | return if (pageKey.isNullOrEmpty()) {
12 | ApiService.getNewsList()
13 | } else {
14 | ApiService.getNewsList("${pageKey}&vc=6030000&deviceModel=Android")
15 | }
16 | }
17 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/fmt/compose/eyepetizer/pages/discover/viewmodel/RecommendViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.fmt.compose.eyepetizer.pages.discover.viewmodel
2 |
3 | import com.fmt.compose.eyepetizer.ext.AbsPagingViewModel
4 | import com.fmt.compose.eyepetizer.http.ApiService
5 | import com.fmt.compose.eyepetizer.model.BaseApiResult
6 | import com.fmt.compose.eyepetizer.model.RecommendItem
7 |
8 | const val HORIZONTAL_SCROLL_CARD = "horizontalScrollCard"
9 |
10 | class RecommendViewModel : AbsPagingViewModel() {
11 | override suspend fun doLoadPage(pageKey: String?): BaseApiResult {
12 | return if (pageKey.isNullOrEmpty()) {
13 | val recommend = ApiService.getRecommendList()
14 | recommend.itemList.removeAll {
15 | it.type == HORIZONTAL_SCROLL_CARD
16 | }
17 | recommend
18 | } else {
19 | val recommend = ApiService.getRecommendList(pageKey)
20 | recommend.itemList.removeAll {
21 | it.type == HORIZONTAL_SCROLL_CARD
22 | }
23 | recommend
24 | }
25 | }
26 |
27 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/fmt/compose/eyepetizer/pages/discover/viewmodel/TopicViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.fmt.compose.eyepetizer.pages.discover.viewmodel
2 |
3 | import com.fmt.compose.eyepetizer.ext.AbsPagingViewModel
4 | import com.fmt.compose.eyepetizer.http.ApiService
5 | import com.fmt.compose.eyepetizer.model.BaseApiResult
6 | import com.fmt.compose.eyepetizer.model.Topic
7 |
8 | class TopicViewModel : AbsPagingViewModel() {
9 |
10 | override suspend fun doLoadPage(pageKey: String?): BaseApiResult {
11 | return if (pageKey.isNullOrEmpty()) {
12 | ApiService.getTopicList()
13 | } else {
14 | ApiService.getTopicList(pageKey)
15 | }
16 | }
17 |
18 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/fmt/compose/eyepetizer/pages/hot/HotPage.kt:
--------------------------------------------------------------------------------
1 | @file:OptIn(ExperimentalFoundationApi::class, ExperimentalMaterialApi::class)
2 |
3 | package com.fmt.compose.eyepetizer.pages.hot
4 |
5 | import androidx.compose.foundation.ExperimentalFoundationApi
6 | import androidx.compose.foundation.layout.*
7 | import androidx.compose.foundation.lazy.LazyColumn
8 | import androidx.compose.foundation.lazy.items
9 | import androidx.compose.foundation.pager.HorizontalPager
10 | import androidx.compose.foundation.pager.PagerState
11 | import androidx.compose.foundation.pager.rememberPagerState
12 | import androidx.compose.material.*
13 | import androidx.compose.material.TabRowDefaults.tabIndicatorOffset
14 | import androidx.compose.material.pullrefresh.PullRefreshIndicator
15 | import androidx.compose.material.pullrefresh.pullRefresh
16 | import androidx.compose.material.pullrefresh.rememberPullRefreshState
17 | import androidx.compose.runtime.*
18 | import androidx.compose.ui.Alignment
19 | import androidx.compose.ui.Modifier
20 | import androidx.compose.ui.graphics.Color
21 | import androidx.compose.ui.res.stringResource
22 | import androidx.compose.ui.text.font.FontWeight
23 | import androidx.compose.ui.text.style.TextAlign
24 | import androidx.compose.ui.text.style.TextOverflow
25 | import androidx.compose.ui.unit.dp
26 | import androidx.compose.ui.unit.sp
27 | import androidx.compose.ui.zIndex
28 | import androidx.lifecycle.viewmodel.compose.viewModel
29 | import com.fmt.compose.eyepetizer.R
30 | import com.fmt.compose.eyepetizer.model.Tab
31 | import com.fmt.compose.eyepetizer.pages.daily.RankItemWidget
32 | import com.fmt.compose.eyepetizer.pages.hot.viewmodel.HotTabViewModel
33 | import com.fmt.compose.eyepetizer.pages.hot.viewmodel.HotViewModel
34 | import com.fmt.compose.eyepetizer.ui.theme.UnselectedItemColor
35 | import kotlinx.coroutines.launch
36 |
37 | @Composable
38 | fun HotPage(viewModel: HotViewModel = viewModel()) {
39 | val pagerState = rememberPagerState()
40 | val coroutineScope = rememberCoroutineScope()
41 |
42 | LaunchedEffect(pagerState.currentPage) {
43 | viewModel.selectedIndex.value = pagerState.currentPage
44 | }
45 |
46 | Column(Modifier.fillMaxSize()) {
47 | TitleBarWidget(stringResource(id = R.string.hot))
48 | HotTabRowWidget(viewModel.tabList, viewModel.selectedIndex.value) { index ->
49 | coroutineScope.launch {
50 | viewModel.selectedIndex.value = index
51 | pagerState.animateScrollToPage(index)
52 | }
53 | }
54 | HotTabPageWidget(viewModel.tabList, pagerState, modifier = Modifier
55 | .weight(1f)
56 | .zIndex(-1f))
57 | }
58 | }
59 |
60 | @Composable
61 | fun TitleBarWidget(title: String) {
62 | TopAppBar(title = {
63 | Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxWidth()) {
64 | Text(text = title,
65 | fontSize = 18.sp,
66 | fontWeight = FontWeight.Bold,
67 | color = Color.Black,
68 | textAlign = TextAlign.Center,
69 | maxLines = 1,
70 | overflow = TextOverflow.Ellipsis)
71 | }
72 | }, backgroundColor = Color.White)
73 | }
74 |
75 | @Composable
76 | fun HotTabRowWidget(
77 | tabs: List,
78 | selectedIndex: Int,
79 | onTabClick: (Int) -> Unit,
80 | ) {
81 | if (tabs.isEmpty()) {
82 | return
83 | }
84 |
85 | TabRow(selectedTabIndex = selectedIndex,
86 | backgroundColor = Color.White,
87 | indicator = { tabPositions ->
88 | Box(modifier = Modifier.tabIndicatorOffset(tabPositions[selectedIndex]),
89 | contentAlignment = Alignment.Center) {
90 | Divider(modifier = Modifier.width(80.dp),
91 | thickness = 3.dp,
92 | color = LocalContentColor.current)
93 | }
94 | }) {
95 | tabs.forEachIndexed { index, tabInfo ->
96 | Tab(selected = selectedIndex == index, onClick = {
97 | onTabClick(index)
98 | }, text = {
99 | Text(text = tabInfo.name, fontSize = 14.sp)
100 | }, selectedContentColor = Color.Black, unselectedContentColor = UnselectedItemColor)
101 | }
102 | }
103 | }
104 |
105 | @Composable
106 | fun HotTabPageWidget(
107 | tabs: List,
108 | pagerState: PagerState,
109 | modifier: Modifier = Modifier,
110 | ) {
111 | HorizontalPager(pageCount = tabs.size, state = pagerState, modifier = modifier) { pageIndex ->
112 | TabHotWidget(tabs[pageIndex])
113 | }
114 | }
115 |
116 | @Composable
117 | fun TabHotWidget(tab: Tab) {
118 | val viewModel: HotTabViewModel = viewModel(key = tab.name)
119 | LaunchedEffect(Unit) {
120 | viewModel.getTabData(tab.apiUrl)
121 | }
122 |
123 | val pullRefreshState =
124 | rememberPullRefreshState(refreshing = viewModel.refreshing.value, onRefresh = {
125 | viewModel.getTabData(tab.apiUrl, isRefresh = true)
126 | })
127 | Box(modifier = Modifier.pullRefresh(pullRefreshState)) {
128 | LazyColumn(contentPadding = PaddingValues(top = 5.dp)) {
129 | items(viewModel.itemList) { item ->
130 | item.data?.let { RankItemWidget(itemData = item.data) }
131 | }
132 | }
133 | PullRefreshIndicator(refreshing = viewModel.refreshing.value,
134 | state = pullRefreshState,
135 | modifier = Modifier.align(Alignment.TopCenter))
136 | }
137 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/fmt/compose/eyepetizer/pages/hot/viewmodel/HotTabViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.fmt.compose.eyepetizer.pages.hot.viewmodel
2 |
3 | import androidx.compose.runtime.*
4 | import androidx.lifecycle.ViewModel
5 | import androidx.lifecycle.viewModelScope
6 | import com.fmt.compose.eyepetizer.ext.errorToast
7 | import com.fmt.compose.eyepetizer.http.ApiService
8 | import com.fmt.compose.eyepetizer.model.Item
9 | import kotlinx.coroutines.delay
10 | import kotlinx.coroutines.launch
11 |
12 | class HotTabViewModel : ViewModel() {
13 |
14 | var itemList = mutableStateListOf- ()
15 | var refreshing = mutableStateOf(false)
16 |
17 | fun getTabData(url: String, isRefresh: Boolean = false) {
18 | if (itemList.isEmpty() || isRefresh) {
19 | viewModelScope.launch {
20 | runCatching {
21 | if (isRefresh) {
22 | refreshing.value = true
23 | }
24 | itemList.clear()
25 | itemList.addAll(ApiService.getHotTabData(url).itemList)
26 | if (isRefresh) {
27 | refreshing.value = false
28 | }
29 | }.onFailure {
30 | errorToast(it.message ?: "")
31 | delay(200)
32 | refreshing.value = false
33 | }
34 | }
35 | }
36 | }
37 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/fmt/compose/eyepetizer/pages/hot/viewmodel/HotViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.fmt.compose.eyepetizer.pages.hot.viewmodel
2 |
3 | import androidx.compose.runtime.mutableStateListOf
4 | import androidx.compose.runtime.mutableStateOf
5 | import androidx.lifecycle.ViewModel
6 | import androidx.lifecycle.viewModelScope
7 | import com.fmt.compose.eyepetizer.ext.errorToast
8 | import com.fmt.compose.eyepetizer.http.ApiService
9 | import com.fmt.compose.eyepetizer.model.Tab
10 | import kotlinx.coroutines.launch
11 |
12 | class HotViewModel : ViewModel() {
13 |
14 | var tabList = mutableStateListOf
()
15 | var selectedIndex = mutableStateOf(0)
16 |
17 | init {
18 | getTabList()
19 | }
20 |
21 | private fun getTabList() {
22 | viewModelScope.launch {
23 | runCatching {
24 | val tabs = ApiService.getHotTabList().tabInfo.tabList
25 | if (tabs.isNotEmpty()) {
26 | tabList.clear()
27 | tabList.addAll(tabs)
28 | }
29 | }.onFailure {
30 | errorToast(it.message ?: "")
31 | }
32 | }
33 | }
34 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/fmt/compose/eyepetizer/pages/person/PersonPage.kt:
--------------------------------------------------------------------------------
1 | @file:OptIn(ExperimentalFoundationApi::class)
2 |
3 | package com.fmt.compose.eyepetizer.pages.person
4 |
5 | import androidx.compose.foundation.*
6 | import androidx.compose.foundation.layout.*
7 | import androidx.compose.foundation.lazy.LazyColumn
8 | import androidx.compose.foundation.lazy.items
9 | import androidx.compose.foundation.shape.CircleShape
10 | import androidx.compose.material.Icon
11 | import androidx.compose.material.Text
12 | import androidx.compose.material.icons.Icons
13 | import androidx.compose.material.icons.outlined.Collections
14 | import androidx.compose.material.icons.outlined.CommentBank
15 | import androidx.compose.runtime.Composable
16 | import androidx.compose.ui.Alignment
17 | import androidx.compose.ui.Modifier
18 | import androidx.compose.ui.draw.alpha
19 | import androidx.compose.ui.draw.blur
20 | import androidx.compose.ui.draw.clip
21 | import androidx.compose.ui.geometry.Offset
22 | import androidx.compose.ui.graphics.Color
23 | import androidx.compose.ui.layout.ContentScale
24 | import androidx.compose.ui.platform.LocalContext
25 | import androidx.compose.ui.res.painterResource
26 | import androidx.compose.ui.res.stringResource
27 | import androidx.compose.ui.unit.dp
28 | import androidx.compose.ui.unit.sp
29 | import com.fmt.compose.eyepetizer.R
30 | import com.fmt.compose.eyepetizer.pages.person.viewmodel.PersonViewModel
31 | import com.fmt.compose.eyepetizer.ui.theme.Black_26
32 | import com.fmt.compose.eyepetizer.ui.theme.UnselectedItemColor
33 |
34 | @Composable
35 | fun PersonPage(viewModel: PersonViewModel = androidx.lifecycle.viewmodel.compose.viewModel()) {
36 | LazyColumn(modifier = Modifier
37 | .fillMaxSize(),
38 | contentPadding = PaddingValues(bottom = 30.dp)) {
39 | item {
40 | ProfileHeaderWidget()
41 | }
42 | stickyHeader {
43 | ProfileTabWidget()
44 | }
45 | items(viewModel.titles) { title ->
46 | SettingWidget(title)
47 | }
48 | }
49 | }
50 |
51 | @Composable
52 | fun ProfileHeaderWidget() {
53 | Box(modifier = Modifier
54 | .fillMaxWidth()
55 | .wrapContentHeight()) {
56 |
57 | Image(painter = painterResource(id = R.mipmap.ic_head_bg),
58 | contentDescription = null,
59 | modifier = Modifier
60 | .fillMaxSize()
61 | .height(200.dp)
62 | .alpha(0.75f)
63 | .blur(20.dp),
64 | contentScale = ContentScale.FillBounds)
65 |
66 | Column(modifier = Modifier
67 | .fillMaxWidth()
68 | .align(Alignment.Center),
69 | horizontalAlignment = Alignment.CenterHorizontally) {
70 | Image(painter = painterResource(id = R.mipmap.home_ic_img_avatar),
71 | contentDescription = null,
72 | modifier = Modifier
73 | .size(88.dp)
74 | .clip(
75 | CircleShape),
76 | contentScale = ContentScale.Crop)
77 | Text(text = stringResource(id = R.string.view_personal_homepage),
78 | fontSize = 12.sp,
79 | color = Color.White,
80 | modifier = Modifier
81 | .padding(top = 10.dp))
82 | }
83 | }
84 | }
85 |
86 | @Composable
87 | fun ProfileTabWidget() {
88 | Column(horizontalAlignment = Alignment.CenterHorizontally,
89 | modifier = Modifier.background(Color.White)) {
90 | Row(modifier = Modifier
91 | .padding(top = 20.dp)
92 | .fillMaxWidth(),
93 | horizontalArrangement = Arrangement.SpaceAround,
94 | verticalAlignment = Alignment.CenterVertically) {
95 | Row(verticalAlignment = Alignment.CenterVertically) {
96 | Icon(imageVector = Icons.Outlined.Collections,
97 | contentDescription = null,
98 | tint = Black_26, modifier = Modifier.size(14.dp))
99 | Text(
100 | text = stringResource(id = R.string.collect), fontSize = 15.sp,
101 | color = Black_26,
102 | modifier = Modifier.padding(start = 5.dp)
103 | )
104 | }
105 | Canvas(modifier = Modifier.size(0.5.dp, 40.dp)) {
106 | drawLine(color = UnselectedItemColor,
107 | start = Offset(0f, 0f),
108 | end = Offset(0f, size.height))
109 | }
110 | Row(verticalAlignment = Alignment.CenterVertically) {
111 | Icon(imageVector = Icons.Outlined.CommentBank,
112 | contentDescription = null,
113 | tint = Black_26, modifier = Modifier.size(14.dp))
114 | Text(
115 | text = stringResource(id = R.string.comment), fontSize = 15.sp,
116 | color = Black_26,
117 | modifier = Modifier.padding(start = 5.dp)
118 | )
119 | }
120 | }
121 | Canvas(modifier = Modifier
122 | .padding(top = 20.dp)
123 | .fillMaxWidth()
124 | .height(0.5.dp)
125 | ) {
126 | drawLine(color = UnselectedItemColor,
127 | start = Offset(0f, 0f),
128 | end = Offset(size.width, size.height))
129 | }
130 | }
131 | }
132 |
133 | @Composable
134 | fun SettingWidget(text: String) {
135 | val context = LocalContext.current
136 | Box(modifier = Modifier
137 | .fillMaxWidth()
138 | .padding(top = 30.dp)
139 | .background(Color.White),
140 | contentAlignment = Alignment.Center) {
141 | Text(text = text,
142 | fontSize = 16.sp,
143 | color = Black_26,
144 | modifier = Modifier
145 | .clickable {
146 | when (text) {
147 | context.getString(R.string.watch_record) -> {
148 | WatchRecordActivity.start(context)
149 | }
150 | }
151 | })
152 | }
153 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/fmt/compose/eyepetizer/pages/person/WatchRecordActivity.kt:
--------------------------------------------------------------------------------
1 | package com.fmt.compose.eyepetizer.pages.person
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.os.Bundle
6 | import androidx.activity.ComponentActivity
7 | import androidx.activity.compose.setContent
8 | import androidx.core.view.WindowInsetsControllerCompat
9 |
10 | class WatchRecordActivity : ComponentActivity() {
11 |
12 | override fun onCreate(savedInstanceState: Bundle?) {
13 | super.onCreate(savedInstanceState)
14 | window.statusBarColor = android.graphics.Color.WHITE
15 | WindowInsetsControllerCompat(window, window.decorView).apply {
16 | isAppearanceLightStatusBars = true
17 | }
18 | setContent {
19 | WatchRecordPage()
20 | }
21 | }
22 |
23 | companion object {
24 | fun start(context: Context) {
25 | val intent = Intent(context, WatchRecordActivity::class.java)
26 | context.startActivity(intent)
27 | }
28 | }
29 |
30 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/fmt/compose/eyepetizer/pages/person/WatchRecordPage.kt:
--------------------------------------------------------------------------------
1 | @file:OptIn(ExperimentalFoundationApi::class)
2 |
3 | package com.fmt.compose.eyepetizer.pages.person
4 |
5 | import android.app.Activity
6 | import androidx.compose.foundation.ExperimentalFoundationApi
7 | import androidx.compose.foundation.Image
8 | import androidx.compose.foundation.background
9 | import androidx.compose.foundation.clickable
10 | import androidx.compose.foundation.layout.*
11 | import androidx.compose.foundation.lazy.LazyColumn
12 | import androidx.compose.foundation.lazy.items
13 | import androidx.compose.foundation.shape.RoundedCornerShape
14 | import androidx.compose.material.Text
15 | import androidx.compose.runtime.Composable
16 | import androidx.compose.runtime.LaunchedEffect
17 | import androidx.compose.runtime.rememberCoroutineScope
18 | import androidx.compose.ui.Alignment
19 | import androidx.compose.ui.Modifier
20 | import androidx.compose.ui.draw.clip
21 | import androidx.compose.ui.graphics.Color
22 | import androidx.compose.ui.layout.ContentScale
23 | import androidx.compose.ui.platform.LocalContext
24 | import androidx.compose.ui.res.stringResource
25 | import androidx.compose.ui.text.font.FontWeight
26 | import androidx.compose.ui.unit.dp
27 | import androidx.compose.ui.unit.sp
28 | import androidx.lifecycle.viewmodel.compose.viewModel
29 | import coil.compose.rememberAsyncImagePainter
30 | import com.fmt.compose.eyepetizer.R
31 | import com.fmt.compose.eyepetizer.db.Video
32 | import com.fmt.compose.eyepetizer.ext.toJson
33 | import com.fmt.compose.eyepetizer.model.ItemData
34 | import com.fmt.compose.eyepetizer.pages.detail.VideoDetailActivity
35 | import com.fmt.compose.eyepetizer.pages.person.viewmodel.WatchRecordViewModel
36 | import com.fmt.compose.eyepetizer.ui.theme.Black_26
37 | import com.fmt.compose.eyepetizer.ui.theme.Black_54
38 | import com.fmt.compose.eyepetizer.ui.theme.Black_87
39 | import com.fmt.compose.eyepetizer.util.DateUtils
40 | import com.fmt.compose.eyepetizer.view.Swipe
41 | import com.fmt.compose.eyepetizer.view.TopTitleAppBar
42 | import com.fmt.compose.eyepetizer.view.rememberSwipeState
43 | import kotlinx.coroutines.CoroutineScope
44 | import kotlinx.coroutines.launch
45 |
46 | @Composable
47 | internal fun WatchRecordPage(viewModel: WatchRecordViewModel = viewModel()) {
48 | LaunchedEffect(Unit) {
49 | viewModel.getVideoList()
50 | }
51 | val coroutineScope = rememberCoroutineScope()
52 | val context = LocalContext.current
53 |
54 | Column(Modifier.fillMaxSize()) {
55 | TopTitleAppBar(title = stringResource(id = R.string.watch_record)) {
56 | if (context is Activity) {
57 | context.finish()
58 | }
59 | }
60 | LazyColumn {
61 | items(viewModel.videoList) { videoItem ->
62 | VideoItemWidget(modifier = Modifier.animateItemPlacement(),
63 | videoItem,
64 | coroutineScope) {
65 | viewModel.deleteVideo(Video(it.id, toJson(it)))
66 | }
67 | }
68 | }
69 | }
70 | }
71 |
72 | @Composable
73 | fun VideoItemWidget(
74 | modifier: Modifier = Modifier,
75 | itemData: ItemData?,
76 | scope: CoroutineScope,
77 | onDelete: (itemData: ItemData) -> Unit,
78 | ) {
79 | val context = LocalContext.current
80 | val swipeState = rememberSwipeState()
81 | itemData?.run {
82 | Swipe(modifier = modifier, state = swipeState, background = {
83 | Box(modifier = Modifier
84 | .width(66.dp)
85 | .fillMaxHeight()
86 | .background(Color.Red)
87 | .clickable {
88 | scope.launch {
89 | swipeState.close()
90 | }
91 | onDelete(itemData)
92 | }, contentAlignment = Alignment.Center) {
93 | Text(text = "删除", color = Color.White)
94 | }
95 | }) {
96 | Row(modifier = Modifier
97 | .padding(start = 10.dp, top = 10.dp, end = 10.dp, bottom = 5.dp)
98 | .clickable {
99 | VideoDetailActivity.start(context, itemData)
100 | }) {
101 | Box {
102 | Image(painter = rememberAsyncImagePainter(model = itemData.cover.detail),
103 | contentDescription = null,
104 | modifier = Modifier
105 | .width(135.dp)
106 | .height(80.dp)
107 | .clip(RoundedCornerShape(5.dp)),
108 | contentScale = ContentScale.Crop)
109 | Text(text = DateUtils.formatDateMsByMS(itemData.duration.toLong() * 1000),
110 | color = Color.White,
111 | fontWeight = FontWeight.Bold,
112 | fontSize = 10.sp,
113 | modifier = Modifier
114 | .padding(end = 5.dp, bottom = 5.dp)
115 | .clip(RoundedCornerShape(5.dp))
116 | .background(Black_54)
117 | .padding(3.dp)
118 | .align(Alignment.BottomEnd))
119 | }
120 | Column(modifier = Modifier
121 | .padding(10.dp)
122 | .weight(1f)) {
123 | Text(
124 | text = itemData.title,
125 | color = Black_87,
126 | fontWeight = FontWeight.Bold,
127 | fontSize = 14.sp,
128 | )
129 | Text(text = "#${itemData.category} / ${itemData.author?.name}",
130 | color = Black_26,
131 | fontSize = 12.sp,
132 | modifier = Modifier.padding(top = 15.dp))
133 | }
134 | }
135 | }
136 | }
137 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/fmt/compose/eyepetizer/pages/person/viewmodel/PersonViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.fmt.compose.eyepetizer.pages.person.viewmodel
2 |
3 | import androidx.compose.runtime.mutableStateListOf
4 | import androidx.lifecycle.ViewModel
5 | import com.fmt.compose.eyepetizer.R
6 | import com.fmt.compose.eyepetizer.mainApplication
7 |
8 | class PersonViewModel : ViewModel() {
9 |
10 | val titles = mutableStateListOf(
11 | mainApplication.getString(R.string.my_message),
12 | mainApplication.getString(R.string.my_record),
13 | mainApplication.getString(R.string.my_cache),
14 | mainApplication.getString(R.string.watch_record),
15 | mainApplication.getString(R.string.my_barrage),
16 | mainApplication.getString(R.string.my_add_chase),
17 | mainApplication.getString(R.string.personalized_attire),
18 | mainApplication.getString(R.string.my_bookshelf),
19 | mainApplication.getString(R.string.my_chicken_leg),
20 | mainApplication.getString(R.string.my_circle),
21 | mainApplication.getString(R.string.my_order),
22 | mainApplication.getString(R.string.my_love),
23 | mainApplication.getString(R.string.study_center),
24 | mainApplication.getString(R.string.feedback),
25 | )
26 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/fmt/compose/eyepetizer/pages/person/viewmodel/WatchRecordViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.fmt.compose.eyepetizer.pages.person.viewmodel
2 |
3 | import androidx.compose.runtime.mutableStateListOf
4 | import androidx.lifecycle.ViewModel
5 | import androidx.lifecycle.viewModelScope
6 | import com.fmt.compose.eyepetizer.db.CacheManager
7 | import com.fmt.compose.eyepetizer.db.Video
8 | import com.fmt.compose.eyepetizer.ext.fromJson
9 | import com.fmt.compose.eyepetizer.model.ItemData
10 | import kotlinx.coroutines.launch
11 |
12 | class WatchRecordViewModel : ViewModel() {
13 |
14 | val videoList = mutableStateListOf()
15 |
16 | fun getVideoList() {
17 | viewModelScope.launch {
18 | val videos = CacheManager.get().videoDao.getVideoList()
19 | if (videos.isNotEmpty()) {
20 | videoList.clear()
21 | videoList.addAll(videos.map { fromJson(it.content) }.toList())
22 | }
23 | }
24 | }
25 |
26 | fun deleteVideo(video: Video) {
27 | viewModelScope.launch {
28 | CacheManager.get().videoDao.delete(video)
29 | videoList.removeAll {
30 | it.id == video.videoId
31 | }
32 | }
33 | }
34 |
35 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/fmt/compose/eyepetizer/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package com.fmt.compose.eyepetizer.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val Purple200 = Color(0xFFBB86FC)
6 | val Purple500 = Color(0xFF6200EE)
7 | val Purple700 = Color(0xFF3700B3)
8 | val Teal200 = Color(0xFF03DAC5)
9 | val SelectedItemColor = Color(0xff000000)
10 | val UnselectedItemColor = Color(0xff9a9a9a)
11 | val White_12 = Color(0x3DFFFFFF)
12 | val White_54 = Color(0x8AFFFFFF)
13 | val Black_12 = Color(0x1F000000)
14 | val Black_26 = Color(0xFF424242)
15 | val Black_38 = Color(0x61000000)
16 | val Black_54 = Color(0x8A000000)
17 | val Black_87 = Color(0xDE000000)
18 | val Follow_bg = Color(0xFFF4F4F4)
19 | val Border_bg = Color(0xFFF5F5F5)
20 | val News_bg = Color(0xFFEDEDED)
--------------------------------------------------------------------------------
/app/src/main/java/com/fmt/compose/eyepetizer/ui/theme/Shape.kt:
--------------------------------------------------------------------------------
1 | package com.fmt.compose.eyepetizer.ui.theme
2 |
3 | import androidx.compose.foundation.shape.RoundedCornerShape
4 | import androidx.compose.material.Shapes
5 | import androidx.compose.ui.unit.dp
6 |
7 | val Shapes = Shapes(
8 | small = RoundedCornerShape(4.dp),
9 | medium = RoundedCornerShape(4.dp),
10 | large = RoundedCornerShape(0.dp)
11 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/fmt/compose/eyepetizer/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package com.fmt.compose.eyepetizer.ui.theme
2 |
3 | import androidx.compose.foundation.isSystemInDarkTheme
4 | import androidx.compose.material.MaterialTheme
5 | import androidx.compose.material.darkColors
6 | import androidx.compose.material.lightColors
7 | import androidx.compose.runtime.Composable
8 |
9 | private val DarkColorPalette = darkColors(
10 | primary = Purple200,
11 | primaryVariant = Purple700,
12 | secondary = Teal200
13 | )
14 |
15 | private val LightColorPalette = lightColors(
16 | primary = Purple500,
17 | primaryVariant = Purple700,
18 | secondary = Teal200
19 |
20 | /* Other default colors to override
21 | background = Color.White,
22 | surface = Color.White,
23 | onPrimary = Color.White,
24 | onSecondary = Color.Black,
25 | onBackground = Color.Black,
26 | onSurface = Color.Black,
27 | */
28 | )
29 |
30 | @Composable
31 | fun Compose_EyepetizerTheme(
32 | darkTheme: Boolean = isSystemInDarkTheme(),
33 | content: @Composable () -> Unit
34 | ) {
35 | val colors = if (darkTheme) {
36 | DarkColorPalette
37 | } else {
38 | LightColorPalette
39 | }
40 |
41 | MaterialTheme(
42 | colors = colors,
43 | typography = Typography,
44 | shapes = Shapes,
45 | content = content
46 | )
47 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/fmt/compose/eyepetizer/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package com.fmt.compose.eyepetizer.ui.theme
2 |
3 | import androidx.compose.material.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.FontFamily
6 | import androidx.compose.ui.text.font.FontWeight
7 | import androidx.compose.ui.unit.sp
8 |
9 | // Set of Material typography styles to start with
10 | val Typography = Typography(
11 | body1 = TextStyle(
12 | fontFamily = FontFamily.Default,
13 | fontWeight = FontWeight.Normal,
14 | fontSize = 16.sp
15 | )
16 | /* Other default text styles to override
17 | button = TextStyle(
18 | fontFamily = FontFamily.Default,
19 | fontWeight = FontWeight.W500,
20 | fontSize = 14.sp
21 | ),
22 | caption = TextStyle(
23 | fontFamily = FontFamily.Default,
24 | fontWeight = FontWeight.Normal,
25 | fontSize = 12.sp
26 | )
27 | */
28 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/fmt/compose/eyepetizer/util/DateUtils.kt:
--------------------------------------------------------------------------------
1 | package com.fmt.compose.eyepetizer.util
2 |
3 | import java.text.SimpleDateFormat
4 | import java.util.*
5 |
6 | object DateUtils {
7 |
8 | private val msDateFormat = SimpleDateFormat("mm:ss", Locale.CHINA)
9 | private val ymsDateFormat = SimpleDateFormat("yyyy/MM/dd", Locale.CHINA)
10 | private val ymdHsDateFormat = SimpleDateFormat("yyyy/MM/dd HH:mm", Locale.CHINA)
11 |
12 | fun formatDateMsByMS(milliseconds: Long): String {
13 | return msDateFormat.format(Date(milliseconds))
14 | }
15 |
16 | fun formatDateMsByYMD(milliseconds: Long): String {
17 | return ymsDateFormat.format(Date(milliseconds))
18 | }
19 |
20 | fun formatDateMsByYMDHM(milliseconds: Long): String {
21 | return ymdHsDateFormat.format(Date(milliseconds))
22 | }
23 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/fmt/compose/eyepetizer/util/ScreenUtils.kt:
--------------------------------------------------------------------------------
1 | package com.fmt.compose.eyepetizer.util
2 |
3 | import com.fmt.compose.eyepetizer.mainApplication
4 |
5 | class ScreenUtils private constructor() {
6 |
7 | private var screenWidth = 0
8 | private var screenHeight = 0
9 |
10 | init {
11 | screenWidth = mainApplication.resources.displayMetrics.widthPixels
12 | screenHeight = mainApplication.resources.displayMetrics.heightPixels
13 | }
14 |
15 | companion object {
16 | val instance by lazy { ScreenUtils() }
17 | }
18 |
19 | fun getScreenWidth(): Int {
20 | return screenWidth
21 | }
22 |
23 | fun getScreenHeight(): Int {
24 | return screenHeight
25 | }
26 |
27 |
28 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/fmt/compose/eyepetizer/view/Swipe.kt:
--------------------------------------------------------------------------------
1 | package com.fmt.compose.eyepetizer.view
2 |
3 | import android.annotation.SuppressLint
4 | import android.util.Log
5 | import androidx.compose.foundation.gestures.Orientation
6 | import androidx.compose.foundation.layout.Box
7 | import androidx.compose.foundation.layout.fillMaxWidth
8 | import androidx.compose.foundation.layout.height
9 | import androidx.compose.foundation.layout.offset
10 | import androidx.compose.material.*
11 | import androidx.compose.runtime.*
12 | import androidx.compose.runtime.saveable.Saver
13 | import androidx.compose.runtime.saveable.rememberSaveable
14 | import androidx.compose.ui.Modifier
15 | import androidx.compose.ui.draw.clipToBounds
16 | import androidx.compose.ui.layout.onSizeChanged
17 | import androidx.compose.ui.platform.LocalDensity
18 | import androidx.compose.ui.unit.IntOffset
19 | import kotlinx.coroutines.launch
20 | import kotlin.math.roundToInt
21 |
22 | @SuppressLint("RememberReturnType")
23 | @OptIn(ExperimentalMaterialApi::class)
24 | @Composable
25 | fun Swipe(
26 | modifier: Modifier = Modifier,
27 | state: SwipeState = rememberSwipeState(),
28 | threshold: Float = 0.3f,
29 | direction: SwipeDirection = SwipeDirection.RightToLeft,
30 | onChange: ((open: Boolean) -> Unit)? = null,
31 | background: @Composable () -> Unit,
32 | content: @Composable () -> Unit,
33 | ) {
34 | var boxWidthPx by remember {
35 | mutableStateOf(0)
36 | }
37 | var boxHeightPx by remember {
38 | mutableStateOf(0)
39 | }
40 | var backgroundWidthPx by remember {
41 | mutableStateOf(0)
42 | }
43 |
44 | val scope = rememberCoroutineScope()
45 | val swipeAbleState = rememberSwipeableState(0)
46 | state.swipeableState = swipeAbleState
47 | val anchors by remember(backgroundWidthPx, direction) {
48 | if (direction == SwipeDirection.RightToLeft) {
49 | mutableStateOf(mapOf(0f to 0, -backgroundWidthPx.toFloat() to 1))
50 | } else {
51 | mutableStateOf(mapOf(0f to 0, backgroundWidthPx.toFloat() to 1))
52 | }
53 | }
54 |
55 | remember(swipeAbleState.currentValue) {
56 | Log.d("Swipe", "swipeAbleState.currentValue:${swipeAbleState.currentValue}")
57 | onChange?.invoke(swipeAbleState.currentValue == 1)
58 | }
59 |
60 | LaunchedEffect(key1 = Unit) {
61 | when (state.currentValue) {
62 | SwipeValue.Hidden -> scope.launch {
63 | swipeAbleState.animateTo(0)
64 | }
65 | SwipeValue.Open -> scope.launch { swipeAbleState.animateTo(1) }
66 | }
67 | }
68 | val swipeModifier = if (backgroundWidthPx > 0) Modifier.swipeable(
69 | state = swipeAbleState,
70 | anchors = anchors,
71 | thresholds = { _, _ -> FractionalThreshold(threshold) },
72 | orientation = Orientation.Horizontal,
73 | ) else Modifier
74 | Box(modifier = Modifier
75 | .onSizeChanged {
76 | boxWidthPx = it.width
77 | boxHeightPx = it.height
78 | }
79 | .clipToBounds()
80 | .then(swipeModifier)
81 | .then(modifier)
82 | ) {
83 |
84 | val backgroundOffsetX = when (direction) {
85 | SwipeDirection.LeftToRight -> -backgroundWidthPx + swipeAbleState.offset.value.roundToInt()
86 | SwipeDirection.RightToLeft -> boxWidthPx + swipeAbleState.offset.value.roundToInt()
87 | }
88 | Box(modifier = Modifier
89 | .fillMaxWidth()
90 | .offset {
91 | IntOffset(
92 | x = swipeAbleState.offset.value.roundToInt(),
93 | y = 0
94 | )
95 | }) {
96 | content()
97 | }
98 | Box(modifier = Modifier
99 | .height(with(LocalDensity.current) { boxHeightPx.toDp() })
100 | .onSizeChanged {
101 | backgroundWidthPx = it.width
102 | }
103 | .offset {
104 | IntOffset(
105 | x = backgroundOffsetX,
106 | y = 0
107 | )
108 | }
109 | ) {
110 | background()
111 | }
112 |
113 | }
114 | }
115 |
116 | sealed class SwipeDirection {
117 | object LeftToRight : SwipeDirection()
118 | object RightToLeft : SwipeDirection()
119 | }
120 |
121 | enum class SwipeValue {
122 | Hidden,
123 | Open,
124 | }
125 |
126 |
127 | @Composable
128 | fun rememberSwipeState(
129 | initialValue: SwipeValue = SwipeValue.Hidden
130 | ): SwipeState = rememberSaveable(saver = SwipeState.SAVER) {
131 | SwipeState(
132 | initialValue = initialValue,
133 | )
134 | }
135 |
136 | @OptIn(ExperimentalMaterialApi::class)
137 | class SwipeState(
138 | val initialValue: SwipeValue,
139 | ) {
140 | internal lateinit var swipeableState: SwipeableState
141 | private var _currentValue: SwipeValue by mutableStateOf(initialValue)
142 | val currentValue: SwipeValue
143 | get() = _currentValue
144 |
145 | suspend fun open() {
146 | if (swipeableState.currentValue != 1) {
147 | swipeableState.animateTo(1)
148 | }
149 | }
150 |
151 | suspend fun close() {
152 | if (swipeableState.currentValue != 0) {
153 | swipeableState.animateTo(0)
154 | }
155 | }
156 |
157 | fun isOpen(): Boolean = swipeableState.currentValue == 1
158 |
159 | companion object {
160 | val SAVER: Saver = Saver(
161 | save = {
162 | it.initialValue
163 | },
164 | restore = {
165 | SwipeState(it)
166 | }
167 | )
168 | }
169 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/fmt/compose/eyepetizer/view/TopAppBar.kt:
--------------------------------------------------------------------------------
1 | package com.fmt.compose.eyepetizer.view
2 |
3 | import androidx.compose.material.Icon
4 | import androidx.compose.material.IconButton
5 | import androidx.compose.material.Text
6 | import androidx.compose.material.TopAppBar
7 | import androidx.compose.material.icons.Icons
8 | import androidx.compose.material.icons.outlined.ArrowBack
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.ui.graphics.Color
11 | import androidx.compose.ui.text.font.FontWeight
12 | import androidx.compose.ui.text.style.TextAlign
13 | import androidx.compose.ui.text.style.TextOverflow
14 | import androidx.compose.ui.unit.sp
15 |
16 | @Composable
17 | fun TopTitleAppBar(title: String, onBack: () -> Unit) {
18 | TopAppBar(title = {
19 | Text(text = title,
20 | fontSize = 18.sp,
21 | fontWeight = FontWeight.Bold,
22 | color = Color.Black,
23 | textAlign = TextAlign.Center,
24 | maxLines = 1,
25 | overflow = TextOverflow.Ellipsis)
26 | }, backgroundColor = Color.White,
27 | navigationIcon = {
28 | IconButton(onClick = {
29 | onBack()
30 | }) {
31 | Icon(imageVector = Icons.Outlined.ArrowBack, contentDescription = null)
32 | }
33 | })
34 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/launch_layer_list.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | -
5 |
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fmtjava/Compose_Eyepetizer/3c3fd7842697368451550976638fb64ed3bfd817/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fmtjava/Compose_Eyepetizer/3c3fd7842697368451550976638fb64ed3bfd817/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fmtjava/Compose_Eyepetizer/3c3fd7842697368451550976638fb64ed3bfd817/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fmtjava/Compose_Eyepetizer/3c3fd7842697368451550976638fb64ed3bfd817/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fmtjava/Compose_Eyepetizer/3c3fd7842697368451550976638fb64ed3bfd817/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fmtjava/Compose_Eyepetizer/3c3fd7842697368451550976638fb64ed3bfd817/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/home__ic_mine_normal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fmtjava/Compose_Eyepetizer/3c3fd7842697368451550976638fb64ed3bfd817/app/src/main/res/mipmap-xxhdpi/home__ic_mine_normal.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/home_ic_discovery_normal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fmtjava/Compose_Eyepetizer/3c3fd7842697368451550976638fb64ed3bfd817/app/src/main/res/mipmap-xxhdpi/home_ic_discovery_normal.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/home_ic_discovery_selected.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fmtjava/Compose_Eyepetizer/3c3fd7842697368451550976638fb64ed3bfd817/app/src/main/res/mipmap-xxhdpi/home_ic_discovery_selected.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/home_ic_hot_normal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fmtjava/Compose_Eyepetizer/3c3fd7842697368451550976638fb64ed3bfd817/app/src/main/res/mipmap-xxhdpi/home_ic_hot_normal.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/home_ic_hot_selected.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fmtjava/Compose_Eyepetizer/3c3fd7842697368451550976638fb64ed3bfd817/app/src/main/res/mipmap-xxhdpi/home_ic_hot_selected.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/home_ic_img_avatar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fmtjava/Compose_Eyepetizer/3c3fd7842697368451550976638fb64ed3bfd817/app/src/main/res/mipmap-xxhdpi/home_ic_img_avatar.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/home_ic_mine_selected.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fmtjava/Compose_Eyepetizer/3c3fd7842697368451550976638fb64ed3bfd817/app/src/main/res/mipmap-xxhdpi/home_ic_mine_selected.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/home_ic_normal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fmtjava/Compose_Eyepetizer/3c3fd7842697368451550976638fb64ed3bfd817/app/src/main/res/mipmap-xxhdpi/home_ic_normal.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/home_ic_selected.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fmtjava/Compose_Eyepetizer/3c3fd7842697368451550976638fb64ed3bfd817/app/src/main/res/mipmap-xxhdpi/home_ic_selected.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/home_launch_screen.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fmtjava/Compose_Eyepetizer/3c3fd7842697368451550976638fb64ed3bfd817/app/src/main/res/mipmap-xxhdpi/home_launch_screen.jpg
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_head_bg.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fmtjava/Compose_Eyepetizer/3c3fd7842697368451550976638fb64ed3bfd817/app/src/main/res/mipmap-xxhdpi/ic_head_bg.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fmtjava/Compose_Eyepetizer/3c3fd7842697368451550976638fb64ed3bfd817/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fmtjava/Compose_Eyepetizer/3c3fd7842697368451550976638fb64ed3bfd817/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_splash.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fmtjava/Compose_Eyepetizer/3c3fd7842697368451550976638fb64ed3bfd817/app/src/main/res/mipmap-xxhdpi/ic_splash.jpg
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fmtjava/Compose_Eyepetizer/3c3fd7842697368451550976638fb64ed3bfd817/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fmtjava/Compose_Eyepetizer/3c3fd7842697368451550976638fb64ed3bfd817/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Eyepetizer
3 | 日报
4 | 发现
5 | 热门
6 | 我的
7 | 加载失败
8 | +关注
9 | 查看个人主页 >
10 | 收藏
11 | 评论
12 | 我的消息
13 | 我的记录
14 | 我的缓存
15 | 观看记录
16 | 我的弹幕
17 | 我的加追
18 | 个性装扮
19 | 我的书架
20 | 我的鸡腿
21 | 我的圈儿
22 | 我的订单
23 | 我的点赞
24 | 学习中心
25 | 意见反馈
26 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/app/src/test/java/com/fmt/compose/eyepetizer/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.fmt.compose.eyepetizer
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | ext {
3 | compose_ui_version = '1.2.0'
4 | }
5 | repositories {
6 | maven { url "https://www.jitpack.io" }
7 | }
8 | }// Top-level build file where you can add configuration options common to all sub-projects/modules.
9 | plugins {
10 | id 'com.android.application' version '7.3.1' apply false
11 | id 'com.android.library' version '7.3.1' apply false
12 | id 'org.jetbrains.kotlin.android' version '1.8.0' apply false
13 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fmtjava/Compose_Eyepetizer/3c3fd7842697368451550976638fb64ed3bfd817/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Mon Aug 14 17:58:50 CST 2023
2 | distributionBase=GRADLE_USER_HOME
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
4 | distributionPath=wrapper/dists
5 | zipStorePath=wrapper/dists
6 | zipStoreBase=GRADLE_USER_HOME
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | gradlePluginPortal()
4 | google()
5 | mavenCentral()
6 | maven { url 'https://maven.aliyun.com/repository/releases' }
7 | maven { url 'https://maven.aliyun.com/repository/google' }
8 | maven { url 'https://maven.aliyun.com/repository/central' }
9 | maven { url 'https://maven.aliyun.com/repository/gradle-plugin' }
10 | maven { url 'https://maven.aliyun.com/repository/public' }
11 | maven { url 'https://www.jitpack.io' }
12 | }
13 | }
14 | dependencyResolutionManagement {
15 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
16 | repositories {
17 | google()
18 | mavenCentral()
19 | maven { url 'https://maven.aliyun.com/repository/releases' }
20 | maven { url 'https://maven.aliyun.com/repository/google' }
21 | maven { url 'https://maven.aliyun.com/repository/central' }
22 | maven { url 'https://maven.aliyun.com/repository/gradle-plugin' }
23 | maven { url 'https://maven.aliyun.com/repository/public' }
24 | maven { url 'https://www.jitpack.io' }
25 | }
26 | }
27 | rootProject.name = "Compose_Eyepetizer"
28 | include ':app'
29 |
--------------------------------------------------------------------------------