├── .gitignore ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── imcys │ │ └── foodchoice │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── ic_launcher-playstore.png │ ├── java │ │ └── com │ │ │ └── imcys │ │ │ └── foodchoice │ │ │ ├── FoodApplication.kt │ │ │ ├── MainActivity.kt │ │ │ ├── MainActivityIntent.kt │ │ │ ├── MainActivityState.kt │ │ │ ├── MainActivityViewModel.kt │ │ │ ├── data │ │ │ └── NavItem.kt │ │ │ ├── navigation │ │ │ ├── FCNavHost.kt │ │ │ ├── HomeNavigation.kt │ │ │ └── SettingNavigation.kt │ │ │ ├── ui │ │ │ ├── FoodApp.kt │ │ │ ├── home │ │ │ │ ├── HomeIntent.kt │ │ │ │ ├── HomeScreen.kt │ │ │ │ ├── HomeState.kt │ │ │ │ └── HomeViewModel.kt │ │ │ ├── index │ │ │ │ ├── IndexIntent.kt │ │ │ │ ├── IndexScreen.kt │ │ │ │ ├── IndexState.kt │ │ │ │ └── IndexViewModel.kt │ │ │ └── setting │ │ │ │ ├── SettingIntent.kt │ │ │ │ ├── SettingScreen.kt │ │ │ │ ├── SettingState.kt │ │ │ │ └── SettingViewModel.kt │ │ │ └── weight │ │ │ ├── Konfetti.kt │ │ │ └── WaifuBoostKonfettiView.kt │ └── res │ │ ├── drawable-v31 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── cookingpot.xml │ │ ├── ic_github_24.xml │ │ ├── ic_launcher_background.xml │ │ ├── ic_launcher_foreground.xml │ │ └── ic_lib_bilibili.png │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.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 │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── values │ │ ├── colors.xml │ │ ├── ic_launcher_background.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ └── data_extraction_rules.xml │ └── test │ └── java │ └── com │ └── imcys │ └── foodchoice │ └── ExampleUnitTest.kt ├── build.gradle.kts ├── core ├── common │ ├── .gitignore │ ├── build.gradle.kts │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ └── src │ │ ├── androidTest │ │ └── java │ │ │ └── com │ │ │ └── imcys │ │ │ └── core │ │ │ └── common │ │ │ └── ExampleInstrumentedTest.kt │ │ ├── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── com │ │ │ └── imcys │ │ │ └── core │ │ │ └── common │ │ │ ├── data │ │ │ ├── BaseRepository.kt │ │ │ └── IBaseRepository.kt │ │ │ ├── extend │ │ │ ├── Coroutines.kt │ │ │ └── Number.kt │ │ │ ├── utils │ │ │ └── VibrationUtils.kt │ │ │ └── viewmodel │ │ │ ├── ComposeBaseViewModel.kt │ │ │ ├── info │ │ │ ├── IViewModelHandle.kt │ │ │ ├── UiEffect.kt │ │ │ ├── UiIntent.kt │ │ │ └── UiState.kt │ │ │ └── ui │ │ │ └── BaseComponentActivity.kt │ │ └── test │ │ └── java │ │ └── com │ │ └── imcys │ │ └── core │ │ └── common │ │ └── ExampleUnitTest.kt ├── data │ ├── .gitignore │ ├── build.gradle.kts │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ └── src │ │ ├── androidTest │ │ └── java │ │ │ └── com │ │ │ └── imcys │ │ │ └── core │ │ │ └── data │ │ │ └── ExampleInstrumentedTest.kt │ │ ├── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── com │ │ │ └── imcys │ │ │ └── core │ │ │ └── data │ │ │ ├── extend │ │ │ ├── Convert.kt │ │ │ └── Request.kt │ │ │ └── repository │ │ │ └── cook │ │ │ ├── CookFoodInfoRepository.kt │ │ │ └── CookingIngredientRepository.kt │ │ └── test │ │ └── java │ │ └── com │ │ └── imcys │ │ └── core │ │ └── data │ │ └── ExampleUnitTest.kt ├── database │ ├── .gitignore │ ├── build.gradle.kts │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ └── src │ │ ├── androidTest │ │ └── java │ │ │ └── com │ │ │ └── imcys │ │ │ └── core │ │ │ └── database │ │ │ └── ExampleInstrumentedTest.kt │ │ ├── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── com │ │ │ └── imcys │ │ │ └── core │ │ │ └── database │ │ │ ├── AppDatabase.kt │ │ │ ├── DaosModule.kt │ │ │ ├── DatabaseModule.kt │ │ │ ├── dao │ │ │ ├── CookFoodDao.kt │ │ │ └── CookingIngredientDao.kt │ │ │ └── entity │ │ │ ├── CookFoodEntity.kt │ │ │ └── CookingIngredientEntity.kt │ │ └── test │ │ └── java │ │ └── com │ │ └── imcys │ │ └── core │ │ └── database │ │ └── ExampleUnitTest.kt ├── designsystem │ ├── .gitignore │ ├── build.gradle.kts │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ └── src │ │ ├── androidTest │ │ └── java │ │ │ └── com │ │ │ └── imcys │ │ │ └── core │ │ │ └── designsystem │ │ │ └── ExampleInstrumentedTest.kt │ │ ├── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── com │ │ │ └── imcys │ │ │ └── core │ │ │ └── designsystem │ │ │ └── theme │ │ │ ├── Color.kt │ │ │ ├── Theme.kt │ │ │ └── Type.kt │ │ └── test │ │ └── java │ │ └── com │ │ └── imcys │ │ └── core │ │ └── designsystem │ │ └── ExampleUnitTest.kt ├── model │ ├── .gitignore │ ├── build.gradle.kts │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ └── src │ │ ├── androidTest │ │ └── java │ │ │ └── com │ │ │ └── imcys │ │ │ └── core │ │ │ └── model │ │ │ └── ExampleInstrumentedTest.kt │ │ ├── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── com │ │ │ └── imcys │ │ │ └── core │ │ │ └── model │ │ │ ├── HomeItemInfo.kt │ │ │ └── cook │ │ │ ├── CookFoodInfo.kt │ │ │ ├── CookFoodVideoInfo.kt │ │ │ ├── CookingIngredient.kt │ │ │ └── CookingIngredientsInfo.kt │ │ └── test │ │ └── java │ │ └── com │ │ └── imcys │ │ └── core │ │ └── model │ │ └── ExampleUnitTest.kt ├── network │ ├── .gitignore │ ├── build.gradle.kts │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ └── src │ │ ├── androidTest │ │ └── java │ │ │ └── com │ │ │ └── imcys │ │ │ └── core │ │ │ └── network │ │ │ └── ExampleInstrumentedTest.kt │ │ ├── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── com │ │ │ └── imcys │ │ │ └── core │ │ │ └── network │ │ │ ├── interceptor │ │ │ └── RFErrorHandlingInterceptor.kt │ │ │ └── retrofit │ │ │ ├── RetrofitAppApi.kt │ │ │ ├── RetrofitAppNetwork.kt │ │ │ └── RetrofitBiliBiliApi.kt │ │ └── test │ │ └── java │ │ └── com │ │ └── imcys │ │ └── core │ │ └── network │ │ └── ExampleUnitTest.kt └── ui │ ├── .gitignore │ ├── build.gradle.kts │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── imcys │ │ └── core │ │ └── ui │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── imcys │ │ │ └── core │ │ │ └── ui │ │ │ ├── CookInfoVideoCard.kt │ │ │ ├── HomeFunctionCard.kt │ │ │ ├── PageContentColumn.kt │ │ │ ├── SettingsItem.kt │ │ │ ├── WaifuBoostAlertDialog.kt │ │ │ └── base │ │ │ └── BaseScreen.kt │ └── res │ │ └── drawable │ │ └── unnamed.jpg │ └── test │ └── java │ └── com │ └── imcys │ └── core │ └── ui │ └── ExampleUnitTest.kt ├── feature └── cook │ ├── .gitignore │ ├── build.gradle.kts │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── imcys │ │ └── feature │ │ └── cook │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── imcys │ │ │ └── feature │ │ │ └── cook │ │ │ ├── CookIntent.kt │ │ │ ├── CookScreen.kt │ │ │ ├── CookState.kt │ │ │ ├── CookViewModel.kt │ │ │ ├── menu │ │ │ └── CookSearchType.kt │ │ │ ├── navigation │ │ │ ├── CookInfoNavigation.kt │ │ │ └── CookNavigation.kt │ │ │ └── ui │ │ │ └── info │ │ │ ├── CookInfoScreen.kt │ │ │ ├── CookInfoState.kt │ │ │ └── CookInfoViewModel.kt │ └── res │ │ └── drawable │ │ └── bilibilias.png │ └── test │ └── java │ └── com │ └── imcys │ └── feature │ └── cook │ └── ExampleUnitTest.kt ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle.kts /.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 | /.idea 12 | /build 13 | /captures 14 | .externalNativeBuild 15 | .cxx 16 | local.properties 17 | /app/release/ 18 | /app/debug/ 19 | .idea 20 | .kotlin 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FoodChoice 2 | 3 | 食选,解决生活中每天吃饭,吃什么,做什么,怎么做的问题,此项目也是我对JetpackCompose的MVI架构学习的一次实践。 4 | 5 | ## 项目介绍 6 | 7 | 此项目为JetpackCompose的一个学习项目,主要对学到的一些简单知识的应用,同时对程序模块化进行一次进一步的尝试。 8 | 9 | 主要采用了MVI设计,实现了离线数据加载,网络加载,依赖注入,导航管理等,使其成为一个稳定可用的程序。 10 | 11 | 大体框架主要参考了谷歌的[nowinandroid](https://github.com/android/nowinandroid/)(只实现了其中的一小部分)。 12 | 13 | ## 程序现阶段框架 14 | 15 | ![](http://message.biliimg.com/bfs/im/20a058ef1cb3d919269fa15cf0d2d60a351201307.png) 16 | 17 | 其中core层主要对公共依赖,公共UI组件,全局主题样式,网络请求,数据持久性以及数据适配器进行了分模块。 18 | 19 | 这样看是杀鸡用了宰牛刀,但FoodChoice主要是对模块化进行学习,另外这样的分模块其实也是有必要的,这让我的代码更有拓展性和维护性,使得项目代码比较茁壮。 20 | 21 | 而feature层主要对各个功能进行了模块化,只是由于我们用了Compose可用用一个activity来完成各个界面的展示,这也是为什么 22 | 没有用**服务发现**,或者**路由组件库**什么的。 23 | 24 | 25 | ## 项目进度 26 | - [x] 实现Cook,食物选择功能 27 | - [x] 各模块适当采用依赖注入 28 | - [ ] 使用统一版本管理(Version Catalog) 29 | - [ ] 考虑对数据同步改为WorkManager任务 30 | - [ ] 实现食物抽签功能 31 | 32 | 33 | ## 参考项目 34 | 首页食物选择参考了[Cook](https://github.com/YunYouJun/cook),[Web项目源地址](https://cook.yunyoujun.cn) 35 | FoodChoice利用Compose进行了一次复刻,其中数据都来自[Cook](https://github.com/YunYouJun/cook)项目。 36 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.application") 3 | id("org.jetbrains.kotlin.android") 4 | id("com.google.devtools.ksp") 5 | id("com.google.dagger.hilt.android") 6 | id("org.jetbrains.kotlin.plugin.compose") 7 | } 8 | 9 | android { 10 | namespace = "com.imcys.foodchoice" 11 | compileSdk = 35 12 | 13 | defaultConfig { 14 | applicationId = "com.imcys.foodchoice" 15 | minSdk = 21 16 | targetSdk = 34 17 | versionCode = 100 18 | versionName = "1.0.0" 19 | 20 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 21 | vectorDrawables { 22 | useSupportLibrary = true 23 | } 24 | } 25 | 26 | buildTypes { 27 | release { 28 | isMinifyEnabled = true 29 | 30 | proguardFiles( 31 | getDefaultProguardFile("proguard-android-optimize.txt"), 32 | "proguard-rules.pro", 33 | ) 34 | } 35 | } 36 | compileOptions { 37 | sourceCompatibility(JavaVersion.VERSION_17) 38 | targetCompatibility(JavaVersion.VERSION_17) 39 | } 40 | 41 | kotlinOptions { 42 | jvmTarget = "17" 43 | } 44 | 45 | buildFeatures { 46 | compose = true 47 | } 48 | 49 | composeOptions { 50 | kotlinCompilerExtensionVersion = "1.5.3" 51 | } 52 | 53 | packaging { 54 | resources { 55 | excludes.add("/META-INF/{AL2.0,LGPL2.1}") 56 | } 57 | } 58 | } 59 | 60 | dependencies { 61 | 62 | // hilt库,实现依赖注入 63 | api(libs.hilt.android) 64 | ksp(libs.hilt.compiler) 65 | 66 | implementation(libs.konfetti.compose) 67 | 68 | implementation(project(":core:common")) 69 | implementation(project(":core:network")) 70 | implementation(project(":core:designsystem")) 71 | implementation(project(":core:ui")) 72 | implementation(project(":core:model")) 73 | implementation(project(":feature:cook")) 74 | 75 | // implementation("androidx.core:core-ktx:1.8.0") 76 | testImplementation(libs.junit) 77 | androidTestImplementation(libs.androidx.junit) 78 | androidTestImplementation(libs.androidx.espresso.core) 79 | } 80 | -------------------------------------------------------------------------------- /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.kts. 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 22 | 23 | # Keep generic signature of Call, Response (R8 full mode strips signatures from non-kept items). 24 | -keep,allowobfuscation,allowshrinking interface retrofit2.Call 25 | -keep,allowobfuscation,allowshrinking class retrofit2.Response 26 | 27 | # With R8 full mode generic signatures are stripped for classes that are not 28 | # kept. Suspend functions are wrapped in continuations where the type argument 29 | # is used. 30 | -keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation 31 | 32 | #保持 Serializable 不被混淆 33 | -keepnames class * implements java.io.Serializable 34 | 35 | -keepclassmembers class * implements android.os.Parcelable { 36 | static final long serialVersionUID; 37 | private static final java.io.ObjectStreamField[] serialPersistentFields; 38 | !static !transient ; 39 | private void writeObject(java.io.ObjectOutputStream); 40 | private void readObject(java.io.ObjectInputStream); 41 | java.lang.Object writeReplace(); 42 | java.lang.Object readResolve(); 43 | } 44 | 45 | 46 | #保持 Serializable 不被混淆并且enum 类也不被混淆 47 | -keepclassmembers class * implements java.io.Serializable { 48 | static final long serialVersionUID; 49 | private static final java.io.ObjectStreamField[] serialPersistentFields; 50 | !static !transient ; 51 | !private ; 52 | !private ; 53 | private void writeObject(java.io.ObjectOutputStream); 54 | private void readObject(java.io.ObjectInputStream); 55 | java.lang.Object writeReplace(); 56 | java.lang.Object readResolve(); 57 | } 58 | 59 | -keep class com.imcys.core.model.**{*;} # 自定义数据模型的bean目录 60 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/imcys/foodchoice/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.foodchoice 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.imcys.foodchoice", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 8 | 9 | 22 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1250422131/FoodChoice/18de8536cd5829fd7bdb0ddb73001ddb98c7fef0/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/com/imcys/foodchoice/FoodApplication.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.foodchoice 2 | 3 | import android.app.Application 4 | import com.microsoft.appcenter.AppCenter 5 | import com.microsoft.appcenter.analytics.Analytics 6 | import com.microsoft.appcenter.crashes.Crashes 7 | import com.microsoft.appcenter.distribute.Distribute 8 | import dagger.hilt.android.HiltAndroidApp 9 | 10 | @HiltAndroidApp 11 | class FoodApplication : Application(){ 12 | override fun onCreate() { 13 | super.onCreate() 14 | application = this 15 | } 16 | companion object{ 17 | lateinit var application:Application 18 | fun initAppCenter(state: Int){ 19 | if (state == 1) { 20 | AppCenter.start( 21 | application, 22 | "0391335a-2bae-4bef-ae0a-c23f592a7613", 23 | Analytics::class.java, 24 | Crashes::class.java, 25 | Distribute::class.java 26 | ) 27 | } else { 28 | AppCenter.start( 29 | application, 30 | "0391335a-2bae-4bef-ae0a-c23f592a7613", 31 | Distribute::class.java 32 | ) 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/com/imcys/foodchoice/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.foodchoice 2 | 3 | import android.content.Context 4 | import android.content.SharedPreferences 5 | import android.os.Bundle 6 | import androidx.activity.compose.setContent 7 | import androidx.activity.viewModels 8 | import androidx.compose.foundation.layout.Box 9 | import androidx.compose.foundation.layout.fillMaxSize 10 | import androidx.compose.ui.Modifier 11 | import androidx.core.view.WindowCompat 12 | import com.imcys.core.common.viewmodel.ui.BaseComponentActivity 13 | import com.imcys.core.designsystem.theme.FoodChoiceTheme 14 | import com.imcys.foodchoice.ui.FoodApp 15 | import com.microsoft.appcenter.AppCenter 16 | import com.microsoft.appcenter.analytics.Analytics 17 | import com.microsoft.appcenter.crashes.Crashes 18 | import com.microsoft.appcenter.distribute.Distribute 19 | import dagger.hilt.android.AndroidEntryPoint 20 | 21 | @AndroidEntryPoint 22 | class MainActivity : BaseComponentActivity() { 23 | 24 | private val viewModel: MainActivityViewModel by viewModels() 25 | 26 | override fun onCreate(savedInstanceState: Bundle?) { 27 | super.onCreate(savedInstanceState) 28 | WindowCompat.setDecorFitsSystemWindows(window, false) 29 | // 统计接入 30 | val shardedPreferences: SharedPreferences = 31 | getSharedPreferences("app_config", Context.MODE_PRIVATE) 32 | val privacyPolicyState = shardedPreferences.getInt("privacy_policy_state", -1) 33 | if (privacyPolicyState == 1) { 34 | AppCenter.start( 35 | application, 36 | "0391335a-2bae-4bef-ae0a-c23f592a7613", 37 | Analytics::class.java, 38 | Crashes::class.java, 39 | Distribute::class.java 40 | ) 41 | } else { 42 | AppCenter.start( 43 | application, 44 | "0391335a-2bae-4bef-ae0a-c23f592a7613", 45 | Distribute::class.java 46 | ) 47 | } 48 | 49 | setContent { 50 | FoodChoiceTheme { 51 | Box( 52 | modifier = Modifier 53 | .fillMaxSize(), 54 | 55 | ) { 56 | FoodApp(viewModel) 57 | } 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/src/main/java/com/imcys/foodchoice/MainActivityIntent.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.foodchoice 2 | 3 | import com.imcys.core.common.viewmodel.info.UiIntent 4 | import javax.inject.Inject 5 | 6 | sealed class MainActivityIntent : UiIntent { 7 | data class SelectNavItem(var index: Int) : MainActivityIntent() 8 | data class SetShowBottomBar(val state: Boolean) : MainActivityIntent() 9 | data class SetPrivacyPolicyState(val state: Int) : MainActivityIntent() 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/imcys/foodchoice/MainActivityState.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.foodchoice 2 | 3 | import androidx.compose.material.icons.Icons 4 | import androidx.compose.material.icons.filled.Home 5 | import androidx.compose.material.icons.filled.Settings 6 | import androidx.compose.material.icons.outlined.Home 7 | import androidx.compose.material.icons.outlined.Settings 8 | import com.imcys.core.common.viewmodel.info.UiState 9 | import com.imcys.foodchoice.data.NavItem 10 | 11 | data class MainActivityState( 12 | val navItemIndex: Int = 0, 13 | val navItems: MutableList = mutableListOf( 14 | NavItem("首页", Icons.Filled.Home, Icons.Outlined.Home), 15 | NavItem("设置", Icons.Filled.Settings, Icons.Outlined.Settings), 16 | ), 17 | val titleState: Boolean = true, 18 | val isShowBottomBar: Boolean = true, 19 | val privacyPolicyState: Int = -1, // -1未知 0拒绝 1接受 20 | 21 | ) : UiState 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/imcys/foodchoice/MainActivityViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.foodchoice 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import android.content.SharedPreferences 6 | import androidx.core.content.edit 7 | import androidx.lifecycle.viewModelScope 8 | import com.imcys.core.common.viewmodel.ComposeBaseViewModel 9 | import com.microsoft.appcenter.AppCenter 10 | import com.microsoft.appcenter.analytics.Analytics 11 | import com.microsoft.appcenter.crashes.Crashes 12 | import com.microsoft.appcenter.distribute.Distribute 13 | import dagger.hilt.android.lifecycle.HiltViewModel 14 | import dagger.hilt.android.qualifiers.ApplicationContext 15 | import kotlinx.coroutines.delay 16 | import kotlinx.coroutines.launch 17 | import javax.inject.Inject 18 | 19 | @HiltViewModel 20 | class MainActivityViewModel @Inject constructor( 21 | @ApplicationContext private val context: Context, 22 | ) : ComposeBaseViewModel( 23 | MainActivityState(), 24 | ) { 25 | private val shardedPreferences: SharedPreferences = 26 | context.getSharedPreferences("app_config", Context.MODE_PRIVATE) 27 | 28 | init { 29 | val privacyPolicyState = shardedPreferences.getInt("privacy_policy_state", -1) 30 | viewStates = 31 | viewStates.copy(privacyPolicyState = privacyPolicyState) 32 | } 33 | 34 | override fun handleEvent(event: MainActivityIntent, state: MainActivityState) { 35 | when (event) { 36 | is MainActivityIntent.SelectNavItem -> selectNavItem(event.index) 37 | is MainActivityIntent.SetShowBottomBar -> { 38 | viewStates.update { copy(isShowBottomBar = event.state) } 39 | } 40 | 41 | is MainActivityIntent.SetPrivacyPolicyState -> { 42 | setPrivacyPolicyState(event.state) 43 | } 44 | } 45 | } 46 | 47 | private fun setPrivacyPolicyState(state: Int) { 48 | viewStates = viewStates.copy(privacyPolicyState = state) 49 | shardedPreferences.edit { 50 | putInt("privacy_policy_state", state) 51 | } 52 | FoodApplication.initAppCenter(state) 53 | } 54 | 55 | private fun selectNavItem(index: Int) { 56 | viewStates = viewStates.copy(titleState = false) 57 | viewModelScope.launch { 58 | delay(250L) 59 | viewStates.update { 60 | copy(titleState = true) 61 | } 62 | } 63 | viewStates.update { 64 | copy(navItemIndex = index) 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /app/src/main/java/com/imcys/foodchoice/data/NavItem.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.foodchoice.data 2 | 3 | import androidx.compose.ui.graphics.vector.ImageVector 4 | 5 | data class NavItem( 6 | val label:String, 7 | val checked: ImageVector, 8 | val unchecked: ImageVector, 9 | ) 10 | -------------------------------------------------------------------------------- /app/src/main/java/com/imcys/foodchoice/navigation/FCNavHost.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.foodchoice.navigation 2 | 3 | import androidx.compose.animation.core.CubicBezierEasing 4 | import androidx.compose.animation.core.tween 5 | import androidx.compose.animation.fadeIn 6 | import androidx.compose.animation.fadeOut 7 | import androidx.compose.animation.scaleIn 8 | import androidx.compose.animation.scaleOut 9 | import androidx.compose.foundation.pager.PagerState 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Modifier 12 | import androidx.navigation.NavHostController 13 | import androidx.navigation.NavType 14 | import androidx.navigation.compose.NavHost 15 | import androidx.navigation.compose.composable 16 | import androidx.navigation.navArgument 17 | import com.imcys.feature.cook.CookRoute 18 | import com.imcys.feature.cook.navigation.cookInfoRoute 19 | import com.imcys.feature.cook.navigation.cookRoute 20 | import com.imcys.feature.cook.ui.info.CookInfoRoute 21 | import com.imcys.foodchoice.ui.home.HomeRoute 22 | import com.imcys.foodchoice.ui.index.IndexRoute 23 | import com.imcys.foodchoice.ui.setting.SettingRoute 24 | 25 | /** 26 | * 路由控制器 27 | * @param navController NavHostController 28 | * @param modifier Modifier 29 | * @param startDestination String 30 | */ 31 | @Composable 32 | fun FCNavHost( 33 | navController: NavHostController, 34 | startDestination: String = "app_index", 35 | modifier: Modifier, 36 | pageState: PagerState, 37 | ) { 38 | NavHost( 39 | navController = navController, 40 | startDestination = startDestination, 41 | modifier = modifier, 42 | popEnterTransition = { 43 | scaleIn( 44 | animationSpec = tween( 45 | durationMillis = 500, 46 | delayMillis = 35, 47 | ), 48 | initialScale = 1.1F, 49 | ) + fadeIn( 50 | animationSpec = tween( 51 | durationMillis = 500, 52 | delayMillis = 35, 53 | ), 54 | ) 55 | }, 56 | popExitTransition = { 57 | scaleOut( 58 | targetScale = 0.9F, 59 | ) + fadeOut( 60 | animationSpec = tween( 61 | durationMillis = 35, 62 | easing = CubicBezierEasing(0.1f, 0.1f, 0f, 1f), 63 | ), 64 | ) 65 | }, 66 | ) { 67 | composable("app_index") { 68 | IndexRoute(modifier = Modifier, navController = navController, pageState = pageState) 69 | } 70 | composable(homeRoute) { 71 | HomeRoute(modifier = Modifier, navController = navController) 72 | } 73 | composable(cookRoute) { 74 | CookRoute(modifier = Modifier, navController = navController) 75 | } 76 | composable( 77 | cookInfoRoute, 78 | arguments = listOf( 79 | navArgument("bvId") { type = NavType.StringType }, 80 | ), 81 | ) { 82 | CookInfoRoute( 83 | it.arguments?.getString("bvId") ?: "", 84 | modifier = Modifier, 85 | navController = navController, 86 | ) 87 | } 88 | composable(settingRoute) { 89 | SettingRoute(modifier = Modifier, navController = navController) 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /app/src/main/java/com/imcys/foodchoice/navigation/HomeNavigation.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.foodchoice.navigation 2 | 3 | import androidx.navigation.NavController 4 | 5 | const val homeRoute = "app_home" 6 | 7 | fun NavController.navigateToHome() { 8 | this.navigate( 9 | route = homeRoute, 10 | builder = { 11 | popUpTo(homeRoute) { 12 | inclusive = true 13 | } 14 | }, 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/imcys/foodchoice/navigation/SettingNavigation.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.foodchoice.navigation 2 | 3 | import androidx.navigation.NavController 4 | 5 | const val settingRoute = "app_setting" 6 | 7 | fun NavController.navigateToSetting() { 8 | this.popBackStack() 9 | this.navigate( 10 | route = settingRoute, 11 | builder = { 12 | saveState() 13 | restoreState = true 14 | }, 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/imcys/foodchoice/ui/home/HomeIntent.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.foodchoice.ui.home 2 | 3 | import com.imcys.core.common.viewmodel.info.UiIntent 4 | import javax.inject.Inject 5 | 6 | open class HomeIntent @Inject constructor() : UiIntent 7 | -------------------------------------------------------------------------------- /app/src/main/java/com/imcys/foodchoice/ui/home/HomeScreen.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.foodchoice.ui.home 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.PaddingValues 6 | import androidx.compose.foundation.layout.fillMaxHeight 7 | import androidx.compose.foundation.layout.padding 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.material3.windowsizeclass.WindowWidthSizeClass 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.runtime.CompositionLocalProvider 14 | import androidx.compose.runtime.compositionLocalOf 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.unit.dp 17 | import androidx.hilt.navigation.compose.hiltViewModel 18 | import androidx.navigation.NavHostController 19 | import com.imcys.core.ui.HomeFunctionCard 20 | import com.imcys.core.ui.base.getWidthSizeClass 21 | 22 | private val LocalViewModel = compositionLocalOf { error("No init!") } 23 | private val LocalViewState = compositionLocalOf { error("No init!") } 24 | private val LocalNavController = compositionLocalOf { error("No init!") } 25 | 26 | @Composable 27 | internal fun HomeRoute( 28 | modifier: Modifier = Modifier, 29 | viewModel: HomeViewModel = hiltViewModel(), 30 | navController: NavHostController, 31 | ) { 32 | CompositionLocalProvider( 33 | LocalViewModel provides viewModel, 34 | LocalViewState provides viewModel.viewStates, 35 | LocalNavController provides navController, 36 | ) { 37 | HomeScreen(modifier) 38 | } 39 | } 40 | 41 | @Composable 42 | fun HomeScreen( 43 | modifier: Modifier = Modifier, 44 | ) { 45 | val navController = LocalNavController.current 46 | val viewState = LocalViewState.current 47 | 48 | LazyVerticalGrid( 49 | horizontalArrangement = Arrangement.spacedBy(12.dp), 50 | verticalArrangement = Arrangement.spacedBy(8.dp), 51 | modifier = modifier.padding(16.dp, 0.dp, 16.dp, 0.dp) 52 | .fillMaxHeight(), 53 | contentPadding = PaddingValues(10.dp), 54 | columns = GridCells.Fixed( 55 | when (getWidthSizeClass()) { 56 | WindowWidthSizeClass.Compact -> 1 57 | WindowWidthSizeClass.Medium -> 2 58 | WindowWidthSizeClass.Expanded -> 3 59 | else -> 3 60 | }, 61 | ), 62 | reverseLayout = false, 63 | ) { 64 | items(viewState.homeItems) { 65 | HomeFunctionCard( 66 | it.title, 67 | it.content, 68 | it.faceUrl, 69 | Modifier 70 | .clickable { 71 | navController.navigate(it.route) 72 | }, 73 | ) 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /app/src/main/java/com/imcys/foodchoice/ui/home/HomeState.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.foodchoice.ui.home 2 | 3 | import com.imcys.core.common.viewmodel.info.UiState 4 | import com.imcys.core.model.HomeItemInfo 5 | import com.imcys.feature.cook.navigation.cookRoute 6 | 7 | data class HomeState( 8 | val homeItems: MutableList = mutableListOf( 9 | HomeItemInfo( 10 | "烹饪指南", 11 | "不知道做什么菜?来这里看看", 12 | "https://message.biliimg.com/bfs/im/b9d53ed962c7734c2803ac4a9c409994a4a491b9.jpg", 13 | route = cookRoute, 14 | ) 15 | ), 16 | ) : UiState 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/imcys/foodchoice/ui/home/HomeViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.foodchoice.ui.home 2 | 3 | import com.imcys.core.common.viewmodel.ComposeBaseViewModel 4 | import javax.inject.Inject 5 | 6 | class HomeViewModel @Inject constructor() : 7 | ComposeBaseViewModel(HomeState()) { 8 | 9 | override fun handleEvent(event: HomeIntent, state: HomeState) { 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/imcys/foodchoice/ui/index/IndexIntent.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.foodchoice.ui.index 2 | 3 | import com.imcys.core.common.viewmodel.info.UiIntent 4 | 5 | class IndexIntent : UiIntent 6 | -------------------------------------------------------------------------------- /app/src/main/java/com/imcys/foodchoice/ui/index/IndexScreen.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.foodchoice.ui.index 2 | 3 | import androidx.compose.foundation.ExperimentalFoundationApi 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.Spacer 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.foundation.layout.width 8 | import androidx.compose.foundation.pager.HorizontalPager 9 | import androidx.compose.foundation.pager.PagerState 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.unit.dp 13 | import androidx.hilt.navigation.compose.hiltViewModel 14 | import androidx.navigation.NavHostController 15 | import com.imcys.foodchoice.ui.home.HomeRoute 16 | import com.imcys.foodchoice.ui.setting.SettingRoute 17 | 18 | @OptIn(ExperimentalFoundationApi::class) 19 | @Composable 20 | internal fun IndexRoute( 21 | modifier: Modifier = Modifier, 22 | viewModel: IndexViewModel = hiltViewModel(), 23 | navController: NavHostController, 24 | pageState: PagerState, 25 | ) { 26 | IndexScreen( 27 | modifier = modifier, 28 | viewModel = viewModel, 29 | viewState = viewModel.viewStates, 30 | navController = navController, 31 | pageState = pageState, 32 | ) 33 | } 34 | 35 | @OptIn(ExperimentalFoundationApi::class) 36 | @Composable 37 | fun IndexScreen( 38 | modifier: Modifier, 39 | viewModel: IndexViewModel, 40 | viewState: IndexState, 41 | navController: NavHostController, 42 | pageState: PagerState, 43 | ) { 44 | Column(modifier = modifier) { 45 | Spacer(modifier = Modifier.width(10.dp)) 46 | 47 | HorizontalPager( 48 | userScrollEnabled = false, 49 | state = pageState, 50 | modifier = Modifier.fillMaxSize(), 51 | ) { pager -> 52 | when (pager) { 53 | 0 -> { 54 | HomeRoute(navController = navController) 55 | } 56 | 57 | 1 -> { 58 | SettingRoute(navController = navController) 59 | } 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/src/main/java/com/imcys/foodchoice/ui/index/IndexState.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.foodchoice.ui.index 2 | 3 | import com.imcys.core.common.viewmodel.info.UiState 4 | 5 | class IndexState : UiState 6 | -------------------------------------------------------------------------------- /app/src/main/java/com/imcys/foodchoice/ui/index/IndexViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.foodchoice.ui.index 2 | 3 | import com.imcys.core.common.viewmodel.ComposeBaseViewModel 4 | import dagger.hilt.android.lifecycle.HiltViewModel 5 | import javax.inject.Inject 6 | 7 | @HiltViewModel 8 | class IndexViewModel @Inject constructor() : 9 | ComposeBaseViewModel(IndexState()) { 10 | override fun handleEvent(event: IndexIntent, state: IndexState) { 11 | 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/imcys/foodchoice/ui/setting/SettingIntent.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.foodchoice.ui.setting 2 | 3 | import com.imcys.core.common.viewmodel.info.UiIntent 4 | import com.imcys.foodchoice.MainActivityIntent 5 | 6 | sealed class SettingIntent : UiIntent{ 7 | data class SetPrivacyPolicyState(val state: Int) : SettingIntent() 8 | } 9 | -------------------------------------------------------------------------------- /app/src/main/java/com/imcys/foodchoice/ui/setting/SettingScreen.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.foodchoice.ui.setting 2 | 3 | import android.content.Intent 4 | import android.net.Uri 5 | import android.text.method.LinkMovementMethod 6 | import android.widget.TextView 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.foundation.lazy.LazyColumn 9 | import androidx.compose.material.icons.Icons 10 | import androidx.compose.material.icons.filled.Category 11 | import androidx.compose.material.icons.filled.Email 12 | import androidx.compose.material.icons.filled.PrivacyTip 13 | import androidx.compose.material.icons.filled.TripOrigin 14 | import androidx.compose.material.icons.outlined.Person 15 | import androidx.compose.material.icons.outlined.PrivacyTip 16 | import androidx.compose.material.icons.outlined.Web 17 | import androidx.compose.material3.Icon 18 | import androidx.compose.material3.MaterialTheme 19 | import androidx.compose.material3.Text 20 | import androidx.compose.material3.TextButton 21 | import androidx.compose.runtime.Composable 22 | import androidx.compose.runtime.getValue 23 | import androidx.compose.runtime.mutableStateOf 24 | import androidx.compose.runtime.remember 25 | import androidx.compose.runtime.setValue 26 | import androidx.compose.ui.Modifier 27 | import androidx.compose.ui.graphics.toArgb 28 | import androidx.compose.ui.graphics.vector.rememberVectorPainter 29 | import androidx.compose.ui.platform.LocalContext 30 | import androidx.compose.ui.res.painterResource 31 | import androidx.compose.ui.res.stringResource 32 | import androidx.compose.ui.viewinterop.AndroidView 33 | import androidx.core.text.HtmlCompat 34 | import androidx.hilt.navigation.compose.hiltViewModel 35 | import androidx.navigation.NavHostController 36 | import com.imcys.core.ui.BaseSettingsItem 37 | import com.imcys.core.ui.CategorySettingsItem 38 | import com.imcys.core.ui.WaifuBoostAlertDialog 39 | import com.imcys.foodchoice.MainActivityIntent 40 | import com.imcys.foodchoice.R 41 | import com.imcys.foodchoice.ui.PrivacyPolicyDialog 42 | 43 | @Composable 44 | internal fun SettingRoute( 45 | modifier: Modifier = Modifier, 46 | viewModel: SettingViewModel = hiltViewModel(), 47 | navController: NavHostController, 48 | ) { 49 | SettingScreen( 50 | modifier = modifier, 51 | viewModel = viewModel, 52 | viewState = viewModel.viewStates, 53 | navController = navController, 54 | ) 55 | } 56 | 57 | @Composable 58 | fun SettingScreen( 59 | modifier: Modifier, 60 | viewModel: SettingViewModel, 61 | viewState: SettingState, 62 | navController: NavHostController, 63 | ) { 64 | var showOriginalDialogState by remember { mutableStateOf(false) } 65 | val context = LocalContext.current 66 | var showAgreePrivacyPolicyDialogState by remember { mutableStateOf(false) } 67 | 68 | LazyColumn(Modifier.fillMaxSize()) { 69 | item { 70 | CategorySettingsItem( 71 | text = "项目" 72 | ) 73 | } 74 | item { 75 | BaseSettingsItem( 76 | painter = rememberVectorPainter(Icons.Default.TripOrigin), 77 | text = "原始项目", 78 | descriptionText = "本项目复刻自云游君的“cook”项目", 79 | onClick = { 80 | showOriginalDialogState = true 81 | } 82 | ) 83 | } 84 | item { 85 | BaseSettingsItem( 86 | painter = rememberVectorPainter(Icons.Default.Email), 87 | text = "立即投稿", 88 | descriptionText = "为本项目添加菜品!", 89 | onClick = { 90 | context.startActivity( 91 | Intent( 92 | Intent.ACTION_VIEW, 93 | Uri.parse("https://docs.qq.com/form/page/DWk9GWW9oTmlXZU9V") 94 | ) 95 | ) 96 | } 97 | ) 98 | } 99 | item { 100 | BaseSettingsItem( 101 | painter = painterResource(id = R.drawable.ic_github_24), 102 | text = "Github", 103 | descriptionText = "本项持续开源维护,来点个Star?", 104 | onClick = { 105 | context.startActivity( 106 | Intent( 107 | Intent.ACTION_VIEW, 108 | Uri.parse("https://github.com/1250422131/FoodChoice") 109 | ) 110 | ) 111 | } 112 | ) 113 | } 114 | item { 115 | CategorySettingsItem( 116 | text = "隐私" 117 | ) 118 | } 119 | item { 120 | 121 | BaseSettingsItem( 122 | painter = rememberVectorPainter(Icons.Default.PrivacyTip), 123 | text = "隐私政策", 124 | descriptionText = "你可以随时撤销你授权的隐私政策。", 125 | onClick = { 126 | showAgreePrivacyPolicyDialogState = true 127 | } 128 | ) 129 | } 130 | item { 131 | CategorySettingsItem( 132 | text = "开发者" 133 | ) 134 | } 135 | item { 136 | BaseSettingsItem( 137 | painter = painterResource(id = R.drawable.ic_lib_bilibili), 138 | text = "哔哩哔哩", 139 | descriptionText = "关注我,留意项目动态~", 140 | onClick = { 141 | context.startActivity( 142 | Intent( 143 | Intent.ACTION_VIEW, 144 | Uri.parse("https://space.bilibili.com/351201307/") 145 | ) 146 | ) 147 | } 148 | ) 149 | } 150 | } 151 | 152 | 153 | OriginalDialog(agreePrivacyPolicy = showOriginalDialogState, onClickConfirm = { 154 | showOriginalDialogState = false 155 | }) 156 | 157 | PrivacyPolicyDialog( 158 | showAgreePrivacyPolicyDialogState, 159 | onClickConfirm = { 160 | showAgreePrivacyPolicyDialogState = false 161 | viewModel.sendIntent(SettingIntent.SetPrivacyPolicyState(1)) 162 | }, 163 | onClickDismiss = { 164 | showAgreePrivacyPolicyDialogState = false 165 | viewModel.sendIntent(SettingIntent.SetPrivacyPolicyState(0)) 166 | }) 167 | } 168 | 169 | 170 | @Composable 171 | fun OriginalDialog( 172 | agreePrivacyPolicy: Boolean, 173 | onClickConfirm: () -> Unit, 174 | ) { 175 | val dialogContent by remember { 176 | mutableStateOf( 177 | """ 178 | 该项目源自于云游君的开源项目Cook,你也可以从这里使用原项目,本APP目前不定期同步该项目数据资源。 179 | """.trimIndent() 180 | ) 181 | } 182 | WaifuBoostAlertDialog( 183 | showState = agreePrivacyPolicy, 184 | title = { Text(text = "原始项目") }, 185 | icon = { 186 | Icon( 187 | imageVector = Icons.Outlined.Web, 188 | contentDescription = null 189 | ) 190 | }, 191 | text = { 192 | val textColor = MaterialTheme.colorScheme.onSurface.toArgb() 193 | AndroidView( 194 | factory = { TextView(it) }, 195 | update = { 196 | val tip = dialogContent.trimIndent() 197 | it.apply { 198 | it.setTextColor(textColor) 199 | text = HtmlCompat.fromHtml(tip, HtmlCompat.FROM_HTML_MODE_COMPACT) 200 | movementMethod = LinkMovementMethod.getInstance() 201 | } 202 | } 203 | ) 204 | }, 205 | confirmButton = { 206 | TextButton(onClick = onClickConfirm) { 207 | Text(text = "好的") 208 | } 209 | } 210 | ) 211 | } 212 | -------------------------------------------------------------------------------- /app/src/main/java/com/imcys/foodchoice/ui/setting/SettingState.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.foodchoice.ui.setting 2 | 3 | import com.imcys.core.common.viewmodel.info.UiState 4 | 5 | data class SettingState( 6 | val privacyPolicyState: Int = -1, // -1未知 0拒绝 1接受 7 | ) : UiState 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/imcys/foodchoice/ui/setting/SettingViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.foodchoice.ui.setting 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import android.content.SharedPreferences 6 | import androidx.core.content.edit 7 | import com.imcys.core.common.viewmodel.ComposeBaseViewModel 8 | import com.imcys.foodchoice.FoodApplication 9 | import com.microsoft.appcenter.AppCenter 10 | import com.microsoft.appcenter.analytics.Analytics 11 | import com.microsoft.appcenter.crashes.Crashes 12 | import com.microsoft.appcenter.distribute.Distribute 13 | import dagger.hilt.android.lifecycle.HiltViewModel 14 | import dagger.hilt.android.qualifiers.ApplicationContext 15 | import javax.inject.Inject 16 | 17 | @HiltViewModel 18 | class SettingViewModel @Inject constructor( 19 | @ApplicationContext private val context: Context, 20 | ) : ComposeBaseViewModel(SettingState()) { 21 | private val shardedPreferences: SharedPreferences = 22 | context.getSharedPreferences("app_config", Context.MODE_PRIVATE) 23 | 24 | init { 25 | val privacyPolicyState = shardedPreferences.getInt("privacy_policy_state", -1) 26 | viewStates = 27 | viewStates.copy(privacyPolicyState = privacyPolicyState) 28 | } 29 | 30 | override fun handleEvent(event: SettingIntent, state: SettingState) { 31 | when(event){ 32 | is SettingIntent.SetPrivacyPolicyState -> { 33 | setPrivacyPolicyState(event.state) 34 | } 35 | } 36 | } 37 | 38 | private fun setPrivacyPolicyState(state: Int) { 39 | viewStates = viewStates.copy(privacyPolicyState = state) 40 | shardedPreferences.edit { 41 | putInt("privacy_policy_state", state) 42 | } 43 | FoodApplication.initAppCenter(state) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/java/com/imcys/foodchoice/weight/Konfetti.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.foodchoice.weight 2 | 3 | import androidx.compose.foundation.layout.fillMaxSize 4 | import androidx.compose.material3.MaterialTheme 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.runtime.MutableState 7 | import androidx.compose.runtime.getValue 8 | import androidx.compose.runtime.mutableStateOf 9 | import androidx.compose.runtime.remember 10 | import androidx.compose.runtime.setValue 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.graphics.Color 13 | import androidx.compose.ui.graphics.toArgb 14 | import androidx.compose.ui.platform.LocalView 15 | import androidx.core.view.HapticFeedbackConstantsCompat 16 | import nl.dionsegijn.konfetti.compose.KonfettiView 17 | import nl.dionsegijn.konfetti.compose.OnParticleSystemUpdateListener 18 | import nl.dionsegijn.konfetti.core.Angle 19 | import nl.dionsegijn.konfetti.core.Party 20 | import nl.dionsegijn.konfetti.core.PartySystem 21 | import nl.dionsegijn.konfetti.core.Position 22 | import nl.dionsegijn.konfetti.core.Spread 23 | import nl.dionsegijn.konfetti.core.emitter.Emitter 24 | import java.util.concurrent.TimeUnit 25 | import com.imcys.core.designsystem.theme.blend 26 | 27 | 28 | @Composable 29 | fun rememberKonfettiState(visible: Boolean = false): MutableState { 30 | return remember { mutableStateOf(visible) } 31 | } 32 | 33 | @Composable 34 | fun Konfetti( 35 | state: MutableState = rememberKonfettiState(), 36 | modifier: Modifier = Modifier, 37 | primary: Color = MaterialTheme.colorScheme.primary 38 | ) { 39 | var visible by state 40 | if (!visible) { 41 | return 42 | } 43 | val view = LocalView.current 44 | 45 | val listener = remember(state) { 46 | object : OnParticleSystemUpdateListener { 47 | override fun onParticleSystemEnded(system: PartySystem, activeSystems: Int) { 48 | if (activeSystems == 0) { 49 | visible = false 50 | } else { 51 | // 模拟掉落 52 | view.performHapticFeedback(HapticFeedbackConstantsCompat.CONTEXT_CLICK) 53 | } 54 | } 55 | } 56 | } 57 | KonfettiView( 58 | modifier = Modifier 59 | .fillMaxSize() 60 | .then(modifier), 61 | parties = remember { particles(primary.toArgb()) }, 62 | updateListener = listener 63 | ) 64 | } 65 | 66 | private val defaultColors = listOf( 67 | 0xfce18a, 68 | 0x009688, 69 | 0xff726d, 70 | 0xf4306d, 71 | 0xb48def, 72 | 0x95FF82, 73 | 0x82ECFF, 74 | 0xFF9800, 75 | 0x0E008A, 76 | ) 77 | private const val colorBlendFraction = 0.3f 78 | 79 | private fun particles(primary: Int) = listOf( 80 | Party( 81 | speed = 0f, 82 | maxSpeed = 12f, 83 | damping = 0.9f, 84 | angle = Angle.BOTTOM, 85 | spread = Spread.ROUND, 86 | colors = defaultColors.map { it.blend(primary, colorBlendFraction) }, 87 | emitter = Emitter(duration = 2, TimeUnit.SECONDS).perSecond(100), 88 | position = Position.Relative(0.0, 0.0).between(Position.Relative(1.0, 0.0)), 89 | ), 90 | Party( 91 | speed = 10f, 92 | maxSpeed = 30f, 93 | damping = 0.9f, 94 | angle = Angle.RIGHT - 55, 95 | spread = 60, 96 | colors = defaultColors.map { it.blend(primary, colorBlendFraction) }, 97 | emitter = Emitter(duration = 2, TimeUnit.SECONDS).perSecond(100), 98 | position = Position.Relative(0.0, 1.0) 99 | ), 100 | Party( 101 | speed = 10f, 102 | maxSpeed = 30f, 103 | damping = 0.9f, 104 | angle = Angle.RIGHT - 125, 105 | spread = 60, 106 | colors = defaultColors.map { it.blend(primary, colorBlendFraction) }, 107 | emitter = Emitter(duration = 2, TimeUnit.SECONDS).perSecond(100), 108 | position = Position.Relative(1.0, 1.0) 109 | ) 110 | ) 111 | -------------------------------------------------------------------------------- /app/src/main/java/com/imcys/foodchoice/weight/WaifuBoostKonfettiView.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.foodchoice.weight 2 | 3 | import android.content.res.Resources 4 | import android.graphics.Rect 5 | import androidx.compose.foundation.Canvas 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.runtime.LaunchedEffect 8 | import androidx.compose.runtime.mutableIntStateOf 9 | import androidx.compose.runtime.mutableStateOf 10 | import androidx.compose.runtime.remember 11 | import androidx.compose.runtime.withFrameMillis 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.geometry.Offset 14 | import androidx.compose.ui.graphics.drawscope.withTransform 15 | import androidx.compose.ui.layout.onGloballyPositioned 16 | import nl.dionsegijn.konfetti.compose.draw 17 | import nl.dionsegijn.konfetti.compose.getTotalTimeRunning 18 | import nl.dionsegijn.konfetti.core.Particle 19 | import nl.dionsegijn.konfetti.core.Party 20 | import nl.dionsegijn.konfetti.core.emitter.BaseEmitter 21 | import nl.dionsegijn.konfetti.core.emitter.Confetti 22 | import nl.dionsegijn.konfetti.core.emitter.PartyEmitter 23 | import nl.dionsegijn.konfetti.core.toParticle 24 | 25 | 26 | interface OnParticleSystemUpdateListener { 27 | fun onParticleSystemEnded(system: WaifuBoostPartySystem, activeSystems: Int) 28 | fun onPartySystemsEnded() 29 | } 30 | 31 | 32 | class WaifuBoostPartySystem( 33 | val party: Party, 34 | val createdAt: Long = System.currentTimeMillis(), 35 | pixelDensity: Float = Resources.getSystem().displayMetrics.density, 36 | val onPartySystemsEnded: (() -> Unit)? = null 37 | ) { 38 | 39 | var enabled = true 40 | 41 | private var emitter: BaseEmitter = PartyEmitter(party.emitter, pixelDensity) 42 | 43 | private val activeParticles = mutableListOf() 44 | 45 | // Called every frame to create and update the particles state 46 | // returns a list of particles that are ready to be rendered 47 | fun render(deltaTime: Float, drawArea: Rect): List { 48 | if (enabled) { 49 | activeParticles.addAll(emitter.createConfetti(deltaTime, party, drawArea)) 50 | } 51 | 52 | activeParticles.forEach { 53 | it.render(deltaTime, drawArea) 54 | } 55 | 56 | activeParticles.removeAll { 57 | onPartySystemsEnded?.invoke() 58 | it.isDead() 59 | } 60 | 61 | return activeParticles.filter { it.drawParticle }.map { it.toParticle() } 62 | } 63 | 64 | /** 65 | * When the emitter is done emitting. 66 | * @return true if the emitter is done emitting or false when it's still busy or needs to start 67 | * based on the delay 68 | */ 69 | fun isDoneEmitting(): Boolean = 70 | (emitter.isFinished() && activeParticles.size == 0) || (!enabled && activeParticles.size == 0) 71 | 72 | fun getActiveParticleAmount() = activeParticles.size 73 | } 74 | 75 | 76 | @Composable 77 | fun WaifuBoostKonfettiView( 78 | modifier: Modifier = Modifier, 79 | parties: List, 80 | updateListener: OnParticleSystemUpdateListener? = null 81 | ) { 82 | 83 | lateinit var partySystems: List 84 | 85 | /** 86 | * Particles to draw 87 | */ 88 | val particles = remember { mutableStateOf(emptyList()) } 89 | 90 | /** 91 | * Latest stored frameTimeMilliseconds 92 | */ 93 | val frameTime = remember { mutableStateOf(0L) } 94 | 95 | /** 96 | * Area in which the particles are being drawn 97 | */ 98 | val drawArea = remember { mutableStateOf(Rect()) } 99 | 100 | 101 | val allHandleParticles = remember { 102 | mutableIntStateOf(0) 103 | } 104 | 105 | LaunchedEffect(Unit) { 106 | partySystems = parties.map { WaifuBoostPartySystem( 107 | party = it, 108 | onPartySystemsEnded = { 109 | updateListener?.onPartySystemsEnded() 110 | } 111 | ) } 112 | while (true) { 113 | withFrameMillis { frameMs -> 114 | // Calculate time between frames, fallback to 0 when previous frame doesn't exist 115 | val deltaMs = if (frameTime.value > 0) (frameMs - frameTime.value) else 0 116 | frameTime.value = frameMs 117 | 118 | particles.value = partySystems.map { particleSystem -> 119 | 120 | val totalTimeRunning = getTotalTimeRunning(particleSystem.createdAt) 121 | // Do not start particleSystem yet if totalTimeRunning is below delay 122 | if (totalTimeRunning < particleSystem.party.delay) return@map listOf() 123 | 124 | if (particleSystem.isDoneEmitting()) { 125 | updateListener?.onParticleSystemEnded( 126 | system = particleSystem, 127 | activeSystems = partySystems.count { !it.isDoneEmitting() } 128 | ) 129 | } 130 | 131 | 132 | particleSystem.render(deltaMs.div(1000f), drawArea.value) 133 | }.flatten() 134 | } 135 | } 136 | } 137 | 138 | Canvas( 139 | modifier = modifier 140 | .onGloballyPositioned { 141 | drawArea.value = Rect(0, 0, it.size.width, it.size.height) 142 | }, 143 | onDraw = { 144 | particles.value.forEach { particle -> 145 | withTransform({ 146 | rotate( 147 | degrees = particle.rotation, 148 | pivot = Offset( 149 | x = particle.x + (particle.width / 2), 150 | y = particle.y + (particle.height / 2) 151 | ) 152 | ) 153 | scale( 154 | scaleX = particle.scaleX, 155 | scaleY = 1f, 156 | pivot = Offset(particle.x + (particle.width / 2), particle.y) 157 | ) 158 | }) { 159 | particle.shape.draw(this, particle) 160 | } 161 | } 162 | } 163 | ) 164 | } 165 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v31/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 11 | 20 | 26 | 34 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/cookingpot.xml: -------------------------------------------------------------------------------- 1 | 6 | 14 | 20 | 27 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_github_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | -------------------------------------------------------------------------------- /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/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 11 | 20 | 26 | 34 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_lib_bilibili.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1250422131/FoodChoice/18de8536cd5829fd7bdb0ddb73001ddb98c7fef0/app/src/main/res/drawable/ic_lib_bilibili.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1250422131/FoodChoice/18de8536cd5829fd7bdb0ddb73001ddb98c7fef0/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1250422131/FoodChoice/18de8536cd5829fd7bdb0ddb73001ddb98c7fef0/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1250422131/FoodChoice/18de8536cd5829fd7bdb0ddb73001ddb98c7fef0/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1250422131/FoodChoice/18de8536cd5829fd7bdb0ddb73001ddb98c7fef0/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1250422131/FoodChoice/18de8536cd5829fd7bdb0ddb73001ddb98c7fef0/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1250422131/FoodChoice/18de8536cd5829fd7bdb0ddb73001ddb98c7fef0/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1250422131/FoodChoice/18de8536cd5829fd7bdb0ddb73001ddb98c7fef0/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1250422131/FoodChoice/18de8536cd5829fd7bdb0ddb73001ddb98c7fef0/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1250422131/FoodChoice/18de8536cd5829fd7bdb0ddb73001ddb98c7fef0/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1250422131/FoodChoice/18de8536cd5829fd7bdb0ddb73001ddb98c7fef0/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/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFFFFF 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 食选 3 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /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/imcys/foodchoice/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.foodchoice 2 | 3 | import org.junit.Assert.* 4 | import org.junit.Test 5 | 6 | /** 7 | * Example local unit test, which will execute on the development machine (host). 8 | * 9 | * See [testing documentation](http://d.android.com/tools/testing). 10 | */ 11 | class ExampleUnitTest { 12 | @Test 13 | fun addition_isCorrect() { 14 | assertEquals(4, 2 + 2) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | // Top-level(build file where you can add configuration options common to all sub-projects/modules.) 2 | plugins { 3 | id("com.android.application") version "8.7.0" apply false 4 | id("com.android.library") version "8.7.0" apply false 5 | id("org.jetbrains.kotlin.android") version "2.1.0" apply false 6 | id("com.google.devtools.ksp") version "2.1.0-1.0.29" apply false 7 | id("com.google.dagger.hilt.android") version "2.54" apply false 8 | id("io.gitlab.arturbosch.detekt") version "1.23.0" 9 | id("org.jetbrains.kotlin.plugin.compose") version "2.1.0" 10 | } 11 | -------------------------------------------------------------------------------- /core/common/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /core/common/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.library") 3 | id("org.jetbrains.kotlin.android") 4 | id("com.google.devtools.ksp") 5 | id("com.google.dagger.hilt.android") 6 | } 7 | android { 8 | compileSdk = 34 9 | 10 | namespace = "com.imcys.core.common" 11 | 12 | composeOptions { 13 | kotlinCompilerExtensionVersion = "1.5.3" 14 | } 15 | 16 | compileOptions { 17 | sourceCompatibility = JavaVersion.VERSION_17 18 | targetCompatibility = JavaVersion.VERSION_17 19 | } 20 | kotlinOptions { 21 | jvmTarget = "17" 22 | } 23 | 24 | } 25 | 26 | 27 | 28 | dependencies { 29 | // hilt库,实现依赖注入 30 | api(libs.hilt.android) 31 | ksp(libs.hilt.compiler) 32 | api("androidx.hilt:hilt-navigation-compose:1.0.0") 33 | // 路由 34 | api("androidx.navigation:navigation-compose:2.8.5") 35 | 36 | api("com.microsoft.appcenter:appcenter-analytics:5.0.4") 37 | api("com.microsoft.appcenter:appcenter-crashes:5.0.4") 38 | api("com.microsoft.appcenter:appcenter-distribute:5.0.4") 39 | 40 | implementation("androidx.core:core-ktx:1.8.0") 41 | implementation("androidx.appcompat:appcompat:1.4.1") 42 | implementation("com.google.android.material:material:1.5.0") 43 | testImplementation("junit:junit:4.13.2") 44 | androidTestImplementation("androidx.test.ext:junit:1.1.3") 45 | androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0") 46 | } 47 | -------------------------------------------------------------------------------- /core/common/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1250422131/FoodChoice/18de8536cd5829fd7bdb0ddb73001ddb98c7fef0/core/common/consumer-rules.pro -------------------------------------------------------------------------------- /core/common/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle.kts. 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 -------------------------------------------------------------------------------- /core/common/src/androidTest/java/com/imcys/core/common/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.core.common 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import org.junit.Assert.* 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | /** 10 | * Instrumented test, which will execute on an Android device. 11 | * 12 | * See [testing documentation](http://d.android.com/tools/testing). 13 | */ 14 | @RunWith(AndroidJUnit4::class) 15 | class ExampleInstrumentedTest { 16 | @Test 17 | fun useAppContext() { 18 | // Context of the app under test. 19 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 20 | assertEquals("com.imcys.common.test", appContext.packageName) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /core/common/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /core/common/src/main/java/com/imcys/core/common/data/BaseRepository.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.core.common.data 2 | 3 | abstract class BaseRepository : IBaseRepository 4 | -------------------------------------------------------------------------------- /core/common/src/main/java/com/imcys/core/common/data/IBaseRepository.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.core.common.data 2 | 3 | interface IBaseRepository { 4 | suspend fun syncWithData(): Boolean 5 | } -------------------------------------------------------------------------------- /core/common/src/main/java/com/imcys/core/common/extend/Coroutines.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.core.common.extend 2 | 3 | import androidx.fragment.app.Fragment 4 | import androidx.fragment.app.FragmentActivity 5 | import androidx.lifecycle.lifecycleScope 6 | import kotlinx.coroutines.CoroutineScope 7 | import kotlinx.coroutines.CoroutineStart 8 | import kotlinx.coroutines.Dispatchers 9 | import kotlinx.coroutines.Job 10 | import kotlinx.coroutines.launch 11 | 12 | fun CoroutineScope.launchIO( 13 | start: CoroutineStart = CoroutineStart.DEFAULT, 14 | block: suspend CoroutineScope.() -> Unit, 15 | ): Job { 16 | return this.launch(Dispatchers.IO, start, block) 17 | } 18 | 19 | fun CoroutineScope.launchUI( 20 | start: CoroutineStart = CoroutineStart.DEFAULT, 21 | block: suspend CoroutineScope.() -> Unit, 22 | ): Job { 23 | return this.launch(Dispatchers.Main, start, block) 24 | } 25 | 26 | -------------------------------------------------------------------------------- /core/common/src/main/java/com/imcys/core/common/extend/Number.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.core.common.extend 2 | 3 | /** 4 | * 数字格式化 5 | * 10000 -> 1万 6 | */ 7 | fun Long.digitalConversion(): String { 8 | val num = this.toString() 9 | return if (this >= 10000) { 10 | val count = num.length 11 | val unit = when { 12 | count <= 4 -> "" 13 | count <= 8 -> "万" 14 | count <= 12 -> "亿" 15 | count <= 16 -> "万亿" 16 | else -> "亿亿" 17 | } 18 | val formattedNum = when { 19 | count <= 8 -> "${num.substring(0, count - 4)}.${num.substring(count - 4, count - 3)}" 20 | count <= 12 -> "${num.substring(0, count - 8)}.${num.substring(count - 8, count - 7)}" 21 | count <= 16 -> "${num.substring(0, count - 12)}.${num.substring(count - 12, count - 11)}" 22 | else -> "${num.substring(0, count - 16)}.${num.substring(count - 16, count - 15)}" 23 | } 24 | "$formattedNum$unit" 25 | } else { 26 | num 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /core/common/src/main/java/com/imcys/core/common/utils/VibrationUtils.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.core.common.utils 2 | 3 | import android.annotation.SuppressLint 4 | import android.app.Activity 5 | import android.content.Context 6 | import android.os.Vibrator 7 | import android.view.HapticFeedbackConstants 8 | import android.view.View 9 | import androidx.core.content.ContextCompat.getSystemService 10 | 11 | 12 | object VibrationUtils { 13 | /** 14 | * 实现长按触感反馈 15 | * @param context Context 16 | */ 17 | @SuppressLint("NewApi") 18 | fun performHapticFeedback(context: Context){ 19 | val window = (context as? Activity)?.window 20 | val decorView = window?.decorView 21 | decorView?.performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK) 22 | } 23 | } -------------------------------------------------------------------------------- /core/common/src/main/java/com/imcys/core/common/viewmodel/ComposeBaseViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.core.common.viewmodel 2 | 3 | import androidx.compose.runtime.getValue 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.compose.runtime.setValue 6 | import androidx.lifecycle.ViewModel 7 | import androidx.lifecycle.viewModelScope 8 | import com.imcys.core.common.viewmodel.info.IViewModelHandle 9 | import com.imcys.core.common.viewmodel.info.UiIntent 10 | import com.imcys.core.common.viewmodel.info.UiState 11 | import kotlinx.coroutines.CoroutineScope 12 | import kotlinx.coroutines.CoroutineStart 13 | import kotlinx.coroutines.Dispatchers 14 | import kotlinx.coroutines.channels.Channel 15 | import kotlinx.coroutines.flow.asFlow 16 | import kotlinx.coroutines.flow.collect 17 | import kotlinx.coroutines.flow.consumeAsFlow 18 | import kotlinx.coroutines.flow.filterNotNull 19 | import kotlinx.coroutines.launch 20 | 21 | abstract class ComposeBaseViewModel(viewState: S) : 22 | IViewModelHandle, 23 | ViewModel() { 24 | 25 | private val intentChannel = Channel(Channel.UNLIMITED) 26 | 27 | /** 28 | * 界面状态初始化,这里没用flow,mutableState看上去已经帮我们完成了既定目的 29 | * 你可以考虑在这里切换为MutableStateFlow,但我仍然认为mutableStateOf对于Compose要更加发布 30 | */ 31 | var viewStates by mutableStateOf(viewState) 32 | protected set 33 | 34 | init { 35 | handleIntent() 36 | } 37 | 38 | private fun handleIntent() { 39 | launchIO { 40 | intentChannel.consumeAsFlow().filterNotNull().collect { 41 | handleEvent(it, viewStates) 42 | } 43 | } 44 | } 45 | 46 | /** 47 | * 更新意图 48 | */ 49 | fun S.update(content: S.() -> S) { 50 | viewStates = content.invoke(this@update) 51 | } 52 | 53 | /** 54 | * 发送意图 55 | */ 56 | fun sendIntent(viewIntent: I) { 57 | 58 | 59 | launchIO { 60 | intentChannel.send(viewIntent) 61 | } 62 | } 63 | 64 | protected fun launchIO( 65 | start: CoroutineStart = CoroutineStart.DEFAULT, 66 | block: suspend CoroutineScope.() -> Unit, 67 | ) { 68 | viewModelScope.launch(Dispatchers.IO, start, block) 69 | } 70 | 71 | protected fun launchUI( 72 | start: CoroutineStart = CoroutineStart.DEFAULT, 73 | block: suspend CoroutineScope.() -> Unit, 74 | ) { 75 | viewModelScope.launch(Dispatchers.Main, start, block) 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /core/common/src/main/java/com/imcys/core/common/viewmodel/info/IViewModelHandle.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.core.common.viewmodel.info 2 | 3 | interface IViewModelHandle { 4 | 5 | fun handleEvent(event: I, state: S) 6 | } 7 | -------------------------------------------------------------------------------- /core/common/src/main/java/com/imcys/core/common/viewmodel/info/UiEffect.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.core.common.viewmodel.info 2 | 3 | /** 4 | * 进一步抽离简化,将提示部分都单独拿出来 5 | */ 6 | interface UiEffect 7 | -------------------------------------------------------------------------------- /core/common/src/main/java/com/imcys/core/common/viewmodel/info/UiIntent.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.core.common.viewmodel.info 2 | 3 | interface UiIntent 4 | -------------------------------------------------------------------------------- /core/common/src/main/java/com/imcys/core/common/viewmodel/info/UiState.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.core.common.viewmodel.info 2 | 3 | interface UiState 4 | -------------------------------------------------------------------------------- /core/common/src/main/java/com/imcys/core/common/viewmodel/ui/BaseComponentActivity.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.core.common.viewmodel.ui 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | 6 | open class BaseComponentActivity : ComponentActivity() { 7 | override fun onCreate(savedInstanceState: Bundle?) { 8 | super.onCreate(savedInstanceState) 9 | } 10 | } -------------------------------------------------------------------------------- /core/common/src/test/java/com/imcys/core/common/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.core.common 2 | 3 | import org.junit.Assert.* 4 | import org.junit.Test 5 | 6 | /** 7 | * Example local unit test, which will execute on the development machine (host). 8 | * 9 | * See [testing documentation](http://d.android.com/tools/testing). 10 | */ 11 | class ExampleUnitTest { 12 | @Test 13 | fun addition_isCorrect() { 14 | assertEquals(4, 2 + 2) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /core/data/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /core/data/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.library") 3 | id("org.jetbrains.kotlin.android") 4 | id("com.google.devtools.ksp") 5 | id("com.google.dagger.hilt.android") 6 | } 7 | 8 | android { 9 | namespace = "com.imcys.core.data" 10 | compileSdk = 35 11 | 12 | defaultConfig { 13 | minSdk = 21 14 | 15 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 16 | consumerProguardFiles("consumer-rules.pro") 17 | } 18 | 19 | buildTypes { 20 | release { 21 | isMinifyEnabled = false 22 | proguardFiles( 23 | getDefaultProguardFile("proguard-android-optimize.txt"), 24 | "proguard-rules.pro", 25 | ) 26 | } 27 | } 28 | composeOptions { 29 | kotlinCompilerExtensionVersion = "1.5.3" 30 | } 31 | 32 | compileOptions { 33 | sourceCompatibility = JavaVersion.VERSION_17 34 | targetCompatibility = JavaVersion.VERSION_17 35 | } 36 | kotlinOptions { 37 | jvmTarget = "17" 38 | } 39 | } 40 | 41 | dependencies { 42 | // hilt库,实现依赖注入 43 | api(libs.hilt.android) 44 | ksp(libs.hilt.compiler) 45 | 46 | implementation(project(":core:common")) 47 | implementation(project(":core:database")) 48 | implementation(project(":core:network")) 49 | implementation(project(":core:model")) 50 | 51 | implementation("androidx.core:core-ktx:1.8.0") 52 | implementation("androidx.appcompat:appcompat:1.4.1") 53 | implementation("com.google.android.material:material:1.5.0") 54 | testImplementation("junit:junit:4.13.2") 55 | androidTestImplementation("androidx.test.ext:junit:1.1.3") 56 | androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0") 57 | } 58 | -------------------------------------------------------------------------------- /core/data/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1250422131/FoodChoice/18de8536cd5829fd7bdb0ddb73001ddb98c7fef0/core/data/consumer-rules.pro -------------------------------------------------------------------------------- /core/data/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | 23 | -------------------------------------------------------------------------------- /core/data/src/androidTest/java/com/imcys/core/data/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.core.data 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import org.junit.Assert.* 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | /** 10 | * Instrumented test, which will execute on an Android device. 11 | * 12 | * See [testing documentation](http://d.android.com/tools/testing). 13 | */ 14 | @RunWith(AndroidJUnit4::class) 15 | class ExampleInstrumentedTest { 16 | @Test 17 | fun useAppContext() { 18 | // Context of the app under test. 19 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 20 | assertEquals("com.imcys.core.data.test", appContext.packageName) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /core/data/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /core/data/src/main/java/com/imcys/core/data/extend/Convert.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.core.data.extend 2 | 3 | import com.imcys.core.database.entity.CookFoodEntity 4 | import com.imcys.core.database.entity.CookingIngredientEntity 5 | import com.imcys.core.model.cook.CookFoodInfo 6 | import com.imcys.core.model.cook.CookingIngredient 7 | 8 | fun CookFoodInfo.Data.asCookFoodEntity() = 9 | CookFoodEntity( 10 | bv = this.bv, 11 | difficulty = this.difficulty, 12 | methods = this.methods, 13 | name = this.name, 14 | stuff = this.stuff, 15 | tags = this.tags, 16 | tools = this.tools, 17 | ) 18 | 19 | fun CookingIngredient.asCookingIngredientEntity() = 20 | CookingIngredientEntity( 21 | name = this.name, 22 | label = this.label, 23 | emoji = this.emoji, 24 | alias = this.alias, 25 | image = this.image, 26 | ) 27 | -------------------------------------------------------------------------------- /core/data/src/main/java/com/imcys/core/data/extend/Request.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.core.data.extend 2 | 3 | import kotlinx.coroutines.Dispatchers 4 | import kotlinx.coroutines.flow.Flow 5 | import kotlinx.coroutines.flow.FlowCollector 6 | import kotlinx.coroutines.flow.flow 7 | import kotlinx.coroutines.flow.flowOn 8 | import kotlin.experimental.ExperimentalTypeInference 9 | 10 | @OptIn(ExperimentalTypeInference::class) 11 | fun makeRequestInFlow(@BuilderInference requestBlock: suspend FlowCollector.() -> Unit): Flow { 12 | // 切换流到IO线程,特别的,由于协程上下文传递,ViewModel处理意图本身就是在IO里,可以不可以这么做,观察后是否 13 | return flow(block = requestBlock).flowOn(Dispatchers.IO) 14 | } 15 | -------------------------------------------------------------------------------- /core/data/src/main/java/com/imcys/core/data/repository/cook/CookFoodInfoRepository.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.core.data.repository.cook 2 | 3 | import com.imcys.core.common.data.BaseRepository 4 | import com.imcys.core.data.extend.asCookFoodEntity 5 | import com.imcys.core.data.extend.makeRequestInFlow 6 | import com.imcys.core.database.dao.CookFoodDao 7 | import com.imcys.core.network.retrofit.RetrofitAppNetwork 8 | import kotlinx.coroutines.flow.catch 9 | import javax.inject.Inject 10 | 11 | class CookFoodInfoRepository @Inject constructor( 12 | private val cookFoodDao: CookFoodDao, 13 | ) : BaseRepository() { 14 | suspend fun getCookingFood(bvId: String) = 15 | run { 16 | cookFoodDao.selectByBvId(bvId) 17 | } 18 | 19 | suspend fun getCookingFoods() = 20 | run { cookFoodDao.selectList() } 21 | 22 | override suspend fun syncWithData(): Boolean { 23 | var isSuccess = true 24 | 25 | makeRequestInFlow { 26 | emit(RetrofitAppNetwork.networkApi.getCookFoodData()) 27 | }.catch { 28 | isSuccess = false 29 | }.collect { 30 | it.data.forEach { food -> 31 | cookFoodDao.selectByName(food.name)?.apply { 32 | // 查询到了就更新 33 | cookFoodDao.update(food.asCookFoodEntity().copy(id = id)) 34 | } ?: apply { 35 | // 没查到就插入 36 | cookFoodDao.inserts(food.asCookFoodEntity()) 37 | } 38 | } 39 | } 40 | return isSuccess 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /core/data/src/main/java/com/imcys/core/data/repository/cook/CookingIngredientRepository.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.core.data.repository.cook 2 | 3 | import com.imcys.core.common.data.BaseRepository 4 | import com.imcys.core.data.extend.asCookingIngredientEntity 5 | import com.imcys.core.data.extend.makeRequestInFlow 6 | import com.imcys.core.database.dao.CookingIngredientDao 7 | import com.imcys.core.database.entity.CookingIngredientEntity 8 | import com.imcys.core.model.cook.CookingIngredient 9 | import com.imcys.core.network.retrofit.RetrofitAppNetwork 10 | import kotlinx.coroutines.flow.catch 11 | import javax.inject.Inject 12 | 13 | class CookingIngredientRepository @Inject constructor( 14 | private val cookingIngredientDao: CookingIngredientDao, 15 | ) : BaseRepository() { 16 | 17 | suspend fun getCookingIngredients(type: Int) = 18 | run { cookingIngredientDao.selectByTypeList(type) } 19 | 20 | suspend fun getCookingIngredients() = run { cookingIngredientDao.selectList() } 21 | 22 | suspend fun getCookingIngredient(name: String) = run { cookingIngredientDao.selectByName(name) } 23 | 24 | override suspend fun syncWithData(): Boolean { 25 | var isSuccess = true // 默认为 true,表示成功 26 | 27 | makeRequestInFlow { 28 | emit(RetrofitAppNetwork.networkApi.getCookingIngredients()) 29 | }.catch { 30 | // 出现异常 31 | isSuccess = true 32 | }.collect { 33 | it.data.meat.let { meat -> updateCheck(meat, CookingIngredientEntity.MEAT) } 34 | it.data.staple.let { staple -> updateCheck(staple, CookingIngredientEntity.STAPLE) } 35 | it.data.vegetable.let { vegetable -> 36 | updateCheck( 37 | vegetable, 38 | CookingIngredientEntity.VEGETABLE, 39 | ) 40 | } 41 | it.data.tools.let { tools -> updateCheck(tools, CookingIngredientEntity.TOOL) } 42 | } 43 | 44 | return isSuccess 45 | } 46 | 47 | private suspend fun updateCheck(cookingIngredientList: List, type: Int) { 48 | cookingIngredientList.forEach { 49 | cookingIngredientDao.selectByName(it.name)?.apply { 50 | cookingIngredientDao.update( 51 | it.asCookingIngredientEntity().copy(id = this.id, type = type), 52 | ) 53 | } ?: apply { 54 | // 反之插入 55 | cookingIngredientDao.inserts(it.asCookingIngredientEntity().copy(type = type)) 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /core/data/src/test/java/com/imcys/core/data/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.core.data 2 | 3 | import org.junit.Assert.* 4 | import org.junit.Test 5 | 6 | /** 7 | * Example local unit test, which will execute on the development machine (host). 8 | * 9 | * See [testing documentation](http://d.android.com/tools/testing). 10 | */ 11 | class ExampleUnitTest { 12 | @Test 13 | fun addition_isCorrect() { 14 | assertEquals(4, 2 + 2) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /core/database/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /core/database/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.library") 3 | id("org.jetbrains.kotlin.android") 4 | id("com.google.devtools.ksp") 5 | id("com.google.dagger.hilt.android") 6 | } 7 | 8 | android { 9 | namespace = "com.imcys.core.database" 10 | compileSdk = 35 11 | 12 | defaultConfig { 13 | minSdk = 21 14 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 15 | consumerProguardFiles("consumer-rules.pro") 16 | } 17 | 18 | buildTypes { 19 | release { 20 | isMinifyEnabled = false 21 | proguardFiles( 22 | getDefaultProguardFile("proguard-android-optimize.txt"), 23 | "proguard-rules.pro", 24 | ) 25 | } 26 | } 27 | composeOptions { 28 | kotlinCompilerExtensionVersion = "1.5.3" 29 | } 30 | 31 | compileOptions { 32 | sourceCompatibility = JavaVersion.VERSION_17 33 | targetCompatibility = JavaVersion.VERSION_17 34 | } 35 | kotlinOptions { 36 | jvmTarget = "17" 37 | } 38 | } 39 | 40 | dependencies { 41 | implementation(project(":core:common")) 42 | 43 | // hilt库,实现依赖注入 44 | api(libs.hilt.android) 45 | ksp(libs.hilt.compiler) 46 | 47 | implementation("androidx.room:room-runtime:2.4.3") 48 | implementation("androidx.room:room-ktx:2.4.3") 49 | ksp("androidx.room:room-compiler:2.4.3") 50 | 51 | implementation("androidx.core:core-ktx:1.8.0") 52 | implementation("androidx.appcompat:appcompat:1.4.1") 53 | implementation("com.google.android.material:material:1.5.0") 54 | testImplementation("junit:junit:4.13.2") 55 | androidTestImplementation("androidx.test.ext:junit:1.1.3") 56 | androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0") 57 | } 58 | -------------------------------------------------------------------------------- /core/database/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1250422131/FoodChoice/18de8536cd5829fd7bdb0ddb73001ddb98c7fef0/core/database/consumer-rules.pro -------------------------------------------------------------------------------- /core/database/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 -------------------------------------------------------------------------------- /core/database/src/androidTest/java/com/imcys/core/database/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.core.database 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import org.junit.Assert.* 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | /** 10 | * Instrumented test, which will execute on an Android device. 11 | * 12 | * See [testing documentation](http://d.android.com/tools/testing). 13 | */ 14 | @RunWith(AndroidJUnit4::class) 15 | class ExampleInstrumentedTest { 16 | @Test 17 | fun useAppContext() { 18 | // Context of the app under test. 19 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 20 | assertEquals("com.imcys.core.database.test", appContext.packageName) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /core/database/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /core/database/src/main/java/com/imcys/core/database/AppDatabase.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.core.database 2 | 3 | import androidx.room.Database 4 | import androidx.room.RoomDatabase 5 | import com.imcys.core.database.dao.CookFoodDao 6 | import com.imcys.core.database.dao.CookingIngredientDao 7 | import com.imcys.core.database.entity.CookFoodEntity 8 | import com.imcys.core.database.entity.CookingIngredientEntity 9 | 10 | @Database( 11 | entities = [CookingIngredientEntity::class, CookFoodEntity::class], 12 | version = 1, 13 | exportSchema = false, 14 | ) 15 | abstract class AppDatabase : RoomDatabase() { 16 | 17 | abstract fun cookingIngredientDao(): CookingIngredientDao 18 | 19 | abstract fun cookFoodDao(): CookFoodDao 20 | } 21 | -------------------------------------------------------------------------------- /core/database/src/main/java/com/imcys/core/database/DaosModule.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.core.database 2 | 3 | import com.imcys.core.database.dao.CookFoodDao 4 | import com.imcys.core.database.dao.CookingIngredientDao 5 | import dagger.Module 6 | import dagger.Provides 7 | import dagger.hilt.InstallIn 8 | import dagger.hilt.components.SingletonComponent 9 | 10 | @Module 11 | @InstallIn(SingletonComponent::class) 12 | object DaosModule { 13 | 14 | @Provides 15 | fun providesCookingIngredientDao( 16 | database: AppDatabase, 17 | ): CookingIngredientDao = database.cookingIngredientDao() 18 | 19 | @Provides 20 | fun providesCookFoodDao( 21 | database: AppDatabase, 22 | ): CookFoodDao = database.cookFoodDao() 23 | } 24 | -------------------------------------------------------------------------------- /core/database/src/main/java/com/imcys/core/database/DatabaseModule.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.core.database 2 | 3 | import android.content.Context 4 | import androidx.room.Room 5 | import dagger.Module 6 | import dagger.Provides 7 | import dagger.hilt.InstallIn 8 | import dagger.hilt.android.qualifiers.ApplicationContext 9 | import dagger.hilt.components.SingletonComponent 10 | import javax.inject.Singleton 11 | 12 | @Module 13 | @InstallIn(SingletonComponent::class) 14 | object DatabaseModule { 15 | private const val DB_NAME = "db_food_choice" 16 | 17 | @Provides 18 | @Singleton 19 | fun providesFdDatabase( 20 | @ApplicationContext context: Context, 21 | ): AppDatabase = Room.databaseBuilder( 22 | context, 23 | AppDatabase::class.java, 24 | DB_NAME, 25 | ) // 是否允许在主线程进行查询 26 | .allowMainThreadQueries() 27 | // 数据库升级异常之后的回滚 28 | .fallbackToDestructiveMigration().build() 29 | } 30 | -------------------------------------------------------------------------------- /core/database/src/main/java/com/imcys/core/database/dao/CookFoodDao.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.core.database.dao 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Insert 5 | import androidx.room.Query 6 | import androidx.room.Update 7 | import com.imcys.core.database.entity.CookFoodEntity 8 | 9 | @Dao 10 | interface CookFoodDao { 11 | @Query("SELECT * from fc_cook_food where stuff = :stuff ORDER BY id DESC") 12 | suspend fun selectByStuffList(stuff: String): MutableList 13 | 14 | @Query("SELECT * from fc_cook_food where name = :name") 15 | suspend fun selectByName(name: String): CookFoodEntity? 16 | 17 | @Query("SELECT * from fc_cook_food where bv = :bvId") 18 | suspend fun selectByBvId(bvId: String): CookFoodEntity? 19 | 20 | @Query("SELECT * from fc_cook_food ORDER BY id DESC") 21 | suspend fun selectList(): MutableList 22 | 23 | @Insert 24 | suspend fun inserts(vararg cookFoodEntity: CookFoodEntity) 25 | 26 | @Update 27 | suspend fun update(cookFoodEntity: CookFoodEntity) 28 | } 29 | -------------------------------------------------------------------------------- /core/database/src/main/java/com/imcys/core/database/dao/CookingIngredientDao.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.core.database.dao 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Insert 5 | import androidx.room.Query 6 | import androidx.room.Update 7 | import com.imcys.core.database.entity.CookingIngredientEntity 8 | 9 | @Dao 10 | interface CookingIngredientDao { 11 | @Query("SELECT * from fc_cooking_ingredient where type = :type ORDER BY id DESC") 12 | suspend fun selectByTypeList(type: Int): MutableList 13 | 14 | @Query("SELECT * from fc_cooking_ingredient ORDER BY id DESC") 15 | suspend fun selectList(): MutableList 16 | 17 | @Query("SELECT * from fc_cooking_ingredient where name = :name ORDER BY id DESC") 18 | suspend fun selectByName(name: String): CookingIngredientEntity? 19 | 20 | @Insert 21 | suspend fun inserts(vararg cookingIngredientEntity: CookingIngredientEntity) 22 | 23 | @Update 24 | suspend fun update(cookingIngredientEntity: CookingIngredientEntity) 25 | } 26 | -------------------------------------------------------------------------------- /core/database/src/main/java/com/imcys/core/database/entity/CookFoodEntity.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.core.database.entity 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.Ignore 6 | import androidx.room.PrimaryKey 7 | 8 | @Entity( 9 | tableName = "fc_cook_food", 10 | ) 11 | data class CookFoodEntity( 12 | @PrimaryKey(autoGenerate = true) 13 | var id: Int = 0, 14 | @ColumnInfo(name = "bv") 15 | val bv: String, 16 | @ColumnInfo(name = "difficulty") 17 | val difficulty: String, 18 | @ColumnInfo(name = "methods") 19 | val methods: String, 20 | @ColumnInfo(name = "name") 21 | val name: String, 22 | @ColumnInfo(name = "stuff") 23 | var stuff: String, // 腊肠、米 24 | @ColumnInfo(name = "tags") 25 | val tags: String, // 广式 26 | @ColumnInfo(name = "tools") 27 | var tools: String, // 电饭煲 28 | ) { 29 | @Ignore 30 | var emoji: String = "" // 电饭煲 31 | 32 | @Ignore 33 | var image: String = "" // 电饭煲 34 | } 35 | -------------------------------------------------------------------------------- /core/database/src/main/java/com/imcys/core/database/entity/CookingIngredientEntity.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.core.database.entity 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.PrimaryKey 6 | 7 | @Entity( 8 | tableName = "fc_cooking_ingredient", 9 | ) 10 | data class CookingIngredientEntity( 11 | @PrimaryKey(autoGenerate = true) 12 | val id: Int = 0, 13 | @ColumnInfo(name = "name") 14 | var name: String, 15 | @ColumnInfo(name = "emoji") 16 | var emoji: String, 17 | @ColumnInfo(name = "image") 18 | var image: String? = null, 19 | @ColumnInfo(name = "alias") 20 | var alias: String? = null, 21 | @ColumnInfo(name = "label") 22 | var label: String? = null, 23 | @ColumnInfo(name = "type") 24 | var type: Int = VEGETABLE, // 1蔬菜水果 2肉类 3工具 25 | ) { 26 | companion object { 27 | const val VEGETABLE = 1 28 | const val MEAT = 2 29 | const val TOOL = 3 30 | const val STAPLE = 4 // 主食 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /core/database/src/test/java/com/imcys/core/database/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.core.database 2 | 3 | import org.junit.Assert.* 4 | import org.junit.Test 5 | 6 | /** 7 | * Example local unit test, which will execute on the development machine (host). 8 | * 9 | * See [testing documentation](http://d.android.com/tools/testing). 10 | */ 11 | class ExampleUnitTest { 12 | @Test 13 | fun addition_isCorrect() { 14 | assertEquals(4, 2 + 2) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /core/designsystem/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /core/designsystem/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.library") 3 | id("org.jetbrains.kotlin.android") 4 | id("org.jetbrains.kotlin.plugin.compose") 5 | } 6 | 7 | android { 8 | namespace = "com.imcys.core.designsystem" 9 | compileSdk = 34 10 | 11 | defaultConfig { 12 | minSdk = 21 13 | 14 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 15 | consumerProguardFiles("consumer-rules.pro") 16 | } 17 | 18 | buildTypes { 19 | release { 20 | isMinifyEnabled = false 21 | proguardFiles( 22 | getDefaultProguardFile("proguard-android-optimize.txt"), 23 | "proguard-rules.pro", 24 | ) 25 | } 26 | } 27 | composeOptions { 28 | kotlinCompilerExtensionVersion = "1.5.3" 29 | } 30 | 31 | compileOptions { 32 | sourceCompatibility = JavaVersion.VERSION_17 33 | targetCompatibility = JavaVersion.VERSION_17 34 | } 35 | kotlinOptions { 36 | jvmTarget = "17" 37 | } 38 | buildFeatures { 39 | compose = true 40 | } 41 | } 42 | 43 | dependencies { 44 | api(libs.androidx.material3.window.size) 45 | 46 | // 网络图片加载 47 | api("io.coil-kt:coil-compose:2.4.0") 48 | // systemuicontroller沉浸式实现 49 | api("com.google.accompanist:accompanist-insets:0.28.0") 50 | api("com.google.accompanist:accompanist-systemuicontroller:0.28.0") 51 | 52 | // kotlin依赖 53 | api("androidx.lifecycle:lifecycle-runtime-ktx:2.3.1") 54 | api("androidx.activity:activity-compose:1.7.2") 55 | 56 | // compose 57 | api(platform(libs.androidx.compose.bom)) 58 | api(libs.androidx.compose.ui.ui) 59 | api(libs.androidx.compose.ui.ui.graphics) 60 | api(libs.androidx.compose.ui.ui.tooling.preview) 61 | api(libs.androidx.compose.material.material.icons.extended) 62 | api(libs.androidx.compose.material3.material3) 63 | api(libs.com.airbnb.android.lottie) 64 | 65 | implementation("androidx.core:core-ktx:1.8.0") 66 | implementation("androidx.appcompat:appcompat:1.4.1") 67 | implementation("com.google.android.material:material:1.5.0") 68 | testImplementation("junit:junit:4.13.2") 69 | androidTestImplementation("androidx.test.ext:junit:1.1.3") 70 | androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0") 71 | 72 | androidTestImplementation(platform(libs.androidx.compose.bom)) 73 | androidTestImplementation("androidx.compose.ui:ui-test-junit4") 74 | debugImplementation("androidx.compose.ui:ui-tooling") 75 | debugImplementation("androidx.compose.ui:ui-test-manifest") 76 | } 77 | -------------------------------------------------------------------------------- /core/designsystem/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1250422131/FoodChoice/18de8536cd5829fd7bdb0ddb73001ddb98c7fef0/core/designsystem/consumer-rules.pro -------------------------------------------------------------------------------- /core/designsystem/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 -------------------------------------------------------------------------------- /core/designsystem/src/androidTest/java/com/imcys/core/designsystem/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.core.designsystem 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import org.junit.Assert.* 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | /** 10 | * Instrumented test, which will execute on an Android device. 11 | * 12 | * See [testing documentation](http://d.android.com/tools/testing). 13 | */ 14 | @RunWith(AndroidJUnit4::class) 15 | class ExampleInstrumentedTest { 16 | @Test 17 | fun useAppContext() { 18 | // Context of the app under test. 19 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 20 | assertEquals("com.imcys.core.designsystem.test", appContext.packageName) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /core/designsystem/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /core/designsystem/src/main/java/com/imcys/core/designsystem/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.core.designsystem.theme 2 | 3 | import androidx.annotation.FloatRange 4 | import androidx.compose.ui.graphics.Color 5 | import androidx.core.graphics.ColorUtils 6 | 7 | val Purple80 = Color(0xFFD0BCFF) 8 | val PurpleGrey80 = Color(0xFFCCC2DC) 9 | val Pink80 = Color(0xFFEFB8C8) 10 | 11 | val Purple40 = Color(0xFF6650a4) 12 | val PurpleGrey40 = Color(0xFF625b71) 13 | val Pink40 = Color(0xFF7D5260) 14 | 15 | fun Int.blend( 16 | color: Int, 17 | @FloatRange(from = 0.0, to = 1.0) fraction: Float = 0.5f, 18 | ): Int = ColorUtils.blendARGB(this, color, fraction) 19 | -------------------------------------------------------------------------------- /core/designsystem/src/main/java/com/imcys/core/designsystem/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.core.designsystem.theme 2 | 3 | import android.app.Activity 4 | import android.os.Build 5 | import androidx.compose.foundation.isSystemInDarkTheme 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.material3.darkColorScheme 8 | import androidx.compose.material3.dynamicDarkColorScheme 9 | import androidx.compose.material3.dynamicLightColorScheme 10 | import androidx.compose.material3.lightColorScheme 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.SideEffect 13 | import androidx.compose.ui.platform.LocalContext 14 | import androidx.compose.ui.platform.LocalView 15 | import androidx.core.view.WindowCompat 16 | 17 | private val DarkColorScheme = darkColorScheme( 18 | primary = Purple80, 19 | secondary = PurpleGrey80, 20 | tertiary = Pink80, 21 | 22 | ) 23 | 24 | private val LightColorScheme = lightColorScheme( 25 | primary = Purple40, 26 | secondary = PurpleGrey40, 27 | tertiary = Pink40, 28 | 29 | /* Other default colors to override 30 | background = Color(0xFFFFFBFE), 31 | surface = Color(0xFFFFFBFE), 32 | onPrimary = Color.White, 33 | onSecondary = Color.White, 34 | onTertiary = Color.White, 35 | onBackground = Color(0xFF1C1B1F), 36 | onSurface = Color(0xFF1C1B1F), 37 | */ 38 | ) 39 | 40 | @Composable 41 | fun FoodChoiceTheme( 42 | darkTheme: Boolean = isSystemInDarkTheme(), 43 | // Dynamic color is available on Android 12+ 44 | dynamicColor: Boolean = true, 45 | content: @Composable () -> Unit, 46 | ) { 47 | val colorScheme = when { 48 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { 49 | val context = LocalContext.current 50 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) 51 | } 52 | 53 | darkTheme -> DarkColorScheme 54 | else -> LightColorScheme 55 | } 56 | val view = LocalView.current 57 | if (!view.isInEditMode) { 58 | SideEffect { 59 | val window = (view.context as Activity).window 60 | // window.statusBarColor = colorScheme.primary.toArgb() 61 | WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme 62 | } 63 | } 64 | 65 | MaterialTheme( 66 | colorScheme = colorScheme, 67 | typography = Typography, 68 | content = content, 69 | ) 70 | } 71 | -------------------------------------------------------------------------------- /core/designsystem/src/main/java/com/imcys/core/designsystem/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.core.designsystem.theme 2 | 3 | import androidx.compose.material3.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 | bodyLarge = TextStyle( 12 | fontFamily = FontFamily.Default, 13 | fontWeight = FontWeight.Normal, 14 | fontSize = 16.sp, 15 | lineHeight = 24.sp, 16 | letterSpacing = 0.5.sp, 17 | ), 18 | /* Other default text styles to override 19 | titleLarge = TextStyle( 20 | fontFamily = FontFamily.Default, 21 | fontWeight = FontWeight.Normal, 22 | fontSize = 22.sp, 23 | lineHeight = 28.sp, 24 | letterSpacing = 0.sp 25 | ), 26 | labelSmall = TextStyle( 27 | fontFamily = FontFamily.Default, 28 | fontWeight = FontWeight.Medium, 29 | fontSize = 11.sp, 30 | lineHeight = 16.sp, 31 | letterSpacing = 0.5.sp 32 | ) 33 | */ 34 | ) 35 | -------------------------------------------------------------------------------- /core/designsystem/src/test/java/com/imcys/core/designsystem/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.core.designsystem 2 | 3 | import org.junit.Assert.* 4 | import org.junit.Test 5 | 6 | /** 7 | * Example local unit test, which will execute on the development machine (host). 8 | * 9 | * See [testing documentation](http://d.android.com/tools/testing). 10 | */ 11 | class ExampleUnitTest { 12 | @Test 13 | fun addition_isCorrect() { 14 | assertEquals(4, 2 + 2) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /core/model/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /core/model/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.library") 3 | id("org.jetbrains.kotlin.android") 4 | } 5 | 6 | android { 7 | namespace = "com.imcys.core.model" 8 | compileSdk = 34 9 | 10 | defaultConfig { 11 | minSdk = 21 12 | 13 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 14 | consumerProguardFiles("consumer-rules.pro") 15 | } 16 | 17 | buildTypes { 18 | release { 19 | isMinifyEnabled = false 20 | proguardFiles( 21 | getDefaultProguardFile("proguard-android-optimize.txt"), 22 | "proguard-rules.pro", 23 | ) 24 | } 25 | } 26 | composeOptions { 27 | kotlinCompilerExtensionVersion = "1.5.3" 28 | } 29 | compileOptions { 30 | sourceCompatibility = JavaVersion.VERSION_17 31 | targetCompatibility = JavaVersion.VERSION_17 32 | } 33 | kotlinOptions { 34 | jvmTarget = "17" 35 | } 36 | } 37 | 38 | dependencies { 39 | 40 | implementation(project(":core:common")) 41 | implementation(libs.com.squareup.retrofit2.converter.moshi) 42 | implementation("com.squareup.retrofit2:converter-gson:2.9.0") 43 | 44 | implementation("androidx.core:core-ktx:1.8.0") 45 | implementation("androidx.appcompat:appcompat:1.4.1") 46 | implementation("com.google.android.material:material:1.5.0") 47 | testImplementation("junit:junit:4.13.2") 48 | androidTestImplementation("androidx.test.ext:junit:1.1.3") 49 | androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0") 50 | } 51 | -------------------------------------------------------------------------------- /core/model/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1250422131/FoodChoice/18de8536cd5829fd7bdb0ddb73001ddb98c7fef0/core/model/consumer-rules.pro -------------------------------------------------------------------------------- /core/model/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 -------------------------------------------------------------------------------- /core/model/src/androidTest/java/com/imcys/core/model/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.core.model 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import org.junit.Assert.* 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | /** 10 | * Instrumented test, which will execute on an Android device. 11 | * 12 | * See [testing documentation](http://d.android.com/tools/testing). 13 | */ 14 | @RunWith(AndroidJUnit4::class) 15 | class ExampleInstrumentedTest { 16 | @Test 17 | fun useAppContext() { 18 | // Context of the app under test. 19 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 20 | assertEquals("com.imcys.core.model.test", appContext.packageName) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /core/model/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /core/model/src/main/java/com/imcys/core/model/HomeItemInfo.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.core.model 2 | 3 | import androidx.compose.ui.Modifier 4 | 5 | data class HomeItemInfo( 6 | val title: String, 7 | val content: String, 8 | val faceUrl: String, 9 | val route: String, 10 | val modifier: Modifier = Modifier, 11 | ) 12 | -------------------------------------------------------------------------------- /core/model/src/main/java/com/imcys/core/model/cook/CookFoodInfo.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.core.model.cook 2 | 3 | import com.squareup.moshi.Json 4 | import java.io.Serializable 5 | 6 | data class CookFoodInfo( 7 | val code: Int = 0, 8 | val `data`: List = listOf(), 9 | val msg: String = "", 10 | ) : Serializable { 11 | data class Data( 12 | val bv: String = "", 13 | val difficulty: String = "", 14 | val methods: String = "", 15 | val name: String = "", 16 | val stuff: String = "", 17 | val tags: String = "", 18 | val tools: String = "", 19 | ) : Serializable 20 | } 21 | -------------------------------------------------------------------------------- /core/model/src/main/java/com/imcys/core/model/cook/CookFoodVideoInfo.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.core.model.cook 2 | 3 | import com.google.gson.annotations.SerializedName 4 | import java.io.Serializable 5 | 6 | data class CookFoodVideoInfo( 7 | @SerializedName("code") 8 | val code: Long, 9 | @SerializedName("data") 10 | val `data`: Data, 11 | @SerializedName("message") 12 | val message: String, 13 | @SerializedName("ttl") 14 | val ttl: Long, 15 | ) : Serializable { 16 | data class Data( 17 | @SerializedName("aid") 18 | val aid: Long, 19 | @SerializedName("bvid") 20 | val bvid: String, 21 | @SerializedName("cid") 22 | val cid: Long, 23 | @SerializedName("copyright") 24 | val copyright: Long, 25 | @SerializedName("ctime") 26 | val ctime: Long, 27 | @SerializedName("desc") 28 | val desc: String, 29 | @SerializedName("desc_v2") 30 | val descV2: List, 31 | @SerializedName("dimension") 32 | val dimension: Dimension, 33 | @SerializedName("disable_show_up_info") 34 | val disableShowUpInfo: Boolean, 35 | @SerializedName("duration") 36 | val duration: Long, 37 | @SerializedName("dynamic") 38 | val `dynamic`: String, 39 | @SerializedName("enable_vt") 40 | val enableVt: Long, 41 | @SerializedName("honor_reply") 42 | val honorReply: HonorReply, 43 | @SerializedName("is_chargeable_season") 44 | val isChargeableSeason: Boolean, 45 | @SerializedName("is_season_display") 46 | val isSeasonDisplay: Boolean, 47 | @SerializedName("is_story") 48 | val isStory: Boolean, 49 | @SerializedName("is_upower_exclusive") 50 | val isUpowerExclusive: Boolean, 51 | @SerializedName("is_upower_play") 52 | val isUpowerPlay: Boolean, 53 | @SerializedName("like_icon") 54 | val likeIcon: String, 55 | @SerializedName("need_jump_bv") 56 | val needJumpBv: Boolean, 57 | @SerializedName("no_cache") 58 | val noCache: Boolean, 59 | @SerializedName("owner") 60 | val owner: Owner, 61 | @SerializedName("pages") 62 | val pages: List, 63 | @SerializedName("pic") 64 | val pic: String, 65 | @SerializedName("premiere") 66 | val premiere: Any, 67 | @SerializedName("pubdate") 68 | val pubdate: Long, 69 | @SerializedName("rights") 70 | val rights: Rights, 71 | @SerializedName("stat") 72 | val stat: Stat, 73 | @SerializedName("state") 74 | val state: Long, 75 | @SerializedName("subtitle") 76 | val subtitle: Subtitle, 77 | @SerializedName("teenage_mode") 78 | val teenageMode: Long, 79 | @SerializedName("tid") 80 | val tid: Long, 81 | @SerializedName("title") 82 | val title: String, 83 | @SerializedName("tname") 84 | val tname: String, 85 | @SerializedName("user_garb") 86 | val userGarb: UserGarb, 87 | @SerializedName("videos") 88 | val videos: Long, 89 | ) { 90 | data class DescV2( 91 | @SerializedName("biz_id") 92 | val bizId: Long, 93 | @SerializedName("raw_text") 94 | val rawText: String, 95 | @SerializedName("type") 96 | val type: Long, 97 | ) : Serializable 98 | 99 | data class Dimension( 100 | @SerializedName("height") 101 | val height: Long, 102 | @SerializedName("rotate") 103 | val rotate: Long, 104 | @SerializedName("width") 105 | val width: Long, 106 | ) : Serializable 107 | 108 | class HonorReply : Serializable 109 | 110 | data class Owner( 111 | @SerializedName("face") 112 | val face: String, 113 | @SerializedName("mid") 114 | val mid: Long, 115 | @SerializedName("name") 116 | val name: String, 117 | ) : Serializable 118 | 119 | data class Page( 120 | @SerializedName("cid") 121 | val cid: Long, 122 | @SerializedName("dimension") 123 | val dimension: Dimension, 124 | @SerializedName("duration") 125 | val duration: Long, 126 | @SerializedName("first_frame") 127 | val firstFrame: String, 128 | @SerializedName("from") 129 | val from: String, 130 | @SerializedName("page") 131 | val page: Long, 132 | @SerializedName("part") 133 | val part: String, 134 | @SerializedName("vid") 135 | val vid: String, 136 | @SerializedName("weblink") 137 | val weblink: String, 138 | ) : Serializable { 139 | data class Dimension( 140 | @SerializedName("height") 141 | val height: Long, 142 | @SerializedName("rotate") 143 | val rotate: Long, 144 | @SerializedName("width") 145 | val width: Long, 146 | ) : Serializable 147 | } 148 | 149 | data class Rights( 150 | @SerializedName("arc_pay") 151 | val arcPay: Long, 152 | @SerializedName("autoplay") 153 | val autoplay: Long, 154 | @SerializedName("bp") 155 | val bp: Long, 156 | @SerializedName("clean_mode") 157 | val cleanMode: Long, 158 | @SerializedName("download") 159 | val download: Long, 160 | @SerializedName("elec") 161 | val elec: Long, 162 | @SerializedName("free_watch") 163 | val freeWatch: Long, 164 | @SerializedName("hd5") 165 | val hd5: Long, 166 | @SerializedName("is_360") 167 | val is360: Long, 168 | @SerializedName("is_cooperation") 169 | val isCooperation: Long, 170 | @SerializedName("is_stein_gate") 171 | val isSteinGate: Long, 172 | @SerializedName("movie") 173 | val movie: Long, 174 | @SerializedName("no_background") 175 | val noBackground: Long, 176 | @SerializedName("no_reprint") 177 | val noReprint: Long, 178 | @SerializedName("no_share") 179 | val noShare: Long, 180 | @SerializedName("pay") 181 | val pay: Long, 182 | @SerializedName("ugc_pay") 183 | val ugcPay: Long, 184 | @SerializedName("ugc_pay_preview") 185 | val ugcPayPreview: Long, 186 | ) : Serializable 187 | 188 | data class Stat( 189 | @SerializedName("aid") 190 | val aid: Long, 191 | @SerializedName("argue_msg") 192 | val argueMsg: String, 193 | @SerializedName("coin") 194 | val coin: Long, 195 | @SerializedName("danmaku") 196 | val danmaku: Long, 197 | @SerializedName("dislike") 198 | val dislike: Long, 199 | @SerializedName("evaluation") 200 | val evaluation: String, 201 | @SerializedName("favorite") 202 | val favorite: Long, 203 | @SerializedName("his_rank") 204 | val hisRank: Long, 205 | @SerializedName("like") 206 | val like: Long, 207 | @SerializedName("now_rank") 208 | val nowRank: Long, 209 | @SerializedName("reply") 210 | val reply: Long, 211 | @SerializedName("share") 212 | val share: Long, 213 | @SerializedName("view") 214 | val view: Long, 215 | @SerializedName("vt") 216 | val vt: Long, 217 | ) : Serializable 218 | 219 | data class Subtitle( 220 | @SerializedName("allow_submit") 221 | val allowSubmit: Boolean, 222 | @SerializedName("list") 223 | val list: List, 224 | ) : Serializable 225 | 226 | data class UserGarb( 227 | @SerializedName("url_image_ani_cut") 228 | val urlImageAniCut: String, 229 | ) : Serializable 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /core/model/src/main/java/com/imcys/core/model/cook/CookingIngredient.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.core.model.cook 2 | 3 | import com.squareup.moshi.Json 4 | import com.squareup.moshi.JsonClass 5 | 6 | data class CookingIngredient( 7 | val emoji: String = "", 8 | val image: String? = null, 9 | val label: String? = null, 10 | val name: String = "", 11 | val alias: String? = null, 12 | ) 13 | -------------------------------------------------------------------------------- /core/model/src/main/java/com/imcys/core/model/cook/CookingIngredientsInfo.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.core.model.cook 2 | 3 | import com.squareup.moshi.Json 4 | import com.squareup.moshi.JsonClass 5 | 6 | /** 7 | * 烹饪材料类 8 | * @property code Int 9 | * @property `data` Data 10 | * @property msg String 11 | * @constructor 12 | */ 13 | data class CookingIngredientsInfo( 14 | val code: Int = 0, 15 | val `data`: Data = Data(), 16 | val msg: String = "", 17 | ) { 18 | data class Data( 19 | val meat: List = listOf(), 20 | val staple: List = listOf(), 21 | val tools: List = listOf(), 22 | val vegetable: List = listOf(), 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /core/model/src/test/java/com/imcys/core/model/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.core.model 2 | 3 | import org.junit.Assert.* 4 | import org.junit.Test 5 | 6 | /** 7 | * Example local unit test, which will execute on the development machine (host). 8 | * 9 | * See [testing documentation](http://d.android.com/tools/testing). 10 | */ 11 | class ExampleUnitTest { 12 | @Test 13 | fun addition_isCorrect() { 14 | assertEquals(4, 2 + 2) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /core/network/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /core/network/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.library") 3 | id("org.jetbrains.kotlin.android") 4 | } 5 | 6 | android { 7 | compileSdk = 34 8 | 9 | namespace = "com.imcys.core.network" 10 | 11 | composeOptions { 12 | kotlinCompilerExtensionVersion = "1.5.3" 13 | } 14 | compileOptions { 15 | sourceCompatibility = JavaVersion.VERSION_17 16 | targetCompatibility = JavaVersion.VERSION_17 17 | } 18 | kotlinOptions { 19 | jvmTarget = "17" 20 | } 21 | } 22 | 23 | dependencies { 24 | implementation(project(":core:common")) 25 | implementation(project(":core:model")) 26 | 27 | api(libs.com.squareup.retrofit2.retrofit) 28 | api("com.squareup.retrofit2:converter-gson:2.9.0") 29 | 30 | implementation("androidx.core:core-ktx:1.8.0") 31 | implementation("androidx.appcompat:appcompat:1.4.1") 32 | implementation("com.google.android.material:material:1.5.0") 33 | testImplementation("junit:junit:4.13.2") 34 | androidTestImplementation("androidx.test.ext:junit:1.1.3") 35 | androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0") 36 | } 37 | -------------------------------------------------------------------------------- /core/network/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1250422131/FoodChoice/18de8536cd5829fd7bdb0ddb73001ddb98c7fef0/core/network/consumer-rules.pro -------------------------------------------------------------------------------- /core/network/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.kts. 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 22 | -------------------------------------------------------------------------------- /core/network/src/androidTest/java/com/imcys/core/network/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.core.network 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import org.junit.Assert.* 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | /** 10 | * Instrumented test, which will execute on an Android device. 11 | * 12 | * See [testing documentation](http://d.android.com/tools/testing). 13 | */ 14 | @RunWith(AndroidJUnit4::class) 15 | class ExampleInstrumentedTest { 16 | @Test 17 | fun useAppContext() { 18 | // Context of the app under test. 19 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 20 | assertEquals("com.imcys.network.test", appContext.packageName) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /core/network/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /core/network/src/main/java/com/imcys/core/network/interceptor/RFErrorHandlingInterceptor.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.core.network.interceptor 2 | 3 | import okhttp3.Interceptor 4 | import okhttp3.Response 5 | 6 | class RFErrorHandlingInterceptor : Interceptor { 7 | override fun intercept(chain: Interceptor.Chain): Response { 8 | val request = chain.request() 9 | 10 | return chain.proceed(request) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /core/network/src/main/java/com/imcys/core/network/retrofit/RetrofitAppApi.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.core.network.retrofit 2 | 3 | import com.imcys.core.model.cook.CookFoodInfo 4 | import com.imcys.core.model.cook.CookingIngredientsInfo 5 | import retrofit2.http.GET 6 | 7 | interface RetrofitAppApi { 8 | 9 | @GET("cookInfo.php") 10 | suspend fun getCookFoodData(): CookFoodInfo 11 | 12 | @GET("cookingIngredients.php") 13 | suspend fun getCookingIngredients(): CookingIngredientsInfo 14 | } 15 | -------------------------------------------------------------------------------- /core/network/src/main/java/com/imcys/core/network/retrofit/RetrofitAppNetwork.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.core.network.retrofit 2 | 3 | import okhttp3.OkHttpClient 4 | import retrofit2.Retrofit 5 | import retrofit2.converter.gson.GsonConverterFactory 6 | 7 | object RetrofitAppNetwork { 8 | 9 | private val client = OkHttpClient.Builder() 10 | .build() 11 | 12 | val networkApi: RetrofitAppApi = Retrofit.Builder() 13 | .client(client) 14 | .baseUrl("https://api.misakamoe.com/app/cook/") 15 | .addConverterFactory(GsonConverterFactory.create()) 16 | .build() 17 | .create(RetrofitAppApi::class.java) 18 | 19 | val bilibiliApi: RetrofitBiliBiliApi = Retrofit.Builder() 20 | .client(client) 21 | .baseUrl("https://api.bilibili.com/") 22 | .addConverterFactory(GsonConverterFactory.create()) 23 | .build() 24 | .create(RetrofitBiliBiliApi::class.java) 25 | } 26 | -------------------------------------------------------------------------------- /core/network/src/main/java/com/imcys/core/network/retrofit/RetrofitBiliBiliApi.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.core.network.retrofit 2 | 3 | import com.imcys.core.model.cook.CookFoodVideoInfo 4 | import retrofit2.http.GET 5 | import retrofit2.http.Query 6 | 7 | interface RetrofitBiliBiliApi { 8 | // 获取B站视频信息 9 | @GET("x/web-interface/view") 10 | suspend fun getVideoInfo(@Query("bvid") bvId: String): CookFoodVideoInfo 11 | } 12 | -------------------------------------------------------------------------------- /core/network/src/test/java/com/imcys/core/network/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.core.network 2 | 3 | import org.junit.Assert.* 4 | import org.junit.Test 5 | 6 | /** 7 | * Example local unit test, which will execute on the development machine (host). 8 | * 9 | * See [testing documentation](http://d.android.com/tools/testing). 10 | */ 11 | class ExampleUnitTest { 12 | @Test 13 | fun addition_isCorrect() { 14 | assertEquals(4, 2 + 2) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /core/ui/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /core/ui/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.library") 3 | id("org.jetbrains.kotlin.android") 4 | alias(libs.plugins.kotlin.compose) 5 | } 6 | 7 | android { 8 | namespace = "com.imcys.core.ui" 9 | compileSdk = 34 10 | 11 | defaultConfig { 12 | minSdk = 21 13 | 14 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 15 | consumerProguardFiles("consumer-rules.pro") 16 | } 17 | 18 | buildTypes { 19 | release { 20 | isMinifyEnabled = false 21 | proguardFiles( 22 | getDefaultProguardFile("proguard-android-optimize.txt"), 23 | "proguard-rules.pro", 24 | ) 25 | } 26 | } 27 | 28 | composeOptions { 29 | kotlinCompilerExtensionVersion = "1.5.3" 30 | } 31 | 32 | compileOptions { 33 | sourceCompatibility = JavaVersion.VERSION_17 34 | targetCompatibility = JavaVersion.VERSION_17 35 | } 36 | kotlinOptions { 37 | jvmTarget = "17" 38 | } 39 | 40 | buildFeatures { 41 | compose = true 42 | } 43 | } 44 | 45 | dependencies { 46 | implementation(project(":core:common")) 47 | implementation(project(":core:designsystem")) 48 | 49 | implementation("androidx.core:core-ktx:1.8.0") 50 | implementation("androidx.appcompat:appcompat:1.4.1") 51 | implementation("com.google.android.material:material:1.5.0") 52 | testImplementation("junit:junit:4.13.2") 53 | androidTestImplementation("androidx.test.ext:junit:1.1.3") 54 | androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0") 55 | } 56 | -------------------------------------------------------------------------------- /core/ui/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1250422131/FoodChoice/18de8536cd5829fd7bdb0ddb73001ddb98c7fef0/core/ui/consumer-rules.pro -------------------------------------------------------------------------------- /core/ui/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 -------------------------------------------------------------------------------- /core/ui/src/androidTest/java/com/imcys/core/ui/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.core.ui 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import org.junit.Assert.* 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | /** 10 | * Instrumented test, which will execute on an Android device. 11 | * 12 | * See [testing documentation](http://d.android.com/tools/testing). 13 | */ 14 | @RunWith(AndroidJUnit4::class) 15 | class ExampleInstrumentedTest { 16 | @Test 17 | fun useAppContext() { 18 | // Context of the app under test. 19 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 20 | assertEquals("com.imcys.core.ui.test", appContext.packageName) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /core/ui/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /core/ui/src/main/java/com/imcys/core/ui/CookInfoVideoCard.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.core.ui 2 | 3 | import android.annotation.SuppressLint 4 | import androidx.compose.foundation.Image 5 | import androidx.compose.foundation.isSystemInDarkTheme 6 | import androidx.compose.foundation.layout.Arrangement 7 | import androidx.compose.foundation.layout.Box 8 | import androidx.compose.foundation.layout.Column 9 | import androidx.compose.foundation.layout.Row 10 | import androidx.compose.foundation.layout.Spacer 11 | import androidx.compose.foundation.layout.fillMaxSize 12 | import androidx.compose.foundation.layout.fillMaxWidth 13 | import androidx.compose.foundation.layout.height 14 | import androidx.compose.foundation.layout.padding 15 | import androidx.compose.foundation.layout.size 16 | import androidx.compose.foundation.layout.width 17 | import androidx.compose.foundation.shape.CircleShape 18 | import androidx.compose.foundation.shape.RoundedCornerShape 19 | import androidx.compose.material3.Card 20 | import androidx.compose.material3.CardDefaults 21 | import androidx.compose.material3.Surface 22 | import androidx.compose.material3.Text 23 | import androidx.compose.runtime.Composable 24 | import androidx.compose.ui.Alignment 25 | import androidx.compose.ui.Modifier 26 | import androidx.compose.ui.graphics.Color 27 | import androidx.compose.ui.layout.ContentScale 28 | import androidx.compose.ui.platform.LocalContext 29 | import androidx.compose.ui.res.painterResource 30 | import androidx.compose.ui.text.style.TextOverflow 31 | import androidx.compose.ui.tooling.preview.Preview 32 | import androidx.compose.ui.unit.dp 33 | import androidx.compose.ui.unit.sp 34 | import coil.compose.SubcomposeAsyncImage 35 | import coil.request.ImageRequest 36 | import com.imcys.core.common.extend.digitalConversion 37 | 38 | @Preview 39 | @Composable 40 | fun CookInfoVideoCard( 41 | title: String = "食用手册", 42 | content: String = "不知道做什么菜?来这里看看", 43 | thumbnailUrl: String = "https://img1.imgtp.com/2023/07/08/z0tUwxLu.jpg", 44 | avatarUrl: String = "https://img1.imgtp.com/2023/07/08/z0tUwxLu.jpg", 45 | duration: Long = 0, 46 | view: Long = 0, 47 | @SuppressLint("ModifierParameter") modifier: Modifier = Modifier, 48 | ) { 49 | Card( 50 | modifier = modifier.fillMaxWidth(), 51 | ) { 52 | // 产生上下结构 53 | Column(Modifier.fillMaxWidth().padding(10.dp)) { 54 | // 产生视频封面预览 55 | CookInfoVideoCardThumbnai(thumbnailUrl, duration) 56 | // 产生左右结构 57 | CookInfoVideoCardAvatar(avatarUrl, title, content, view) 58 | } 59 | } 60 | } 61 | 62 | @Composable 63 | private fun CookInfoVideoCardThumbnai(thumbnailUrl: String, duration: Long) { 64 | Surface(Modifier.fillMaxWidth(), shape = CardDefaults.shape) { 65 | // 让视频前可以浮动一个视频时长 66 | Box(Modifier.fillMaxWidth().height(180.dp)) { 67 | SubcomposeAsyncImage( 68 | ImageRequest.Builder(LocalContext.current) 69 | .data(thumbnailUrl) 70 | .crossfade(true) 71 | .build(), 72 | contentScale = ContentScale.Crop, 73 | contentDescription = null, 74 | modifier = Modifier 75 | .fillMaxSize(), 76 | error = { 77 | Image( 78 | painter = painterResource(id = R.drawable.unnamed), 79 | contentDescription = null, 80 | ) 81 | }, 82 | ) 83 | Column( 84 | Modifier.fillMaxSize().padding(10.dp), 85 | verticalArrangement = Arrangement.Bottom, 86 | horizontalAlignment = Alignment.End, 87 | ) { 88 | Surface(shape = RoundedCornerShape(5.dp), color = Color.Black) { 89 | Text( 90 | text = run { 91 | val minutes = duration / 60 92 | val remainingSeconds = duration % 60 93 | "%02d:%02d".format(minutes, remainingSeconds) 94 | }, 95 | color = Color.White, 96 | modifier = Modifier.padding(6.dp), 97 | ) 98 | } 99 | } 100 | } 101 | } 102 | } 103 | 104 | @Composable 105 | private fun CookInfoVideoCardAvatar(avatarUrl: String, title: String, content: String, view: Long) { 106 | Spacer(Modifier.height(10.dp)) 107 | Row(Modifier.fillMaxWidth().height(70.dp), verticalAlignment = Alignment.CenterVertically) { 108 | Surface( 109 | shape = CircleShape, 110 | ) { 111 | SubcomposeAsyncImage( 112 | ImageRequest.Builder(LocalContext.current) 113 | .data(avatarUrl) 114 | .crossfade(true) 115 | .build(), 116 | contentScale = ContentScale.Crop, 117 | contentDescription = null, 118 | modifier = Modifier 119 | .size(60.dp), 120 | error = { 121 | Image( 122 | painter = painterResource(id = R.drawable.unnamed), 123 | contentDescription = null, 124 | ) 125 | }, 126 | ) 127 | } 128 | Spacer(Modifier.width(10.dp)) 129 | 130 | Column(Modifier.fillMaxSize(), verticalArrangement = Arrangement.SpaceEvenly) { 131 | Text( 132 | text = title, 133 | color = if (isSystemInDarkTheme()) Color.White else Color.Black, 134 | maxLines = 1, 135 | overflow = TextOverflow.Ellipsis, 136 | ) 137 | 138 | Text( 139 | text = content, 140 | color = if (isSystemInDarkTheme()) Color.White else Color.Black, 141 | fontSize = 10.sp, 142 | maxLines = 1, 143 | overflow = TextOverflow.Ellipsis, 144 | ) 145 | 146 | Row(verticalAlignment = Alignment.CenterVertically) { 147 | Text( 148 | text = "${view.digitalConversion()} 播放", 149 | color = if (isSystemInDarkTheme()) Color.White else Color.Black, 150 | fontSize = 8.sp, 151 | ) 152 | } 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /core/ui/src/main/java/com/imcys/core/ui/HomeFunctionCard.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.core.ui 2 | 3 | import android.annotation.SuppressLint 4 | import androidx.compose.foundation.Image 5 | import androidx.compose.foundation.clickable 6 | import androidx.compose.foundation.isSystemInDarkTheme 7 | import androidx.compose.foundation.layout.Column 8 | import androidx.compose.foundation.layout.Spacer 9 | import androidx.compose.foundation.layout.fillMaxWidth 10 | import androidx.compose.foundation.layout.height 11 | import androidx.compose.foundation.layout.padding 12 | import androidx.compose.material3.Card 13 | import androidx.compose.material3.CardDefaults 14 | import androidx.compose.material3.Surface 15 | import androidx.compose.material3.Text 16 | import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass 17 | import androidx.compose.runtime.Composable 18 | import androidx.compose.ui.Modifier 19 | import androidx.compose.ui.graphics.Color 20 | import androidx.compose.ui.layout.ContentScale 21 | import androidx.compose.ui.platform.LocalContext 22 | import androidx.compose.ui.res.painterResource 23 | import androidx.compose.ui.text.font.FontWeight 24 | import androidx.compose.ui.tooling.preview.Preview 25 | import androidx.compose.ui.unit.dp 26 | import androidx.compose.ui.unit.sp 27 | import coil.compose.SubcomposeAsyncImage 28 | import coil.request.ImageRequest 29 | import com.imcys.core.ui.base.getWidthSizeClass 30 | 31 | @Preview 32 | @Composable 33 | fun HomeFunctionCard( 34 | title: String = "食用手册", 35 | content: String = "不知道做什么菜?来这里看看", 36 | faceUrl: String = "https://img1.imgtp.com/2023/07/08/z0tUwxLu.jpg", 37 | @SuppressLint("ModifierParameter") modifier: Modifier = Modifier 38 | .fillMaxWidth() 39 | .clickable { }, 40 | ) { 41 | val imageHeight = when (getWidthSizeClass()) { 42 | WindowWidthSizeClass.Compact -> 124.dp 43 | WindowWidthSizeClass.Medium -> 160.dp 44 | WindowWidthSizeClass.Expanded -> 180.dp 45 | else -> 180.dp 46 | } 47 | 48 | Card( 49 | modifier = modifier, 50 | ) { 51 | Column(Modifier.fillMaxWidth()) { 52 | Surface(shape = CardDefaults.shape) { 53 | SubcomposeAsyncImage( 54 | ImageRequest.Builder(LocalContext.current) 55 | .data(faceUrl) 56 | .crossfade(true) 57 | .build(), 58 | contentScale = ContentScale.Crop, 59 | contentDescription = null, 60 | modifier = Modifier 61 | .fillMaxWidth() 62 | .height(imageHeight), 63 | error = { 64 | Image( 65 | contentScale = ContentScale.Crop, 66 | painter = painterResource(id = R.drawable.unnamed), 67 | contentDescription = null, 68 | ) 69 | }, 70 | ) 71 | } 72 | 73 | Column( 74 | Modifier 75 | .fillMaxWidth() 76 | .padding(12.dp), 77 | ) { 78 | Text( 79 | text = title, 80 | fontSize = 20.sp, 81 | fontWeight = FontWeight.ExtraBold, 82 | color = if (isSystemInDarkTheme()) Color.White else Color.Black, 83 | ) 84 | Spacer(Modifier.height(12.dp)) 85 | Text( 86 | text = content, 87 | fontSize = 12.sp, 88 | ) 89 | } 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /core/ui/src/main/java/com/imcys/core/ui/PageContentColumn.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.core.ui 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.ColumnScope 5 | import androidx.compose.foundation.layout.Row 6 | import androidx.compose.foundation.layout.Spacer 7 | import androidx.compose.foundation.layout.width 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.unit.dp 11 | 12 | @Composable 13 | fun PageContentColumn( 14 | modifier: Modifier, 15 | content: @Composable ColumnScope.() -> Unit, 16 | ) { 17 | Row(modifier) { 18 | Spacer(modifier = Modifier.width(16.dp)) 19 | Column(Modifier.weight(1f)) { 20 | content.invoke(this) 21 | } 22 | Spacer(modifier = Modifier.width(16.dp)) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /core/ui/src/main/java/com/imcys/core/ui/SettingsItem.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.core.ui 2 | 3 | import androidx.annotation.IntRange 4 | import androidx.compose.foundation.ExperimentalFoundationApi 5 | import androidx.compose.foundation.Image 6 | import androidx.compose.foundation.LocalIndication 7 | import androidx.compose.foundation.background 8 | import androidx.compose.foundation.combinedClickable 9 | import androidx.compose.foundation.interaction.MutableInteractionSource 10 | import androidx.compose.foundation.layout.Box 11 | import androidx.compose.foundation.layout.Column 12 | import androidx.compose.foundation.layout.Row 13 | import androidx.compose.foundation.layout.Spacer 14 | import androidx.compose.foundation.layout.fillMaxSize 15 | import androidx.compose.foundation.layout.fillMaxWidth 16 | import androidx.compose.foundation.layout.height 17 | import androidx.compose.foundation.layout.padding 18 | import androidx.compose.foundation.layout.size 19 | import androidx.compose.foundation.layout.width 20 | import androidx.compose.foundation.selection.selectable 21 | import androidx.compose.foundation.selection.toggleable 22 | import androidx.compose.foundation.shape.RoundedCornerShape 23 | import androidx.compose.material.icons.Icons 24 | import androidx.compose.material.icons.filled.Info 25 | import androidx.compose.material3.Icon 26 | import androidx.compose.material3.IconButton 27 | import androidx.compose.material3.LocalContentColor 28 | import androidx.compose.material3.MaterialTheme 29 | import androidx.compose.material3.RadioButton 30 | import androidx.compose.material3.Slider 31 | import androidx.compose.material3.Switch 32 | import androidx.compose.material3.Text 33 | import androidx.compose.runtime.Composable 34 | import androidx.compose.runtime.CompositionLocalProvider 35 | import androidx.compose.runtime.compositionLocalOf 36 | import androidx.compose.runtime.remember 37 | import androidx.compose.ui.Alignment 38 | import androidx.compose.ui.Modifier 39 | import androidx.compose.ui.draw.clip 40 | import androidx.compose.ui.graphics.Color 41 | import androidx.compose.ui.graphics.painter.Painter 42 | import androidx.compose.ui.graphics.vector.ImageVector 43 | import androidx.compose.ui.graphics.vector.rememberVectorPainter 44 | import androidx.compose.ui.semantics.Role 45 | import androidx.compose.ui.text.style.TextOverflow 46 | import androidx.compose.ui.unit.dp 47 | 48 | // 摘抄 49 | // https://github.com/SkyD666/Rays-Android/blob/master/app/src/main/java/com/skyd/rays/ui/component/SettingsItem.kt 50 | 51 | val LocalUseColorfulIcon = compositionLocalOf { true } 52 | val LocalVerticalPadding = compositionLocalOf { 16.dp } 53 | 54 | @Composable 55 | fun BannerItem(content: @Composable () -> Unit) { 56 | Column { 57 | Spacer(modifier = Modifier.height(16.dp)) 58 | CompositionLocalProvider( 59 | LocalContentColor provides (LocalContentColor.current ), 60 | LocalVerticalPadding provides 21.dp 61 | ) { 62 | Box( 63 | modifier = Modifier 64 | .padding(horizontal = 16.dp) 65 | .clip(RoundedCornerShape(36)) 66 | .background(MaterialTheme.colorScheme.primaryContainer) 67 | ) { 68 | content() 69 | } 70 | } 71 | Spacer(modifier = Modifier.height(16.dp)) 72 | } 73 | } 74 | 75 | @Composable 76 | fun SliderSettingsItem( 77 | imageVector: ImageVector?, 78 | text: String, 79 | value: Float, 80 | isImage: Boolean = false, 81 | modifier: Modifier = Modifier, 82 | valueRange: ClosedFloatingPointRange = 0f..1f, 83 | @IntRange(from = 0) 84 | steps: Int = 0, 85 | onValueChangeFinished: (() -> Unit)? = null, 86 | valueFormat: String = "%.2f", 87 | enabled: Boolean = true, 88 | onValueChange: (Float) -> Unit, 89 | ) { 90 | SliderSettingsItem( 91 | painter = imageVector?.let { rememberVectorPainter(image = it) }, 92 | text = text, 93 | value = value, 94 | isImage = isImage, 95 | modifier = modifier, 96 | valueRange = valueRange, 97 | steps = steps, 98 | onValueChangeFinished = onValueChangeFinished, 99 | valueFormat = valueFormat, 100 | enabled = enabled, 101 | onValueChange = onValueChange, 102 | ) 103 | } 104 | 105 | @Composable 106 | fun SliderSettingsItem( 107 | painter: Painter?, 108 | text: String, 109 | value: Float, 110 | modifier: Modifier = Modifier, 111 | isImage: Boolean = false, 112 | valueRange: ClosedFloatingPointRange = 0f..1f, 113 | @IntRange(from = 0) 114 | steps: Int = 0, 115 | onValueChangeFinished: (() -> Unit)? = null, 116 | valueFormat: String = "%.2f", 117 | enabled: Boolean = true, 118 | onValueChange: (Float) -> Unit, 119 | ) { 120 | BaseSettingsItem( 121 | painter = painter, 122 | text = text, 123 | modifier = modifier, 124 | enabled = enabled, 125 | isImage = isImage, 126 | description = { 127 | Row(verticalAlignment = Alignment.CenterVertically) { 128 | Slider( 129 | modifier = Modifier.weight(1f), 130 | value = value, 131 | enabled = enabled, 132 | valueRange = valueRange, 133 | steps = steps, 134 | onValueChangeFinished = onValueChangeFinished, 135 | onValueChange = onValueChange, 136 | ) 137 | Spacer(modifier = Modifier.width(6.dp)) 138 | Text(text = String.format(valueFormat, value)) 139 | } 140 | } 141 | ) 142 | } 143 | 144 | @Composable 145 | fun SwitchSettingsItem( 146 | imageVector: ImageVector?, 147 | text: String, 148 | modifier: Modifier = Modifier, 149 | description: String? = null, 150 | checked: Boolean = false, 151 | enabled: Boolean = true, 152 | onCheckedChange: ((Boolean) -> Unit)?, 153 | ) { 154 | SwitchSettingsItem( 155 | painter = imageVector?.let { rememberVectorPainter(image = it) }, 156 | text = text, 157 | modifier = modifier, 158 | description = description, 159 | checked = checked, 160 | enabled = enabled, 161 | onCheckedChange = onCheckedChange, 162 | ) 163 | } 164 | 165 | @Composable 166 | fun SwitchSettingsItem( 167 | painter: Painter?, 168 | text: String, 169 | modifier: Modifier = Modifier, 170 | description: String? = null, 171 | checked: Boolean = false, 172 | enabled: Boolean = true, 173 | onCheckedChange: ((Boolean) -> Unit)?, 174 | ) { 175 | val interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } 176 | BaseSettingsItem( 177 | modifier = modifier.toggleable( 178 | value = checked, 179 | interactionSource = interactionSource, 180 | indication = LocalIndication.current, 181 | enabled = enabled, 182 | role = Role.Switch, 183 | onValueChange = { onCheckedChange?.invoke(it) }, 184 | ), 185 | painter = painter, 186 | text = text, 187 | descriptionText = description, 188 | enabled = enabled, 189 | ) { 190 | Switch( 191 | checked = checked, 192 | enabled = enabled, 193 | onCheckedChange = onCheckedChange, 194 | interactionSource = interactionSource 195 | ) 196 | } 197 | } 198 | 199 | @Composable 200 | fun RadioSettingsItem( 201 | imageVector: ImageVector?, 202 | text: String, 203 | modifier: Modifier = Modifier, 204 | description: String? = null, 205 | selected: Boolean = false, 206 | enabled: Boolean = true, 207 | onClick: (() -> Unit)? = null, 208 | ) { 209 | RadioSettingsItem( 210 | painter = imageVector?.let { rememberVectorPainter(image = it) }, 211 | text = text, 212 | modifier = modifier, 213 | description = description, 214 | selected = selected, 215 | enabled = enabled, 216 | onClick = onClick 217 | ) 218 | } 219 | 220 | @Composable 221 | fun RadioSettingsItem( 222 | painter: Painter?, 223 | text: String, 224 | modifier: Modifier = Modifier, 225 | description: String? = null, 226 | selected: Boolean = false, 227 | enabled: Boolean = true, 228 | onClick: (() -> Unit)? = null, 229 | ) { 230 | val interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } 231 | BaseSettingsItem( 232 | modifier = modifier 233 | .selectable( 234 | selected = selected, 235 | interactionSource = interactionSource, 236 | indication = LocalIndication.current, 237 | enabled = enabled, 238 | role = Role.RadioButton, 239 | onClick = { onClick?.invoke() }, 240 | ), 241 | painter = painter, 242 | text = text, 243 | descriptionText = description, 244 | enabled = enabled, 245 | ) { 246 | RadioButton( 247 | selected = selected, 248 | onClick = onClick, 249 | enabled = enabled, 250 | interactionSource = interactionSource 251 | ) 252 | } 253 | } 254 | 255 | @Composable 256 | fun ColorSettingsItem( 257 | imageVector: ImageVector?, 258 | text: String, 259 | modifier: Modifier = Modifier, 260 | description: String? = null, 261 | onClick: (() -> Unit)? = null, 262 | initColor: Color, 263 | ) { 264 | ColorSettingsItem( 265 | painter = imageVector?.let { rememberVectorPainter(image = it) }, 266 | text = text, 267 | modifier = modifier, 268 | description = description, 269 | onClick = onClick, 270 | initColor = initColor, 271 | ) 272 | } 273 | 274 | @Composable 275 | fun ColorSettingsItem( 276 | painter: Painter?, 277 | text: String, 278 | modifier: Modifier = Modifier, 279 | description: String? = null, 280 | onClick: (() -> Unit)? = null, 281 | initColor: Color, 282 | ) { 283 | BaseSettingsItem( 284 | painter = painter, 285 | text = text, 286 | modifier = modifier, 287 | descriptionText = description, 288 | onClick = onClick 289 | ) { 290 | IconButton(onClick = { onClick?.invoke() }) { 291 | Box( 292 | modifier = Modifier 293 | .fillMaxSize() 294 | .background( 295 | color = initColor, 296 | shape = RoundedCornerShape(50.dp) 297 | ) 298 | ) 299 | } 300 | } 301 | } 302 | 303 | @Composable 304 | fun BaseSettingsItem( 305 | modifier: Modifier = Modifier, 306 | painter: Painter?, 307 | text: String, 308 | descriptionText: String? = null, 309 | enabled: Boolean = true, 310 | isImage: Boolean = false, 311 | onClick: (() -> Unit)? = null, 312 | onLongClick: (() -> Unit)? = null, 313 | dropdownMenu: (@Composable () -> Unit)? = null, 314 | content: (@Composable () -> Unit)? = null 315 | ) { 316 | BaseSettingsItem( 317 | modifier = modifier, 318 | painter = painter, 319 | text = text, 320 | description = if (descriptionText != null) { 321 | { 322 | Text( 323 | text = descriptionText, 324 | style = MaterialTheme.typography.bodyMedium, 325 | maxLines = 3, 326 | overflow = TextOverflow.Ellipsis, 327 | ) 328 | } 329 | } else null, 330 | enabled = enabled, 331 | isImage = isImage, 332 | onClick = if (enabled) onClick else null, 333 | onLongClick = if (enabled) onLongClick else null, 334 | dropdownMenu = dropdownMenu, 335 | content = content, 336 | ) 337 | } 338 | 339 | @OptIn(ExperimentalFoundationApi::class) 340 | @Composable 341 | fun BaseSettingsItem( 342 | modifier: Modifier = Modifier, 343 | painter: Painter?, 344 | text: String, 345 | description: (@Composable () -> Unit)? = null, 346 | isImage: Boolean = false, 347 | enabled: Boolean = true, 348 | onClick: (() -> Unit)? = null, 349 | onLongClick: (() -> Unit)? = null, 350 | dropdownMenu: (@Composable () -> Unit)? = null, 351 | content: (@Composable () -> Unit)? = null 352 | ) { 353 | CompositionLocalProvider( 354 | LocalContentColor provides if (enabled) { 355 | LocalContentColor.current 356 | } else { 357 | LocalContentColor.current.copy(alpha = 0.38f) 358 | }, 359 | ) { 360 | Row( 361 | modifier = modifier 362 | .fillMaxWidth() 363 | .run { 364 | if (onClick != null && enabled) { 365 | combinedClickable(onLongClick = onLongClick) { onClick() } 366 | } else this 367 | } 368 | .padding(horizontal = 16.dp), 369 | verticalAlignment = Alignment.CenterVertically 370 | ) { 371 | if (painter != null) { 372 | if (isImage) { 373 | Image( 374 | modifier = Modifier 375 | .padding(10.dp) 376 | .size(24.dp), 377 | painter = painter, 378 | contentDescription = null 379 | ) 380 | } else { 381 | Icon( 382 | modifier = Modifier 383 | .padding(10.dp) 384 | .size(24.dp), 385 | painter = painter, 386 | contentDescription = null, 387 | ) 388 | } 389 | } 390 | Column( 391 | modifier = Modifier 392 | .weight(1f) 393 | .padding(horizontal = 10.dp, vertical = LocalVerticalPadding.current) 394 | ) { 395 | Text( 396 | text = text, 397 | style = MaterialTheme.typography.titleLarge, 398 | maxLines = 3, 399 | overflow = TextOverflow.Ellipsis, 400 | ) 401 | dropdownMenu?.invoke() 402 | if (description != null) { 403 | Box(modifier = Modifier.padding(top = 5.dp)) { 404 | description.invoke() 405 | } 406 | } 407 | } 408 | content?.let { 409 | Box(modifier = Modifier.padding(end = 5.dp)) { it.invoke() } 410 | } 411 | } 412 | } 413 | } 414 | 415 | @Composable 416 | fun CategorySettingsItem(text: String) { 417 | Text( 418 | modifier = Modifier.padding( 419 | start = 16.dp + 10.dp, 420 | end = 20.dp, 421 | top = 10.dp, 422 | bottom = 5.dp 423 | ), 424 | text = text, 425 | style = MaterialTheme.typography.titleMedium, 426 | color = MaterialTheme.colorScheme.primary, 427 | maxLines = 3, 428 | overflow = TextOverflow.Ellipsis, 429 | ) 430 | } 431 | 432 | @Composable 433 | fun TipSettingsItem(text: String) { 434 | Column( 435 | modifier = Modifier.padding(horizontal = 16.dp + 10.dp, vertical = 10.dp) 436 | ) { 437 | Icon(imageVector = Icons.Default.Info, contentDescription = null) 438 | Spacer(modifier = Modifier.height(10.dp)) 439 | Text(text = text, style = MaterialTheme.typography.bodyMedium) 440 | } 441 | } -------------------------------------------------------------------------------- /core/ui/src/main/java/com/imcys/core/ui/WaifuBoostAlertDialog.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.core.ui 2 | 3 | import androidx.compose.material3.AlertDialog 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.Modifier 6 | 7 | @Composable 8 | fun WaifuBoostAlertDialog( 9 | showState: Boolean = false, 10 | clickBlankDismiss: Boolean = true, 11 | icon: @Composable (() -> Unit)? = null, 12 | title: @Composable (() -> Unit)? = null, 13 | text: @Composable (() -> Unit)? = null, 14 | confirmButton: @Composable () -> Unit, 15 | modifier: Modifier = Modifier, 16 | dismissButton: @Composable (() -> Unit)? = null, 17 | onDismiss: (() -> Unit)? = null, 18 | 19 | ) { 20 | if(showState){ 21 | AlertDialog( 22 | title = title, 23 | text = text, 24 | icon = icon, 25 | modifier = modifier, 26 | onDismissRequest = { 27 | if (clickBlankDismiss) { 28 | onDismiss?.invoke() 29 | } 30 | }, 31 | confirmButton = confirmButton, 32 | dismissButton = dismissButton 33 | ) 34 | } 35 | } 36 | 37 | 38 | -------------------------------------------------------------------------------- /core/ui/src/main/java/com/imcys/core/ui/base/BaseScreen.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.core.ui.base 2 | 3 | import android.content.Context 4 | import androidx.activity.ComponentActivity 5 | import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi 6 | import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass 7 | import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.platform.LocalContext 10 | 11 | @OptIn(ExperimentalMaterial3WindowSizeClassApi::class) 12 | @Composable 13 | fun getWidthSizeClass(context: Context = LocalContext.current): WindowWidthSizeClass { 14 | return calculateWindowSizeClass(context as ComponentActivity).widthSizeClass 15 | } 16 | -------------------------------------------------------------------------------- /core/ui/src/main/res/drawable/unnamed.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1250422131/FoodChoice/18de8536cd5829fd7bdb0ddb73001ddb98c7fef0/core/ui/src/main/res/drawable/unnamed.jpg -------------------------------------------------------------------------------- /core/ui/src/test/java/com/imcys/core/ui/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.core.ui 2 | 3 | import org.junit.Assert.* 4 | import org.junit.Test 5 | 6 | /** 7 | * Example local unit test, which will execute on the development machine (host). 8 | * 9 | * See [testing documentation](http://d.android.com/tools/testing). 10 | */ 11 | class ExampleUnitTest { 12 | @Test 13 | fun addition_isCorrect() { 14 | assertEquals(4, 2 + 2) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /feature/cook/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /feature/cook/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.library") 3 | id("org.jetbrains.kotlin.android") 4 | id("com.google.dagger.hilt.android") 5 | id("com.google.devtools.ksp") 6 | alias(libs.plugins.kotlin.compose) 7 | } 8 | 9 | android { 10 | namespace = "com.imcys.feature.cook" 11 | compileSdk = 35 12 | 13 | defaultConfig { 14 | minSdk = 21 15 | 16 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 17 | consumerProguardFiles("consumer-rules.pro") 18 | } 19 | 20 | buildTypes { 21 | release { 22 | isMinifyEnabled = false 23 | proguardFiles( 24 | getDefaultProguardFile("proguard-android-optimize.txt"), 25 | "proguard-rules.pro", 26 | ) 27 | } 28 | } 29 | composeOptions { 30 | kotlinCompilerExtensionVersion = "1.5.3" 31 | } 32 | compileOptions { 33 | sourceCompatibility = JavaVersion.VERSION_17 34 | targetCompatibility = JavaVersion.VERSION_17 35 | } 36 | kotlinOptions { 37 | jvmTarget = "17" 38 | } 39 | 40 | buildFeatures { 41 | compose = true 42 | } 43 | } 44 | 45 | dependencies { 46 | 47 | implementation("com.imcys.deeprecopy:core:0.0.1-Beta-2") 48 | ksp("com.imcys.deeprecopy:compiler:0.0.1-Beta-2") 49 | // hilt库,实现依赖注入 50 | api(libs.hilt.android) 51 | ksp(libs.hilt.compiler) 52 | 53 | implementation("androidx.room:room-runtime:2.4.3") 54 | implementation("androidx.room:room-ktx:2.4.3") 55 | ksp("androidx.room:room-compiler:2.4.3") 56 | 57 | implementation(project(":core:common")) 58 | implementation(project(":core:designsystem")) 59 | implementation(project(":core:ui")) 60 | implementation(project(":core:model")) 61 | implementation(project(":core:network")) 62 | implementation(project(":core:database")) 63 | implementation(project(":core:data")) 64 | 65 | implementation("androidx.core:core-ktx:1.8.0") 66 | implementation("androidx.appcompat:appcompat:1.4.1") 67 | implementation("com.google.android.material:material:1.5.0") 68 | testImplementation("junit:junit:4.13.2") 69 | androidTestImplementation("androidx.test.ext:junit:1.1.3") 70 | androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0") 71 | } 72 | -------------------------------------------------------------------------------- /feature/cook/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1250422131/FoodChoice/18de8536cd5829fd7bdb0ddb73001ddb98c7fef0/feature/cook/consumer-rules.pro -------------------------------------------------------------------------------- /feature/cook/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 22 | 23 | 24 | -keep class com.imcys.core.model.**{*;} # 自定义数据模型的bean目录 25 | -------------------------------------------------------------------------------- /feature/cook/src/androidTest/java/com/imcys/feature/cook/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.feature.cook 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import org.junit.Assert.* 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | /** 10 | * Instrumented test, which will execute on an Android device. 11 | * 12 | * See [testing documentation](http://d.android.com/tools/testing). 13 | */ 14 | @RunWith(AndroidJUnit4::class) 15 | class ExampleInstrumentedTest { 16 | @Test 17 | fun useAppContext() { 18 | // Context of the app under test. 19 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 20 | assertEquals("com.imcys.feature.cook.test", appContext.packageName) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /feature/cook/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /feature/cook/src/main/java/com/imcys/feature/cook/CookIntent.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.feature.cook 2 | 3 | import com.imcys.core.common.viewmodel.info.UiIntent 4 | import com.imcys.core.database.entity.CookFoodEntity 5 | import com.imcys.core.database.entity.CookingIngredientEntity 6 | import com.imcys.feature.cook.menu.CookSearchType 7 | 8 | sealed class CookIntent : UiIntent { 9 | data class SelectStuff(val stuff: String) : CookIntent() 10 | data class SelectTool(val tool: String) : CookIntent() 11 | data class InputSearchKeyword(val name: String) : CookIntent() 12 | data class ToBiliBili(val bvId: String) : CookIntent() 13 | data class UpdateSearchType(val type: CookSearchType) : CookIntent() 14 | data class PostOpenFoodInfo(val cookFoodEntity: CookFoodEntity) : CookIntent() 15 | data class PostSelectCookingIngredient(val cookingIngredientEntity: CookingIngredientEntity) : CookIntent() 16 | } 17 | -------------------------------------------------------------------------------- /feature/cook/src/main/java/com/imcys/feature/cook/CookState.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.feature.cook 2 | 3 | import com.imcys.core.common.viewmodel.info.UiState 4 | import com.imcys.core.database.entity.CookFoodEntity 5 | import com.imcys.core.database.entity.CookingIngredientEntity 6 | import com.imcys.feature.cook.menu.CookSearchType 7 | 8 | // 踩坑,务必让这里的属性是val而不是var,否则因为出现竞态条件造成线程不安全 9 | data class CookState ( 10 | val isShowBottomBar: Boolean = false, 11 | val cookingIngredientsEntity: MutableList = mutableListOf(), 12 | val searchStuffs: MutableList = mutableListOf(), 13 | val searchResultList: MutableList = mutableListOf(), 14 | val searchTool: String? = null, 15 | val searchName: String? = null, 16 | val searchType: CookSearchType = CookSearchType.FUZZY_MATCHING, 17 | val foodsEntity: MutableList = mutableListOf(), 18 | 19 | val updateUiState: Boolean = false, 20 | ) : UiState 21 | -------------------------------------------------------------------------------- /feature/cook/src/main/java/com/imcys/feature/cook/CookViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.feature.cook 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.net.Uri 7 | import androidx.lifecycle.viewModelScope 8 | import com.imcys.core.common.extend.launchIO 9 | import com.imcys.core.common.viewmodel.ComposeBaseViewModel 10 | import com.imcys.core.data.repository.cook.CookFoodInfoRepository 11 | import com.imcys.core.data.repository.cook.CookingIngredientRepository 12 | import com.imcys.core.database.entity.CookFoodEntity 13 | import com.imcys.core.database.entity.CookingIngredientEntity 14 | import com.imcys.feature.cook.menu.CookSearchType 15 | import com.microsoft.appcenter.analytics.Analytics 16 | import dagger.hilt.android.lifecycle.HiltViewModel 17 | import dagger.hilt.android.qualifiers.ApplicationContext 18 | import kotlinx.coroutines.delay 19 | import javax.inject.Inject 20 | 21 | @HiltViewModel 22 | class CookViewModel @Inject constructor( 23 | private val cookingIngredientRepository: CookingIngredientRepository, 24 | private val cookFoodInfoRepository: CookFoodInfoRepository, 25 | @ApplicationContext private val context: Context, 26 | ) : 27 | ComposeBaseViewModel(CookState()) { 28 | 29 | init { 30 | // appbar动画 31 | launchUI { 32 | delay(200L) 33 | viewStates = viewStates.copy(isShowBottomBar = true) 34 | } 35 | 36 | launchUI { 37 | // 初始化数据 38 | // 加载食材 39 | initCookInfo() 40 | // 加载食物 41 | initFoodsInfo() 42 | } 43 | } 44 | 45 | private suspend fun initFoodsInfo() { 46 | // 无网络数据加载 47 | viewStates = viewStates.copy(foodsEntity = cookFoodInfoRepository.getCookingFoods()) 48 | 49 | if (cookFoodInfoRepository.syncWithData()) { 50 | viewStates = 51 | viewStates.copy(foodsEntity = cookFoodInfoRepository.getCookingFoods()) 52 | } 53 | } 54 | 55 | private suspend fun initCookInfo() { 56 | // 无网络数据加载 57 | viewStates = 58 | viewStates.copy(cookingIngredientsEntity = cookingIngredientRepository.getCookingIngredients()) 59 | 60 | // 同步后更新加载 61 | if (cookingIngredientRepository.syncWithData()) { 62 | // 成功 63 | viewStates = 64 | viewStates.copy(cookingIngredientsEntity = cookingIngredientRepository.getCookingIngredients()) 65 | } 66 | } 67 | 68 | override fun handleEvent(event: CookIntent, state: CookState) { 69 | when (event) { 70 | is CookIntent.SelectStuff -> selectStuff(event.stuff) 71 | is CookIntent.SelectTool -> selectTool(event.tool) 72 | is CookIntent.InputSearchKeyword -> searchFood(event.name) 73 | is CookIntent.ToBiliBili -> toBiliBili(event.bvId) 74 | is CookIntent.UpdateSearchType -> { 75 | viewStates.update { copy(searchType = event.type) } 76 | updateSearchResult() 77 | } 78 | 79 | is CookIntent.PostOpenFoodInfo -> { 80 | postOpenFoodInfo(event.cookFoodEntity) 81 | } 82 | 83 | is CookIntent.PostSelectCookingIngredient -> { 84 | postSelectCookingIngredient(event.cookingIngredientEntity) 85 | } 86 | } 87 | } 88 | 89 | private fun postSelectCookingIngredient(cookingIngredientEntity: CookingIngredientEntity) { 90 | viewModelScope.launchIO { 91 | val properties: MutableMap = HashMap() 92 | properties["name"] = cookingIngredientEntity.name 93 | Analytics.trackEvent("SelectCookingIngredient", properties) 94 | } 95 | } 96 | 97 | private fun postOpenFoodInfo(cookFoodEntity: CookFoodEntity) { 98 | viewModelScope.launchIO { 99 | val properties: MutableMap = HashMap() 100 | properties["FoodName"] = cookFoodEntity.name 101 | properties["VideoBVID"] = cookFoodEntity.bv 102 | Analytics.trackEvent("OpenFoodInfo", properties) 103 | } 104 | } 105 | 106 | @SuppressLint("QueryPermissionsNeeded", "IntentReset") 107 | private fun toBiliBili(bvId: String) { 108 | val intent = Intent( 109 | Intent.ACTION_VIEW, 110 | Uri.parse("https://www.bilibili.com/video/$bvId"), 111 | ).apply { 112 | addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 113 | } 114 | context.startActivity(intent) 115 | } 116 | 117 | private fun searchFood(name: String) { 118 | viewStates.update { copy(searchName = name) } 119 | updateSearchResult() 120 | } 121 | 122 | private fun selectTool(tool: String) { 123 | viewStates.update { copy(searchTool = tool) } 124 | updateSearchResult() 125 | } 126 | 127 | private fun selectStuff(stuff: String) { 128 | if (!viewStates.searchStuffs.remove(stuff)) { 129 | // 这里注意,由于viewStates没有产生新的对象,这样做并不会导致界面更新,但事实上viewStates已经发生了变化 130 | viewStates.searchStuffs.add(stuff) 131 | } 132 | // 因此,我们用一个取巧的办法更新 133 | viewStates.update { copy(updateUiState = !viewStates.updateUiState) } 134 | updateSearchResult() 135 | } 136 | 137 | // 下面逻辑有待优化 138 | private fun updateSearchResult() { 139 | val resultList = mutableListOf().apply { addAll(viewStates.foodsEntity) } 140 | val startIndex = viewStates.foodsEntity.size 141 | 142 | // 通过选择的食材进行过滤 143 | applySearchStuffsFilter(resultList) 144 | // 通过选择的厨具过滤 145 | applySearchToolFilter(resultList) 146 | // 通过搜索的内容 147 | applySearchNameFilter(resultList) 148 | // 检查是否有搜索条件过滤,假如没有就清空 149 | clearResultListIfNoSearchConditions(resultList, startIndex) 150 | // 替换搜索结果的图标 151 | replaceIngredientsAndToolsIcons(resultList) 152 | // 更新执行结果 153 | viewStates.update { copy(searchResultList = resultList.toList().toMutableList()) } 154 | } 155 | 156 | /** 157 | * 过滤搜索的食材 158 | */ 159 | private fun applySearchStuffsFilter(resultList: MutableList) { 160 | // 看看是否存在搜索内容 161 | if (viewStates.searchStuffs.isNotEmpty()) { 162 | // 对每一个元素都检查 163 | resultList.retainAll { food -> 164 | // 拿到这个食物需要的食材 165 | val foodStuffs = food.stuff.split("、") 166 | // 根据搜索形式进行不同的匹配 167 | if (viewStates.searchType == CookSearchType.FUZZY_MATCHING) { 168 | foodStuffs.any { it in viewStates.searchStuffs } 169 | } else { 170 | foodStuffs.all { it in viewStates.searchStuffs } 171 | } 172 | } 173 | } 174 | } 175 | 176 | /** 177 | * 根据用户选择的烹饪厨具进行过滤 178 | */ 179 | private fun applySearchToolFilter(resultList: MutableList) { 180 | if (!viewStates.searchTool.isNullOrBlank()) { 181 | resultList.retainAll { it.tools.contains(viewStates.searchTool ?: "") } 182 | } 183 | } 184 | 185 | /** 186 | * 根据用户输入的名称进行模糊搜索 187 | */ 188 | private fun applySearchNameFilter(resultList: MutableList) { 189 | if (!viewStates.searchName.isNullOrBlank()) { 190 | resultList.retainAll { it.name.contains(viewStates.searchName ?: "") } 191 | } 192 | } 193 | 194 | /** 195 | * 校验是否有搜索条件 196 | */ 197 | private fun clearResultListIfNoSearchConditions( 198 | resultList: MutableList, 199 | startIndex: Int, 200 | ) { 201 | // 方式就是判断原resultList长度是否改变 202 | if (resultList.size == startIndex) { 203 | // 不改变相当于什么也没搜,我们全部清除掉 204 | resultList.clear() 205 | } 206 | } 207 | 208 | private fun replaceIngredientsAndToolsIcons(resultList: MutableList) { 209 | resultList.forEach { food -> 210 | if (food.emoji.isBlank()) { 211 | replaceIngredientsIcons(food) 212 | } 213 | if (food.image.isBlank()) { 214 | replaceToolsIcon(food) 215 | } 216 | } 217 | } 218 | 219 | private fun replaceIngredientsIcons(food: CookFoodEntity) { 220 | val emojis = food.stuff.split("、") 221 | val matchingIngredient = viewStates.cookingIngredientsEntity.find { it.name == emojis[0] } 222 | if (emojis.size > 3 && matchingIngredient != null) { 223 | food.emoji += matchingIngredient.emoji 224 | } else { 225 | emojis.forEach { stuff -> 226 | val matchingIngredient = 227 | viewStates.cookingIngredientsEntity.find { it.name == stuff } 228 | if (matchingIngredient != null) { 229 | food.emoji += matchingIngredient.emoji 230 | } 231 | } 232 | } 233 | } 234 | 235 | private fun replaceToolsIcon(food: CookFoodEntity) { 236 | val matchingCookingIngredient = 237 | viewStates.cookingIngredientsEntity.find { it.name == food.tools } 238 | if (viewStates.searchTool.isNullOrBlank() && matchingCookingIngredient != null) { 239 | food.image = matchingCookingIngredient.image ?: "" 240 | } else if (matchingCookingIngredient != null && matchingCookingIngredient.name == viewStates.searchTool) { 241 | food.image = matchingCookingIngredient.image ?: "" 242 | } 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /feature/cook/src/main/java/com/imcys/feature/cook/menu/CookSearchType.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.feature.cook.menu 2 | 3 | enum class CookSearchType { 4 | FUZZY_MATCHING, 5 | EXACT_MATCH, 6 | } 7 | -------------------------------------------------------------------------------- /feature/cook/src/main/java/com/imcys/feature/cook/navigation/CookInfoNavigation.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.feature.cook.navigation 2 | 3 | import androidx.navigation.NavController 4 | 5 | const val cookInfoRoute = "feature/cook/cookInfo/{bvId}" 6 | 7 | fun NavController.navigateToCookInfoRoute(bvId: String) { 8 | this.navigate("feature/cook/cookInfo/$bvId") 9 | } 10 | -------------------------------------------------------------------------------- /feature/cook/src/main/java/com/imcys/feature/cook/navigation/CookNavigation.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.feature.cook.navigation 2 | 3 | const val cookRoute = "feature/cook" 4 | -------------------------------------------------------------------------------- /feature/cook/src/main/java/com/imcys/feature/cook/ui/info/CookInfoScreen.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.feature.cook.ui.info 2 | 3 | import androidx.compose.animation.AnimatedVisibility 4 | import androidx.compose.foundation.Image 5 | import androidx.compose.foundation.clickable 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.Row 8 | import androidx.compose.foundation.layout.Spacer 9 | import androidx.compose.foundation.layout.fillMaxSize 10 | import androidx.compose.foundation.layout.fillMaxWidth 11 | import androidx.compose.foundation.layout.height 12 | import androidx.compose.foundation.layout.padding 13 | import androidx.compose.foundation.layout.size 14 | import androidx.compose.foundation.layout.width 15 | import androidx.compose.foundation.lazy.LazyColumn 16 | import androidx.compose.foundation.shape.RoundedCornerShape 17 | import androidx.compose.material.icons.Icons 18 | import androidx.compose.material.icons.outlined.ArrowBack 19 | import androidx.compose.material3.Card 20 | import androidx.compose.material3.ExperimentalMaterial3Api 21 | import androidx.compose.material3.Icon 22 | import androidx.compose.material3.IconButton 23 | import androidx.compose.material3.LargeTopAppBar 24 | import androidx.compose.material3.Scaffold 25 | import androidx.compose.material3.Surface 26 | import androidx.compose.material3.Text 27 | import androidx.compose.material3.TopAppBarDefaults 28 | import androidx.compose.runtime.Composable 29 | import androidx.compose.runtime.CompositionLocalProvider 30 | import androidx.compose.runtime.LaunchedEffect 31 | import androidx.compose.runtime.compositionLocalOf 32 | import androidx.compose.ui.Alignment 33 | import androidx.compose.ui.Modifier 34 | import androidx.compose.ui.input.nestedscroll.nestedScroll 35 | import androidx.compose.ui.platform.LocalContext 36 | import androidx.compose.ui.res.painterResource 37 | import androidx.compose.ui.text.font.FontWeight 38 | import androidx.compose.ui.tooling.preview.Preview 39 | import androidx.compose.ui.unit.dp 40 | import androidx.compose.ui.unit.sp 41 | import androidx.hilt.navigation.compose.hiltViewModel 42 | import androidx.navigation.NavHostController 43 | import androidx.navigation.compose.rememberNavController 44 | import com.imcys.core.ui.CookInfoVideoCard 45 | import com.imcys.core.ui.PageContentColumn 46 | import com.imcys.feature.cook.R 47 | 48 | private val LocalViewModel = compositionLocalOf { error("No init!") } 49 | private val LocalViewState = compositionLocalOf { error("No init!") } 50 | private val LocalNavController = compositionLocalOf { error("No init!") } 51 | 52 | @Composable 53 | fun CookInfoRoute( 54 | bvId: String, 55 | modifier: Modifier = Modifier, 56 | viewModel: CookInfoViewModel = hiltViewModel(), 57 | navController: NavHostController = rememberNavController(), 58 | ) { 59 | LaunchedEffect(Unit) { 60 | viewModel.sendIntent(CookInfoIntent.LoadFoodVideoInfo(bvId = bvId)) 61 | } 62 | CompositionLocalProvider( 63 | LocalViewModel provides viewModel, 64 | LocalViewState provides viewModel.viewStates, 65 | LocalNavController provides navController, 66 | ) { 67 | CookInfoScreen(modifier = modifier) 68 | } 69 | } 70 | 71 | @OptIn(ExperimentalMaterial3Api::class) 72 | @Composable 73 | private fun CookInfoScreen( 74 | modifier: Modifier, 75 | ) { 76 | val navController = LocalNavController.current 77 | val viewModel = LocalViewModel.current 78 | val viewStates = LocalViewState.current 79 | val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() 80 | Scaffold( 81 | modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection), 82 | topBar = { 83 | Column { 84 | AnimatedVisibility( 85 | viewStates.isShowBottomBar, 86 | ) { 87 | LargeTopAppBar( 88 | scrollBehavior = scrollBehavior, 89 | title = { 90 | Text( 91 | fontWeight = FontWeight.W900, 92 | text = "详细信息", 93 | ) 94 | }, 95 | navigationIcon = { 96 | IconButton(onClick = { 97 | navController.navigateUp() 98 | }) { 99 | Icon( 100 | imageVector = Icons.Outlined.ArrowBack, 101 | contentDescription = null, 102 | ) 103 | } 104 | }, 105 | ) 106 | } 107 | } 108 | }, 109 | ) { 110 | PageContentColumn(Modifier.padding(top = it.calculateTopPadding())) { 111 | Column( 112 | Modifier.fillMaxSize(), 113 | horizontalAlignment = Alignment.CenterHorizontally, 114 | ) { 115 | Spacer(modifier = Modifier.height(10.dp)) 116 | CookInfoContent() 117 | } 118 | } 119 | } 120 | } 121 | 122 | @Composable 123 | private fun CookInfoContent() { 124 | val viewModel = LocalViewModel.current 125 | val viewStates = LocalViewState.current 126 | 127 | LazyColumn(Modifier.fillMaxWidth()) { 128 | viewStates.foodVideoInfo?.data?.apply { 129 | item { 130 | CookInfoVideoCard( 131 | title = title, 132 | content = desc, 133 | thumbnailUrl = pic, 134 | avatarUrl = owner.face, 135 | duration = duration, 136 | view = stat.view, 137 | modifier = Modifier.clickable { 138 | viewModel.sendIntent(CookInfoIntent.ToBiliBiliPlay(bvid)) 139 | }, 140 | ) 141 | } 142 | item { 143 | Spacer(Modifier.height(20.dp)) 144 | GoToBilibiliASCard(bvid) 145 | } 146 | } 147 | } 148 | } 149 | 150 | @Composable 151 | @Preview 152 | fun GoToBilibiliASCard(bvid: String = "") { 153 | val viewModel = LocalViewModel.current 154 | val context = LocalContext.current 155 | Spacer(Modifier.height(5.dp)) 156 | Card( 157 | modifier = Modifier 158 | .fillMaxWidth().clickable { 159 | viewModel.sendIntent(CookInfoIntent.ToBiliBiliAs(context,bvid)) 160 | } 161 | ) { 162 | Row(Modifier.padding(10.dp), verticalAlignment = Alignment.CenterVertically) { 163 | Surface(shape = RoundedCornerShape(15.dp)) { 164 | Image( 165 | modifier = Modifier.size(40.dp), 166 | painter = painterResource(R.drawable.bilibilias), 167 | contentDescription = null 168 | ) 169 | } 170 | Spacer(Modifier.width(10.dp)) 171 | Text("前往BILIBILIAS缓存", fontSize = 16.sp) 172 | } 173 | } 174 | } -------------------------------------------------------------------------------- /feature/cook/src/main/java/com/imcys/feature/cook/ui/info/CookInfoState.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.feature.cook.ui.info 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import com.imcys.core.common.viewmodel.info.UiIntent 6 | import com.imcys.core.common.viewmodel.info.UiState 7 | import com.imcys.core.database.entity.CookFoodEntity 8 | import com.imcys.core.model.cook.CookFoodVideoInfo 9 | 10 | data class CookInfoState( 11 | val isShowBottomBar: Boolean = false, 12 | val foodVideoBvId: String = "", 13 | val foodEntity: CookFoodEntity? = null, 14 | val foodVideoInfo: CookFoodVideoInfo? = null, 15 | ) : UiState 16 | 17 | sealed class CookInfoIntent : UiIntent { 18 | data class LoadFoodVideoInfo(val bvId: String) : CookInfoIntent() 19 | data class ToBiliBiliPlay(val bvId: String) : CookInfoIntent() 20 | data class ToBiliBiliAs(val context: Context, val bvId: String) : CookInfoIntent() 21 | } 22 | -------------------------------------------------------------------------------- /feature/cook/src/main/java/com/imcys/feature/cook/ui/info/CookInfoViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.feature.cook.ui.info 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.net.Uri 7 | import android.widget.Toast 8 | import com.imcys.core.common.viewmodel.ComposeBaseViewModel 9 | import com.imcys.core.data.repository.cook.CookFoodInfoRepository 10 | import com.imcys.core.network.retrofit.RetrofitAppNetwork.bilibiliApi 11 | import dagger.hilt.android.lifecycle.HiltViewModel 12 | import dagger.hilt.android.qualifiers.ApplicationContext 13 | import kotlinx.coroutines.delay 14 | import javax.inject.Inject 15 | 16 | @HiltViewModel 17 | class CookInfoViewModel @Inject constructor( 18 | private val cookFoodInfoRepository: CookFoodInfoRepository, 19 | @ApplicationContext private val context: Context, 20 | 21 | ) : ComposeBaseViewModel(CookInfoState()) { 22 | override fun handleEvent(event: CookInfoIntent, state: CookInfoState) { 23 | when (event) { 24 | is CookInfoIntent.LoadFoodVideoInfo -> { 25 | launchUI { loadFoodVideoInfo(event.bvId) } 26 | } 27 | 28 | is CookInfoIntent.ToBiliBiliPlay -> { 29 | launchUI { toBiliBiliPlay(event.bvId) } 30 | } 31 | 32 | is CookInfoIntent.ToBiliBiliAs -> { 33 | launchUI { toBiliBiliAs(event.context,event.bvId) } 34 | } 35 | } 36 | } 37 | 38 | private fun toBiliBiliAs(context: Context,bvId: String) { 39 | 40 | val sendIntent = Intent().apply { 41 | action = Intent.ACTION_SEND 42 | putExtra(Intent.EXTRA_TEXT, bvId) // 分享的文本内容 43 | type = "text/plain" 44 | } 45 | val chooserIntent = Intent.createChooser(sendIntent, "分享到BILIBILIAS") 46 | // 使用 context.startActivity 启动选择器 47 | context.startActivity(chooserIntent) 48 | } 49 | 50 | /** 51 | * 跳转B站播放视频 52 | * @param bvId String 53 | */ 54 | private fun toBiliBiliPlay(bvId: String) { 55 | val packName = "tv.danmaku.bili" 56 | context.packageManager.getLaunchIntentForPackage(packName)?.apply { 57 | // 组装跳转 58 | val url = "bilibili://video/$bvId" 59 | Intent(Intent.ACTION_VIEW, Uri.parse(url)).also { 60 | it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 61 | context.startActivity(it) 62 | } 63 | 64 | Toast.makeText(context, "芜湖,前往B站", Toast.LENGTH_SHORT).show() 65 | } ?: apply { 66 | Toast.makeText(context, "呜哇,还没有装B站", Toast.LENGTH_SHORT).show() 67 | } 68 | } 69 | 70 | private suspend fun loadFoodVideoInfo(bvId: String) { 71 | // 获取数据 72 | bilibiliApi.getVideoInfo(bvId).apply { 73 | viewStates.update { copy(foodVideoInfo = this@apply) } 74 | } 75 | 76 | cookFoodInfoRepository.getCookingFood(bvId) 77 | } 78 | 79 | init { 80 | // appbar动画 81 | launchUI { 82 | delay(200L) 83 | viewStates.update { copy(isShowBottomBar = true) } 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /feature/cook/src/main/res/drawable/bilibilias.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1250422131/FoodChoice/18de8536cd5829fd7bdb0ddb73001ddb98c7fef0/feature/cook/src/main/res/drawable/bilibilias.png -------------------------------------------------------------------------------- /feature/cook/src/test/java/com/imcys/feature/cook/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.imcys.feature.cook 2 | 3 | import org.junit.Assert.* 4 | import org.junit.Test 5 | 6 | /** 7 | * Example local unit test, which will execute on the development machine (host). 8 | * 9 | * See [testing documentation](http://d.android.com/tools/testing). 10 | */ 11 | class ExampleUnitTest { 12 | @Test 13 | fun addition_isCorrect() { 14 | assertEquals(4, 2 + 2) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-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/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | androidxComposeBom = "2024.12.01" 3 | espressoCore = "3.4.0" 4 | hiltAndroid = "2.54" 5 | junitVersion = "1.1.3" 6 | junit = "4.13.2" 7 | hiltCompiler = "2.54" 8 | konfettiCompose = "2.0.4" 9 | retrofit2 = "2.11.0" 10 | moshi = "1.15.2" 11 | kotlin = "2.1.0" 12 | 13 | [libraries] 14 | androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "androidxComposeBom" } 15 | androidx-compose-ui-ui = { group = "androidx.compose.ui", name = "ui" } 16 | androidx-compose-ui-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } 17 | androidx-compose-ui-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } 18 | androidx-compose-material3-material3 = { group = "androidx.compose.material3", name = "material3" } 19 | androidx-compose-material-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" } 20 | androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espressoCore" } 21 | androidx-junit = { module = "androidx.test.ext:junit", version.ref = "junitVersion" } 22 | com-airbnb-android-lottie = { group = "com.airbnb.android", name = "lottie", version = "6.1.0" } 23 | com-squareup-retrofit2-retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit2" } 24 | com-squareup-retrofit2-converter-moshi = { group = "com.squareup.retrofit2", name = "converter-moshi", version.ref = "retrofit2" } 25 | hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hiltAndroid" } 26 | junit = { module = "junit:junit", version.ref = "junit" } 27 | hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "hiltCompiler" } 28 | androidx-material3-window-size = { group = "androidx.compose.material3", name = "material3-window-size-class", version = "1.1.2" } 29 | konfetti-compose = { module = "nl.dionsegijn:konfetti-compose", version.ref = "konfettiCompose" } 30 | [plugins] 31 | kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } 32 | 33 | 34 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1250422131/FoodChoice/18de8536cd5829fd7bdb0ddb73001ddb98c7fef0/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Jul 05 14:25:50 CST 2023 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 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.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | maven { 4 | setUrl("https://maven.aliyun.com/repository/public") 5 | } 6 | google() 7 | mavenCentral() 8 | gradlePluginPortal() 9 | } 10 | } 11 | dependencyResolutionManagement { 12 | 13 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 14 | repositories { 15 | mavenCentral() 16 | 17 | maven { 18 | setUrl("https://maven.aliyun.com/repository/public") 19 | } 20 | google() 21 | } 22 | } 23 | 24 | // 25 | //plugins { 26 | // id("com.highcapable.sweetdependency") version "1.0.2" 27 | //} 28 | 29 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") 30 | rootProject.name = "FoodChoice" 31 | include(":app") 32 | include(":core:common") 33 | include(":core:network") 34 | include(":core:ui") 35 | include(":core:model") 36 | include(":core:designsystem") 37 | include(":core:database") 38 | include(":feature:cook") 39 | include(":core:data") 40 | --------------------------------------------------------------------------------